notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

relay_ensure.rs (7705B)


      1 use enostr::Pubkey;
      2 use nostrdb::{Ndb, Subscription, Transaction};
      3 use notedeck::{
      4     Accounts, RelaySelection, RelayType, RemoteApi, ScopedSubEoseStatus, ScopedSubIdentity,
      5     SubConfig, SubKey, SubOwnerKey,
      6 };
      7 
      8 use crate::{
      9     list_ensure_owner_key, list_fetch_sub_key,
     10     nip17::{
     11         build_default_dm_relay_list_note, is_participant_dm_relay_list,
     12         participant_dm_relay_list_filter,
     13     },
     14 };
     15 
     16 /// Local view over the dependencies used by the DM relay-list ensure state machine.
     17 struct EnsureListCtx<'a, 'remote> {
     18     ndb: &'a mut Ndb,
     19     remote: &'a mut RemoteApi<'remote>,
     20     accounts: &'a Accounts,
     21     owner_key: SubOwnerKey,
     22 }
     23 
     24 /// Pure builder for the selected account's own DM relay-list ensure scoped-sub spec.
     25 fn dm_relay_list_spec(selected_account: &Pubkey) -> SubConfig {
     26     SubConfig {
     27         relays: RelaySelection::AccountsRead,
     28         filters: vec![participant_dm_relay_list_filter(selected_account)],
     29         use_transparent: false,
     30     }
     31 }
     32 
     33 #[profiling::function]
     34 pub(crate) fn ensure_selected_account_dm_list(
     35     ndb: &mut Ndb,
     36     remote: &mut RemoteApi<'_>,
     37     accounts: &Accounts,
     38     ensure_state: &mut DmListState,
     39 ) {
     40     let DmListState::Finding(state) = ensure_state else {
     41         return;
     42     };
     43 
     44     let selected_account = *accounts.selected_account_pubkey();
     45     let mut ctx = EnsureListCtx {
     46         ndb,
     47         remote,
     48         accounts,
     49         owner_key: list_ensure_owner_key(selected_account),
     50     };
     51 
     52     let list_found = match &state {
     53         ListFindingState::Idle => handle_idle(&mut ctx, state),
     54         ListFindingState::Waiting {
     55             remote_sub_key,
     56             local_sub,
     57         } => handle_waiting(&mut ctx, *remote_sub_key, *local_sub),
     58     };
     59 
     60     if list_found {
     61         set_list_found(&mut ctx, ensure_state);
     62     }
     63 }
     64 
     65 type ListFound = bool;
     66 
     67 /// Handles the `Idle` ensure phase for the selected account DM relay list.
     68 fn handle_idle(ctx: &mut EnsureListCtx<'_, '_>, ensure_state: &mut ListFindingState) -> ListFound {
     69     tracing::debug!("In idle state");
     70     let pk = ctx.accounts.selected_account_pubkey();
     71     let filter = participant_dm_relay_list_filter(pk);
     72     let local_sub = match ctx.ndb.subscribe(std::slice::from_ref(&filter)) {
     73         Ok(sub) => Some(sub),
     74         Err(err) => {
     75             tracing::error!("failed to subscribe to local dm relay list: {err}");
     76             None
     77         }
     78     };
     79 
     80     let remote_sub_key = list_fetch_sub_key(pk);
     81     let spec = dm_relay_list_spec(pk);
     82     let identity = ScopedSubIdentity::account(ctx.owner_key, remote_sub_key);
     83     let _ = ctx
     84         .remote
     85         .scoped_subs(ctx.accounts)
     86         .ensure_sub(identity, spec);
     87 
     88     tracing::info!("waiting for selected account dm relay list ensure");
     89     *ensure_state = ListFindingState::Waiting {
     90         remote_sub_key,
     91         local_sub,
     92     };
     93 
     94     false
     95 }
     96 
     97 /// Handles the `Waiting` ensure phase for the selected account DM relay list.
     98 fn handle_waiting(
     99     ctx: &mut EnsureListCtx<'_, '_>,
    100     remote_sub_key: SubKey,
    101     local_sub: Option<Subscription>,
    102 ) -> ListFound {
    103     let pk = ctx.accounts.selected_account_pubkey();
    104     if let Some(local_sub) = local_sub {
    105         if received_dm_relay_list_from_poll(ctx.ndb, local_sub, pk) {
    106             tracing::debug!(
    107                 "found selected account dm relay list on ndb poll; still waiting for remote EOSE"
    108             );
    109         }
    110     }
    111 
    112     if !all_eosed(ctx, remote_sub_key) {
    113         return false;
    114     }
    115 
    116     republish_existing_or_publish_default_list(ctx, pk)
    117 }
    118 
    119 fn publish_default_list(ctx: &mut EnsureListCtx<'_, '_>) -> ListFound {
    120     let Some(secret_key) = ctx.accounts.get_selected_account().key.secret_key.as_ref() else {
    121         return false;
    122     };
    123 
    124     let Some(note) = build_default_dm_relay_list_note(secret_key) else {
    125         return false;
    126     };
    127 
    128     let Ok(note_json) = note.json() else {
    129         return false;
    130     };
    131 
    132     if let Err(err) = ctx.ndb.process_client_event(&note_json) {
    133         tracing::error!("failed to ingest default dm relay list: {err}");
    134         return false;
    135     }
    136 
    137     let mut publisher = ctx.remote.publisher(ctx.accounts);
    138     publisher.publish_note(&note, RelayType::AccountsWrite);
    139 
    140     true
    141 }
    142 
    143 /// After all-EOSE, republish the latest local selected-account kind `10050` if present.
    144 ///
    145 /// Falls back to publishing a default kind `10050` when no local list exists.
    146 fn republish_existing_or_publish_default_list(
    147     ctx: &mut EnsureListCtx<'_, '_>,
    148     selected_account: &Pubkey,
    149 ) -> ListFound {
    150     let filter = participant_dm_relay_list_filter(selected_account);
    151     let txn = Transaction::new(ctx.ndb).expect("txn");
    152 
    153     let Ok(results) = ctx.ndb.query(&txn, std::slice::from_ref(&filter), 1) else {
    154         tracing::error!("failed to query selected account dm relay list during ensure");
    155         return false;
    156     };
    157 
    158     match results.first() {
    159         Some(result) => {
    160             tracing::info!("all relays eosed; republishing existing local dm relay list note");
    161             let mut publisher = ctx.remote.publisher(ctx.accounts);
    162             publisher.publish_note(&result.note, RelayType::AccountsWrite);
    163             true
    164         }
    165         None => {
    166             tracing::info!(
    167                 "all relays eosed; no local dm relay list note found, publishing default list"
    168             );
    169             publish_default_list(ctx)
    170         }
    171     }
    172 }
    173 
    174 /// Returns true when the selected-account DM relay-list ensure scoped sub reached all-EOSE.
    175 fn all_eosed(ctx: &mut EnsureListCtx<'_, '_>, remote_sub_key: SubKey) -> bool {
    176     let scoped_subs = ctx.remote.scoped_subs(ctx.accounts);
    177     let identity = ScopedSubIdentity::account(ctx.owner_key, remote_sub_key);
    178     matches!(
    179         scoped_subs.sub_eose_status(identity),
    180         ScopedSubEoseStatus::Live(live) if live.all_eosed
    181     )
    182 }
    183 
    184 /// Returns true when the ensure local subscription delivers a selected-account kind `10050` note.
    185 fn received_dm_relay_list_from_poll(
    186     ndb: &Ndb,
    187     local_sub: Subscription,
    188     selected_account: &Pubkey,
    189 ) -> bool {
    190     let note_keys = ndb.poll_for_notes(local_sub, 1);
    191 
    192     let Some(key) = note_keys.first() else {
    193         return false;
    194     };
    195 
    196     let txn = Transaction::new(ndb).expect("txn");
    197     let Ok(note) = ndb.get_note_by_key(&txn, *key) else {
    198         return false;
    199     };
    200 
    201     is_participant_dm_relay_list(&note, selected_account)
    202 }
    203 
    204 /// Moves DM relay-list ensure state to `Done` and tears down the local ensure subscription.
    205 ///
    206 /// The remote scoped sub is intentionally left declared so it stays alive for the account session
    207 /// and can be shared with later conversation prefetch activity.
    208 fn set_list_found(ctx: &mut EnsureListCtx<'_, '_>, list_state: &mut DmListState) {
    209     let prior = std::mem::replace(list_state, DmListState::Found);
    210     let DmListState::Finding(ListFindingState::Waiting {
    211         remote_sub_key: _,
    212         local_sub,
    213     }) = prior
    214     else {
    215         return;
    216     };
    217 
    218     let Some(local_sub) = local_sub else {
    219         return;
    220     };
    221 
    222     if let Err(err) = ctx.ndb.unsubscribe(local_sub) {
    223         tracing::error!("failed to unsubscribe dm relay-list local sub: {err}");
    224     }
    225 }
    226 
    227 /// Active (non-terminal) phases for selected-account DM relay-list ensure.
    228 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
    229 pub enum ListFindingState {
    230     #[default]
    231     Idle,
    232     Waiting {
    233         remote_sub_key: SubKey,
    234         local_sub: Option<Subscription>,
    235     },
    236 }
    237 
    238 /// Ensure-state for the selected account's kind `10050` DM relay list.
    239 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    240 pub enum DmListState {
    241     Finding(ListFindingState),
    242     Found,
    243 }
    244 
    245 impl Default for DmListState {
    246     fn default() -> Self {
    247         Self::Finding(ListFindingState::Idle)
    248     }
    249 }