Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Fixed

- **Choosing to be vetted no longer abandons the join.** Starting an application
from the join flow left it for the Vetting page, and the community then had to
be found and entered a second time when the statements were in — which is why
vetting read as something to go and do *before* joining rather than a way of
joining. `j` on an application now takes up that join: the community is the one
the application names, so the flow opens where the DID entry page would have
led, with the same routes and the same discovery. The application is the parked
join, so it survives restarts — which matters, because gathering statements
takes days.

### Changed

- **Joining a community that vets now offers every way in at once.** The join
Expand Down
6 changes: 6 additions & 0 deletions openvtc/src/state_handler/actions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,12 @@ pub enum Action {
/// both the degraded loop (State-A first join) and the runtime select loop.
StartJoin,

/// Open the join flow for a community already chosen elsewhere — `j` on a
/// vetting application, which is the join that application was made for.
/// Skips the DID entry page: the community is the one the application
/// names, not one to be found and retyped.
StartJoinFor(String),

/// Submit the entered community VTC DID. With existing personas this opens
/// the identity-choice page (R-B-3); with none it kicks off the mint+join
/// sequence directly.
Expand Down
251 changes: 159 additions & 92 deletions openvtc/src/state_handler/join_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ pub(crate) enum JoinEntry {
/// loop reads inbound messages: only then can the flow ask a community for
/// its requirements and hand the wait for the answer to that loop.
Fresh { hears_replies: bool },
/// For a community already chosen elsewhere — `j` on a vetting application,
/// which is the join that application was made for. The DID entry page is
/// skipped: an application names its community, and making someone find and
/// retype that DID is what turned "go and get vetted" into a dead end.
ForCommunity {
vtc_did: String,
hears_replies: bool,
},
/// Back from waiting for a community's requirements.
Resume {
vtc_did: String,
Expand Down Expand Up @@ -527,7 +535,8 @@ fn apply_for_vetting(
};
if let Some(app) = &known.application {
let message = format!(
"Your application to {name}, as {}. Next: {}",
"Your application to {name}, as {}. Next: {}. Press j here when you are ready to \
join — the join picks up where you left it.",
app.persona_label, app.next_step
);
vetting_actions::focus_application(state, config, &app.id, message);
Expand Down Expand Up @@ -568,7 +577,8 @@ fn apply_for_vetting(
config,
&id,
format!(
"Application to {name} started as {}. Next: {next}",
"Application to {name} started as {}. Next: {next}. The join to {name} is not lost — \
press j here to take it up again, without finding that DID a second time.",
persona.label
),
);
Expand Down Expand Up @@ -628,35 +638,61 @@ impl StateHandler {
state.join.has_invitation = state.invitation_credential.is_some();
state.active_page = ActivePage::Join;
let hears_replies = match &entry {
JoinEntry::Fresh { hears_replies } => *hears_replies,
JoinEntry::Fresh { hears_replies } | JoinEntry::ForCommunity { hears_replies, .. } => {
*hears_replies
}
JoinEntry::Resume { .. } => true,
};
if let JoinEntry::Resume { vtc_did, outcome } = entry {
// `reset` above cleared what the asking pass collected, and the
// routes cannot be drawn without it.
ensure_invitations(state, admin_vta, &vtc_did).await;
match outcome {
RequirementsOutcome::Unanswered(reason) => {
show_unknown(state, config, &vtc_did, reason, true);
match entry {
JoinEntry::Fresh { .. } => {}
// The community is already chosen, so the flow opens where the DID
// entry page would have led.
JoinEntry::ForCommunity { vtc_did, .. } => {
if let Some(exit) = self
.enter_community(
vtc_did,
hears_replies,
interrupt_rx,
state,
tdk,
config,
admin_vta,
profile,
messaging,
)
.await
{
let _ = self.state_tx.send(state.clone());
return Ok(exit);
}
outcome => {
let shown = matches!(outcome, RequirementsOutcome::Learned)
&& show_vetting(state, config, &vtc_did);
if !shown
&& let Some(interrupted) = self
.continue_join(
vtc_did,
interrupt_rx,
state,
tdk,
config,
admin_vta,
profile,
messaging,
)
.await
{
return Ok(JoinExit::Exit(interrupted));
}
JoinEntry::Resume { vtc_did, outcome } => {
// `reset` above cleared what the asking pass collected, and the
// routes cannot be drawn without it.
ensure_invitations(state, admin_vta, &vtc_did).await;
match outcome {
RequirementsOutcome::Unanswered(reason) => {
show_unknown(state, config, &vtc_did, reason, true);
}
outcome => {
let shown = matches!(outcome, RequirementsOutcome::Learned)
&& show_vetting(state, config, &vtc_did);
if !shown
&& let Some(interrupted) = self
.continue_join(
vtc_did,
interrupt_rx,
state,
tdk,
config,
admin_vta,
profile,
messaging,
)
.await
{
return Ok(JoinExit::Exit(interrupted));
}
}
}
}
Expand Down Expand Up @@ -766,70 +802,22 @@ impl StateHandler {
} else {
vtc_did
};
// The invitations before the routes: whether presenting
// one is a way in to this community is a fact about
// this account, and the vetting page cannot say what
// the options are without it.
ensure_invitations(state, admin_vta, &vtc_did).await;
// Peer identity vetting: a community that vets says what
// it requires before anything about the applicant is sent
// (vetting-process.md §6.1). What the book already knows
// decides at once. Otherwise the community is asked, and
// the wait happens in the caller's loop — the one that
// hears the answer — which enters this flow again.
let knowledge = match config.private.vetting.knowledge(&vtc_did) {
Knowledge::Vetting(_) => Some(true),
Knowledge::NoVetting => Some(false),
Knowledge::Unknown => None,
};
match knowledge {
Some(true) => {
show_vetting(state, config, &vtc_did);
}
None if hears_replies => {
if let Ok(awaiting) =
ask_requirements(state, config, tdk, messaging, &vtc_did)
.await
{
let _ = self.state_tx.send(state.clone());
return Ok(JoinExit::AwaitRequirements(awaiting));
}
}
None => {
// Nothing is known and this loop cannot hear an
// answer, so the community is not asked. Say so
// rather than sending an open request on the
// quiet: a community that vets would refer that
// request to its moderators, and the applicant
// would never learn there was a way in they
// could have taken.
show_unknown(
state,
config,
&vtc_did,
"OpenVTC cannot hear its answer until you belong to a \
community, so it was not asked — this is your first join"
.to_string(),
false,
);
}
Some(false) => {
if let Some(interrupted) = self
.continue_join(
vtc_did,
interrupt_rx,
state,
tdk,
config,
admin_vta,
profile,
messaging,
)
.await
{
return Ok(JoinExit::Exit(interrupted));
}
}
if let Some(exit) = self
.enter_community(
vtc_did,
hears_replies,
interrupt_rx,
state,
tdk,
config,
admin_vta,
profile,
messaging,
)
.await
{
let _ = self.state_tx.send(state.clone());
return Ok(exit);
}
}
Action::JoinIdentitySelect(i) => {
Expand Down Expand Up @@ -1180,6 +1168,85 @@ impl StateHandler {
}
}

/// Enter `vtc_did` into the flow: learn what it asks of the people who join,
/// and put up the page that answers it.
///
/// `Some` means leave the flow with that exit; `None` means a page is now up
/// and the loop carries on. Shared by the DID entry page and by a join
/// started for a community already chosen (a vetting application's), so both
/// get the same discovery rather than the entry page's alone.
#[allow(clippy::too_many_arguments)]
async fn enter_community(
&self,
vtc_did: String,
hears_replies: bool,
interrupt_rx: &mut broadcast::Receiver<Interrupted>,
state: &mut State,
tdk: &TDK,
config: &mut Config,
admin_vta: Option<&VtaClient>,
profile: &str,
messaging: Option<&Messaging>,
) -> Option<JoinExit> {
// The invitations before the routes: whether presenting one is a way in
// to this community is a fact about this account, and the vetting page
// cannot say what the options are without it.
ensure_invitations(state, admin_vta, &vtc_did).await;
// Peer identity vetting: a community that vets says what it requires
// before anything about the applicant is sent (vetting-process.md §6.1).
// What the book already knows decides at once. Otherwise the community
// is asked, and the wait happens in the caller's loop — the one that
// hears the answer — which enters this flow again.
let knowledge = match config.private.vetting.knowledge(&vtc_did) {
Knowledge::Vetting(_) => Some(true),
Knowledge::NoVetting => Some(false),
Knowledge::Unknown => None,
};
match knowledge {
Some(true) => {
show_vetting(state, config, &vtc_did);
None
}
None if hears_replies => {
match ask_requirements(state, config, tdk, messaging, &vtc_did).await {
Ok(awaiting) => Some(JoinExit::AwaitRequirements(awaiting)),
// The page now says why the question could not be sent.
Err(()) => None,
}
}
None => {
// Nothing is known and this loop cannot hear an answer, so the
// community is not asked. Say so rather than sending an open
// request on the quiet: a community that vets would refer that
// request to its moderators, and the applicant would never learn
// there was a way in they could have taken.
show_unknown(
state,
config,
&vtc_did,
"OpenVTC cannot hear its answer until you belong to a community, so it was \
not asked — this is your first join"
.to_string(),
false,
);
None
}
Some(false) => self
.continue_join(
vtc_did,
interrupt_rx,
state,
tdk,
config,
admin_vta,
profile,
messaging,
)
.await
.map(JoinExit::Exit),
}
}

/// Go on with a join from the vetting page, presenting the statements of an
/// application that already meets the requirements when there is one.
///
Expand Down
31 changes: 26 additions & 5 deletions openvtc/src/state_handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,15 @@ impl StateHandler {
// entered again.
join_entry = Some(join_flow::JoinEntry::Fresh { hears_replies: true });
},
// The same join, for a community already chosen: `j` on a
// vetting application resumes the join that application was
// made for, without going back for its DID.
Action::StartJoinFor(vtc_did) => {
join_entry = Some(join_flow::JoinEntry::ForCommunity {
vtc_did,
hears_replies: true,
});
},
// A join waiting on a community's requirements keeps its screen
// up while this loop hears the answer, so the two keys that
// page offers land here rather than in the join flow.
Expand Down Expand Up @@ -2258,7 +2267,22 @@ impl StateHandler {
}
}
}
Action::StartJoin => {
// `StartJoinFor` carries a community already chosen — `j` on a
// vetting application. Both enter the same flow; only the
// page it opens on differs, so they share this arm and its
// listener invariant below.
Action::StartJoin | Action::StartJoinFor(_) => {
let entry = match &action {
Action::StartJoinFor(vtc_did) => join_flow::JoinEntry::ForCommunity {
vtc_did: vtc_did.clone(),
// This loop has no inbound arm, so the flow
// cannot wait here for a community's answer.
hears_replies: false,
},
_ => join_flow::JoinEntry::Fresh {
hears_replies: false,
},
};
// Set when a join succeeded: the loop then breaks `Joined`
// so `run()` can start messaging.
let mut joined_a_community = false;
Expand All @@ -2279,10 +2303,7 @@ impl StateHandler {
// the early load-failure callers, which
// cannot reach a join anyway.
messaging,
// This loop has no inbound arm, so the flow
// cannot wait here for a community's
// requirements; it uses what the book knows.
join_flow::JoinEntry::Fresh { hears_replies: false },
entry,
)
.await
{
Expand Down
11 changes: 6 additions & 5 deletions openvtc/src/state_handler/runtime_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1189,11 +1189,12 @@ pub(crate) async fn handle_action(ctx: &mut ActionCtx<'_>, action: Action) -> Ha
| Action::TokenWriteKeys(..) => {}

// Owned by the loop: `Exit` and `UXError` signal the terminator and
// break with its outcome type, and `StartJoin` drives the join flow's
// own action loop using the receiver this one selects on. Listed so the
// match stays exhaustive — the guarantee is that a NEW variant cannot be
// added without a decision here, and these three already have one.
Action::Exit | Action::UXError(..) | Action::StartJoin => {
// break with its outcome type, and the two `StartJoin` verbs drive the
// join flow's own action loop using the receiver this one selects on.
// Listed so the match stays exhaustive — the guarantee is that a NEW
// variant cannot be added without a decision here, and these already
// have one.
Action::Exit | Action::UXError(..) | Action::StartJoin | Action::StartJoinFor(..) => {
debug_assert!(false, "handled by the loop, not the handler");
}
}
Expand Down
Loading
Loading