use std::sync::Arc;
use affinidi_tdk::didcomm::Message;
use chrono::{DateTime, Utc};
use serde::de::DeserializeOwned;
use serde_json::Value;
use tracing::{debug, info, warn};
use trust_tasks_rs::TrustTask;
use vta_sdk::protocols::PROBLEM_REPORT_TYPE;
use vta_sdk::protocols::credential_exchange::ISSUE as CREDENTIAL_ISSUE_TYPE;
use vta_sdk::protocols::vetting::{
VETTER_ROLE, VETTING_DECLINE_TYPE, VETTING_REQUEST_RESPONSE_TYPE, VETTING_REQUEST_TYPE,
VETTING_REVOKE_STATEMENT_RESPONSE_TYPE, VETTING_SESSION_RESPONSE_TYPE, VETTING_SESSION_TYPE,
VETTING_VETTER_LIST_RESPONSE_TYPE, VETTING_VETTER_PROFILE_RESPONSE_TYPE,
VETTING_VETTER_RESEND_RESPONSE_TYPE, decline, request, revoke_statement, role_matches, session,
vetters,
};
use vta_sdk::trust_task_proof::TrustTaskVmResolver;
use vta_sdk::vetting::eligibility::{
EligibilityExpectations, community_roles, verify_eligibility_vp,
};
use super::applicant::{GrantStatus, VetterEligibility};
use super::book::{VetterGrant, VettingBook};
use super::queries::{CommunityAnswer, CommunityQuery, QueryKind, refusal_words};
use super::status::GrantCheck;
use super::vetter::{IncomingRequest, Intake};
use super::wire;
use crate::config::account::{Account, PersonaId};
use crate::messaging::is_trust_task_error_type;
use crate::tasks::TaskType;
pub struct Context<'a> {
pub account: &'a Account,
pub resolver: &'a TrustTaskVmResolver,
pub did_resolver: &'a affinidi_did_resolver_cache_sdk::DIDCacheClient,
pub issued_credential: Option<
&'a Result<
crate::issued_credential::VerifiedIssuedCredential,
crate::issued_credential::IssuedCredentialError,
>,
>,
pub community_answer: Option<
&'a Result<crate::operational::VerifiedOperational, crate::operational::OperationalError>,
>,
pub recipient: Option<(PersonaId, &'a str)>,
pub now: DateTime<Utc>,
}
#[derive(Debug, Default)]
pub struct Handled {
pub changed: bool,
pub reply: Option<Reply>,
pub notice: Option<Notice>,
pub answer: Option<CommunityAnswer>,
pub grant_check: Option<GrantCheck>,
}
#[derive(Debug)]
pub struct Reply {
pub persona: PersonaId,
pub document: TrustTask<Value>,
pub eligibility: Option<EligibilityPresentation>,
}
#[derive(Debug, Clone)]
pub struct EligibilityPresentation {
pub credentials: Vec<Value>,
pub nonce: String,
pub domain: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Notice {
RequestAccepted {
request_id: String,
applicant: String,
community: String,
},
VetterAccepted {
application_id: String,
vetter: String,
shown_eligible: bool,
},
RequestRefused {
applicant: String,
code: String,
},
VetterGranted {
community: String,
valid_until: Option<DateTime<Utc>>,
},
VetterRefused {
application_id: String,
vetter: String,
code: String,
},
SessionOpened {
application_id: String,
session_id: String,
vetter: String,
match_code: String,
},
CardReceived {
request_id: String,
applicant: String,
},
StatementReceived {
application_id: String,
statement_id: String,
vetter: String,
},
Declined {
application_id: String,
vetter: String,
},
RequirementsUpdated {
application_id: String,
community: String,
},
RequirementsUnsupported {
application_id: String,
community: String,
detail: String,
},
HiddenVetterEnrolled {
community: String,
label: String,
},
HiddenTokensDrawn {
community: String,
taken: usize,
},
HiddenEventMode {
community: String,
event_id: String,
approved: bool,
group_size: usize,
group_floor: usize,
},
ChallengeIssued {
application_id: String,
expires_at: DateTime<Utc>,
},
HiddenVettingRefused {
community: String,
what: &'static str,
code: String,
},
AttestationReceived {
application_id: String,
held: usize,
},
WithdrawalRecorded {
statement_id: String,
},
WithdrawalRefused {
statement_id: String,
code: String,
},
ProfilePublished {
community: String,
listed: bool,
},
ProfileRefused {
community: String,
code: String,
},
GrantResent {
community: String,
valid_until: DateTime<Utc>,
},
ResendRefused {
community: String,
code: String,
},
}
impl Notice {
#[must_use]
pub fn describe(&self) -> String {
match self {
Notice::RequestAccepted { applicant, .. } => {
format!(
"Vetting request from {applicant} accepted — open a session when you are together."
)
}
Notice::VetterAccepted {
vetter,
shown_eligible: true,
..
} => format!("{vetter} accepted your vetting request."),
Notice::VetterAccepted { vetter, .. } => format!(
"{vetter} accepted your vetting request, but did not show that the community \
named them a vetter — their statement may not count."
),
Notice::VetterGranted {
community,
valid_until,
} => match valid_until {
Some(until) => format!(
"{community} named you a vetter until {}. You can hand out tickets.",
until.format("%Y-%m-%d")
),
None => format!("{community} named you a vetter."),
},
Notice::RequestRefused { applicant, code } => format!(
"Vetting request from {applicant} refused [{code}] — {}",
super::queries::request_refusal_words(code)
),
Notice::VetterRefused { vetter, code, .. } => format!(
"{vetter} refused your vetting request [{code}] — {}",
super::queries::request_refusal_words(code)
),
Notice::SessionOpened {
vetter, match_code, ..
} => format!(
"{vetter} opened a vetting session. Read the code {match_code} to each other, then send your card."
),
Notice::CardReceived { applicant, .. } => {
format!(
"Vetting card from {applicant} verified — check the person, then attest or decline."
)
}
Notice::StatementReceived { vetter, .. } => {
format!("{vetter} signed a vetting statement for you.")
}
Notice::Declined { vetter, .. } => format!("{vetter} declined to vet you."),
Notice::RequirementsUpdated { community, .. } => {
format!("Vetting requirements for {community} updated.")
}
Notice::RequirementsUnsupported { detail, .. } => detail.clone(),
Notice::HiddenVetterEnrolled { community, label } => format!(
"{community} enrolled you under {label}. Your attestations will name nobody."
),
Notice::HiddenTokensDrawn { taken, .. } => format!(
"Drew {taken} attestation token{}.",
if *taken == 1 { "" } else { "s" }
),
Notice::HiddenEventMode {
event_id,
approved: true,
..
} => format!("You are in {event_id}. Your tokens there draw at the event's rate."),
Notice::HiddenEventMode {
event_id,
group_size,
group_floor,
..
} => format!(
"Asked to vet at {event_id}. {group_size} of {group_floor} vetters so far, and \
it still needs an approver."
),
Notice::ChallengeIssued { expires_at, .. } => format!(
"The community issued your submission challenge; it stands until {}.",
expires_at.format("%H:%M")
),
Notice::HiddenVettingRefused {
community,
what,
code,
} => format!("{community} refused {what} ({code})."),
Notice::AttestationReceived { held, .. } => format!(
"An attestation arrived and verified. You now hold {held} for this application."
),
Notice::WithdrawalRecorded { statement_id } => {
format!("The community recorded the withdrawal of statement {statement_id}.")
}
Notice::WithdrawalRefused { statement_id, code } => format!(
"The community refused the withdrawal of statement {statement_id} [{code}]."
),
Notice::ProfilePublished {
community,
listed: true,
} => format!(
"{community} published your vetter profile — applicants can find you in its \
directory."
),
Notice::ProfilePublished { community, .. } => format!(
"{community} stored your vetter profile. It is not listed, so only people you \
give a ticket to can reach you."
),
Notice::ProfileRefused { community, code } => {
refusal_words(QueryKind::VetterProfile, community, code)
}
Notice::GrantResent {
community,
valid_until,
} => format!(
"{community} is sending your vetter credential again (valid until {}).",
valid_until.format("%Y-%m-%d")
),
Notice::ResendRefused { community, code } => {
refusal_words(QueryKind::VetterResend, community, code)
}
}
}
}
impl Notice {
#[must_use]
pub fn task(&self) -> Option<(String, TaskType)> {
match self {
Notice::RequestAccepted {
request_id,
applicant,
community,
} => Some((
format!("vetting-request-{request_id}"),
TaskType::VettingRequestInbound {
request_id: request_id.clone(),
applicant: Arc::new(applicant.clone()),
community: community.clone(),
},
)),
Notice::SessionOpened {
application_id,
session_id,
vetter,
..
} => Some((
format!("vetting-session-{session_id}"),
TaskType::VettingSessionInbound {
application_id: application_id.clone(),
session_id: session_id.clone(),
vetter: Arc::new(vetter.clone()),
},
)),
Notice::CardReceived {
request_id,
applicant,
} => Some((
format!("vetting-card-{request_id}"),
TaskType::VettingCardReceived {
request_id: request_id.clone(),
applicant: Arc::new(applicant.clone()),
},
)),
_ => None,
}
}
}
#[must_use]
pub fn may_claim(typ: &str) -> bool {
typ.starts_with("https://trusttasks.org/spec/vetting/")
|| matches!(
typ,
VETTING_REVOKE_STATEMENT_RESPONSE_TYPE
| VETTING_VETTER_LIST_RESPONSE_TYPE
| VETTING_VETTER_PROFILE_RESPONSE_TYPE
| VETTING_VETTER_RESEND_RESPONSE_TYPE
| CREDENTIAL_ISSUE_TYPE
| PROBLEM_REPORT_TYPE
)
|| super::protocol::JoinProtocol::from_manifest_response(typ).is_some()
|| is_trust_task_error_type(typ)
}
pub async fn handle(
book: &mut VettingBook,
ctx: &Context<'_>,
seen: &mut crate::operational::SeenDocuments,
message: &Message,
sender: &str,
) -> Option<Handled> {
let pending = if is_community_answer_type(&message.typ) {
match community_signed(ctx, seen, message, sender).await {
Ok(verified) => Some(verified),
Err(e) => {
warn!(typ = %message.typ, reason = %e, "community answer refused");
return Some(Handled::default());
}
}
} else {
None
};
let handled = match message.typ.as_str() {
VETTING_REQUEST_TYPE => take_request(book, ctx, message, sender).await,
VETTING_REQUEST_RESPONSE_TYPE => accepted(book, ctx, message, sender).await,
VETTING_SESSION_TYPE => session(book, ctx, message, sender).await,
VETTING_SESSION_RESPONSE_TYPE => card(book, ctx, message, sender).await,
VETTING_DECLINE_TYPE => declined(book, ctx, message, sender).await,
VETTING_REVOKE_STATEMENT_RESPONSE_TYPE => withdrawal_recorded(book, message, sender),
VETTING_VETTER_LIST_RESPONSE_TYPE => vetter_list(book, message, sender),
VETTING_VETTER_PROFILE_RESPONSE_TYPE => profile_stored(book, message, sender),
VETTING_VETTER_RESEND_RESPONSE_TYPE => resent(book, message, sender),
t if super::protocol::JoinProtocol::from_manifest_response(t).is_some() => {
manifest(book, ctx, message, sender)
}
CREDENTIAL_ISSUE_TYPE => return statement(book, ctx, message, sender).await,
wire::HIDDEN_ATTESTATION_TYPE => return attestation(book, ctx, message, sender),
t if t == wire::pcs::response_of(wire::pcs::ROOT_TYPE) => {
enrolled(book, ctx, message, sender)
}
t if t == wire::pcs::response_of(wire::pcs::TOKENS_TYPE) => {
tokens_served(book, ctx, message, sender)
}
t if t == wire::pcs::response_of(wire::pcs::EVENT_MODE_TYPE) => {
event_mode_answered(book, ctx, message, sender)
}
t if t == wire::pcs::response_of(wire::pcs::CHALLENGE_TYPE) => {
challenge_issued(book, message, sender)
}
t if is_trust_task_error_type(t) => return refused(book, ctx, message, sender),
PROBLEM_REPORT_TYPE => return problem_reported(book, ctx, message, sender),
_ => return None,
};
if let Some(verified) = pending
&& (handled.changed || handled.answer.is_some() || handled.notice.is_some())
&& let Err(e) = verified.commit(seen, ctx.now)
{
warn!(reason = %e, "community answer acted on but not recorded");
}
Some(handled)
}
async fn opened<P: DeserializeOwned>(
message: &Message,
sender: &str,
resolver: &TrustTaskVmResolver,
) -> Option<wire::Opened<P>> {
match wire::open(message, sender, resolver).await {
Ok(opened) => Some(opened),
Err(e) => {
warn!(typ = %message.typ, %sender, error = %e, "vetting document refused");
None
}
}
}
pub fn is_community_answer_type(typ: &str) -> bool {
matches!(
typ,
VETTING_REVOKE_STATEMENT_RESPONSE_TYPE
| VETTING_VETTER_LIST_RESPONSE_TYPE
| VETTING_VETTER_PROFILE_RESPONSE_TYPE
| VETTING_VETTER_RESEND_RESPONSE_TYPE
) || super::protocol::JoinProtocol::from_manifest_response(typ).is_some()
}
async fn community_signed(
ctx: &Context<'_>,
seen: &crate::operational::SeenDocuments,
message: &Message,
sender: &str,
) -> Result<crate::operational::VerifiedOperational, String> {
let Some((_, our_did)) = ctx.recipient else {
return Err("it arrived for no persona of ours".to_string());
};
if let Some(checked) = ctx.community_answer {
let verified = checked.clone().map_err(|e| e.to_string())?;
if verified.recipient() != our_did {
return Err("it is addressed to another persona".to_string());
}
verified.check(seen, ctx.now).map_err(|e| e.to_string())?;
return Ok(verified);
}
let verified = crate::operational::verify_operational(
&message.body,
sender,
&[our_did],
&message.typ,
ctx.did_resolver,
seen,
ctx.now,
)
.await
.map_err(|e| e.to_string())?;
verified.check(seen, ctx.now).map_err(|e| e.to_string())?;
Ok(verified)
}
fn community_thread(message: &Message) -> Option<String> {
message.thid.clone().or_else(|| {
message
.body
.get("threadId")
.and_then(Value::as_str)
.map(str::to_string)
})
}
fn community_payload<P: DeserializeOwned>(message: &Message) -> Result<P, String> {
let payload = message.body.get("payload").cloned().unwrap_or(Value::Null);
serde_json::from_value(payload).map_err(|e| e.to_string())
}
fn community_reply<P: DeserializeOwned>(message: &Message) -> Option<(Option<String>, P)> {
match community_payload(message) {
Ok(p) => Some((community_thread(message), p)),
Err(e) => {
warn!(typ = %message.typ, error = %e, "malformed community reply");
None
}
}
}
fn answer_to<P: DeserializeOwned>(
book: &mut VettingBook,
message: &Message,
sender: &str,
kind: QueryKind,
) -> Option<(CommunityQuery, Result<P, CommunityAnswer>)> {
let Some(thread) = community_thread(message) else {
debug!(typ = %message.typ, %sender, "community answer with no thread — ignored");
return None;
};
let Some(query) = book.take_query(sender, &thread, Some(kind)) else {
debug!(typ = %message.typ, %sender, "community answer to nothing we asked — ignored");
return None;
};
let payload = community_payload::<P>(message).map_err(|detail| {
warn!(typ = %message.typ, %sender, error = %detail, "unreadable community answer");
CommunityAnswer::Unreadable {
query: query.document_id.clone(),
community: sender.to_string(),
kind,
detail,
}
});
Some((query, payload))
}
async fn take_request(
book: &mut VettingBook,
ctx: &Context<'_>,
message: &Message,
sender: &str,
) -> Handled {
let Some((persona, _)) = ctx.recipient else {
return Handled::default();
};
let Some(opened) = opened::<request::v0_1::Payload>(message, sender, ctx.resolver).await else {
return Handled::default();
};
let community = opened.payload.community.as_str().to_string();
let grant = book.vetter_grant(&community, persona, ctx.now).cloned();
let eligible = grant.is_some()
&& ctx
.account
.membership(&community, persona)
.is_some_and(|m| m.status.is_active());
let throttle = book.throttle.clone();
let intake = book.take_request(
IncomingRequest {
document_id: &opened.document.id,
sender,
persona,
body: opened.payload,
eligible,
},
ctx.now,
);
let throttled = book.throttle != throttle;
match intake {
Intake::Accepted(body) => {
let request_id = body.request_id.as_str().to_string();
let eligibility = grant.map(|g| EligibilityPresentation {
credentials: vec![g.credential],
nonce: opened.document.id.clone(),
domain: sender.to_string(),
});
Handled {
changed: true,
reply: wire::response(&opened.document, &body)
.ok()
.map(|document| Reply {
persona,
document,
eligibility,
}),
notice: Some(Notice::RequestAccepted {
request_id,
applicant: sender.to_string(),
community,
}),
..Handled::default()
}
}
Intake::Refused(code) => {
info!(%sender, %code, "vetting request refused");
Handled {
changed: throttled,
reply: wire::refusal(&opened.document, code, None)
.ok()
.map(|document| Reply {
persona,
document,
eligibility: None,
}),
notice: Some(Notice::RequestRefused {
applicant: sender.to_string(),
code: code.to_string(),
}),
..Handled::default()
}
}
Intake::Silent => {
debug!(%sender, "vetting request without a matching ticket — no answer");
Handled {
changed: throttled,
..Handled::default()
}
}
}
}
async fn accepted(
book: &mut VettingBook,
ctx: &Context<'_>,
message: &Message,
sender: &str,
) -> Handled {
let Some(opened) = opened::<request::v0_1::Response>(message, sender, ctx.resolver).await
else {
return Handled::default();
};
let Some(thread) = opened.document.thread_id.as_deref() else {
return Handled::default();
};
let Some(application) = book.applications.iter_mut().find(|a| a.sent(thread)) else {
warn!(%sender, "vetting acceptance for no request of ours");
return Handled::default();
};
let mut grant_status_entry: Option<Option<Value>> = None;
let presented = opened.document.payload.get("eligibilityVp");
let eligibility = match presented {
None => VetterEligibility::NotShown,
Some(vp) => {
let expect = EligibilityExpectations {
vetter: sender,
community: &application.community,
role: application.vetter_role(),
challenge: thread,
domain: &application.join_did,
now: ctx.now,
};
match verify_eligibility_vp(vp, &expect, ctx.resolver).await {
Ok(verified) => {
grant_status_entry = Some(verified.credential_status().cloned());
VetterEligibility::Shown {
credential_id: verified.credential_id().map(str::to_string),
valid_until: verified.valid_until(),
}
}
Err(e) => {
warn!(%sender, error = %e, "vetter eligibility presentation did not verify");
VetterEligibility::Failed {
reason: e.to_string(),
}
}
}
}
};
let shown_eligible = matches!(eligibility, VetterEligibility::Shown { .. });
match application.on_accepted(thread, sender, opened.payload, eligibility, ctx.now) {
Ok(()) => {
let grant_check = match grant_status_entry {
Some(Some(credential_status)) => {
let checking = GrantStatus::Checking { since: ctx.now };
let _ = application.record_grant_status(thread, sender, checking);
Some(GrantCheck {
application_id: application.id.clone(),
request_document_id: thread.to_string(),
vetter: sender.to_string(),
issuer: application.community.clone(),
credential_status,
})
}
Some(None) => {
let unknown = GrantStatus::Unknown {
reason: "the vetter's credential names no status list to check".into(),
checked_at: ctx.now,
};
let _ = application.record_grant_status(thread, sender, unknown);
None
}
None => None,
};
Handled {
changed: true,
notice: Some(Notice::VetterAccepted {
application_id: application.id.clone(),
vetter: sender.to_string(),
shown_eligible,
}),
grant_check,
..Handled::default()
}
}
Err(e) => {
warn!(%sender, error = %e, "vetting acceptance not applied");
Handled::default()
}
}
}
async fn session(
book: &mut VettingBook,
ctx: &Context<'_>,
message: &Message,
sender: &str,
) -> Handled {
let Some((persona, _)) = ctx.recipient else {
return Handled::default();
};
let Some(opened) = opened::<session::v0_1::Payload>(message, sender, ctx.resolver).await else {
return Handled::default();
};
let Some(application) = book.application_mut(opened.payload.domain.as_str(), persona) else {
warn!(%sender, "vetting session for a community we are not applying to");
return Handled::default();
};
match application.on_session(
&opened.document.id,
&message.body,
sender,
opened.payload,
ctx.now,
) {
Ok(session) => Handled {
changed: true,
notice: Some(Notice::SessionOpened {
application_id: application.id.clone(),
session_id: session.id,
vetter: sender.to_string(),
match_code: session.match_code,
}),
..Handled::default()
},
Err(e) => {
warn!(%sender, error = %e, "vetting session not opened");
Handled::default()
}
}
}
async fn card(
book: &mut VettingBook,
ctx: &Context<'_>,
message: &Message,
sender: &str,
) -> Handled {
let Some((_, our_did)) = ctx.recipient else {
return Handled::default();
};
let Some(opened) = opened::<Value>(message, sender, ctx.resolver).await else {
return Handled::default();
};
let Some(session_id) = opened.document.thread_id.clone() else {
return Handled::default();
};
let Some(card) = opened.payload.get("card") else {
warn!(%sender, "vetting session response carries no card");
return Handled::default();
};
match book
.receive_card(our_did, sender, &session_id, card, ctx.resolver, ctx.now)
.await
{
Ok(entry) => Handled {
changed: true,
notice: Some(Notice::CardReceived {
request_id: entry.request_id.clone(),
applicant: sender.to_string(),
}),
..Handled::default()
},
Err(e) => {
warn!(%sender, error = %e, "vetting card refused");
Handled::default()
}
}
}
async fn declined(
book: &mut VettingBook,
ctx: &Context<'_>,
message: &Message,
sender: &str,
) -> Handled {
let Some(opened) = opened::<decline::v0_1::Payload>(message, sender, ctx.resolver).await else {
return Handled::default();
};
for application in &mut book.applications {
if application
.on_decline(sender, opened.payload.clone(), ctx.now)
.is_ok()
{
return Handled {
changed: true,
notice: Some(Notice::Declined {
application_id: application.id.clone(),
vetter: sender.to_string(),
}),
..Handled::default()
};
}
}
warn!(%sender, "vetting decline for no request of ours");
Handled::default()
}
async fn statement(
book: &mut VettingBook,
ctx: &Context<'_>,
message: &Message,
sender: &str,
) -> Option<Handled> {
let issued = crate::messaging::credential_in_issue(message);
if let Some(credential) = issued.as_ref()
&& let Some((community, roles)) = community_roles(credential)
&& roles.iter().any(|role| role_matches(role, VETTER_ROLE))
{
if let Err(e) = wire::open::<Value>(message, sender, ctx.resolver).await {
warn!(%sender, reason = %e, "vetter grant delivery refused");
return Some(Handled::default());
}
return Some(match ctx.issued_credential {
Some(Ok(verified)) if verified.value() == credential => {
vetter_grant(book, ctx, verified.value(), community, sender)
}
Some(Err(e)) => {
warn!(reason = %e, "vetter role credential refused");
Handled::default()
}
_ => {
warn!("vetter role credential not checked — refused");
Handled::default()
}
});
}
wire::delivered_statement(&message.body)?;
let Some((persona, _)) = ctx.recipient else {
return Some(Handled::default());
};
let opened = match wire::open::<Value>(message, sender, ctx.resolver).await {
Ok(opened) => opened,
Err(e) => {
warn!(%sender, reason = %e, "vetting statement delivery refused");
return Some(Handled::default());
}
};
let Some(credential) = wire::statement_in(&opened.payload) else {
warn!(%sender, "signed issue carries no vetting statement — ignored");
return Some(Handled::default());
};
for application in book
.applications
.iter_mut()
.filter(|a| a.persona == persona && a.requests.iter().any(|r| r.vetter == sender))
{
match application
.on_statement(sender, credential, ctx.resolver, ctx.now)
.await
{
Ok(held) => {
let community = application.community.clone();
book.retired
.retain(|note| !(note.contains(&community) && note.contains(sender)));
return Some(Handled {
changed: true,
notice: Some(Notice::StatementReceived {
application_id: application.id.clone(),
statement_id: held.id,
vetter: sender.to_string(),
}),
..Handled::default()
});
}
Err(e) => warn!(%sender, error = %e, "vetting statement refused"),
}
}
Some(Handled::default())
}
fn enrolled(book: &mut VettingBook, ctx: &Context<'_>, message: &Message, sender: &str) -> Handled {
let Some((query, body)) =
answer_to::<wire::pcs::RootResponse>(book, message, sender, QueryKind::PcsRoot)
else {
return Handled::default();
};
let body = match body {
Ok(body) => body,
Err(unreadable) => {
return Handled {
answer: Some(unreadable),
..Handled::default()
};
}
};
let Some(blinding) = book.pending_enrolment.take() else {
warn!(community = %sender, "enrolled, but the blinding state is gone — asking again");
return Handled::default();
};
let Some(state) = book.hidden_vetter_mut(sender, query.persona) else {
return Handled::default();
};
let params = state.params.clone();
let mut snapshot = state.snapshot.clone();
match super::hidden::accept_enrolment(sender, ¶ms, &mut snapshot, &body, &blinding) {
Ok(()) => {
if let Some(state) = book.hidden_vetter_mut(sender, query.persona) {
state.snapshot = snapshot;
}
Handled {
changed: true,
notice: Some(Notice::HiddenVetterEnrolled {
community: sender.to_string(),
label: body.label,
}),
..Handled::default()
}
}
Err(e) => {
warn!(community = %sender, error = %e, "enrolment answer did not unblind");
let _ = ctx;
Handled::default()
}
}
}
fn tokens_served(
book: &mut VettingBook,
ctx: &Context<'_>,
message: &Message,
sender: &str,
) -> Handled {
let Some((query, body)) =
answer_to::<wire::pcs::TokensResponse>(book, message, sender, QueryKind::PcsTokens)
else {
return Handled::default();
};
let body = match body {
Ok(body) => body,
Err(unreadable) => {
return Handled {
answer: Some(unreadable),
..Handled::default()
};
}
};
let Some(state) = book.hidden_vetter_mut(sender, query.persona) else {
return Handled::default();
};
let params = state.params.clone();
let mut snapshot = state.snapshot.clone();
match super::hidden::accept_drip(sender, ¶ms, &mut snapshot, &body) {
Ok(taken) => {
if let Some(state) = book.hidden_vetter_mut(sender, query.persona) {
state.snapshot = snapshot;
state.last_ticks.insert(body.label.clone(), body.tick);
state.last_drawn_at = Some(ctx.now);
}
Handled {
changed: true,
notice: Some(Notice::HiddenTokensDrawn {
community: sender.to_string(),
taken,
}),
..Handled::default()
}
}
Err(e) => {
warn!(community = %sender, error = %e, "served tokens did not verify");
Handled::default()
}
}
}
fn event_mode_answered(
book: &mut VettingBook,
ctx: &Context<'_>,
message: &Message,
sender: &str,
) -> Handled {
let Some((query, body)) =
answer_to::<wire::pcs::EventModeResponse>(book, message, sender, QueryKind::PcsEventMode)
else {
return Handled::default();
};
let body = match body {
Ok(body) => body,
Err(unreadable) => {
return Handled {
answer: Some(unreadable),
..Handled::default()
};
}
};
let approved = body.state == wire::pcs::EVENT_APPROVED;
let Some(state) = book.hidden_vetter_mut(sender, query.persona) else {
return Handled::default();
};
let row = super::book::HiddenEventState {
event_id: body.event_id.clone(),
tier: body.tier,
state: body.state,
group_size: body.group_size,
group_floor: body.group_floor,
label: body.label,
drip_per_tick: body.drip_per_tick,
closes_after: body.closes_after,
answered_at: ctx.now,
};
match state.events.iter_mut().find(|e| e.event_id == row.event_id) {
Some(existing) => *existing = row,
None => state.events.push(row),
}
Handled {
changed: true,
notice: Some(Notice::HiddenEventMode {
community: sender.to_string(),
event_id: body.event_id,
approved,
group_size: body.group_size,
group_floor: body.group_floor,
}),
..Handled::default()
}
}
fn challenge_issued(book: &mut VettingBook, message: &Message, sender: &str) -> Handled {
let Some((query, body)) =
answer_to::<wire::pcs::ChallengeResponse>(book, message, sender, QueryKind::PcsChallenge)
else {
return Handled::default();
};
let body = match body {
Ok(body) => body,
Err(unreadable) => {
return Handled {
answer: Some(unreadable),
..Handled::default()
};
}
};
let Some(application) = book
.applications
.iter_mut()
.find(|a| a.community == sender && a.persona == query.persona)
else {
return Handled::default();
};
application.hidden_challenge = Some(body.challenge);
Handled {
changed: true,
notice: Some(Notice::ChallengeIssued {
application_id: application.id.clone(),
expires_at: body.expires_at,
}),
..Handled::default()
}
}
fn attestation(
book: &mut VettingBook,
ctx: &Context<'_>,
message: &Message,
sender: &str,
) -> Option<Handled> {
let Some((persona, _)) = ctx.recipient else {
return Some(Handled::default());
};
for application in book
.applications
.iter_mut()
.filter(|a| a.persona == persona && a.hidden.is_some())
{
match application.receive_hidden_attestation(&message.body) {
Ok(()) => {
return Some(Handled {
changed: true,
notice: Some(Notice::AttestationReceived {
application_id: application.id.clone(),
held: application.hidden_held(),
}),
..Handled::default()
});
}
Err(e) => warn!(%sender, error = %e, "hidden attestation refused"),
}
}
Some(Handled::default())
}
fn vetter_grant(
book: &mut VettingBook,
ctx: &Context<'_>,
credential: &Value,
community: String,
sender: &str,
) -> Handled {
let Some((persona, our_did)) = ctx.recipient else {
return Handled::default();
};
let issuer = credential
.get("issuer")
.and_then(|i| i.as_str().or_else(|| i.get("id").and_then(Value::as_str)));
let subject = credential
.pointer("/credentialSubject/id")
.and_then(Value::as_str);
if community != sender || issuer != Some(sender) || subject != Some(our_did) {
warn!(%sender, %community, "vetter role credential not from its community, or not for us — ignored");
return Handled::default();
}
if ctx.account.membership(&community, persona).is_none() {
warn!(%community, "vetter role credential from a community we are not a member of — ignored");
return Handled::default();
}
let valid_until = credential
.get("validUntil")
.and_then(Value::as_str)
.and_then(|s| DateTime::parse_from_rfc3339(s).ok())
.map(|d| d.with_timezone(&Utc));
let changed = book.keep_vetter_grant(VetterGrant {
community: community.clone(),
persona,
credential_id: credential
.get("id")
.and_then(Value::as_str)
.map(str::to_string),
valid_until,
received_at: ctx.now,
credential: credential.clone(),
});
Handled {
changed,
notice: changed.then_some(Notice::VetterGranted {
community,
valid_until,
}),
..Handled::default()
}
}
fn refused(
book: &mut VettingBook,
ctx: &Context<'_>,
message: &Message,
sender: &str,
) -> Option<Handled> {
let thread = message
.thid
.as_deref()
.or_else(|| message.body.get("threadId").and_then(Value::as_str))?;
let code = message
.body
.pointer("/payload/code")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let detail = message
.body
.pointer("/payload/message")
.and_then(Value::as_str)
.map(str::to_string);
refusal_of(book, ctx, thread, sender, code, detail)
}
fn problem_reported(
book: &mut VettingBook,
ctx: &Context<'_>,
message: &Message,
sender: &str,
) -> Option<Handled> {
let thread = message.thid.as_deref()?;
let (code, comment) = vta_sdk::protocols::extract_problem_report(&message.body);
let detail = (!comment.is_empty()).then_some(comment);
refusal_of(book, ctx, thread, sender, code, detail)
}
fn retry_manifest_in_older_version(
book: &mut VettingBook,
ctx: &Context<'_>,
query: &super::queries::CommunityQuery,
community: &str,
) -> Option<Handled> {
let asked = book.protocol_for(community);
let applicant_did = ctx.account.personas.get(&query.persona)?.did.clone();
let next = book.fall_back_from(community, asked)?;
let document = match wire::manifest_request(&applicant_did, community, next) {
Ok(d) => d,
Err(e) => {
warn!(%community, error = %e, "could not build the older manifest request");
return None;
}
};
info!(
%community, ?asked, ?next,
"community does not serve this join-requests version; asking in the older one"
);
book.ask(super::queries::CommunityQuery {
document_id: document.id.clone(),
community: community.to_string(),
persona: query.persona,
kind: QueryKind::Manifest,
sent_at: ctx.now,
});
Some(Handled {
changed: true,
reply: Some(Reply {
persona: query.persona,
document,
eligibility: None,
}),
..Handled::default()
})
}
fn refusal_of(
book: &mut VettingBook,
ctx: &Context<'_>,
thread: &str,
sender: &str,
code: String,
detail: Option<String>,
) -> Option<Handled> {
if let Some(application) = book.applications.iter_mut().find(|a| a.sent(thread)) {
return Some(
match application.on_refused(thread, sender, code.clone(), detail, ctx.now) {
Ok(()) => Handled {
changed: true,
notice: Some(Notice::VetterRefused {
application_id: application.id.clone(),
vetter: sender.to_string(),
code,
}),
..Handled::default()
},
Err(e) => {
warn!(%sender, error = %e, "vetting refusal not applied");
Handled::default()
}
},
);
}
if let Some(query) = book.take_query(sender, thread, None) {
if query.kind == QueryKind::Manifest
&& super::protocol::is_version_refusal(&code)
&& let Some(retry) = retry_manifest_in_older_version(book, ctx, &query, sender)
{
return Some(retry);
}
info!(community = %sender, %code, kind = ?query.kind, "community refused a question of ours");
let (changed, notice) = match query.kind {
QueryKind::VetterProfile => (
book.on_profile_refused(sender, query.persona, &code, ctx.now),
Some(Notice::ProfileRefused {
community: sender.to_string(),
code: code.clone(),
}),
),
QueryKind::VetterResend => (
false,
Some(Notice::ResendRefused {
community: sender.to_string(),
code: code.clone(),
}),
),
QueryKind::PcsRoot
| QueryKind::PcsTokens
| QueryKind::PcsEventMode
| QueryKind::PcsChallenge => (
false,
Some(Notice::HiddenVettingRefused {
community: sender.to_string(),
what: query.kind.describe(),
code: code.clone(),
}),
),
QueryKind::Manifest | QueryKind::VetterList => (false, None),
};
return Some(Handled {
changed,
notice,
answer: Some(CommunityAnswer::Refused {
query: query.document_id,
community: sender.to_string(),
kind: query.kind,
code,
message: detail,
}),
..Handled::default()
});
}
let statement_id = book
.issued
.iter()
.find(|s| {
s.community == sender
&& s.withdrawal
.as_ref()
.is_some_and(|w| w.document_id == thread)
})?
.id
.clone();
warn!(community = %sender, %code, "vetting statement withdrawal refused");
Some(Handled {
notice: Some(Notice::WithdrawalRefused { statement_id, code }),
..Handled::default()
})
}
fn withdrawal_recorded(book: &mut VettingBook, message: &Message, sender: &str) -> Handled {
let Some((Some(thread), body)) = community_reply::<revoke_statement::v0_1::Response>(message)
else {
return Handled::default();
};
match book.on_withdrawal_recorded(sender, &thread, body.recorded_at) {
Some(issued) => Handled {
changed: true,
notice: Some(Notice::WithdrawalRecorded {
statement_id: issued.id.clone(),
}),
..Handled::default()
},
None => Handled::default(),
}
}
fn manifest(book: &mut VettingBook, ctx: &Context<'_>, message: &Message, sender: &str) -> Handled {
let thread = community_thread(message);
let solicited = book
.queries
.iter()
.any(|q| q.community == sender && q.kind == super::queries::QueryKind::Manifest)
|| book.applications.iter().any(|a| a.community == sender)
|| !ctx.account.memberships_for(sender).is_empty();
if !solicited {
debug!(typ = %message.typ, "manifest from a community we have no business with — ignored");
return Handled::default();
}
let protocol =
super::protocol::JoinProtocol::from_manifest_response(&message.typ).unwrap_or_default();
let read = community_payload::<Value>(message)
.and_then(|v| super::protocol::read_manifest(protocol, &v).map_err(|e| e.to_string()));
let (body, meta) = match read {
Ok(read) => read,
Err(detail) => {
warn!(typ = %message.typ, error = %detail, "malformed community reply");
let answer = book
.take_manifest_queries(sender, thread.as_deref())
.into_iter()
.next()
.map(|q| CommunityAnswer::Unreadable {
query: q.document_id,
community: sender.to_string(),
kind: QueryKind::Manifest,
detail,
});
return Handled {
answer,
..Handled::default()
};
}
};
let raw = message.body.get("payload").cloned().unwrap_or(Value::Null);
let mut handled = Handled {
changed: book.learn_manifest_in(sender, &body, Some(protocol), &meta, ctx.now),
..Handled::default()
};
let published = raw
.get("criteria")
.and_then(Value::as_array)
.into_iter()
.flatten()
.find_map(|c| match super::hidden::read_mode(c) {
Ok(super::hidden::Mode::Hidden(p)) => Some(*p),
_ => None,
});
match published {
Some(params) => {
if book.hidden_published.get(sender) != Some(¶ms) {
book.hidden_published.insert(sender.to_string(), params);
handled.changed = true;
}
}
None => {
if book.hidden_published.remove(sender).is_some() {
handled.changed = true;
}
}
}
for application in book
.applications
.iter_mut()
.filter(|a| a.community == sender)
{
match application.adopt_manifest(&body, &raw) {
Ok(true) => {
handled.changed = true;
handled.notice = Some(Notice::RequirementsUpdated {
application_id: application.id.clone(),
community: sender.to_string(),
});
}
Ok(false) => {}
Err(e @ super::applicant::ApplicantError::Hidden(_)) => {
handled.notice = Some(Notice::RequirementsUnsupported {
application_id: application.id.clone(),
community: sender.to_string(),
detail: e.to_string(),
});
warn!(community = %sender, error = %e, "community requires an unsupported extension");
}
Err(e) => warn!(community = %sender, error = %e, "community manifest not adopted"),
}
}
if !book
.take_manifest_queries(sender, thread.as_deref())
.is_empty()
{
handled.answer = Some(CommunityAnswer::Manifest {
community: sender.to_string(),
});
}
handled
}
fn vetter_list(book: &mut VettingBook, message: &Message, sender: &str) -> Handled {
let Some((query, page)) =
answer_to::<vetters::list::v0_1::Response>(book, message, sender, QueryKind::VetterList)
else {
return Handled::default();
};
Handled {
answer: Some(match page {
Ok(page) => CommunityAnswer::Vetters {
query: query.document_id,
community: sender.to_string(),
page,
},
Err(unreadable) => unreadable,
}),
..Handled::default()
}
}
fn profile_stored(book: &mut VettingBook, message: &Message, sender: &str) -> Handled {
let Some((query, body)) = answer_to::<vetters::profile::v0_1::Response>(
book,
message,
sender,
QueryKind::VetterProfile,
) else {
return Handled::default();
};
match body {
Ok(body) => Handled {
changed: book.on_profile_stored(sender, query.persona, &body),
notice: Some(Notice::ProfilePublished {
community: sender.to_string(),
listed: body.listed,
}),
answer: Some(CommunityAnswer::ProfileStored {
community: sender.to_string(),
listed: body.listed,
updated_at: body.updated_at,
}),
..Handled::default()
},
Err(unreadable) => Handled {
answer: Some(unreadable),
..Handled::default()
},
}
}
fn resent(book: &mut VettingBook, message: &Message, sender: &str) -> Handled {
let Some((_, body)) = answer_to::<vetters::resend::v0_1::Response>(
book,
message,
sender,
QueryKind::VetterResend,
) else {
return Handled::default();
};
match body {
Ok(body) => Handled {
notice: Some(Notice::GrantResent {
community: sender.to_string(),
valid_until: body.valid_until,
}),
answer: Some(CommunityAnswer::Resent {
community: sender.to_string(),
valid_until: body.valid_until,
}),
..Handled::default()
},
Err(unreadable) => Handled {
answer: Some(unreadable),
..Handled::default()
},
}
}