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(¬e_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(¬e, 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(¬e, 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 }