mod.rs (22867B)
1 use std::cmp::Ordering; 2 use std::collections::{BTreeMap, BTreeSet}; 3 use std::sync::Arc; 4 5 use url::Url; 6 use uuid::Uuid; 7 8 use enostr::{ClientMessage, FilledKeypair, FullKeypair, Keypair, RelayPool}; 9 use nostrdb::{Filter, Ndb, Note, NoteKey, Subscription, Transaction}; 10 11 use crate::{ 12 column::Columns, 13 imgcache::ImageCache, 14 login_manager::AcquireKeyState, 15 muted::Muted, 16 route::{Route, Router}, 17 storage::{KeyStorageResponse, KeyStorageType}, 18 ui::{ 19 account_login_view::{AccountLoginResponse, AccountLoginView}, 20 accounts::{AccountsView, AccountsViewResponse}, 21 }, 22 unknowns::SingleUnkIdAction, 23 unknowns::UnknownIds, 24 user_account::UserAccount, 25 }; 26 use tracing::{debug, error, info}; 27 28 mod route; 29 30 pub use route::{AccountsRoute, AccountsRouteResponse}; 31 32 pub struct AccountRelayData { 33 filter: Filter, 34 subid: String, 35 sub: Option<Subscription>, 36 local: BTreeSet<String>, // used locally but not advertised 37 advertised: BTreeSet<String>, // advertised via NIP-65 38 } 39 40 impl AccountRelayData { 41 pub fn new(ndb: &Ndb, pool: &mut RelayPool, pubkey: &[u8; 32]) -> Self { 42 // Construct a filter for the user's NIP-65 relay list 43 let filter = Filter::new() 44 .authors([pubkey]) 45 .kinds([10002]) 46 .limit(1) 47 .build(); 48 49 // Local ndb subscription 50 let ndbsub = ndb 51 .subscribe(&[filter.clone()]) 52 .expect("ndb relay list subscription"); 53 54 // Query the ndb immediately to see if the user list is already there 55 let txn = Transaction::new(ndb).expect("transaction"); 56 let lim = filter.limit().unwrap_or(crate::filter::default_limit()) as i32; 57 let nks = ndb 58 .query(&txn, &[filter.clone()], lim) 59 .expect("query user relays results") 60 .iter() 61 .map(|qr| qr.note_key) 62 .collect::<Vec<NoteKey>>(); 63 let relays = Self::harvest_nip65_relays(ndb, &txn, &nks); 64 debug!( 65 "pubkey {}: initial relays {:?}", 66 hex::encode(pubkey), 67 relays 68 ); 69 70 // Id for future remote relay subscriptions 71 let subid = Uuid::new_v4().to_string(); 72 73 // Add remote subscription to existing relays 74 pool.subscribe(subid.clone(), vec![filter.clone()]); 75 76 AccountRelayData { 77 filter, 78 subid, 79 sub: Some(ndbsub), 80 local: BTreeSet::new(), 81 advertised: relays.into_iter().collect(), 82 } 83 } 84 85 // standardize the format (ie, trailing slashes) to avoid dups 86 pub fn canonicalize_url(url: &str) -> String { 87 match Url::parse(url) { 88 Ok(parsed_url) => parsed_url.to_string(), 89 Err(_) => url.to_owned(), // If parsing fails, return the original URL. 90 } 91 } 92 93 fn harvest_nip65_relays(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Vec<String> { 94 let mut relays = Vec::new(); 95 for nk in nks.iter() { 96 if let Ok(note) = ndb.get_note_by_key(txn, *nk) { 97 for tag in note.tags() { 98 match tag.get(0).and_then(|t| t.variant().str()) { 99 Some("r") => { 100 if let Some(url) = tag.get(1).and_then(|f| f.variant().str()) { 101 relays.push(Self::canonicalize_url(url)); 102 } 103 } 104 Some("alt") => { 105 // ignore for now 106 } 107 Some(x) => { 108 error!("harvest_nip65_relays: unexpected tag type: {}", x); 109 } 110 None => { 111 error!("harvest_nip65_relays: invalid tag"); 112 } 113 } 114 } 115 } 116 } 117 relays 118 } 119 } 120 121 pub struct AccountMutedData { 122 filter: Filter, 123 subid: String, 124 sub: Option<Subscription>, 125 muted: Arc<Muted>, 126 } 127 128 impl AccountMutedData { 129 pub fn new(ndb: &Ndb, pool: &mut RelayPool, pubkey: &[u8; 32]) -> Self { 130 // Construct a filter for the user's NIP-51 muted list 131 let filter = Filter::new() 132 .authors([pubkey]) 133 .kinds([10000]) 134 .limit(1) 135 .build(); 136 137 // Local ndb subscription 138 let ndbsub = ndb 139 .subscribe(&[filter.clone()]) 140 .expect("ndb muted subscription"); 141 142 // Query the ndb immediately to see if the user's muted list is already there 143 let txn = Transaction::new(ndb).expect("transaction"); 144 let lim = filter.limit().unwrap_or(crate::filter::default_limit()) as i32; 145 let nks = ndb 146 .query(&txn, &[filter.clone()], lim) 147 .expect("query user muted results") 148 .iter() 149 .map(|qr| qr.note_key) 150 .collect::<Vec<NoteKey>>(); 151 let muted = Self::harvest_nip51_muted(ndb, &txn, &nks); 152 debug!("pubkey {}: initial muted {:?}", hex::encode(pubkey), muted); 153 154 // Id for future remote relay subscriptions 155 let subid = Uuid::new_v4().to_string(); 156 157 // Add remote subscription to existing relays 158 pool.subscribe(subid.clone(), vec![filter.clone()]); 159 160 AccountMutedData { 161 filter, 162 subid, 163 sub: Some(ndbsub), 164 muted: Arc::new(muted), 165 } 166 } 167 168 fn harvest_nip51_muted(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Muted { 169 let mut muted = Muted::default(); 170 for nk in nks.iter() { 171 if let Ok(note) = ndb.get_note_by_key(txn, *nk) { 172 for tag in note.tags() { 173 match tag.get(0).and_then(|t| t.variant().str()) { 174 Some("p") => { 175 if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) { 176 muted.pubkeys.insert(*id); 177 } 178 } 179 Some("t") => { 180 if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) { 181 muted.hashtags.insert(str.to_string()); 182 } 183 } 184 Some("word") => { 185 if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) { 186 muted.words.insert(str.to_string()); 187 } 188 } 189 Some("e") => { 190 if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) { 191 muted.threads.insert(*id); 192 } 193 } 194 Some("alt") => { 195 // maybe we can ignore these? 196 } 197 Some(x) => error!("query_nip51_muted: unexpected tag: {}", x), 198 None => error!( 199 "query_nip51_muted: bad tag value: {:?}", 200 tag.get_unchecked(0).variant() 201 ), 202 } 203 } 204 } 205 } 206 muted 207 } 208 } 209 210 pub struct AccountData { 211 relay: AccountRelayData, 212 muted: AccountMutedData, 213 } 214 215 /// The interface for managing the user's accounts. 216 /// Represents all user-facing operations related to account management. 217 pub struct Accounts { 218 currently_selected_account: Option<usize>, 219 accounts: Vec<UserAccount>, 220 key_store: KeyStorageType, 221 account_data: BTreeMap<[u8; 32], AccountData>, 222 forced_relays: BTreeSet<String>, 223 bootstrap_relays: BTreeSet<String>, 224 needs_relay_config: bool, 225 } 226 227 /// Render account management views from a route 228 #[allow(clippy::too_many_arguments)] 229 pub fn render_accounts_route( 230 ui: &mut egui::Ui, 231 ndb: &Ndb, 232 col: usize, 233 columns: &mut Columns, 234 img_cache: &mut ImageCache, 235 accounts: &mut Accounts, 236 login_state: &mut AcquireKeyState, 237 route: AccountsRoute, 238 ) -> SingleUnkIdAction { 239 let router = columns.column_mut(col).router_mut(); 240 let resp = match route { 241 AccountsRoute::Accounts => AccountsView::new(ndb, accounts, img_cache) 242 .ui(ui) 243 .inner 244 .map(AccountsRouteResponse::Accounts), 245 246 AccountsRoute::AddAccount => AccountLoginView::new(login_state) 247 .ui(ui) 248 .inner 249 .map(AccountsRouteResponse::AddAccount), 250 }; 251 252 if let Some(resp) = resp { 253 match resp { 254 AccountsRouteResponse::Accounts(response) => { 255 process_accounts_view_response(accounts, response, router); 256 SingleUnkIdAction::no_action() 257 } 258 AccountsRouteResponse::AddAccount(response) => { 259 let action = process_login_view_response(accounts, response); 260 *login_state = Default::default(); 261 router.go_back(); 262 action 263 } 264 } 265 } else { 266 SingleUnkIdAction::no_action() 267 } 268 } 269 270 pub fn process_accounts_view_response( 271 manager: &mut Accounts, 272 response: AccountsViewResponse, 273 router: &mut Router<Route>, 274 ) { 275 match response { 276 AccountsViewResponse::RemoveAccount(index) => { 277 manager.remove_account(index); 278 } 279 AccountsViewResponse::SelectAccount(index) => { 280 manager.select_account(index); 281 } 282 AccountsViewResponse::RouteToLogin => { 283 router.route_to(Route::add_account()); 284 } 285 } 286 } 287 288 impl Accounts { 289 pub fn new(key_store: KeyStorageType, forced_relays: Vec<String>) -> Self { 290 let accounts = if let KeyStorageResponse::ReceivedResult(res) = key_store.get_keys() { 291 res.unwrap_or_default() 292 } else { 293 Vec::new() 294 }; 295 296 let currently_selected_account = get_selected_index(&accounts, &key_store); 297 let account_data = BTreeMap::new(); 298 let forced_relays: BTreeSet<String> = forced_relays 299 .into_iter() 300 .map(|u| AccountRelayData::canonicalize_url(&u)) 301 .collect(); 302 let bootstrap_relays = [ 303 "wss://relay.damus.io", 304 // "wss://pyramid.fiatjaf.com", // Uncomment if needed 305 "wss://nos.lol", 306 "wss://nostr.wine", 307 "wss://purplepag.es", 308 ] 309 .iter() 310 .map(|&url| url.to_string()) 311 .map(|u| AccountRelayData::canonicalize_url(&u)) 312 .collect(); 313 314 Accounts { 315 currently_selected_account, 316 accounts, 317 key_store, 318 account_data, 319 forced_relays, 320 bootstrap_relays, 321 needs_relay_config: true, 322 } 323 } 324 325 pub fn get_accounts(&self) -> &Vec<UserAccount> { 326 &self.accounts 327 } 328 329 pub fn get_account(&self, ind: usize) -> Option<&UserAccount> { 330 self.accounts.get(ind) 331 } 332 333 pub fn find_account(&self, pk: &[u8; 32]) -> Option<&UserAccount> { 334 self.accounts.iter().find(|acc| acc.pubkey.bytes() == pk) 335 } 336 337 pub fn remove_account(&mut self, index: usize) { 338 if let Some(account) = self.accounts.get(index) { 339 let _ = self.key_store.remove_key(account); 340 self.accounts.remove(index); 341 342 if let Some(selected_index) = self.currently_selected_account { 343 match selected_index.cmp(&index) { 344 Ordering::Greater => { 345 self.select_account(selected_index - 1); 346 } 347 Ordering::Equal => { 348 if self.accounts.is_empty() { 349 // If no accounts remain, clear the selection 350 self.clear_selected_account(); 351 } else if index >= self.accounts.len() { 352 // If the removed account was the last one, select the new last account 353 self.select_account(self.accounts.len() - 1); 354 } else { 355 // Otherwise, select the account at the same position 356 self.select_account(index); 357 } 358 } 359 Ordering::Less => {} 360 } 361 } 362 } 363 } 364 365 fn contains_account(&self, pubkey: &[u8; 32]) -> Option<ContainsAccount> { 366 for (index, account) in self.accounts.iter().enumerate() { 367 let has_pubkey = account.pubkey.bytes() == pubkey; 368 let has_nsec = account.secret_key.is_some(); 369 if has_pubkey { 370 return Some(ContainsAccount { has_nsec, index }); 371 } 372 } 373 374 None 375 } 376 377 #[must_use = "UnknownIdAction's must be handled. Use .process_unknown_id_action()"] 378 pub fn add_account(&mut self, account: Keypair) -> LoginAction { 379 let pubkey = account.pubkey; 380 let switch_to_index = if let Some(contains_acc) = self.contains_account(pubkey.bytes()) { 381 if account.secret_key.is_some() && !contains_acc.has_nsec { 382 info!( 383 "user provided nsec, but we already have npub {}. Upgrading to nsec", 384 pubkey 385 ); 386 let _ = self.key_store.add_key(&account); 387 388 self.accounts[contains_acc.index] = account; 389 } else { 390 info!("already have account, not adding {}", pubkey); 391 } 392 contains_acc.index 393 } else { 394 info!("adding new account {}", pubkey); 395 let _ = self.key_store.add_key(&account); 396 self.accounts.push(account); 397 self.accounts.len() - 1 398 }; 399 400 LoginAction { 401 unk: SingleUnkIdAction::pubkey(pubkey), 402 switch_to_index, 403 } 404 } 405 406 pub fn num_accounts(&self) -> usize { 407 self.accounts.len() 408 } 409 410 pub fn get_selected_account_index(&self) -> Option<usize> { 411 self.currently_selected_account 412 } 413 414 pub fn selected_or_first_nsec(&self) -> Option<FilledKeypair<'_>> { 415 self.get_selected_account() 416 .and_then(|kp| kp.to_full()) 417 .or_else(|| self.accounts.iter().find_map(|a| a.to_full())) 418 } 419 420 pub fn get_selected_account(&self) -> Option<&UserAccount> { 421 if let Some(account_index) = self.currently_selected_account { 422 if let Some(account) = self.get_account(account_index) { 423 Some(account) 424 } else { 425 None 426 } 427 } else { 428 None 429 } 430 } 431 432 pub fn select_account(&mut self, index: usize) { 433 if let Some(account) = self.accounts.get(index) { 434 self.currently_selected_account = Some(index); 435 self.key_store.select_key(Some(account.pubkey)); 436 } 437 } 438 439 pub fn clear_selected_account(&mut self) { 440 self.currently_selected_account = None; 441 self.key_store.select_key(None); 442 } 443 444 pub fn mutefun(&self) -> Box<dyn Fn(&Note) -> bool> { 445 if let Some(index) = self.currently_selected_account { 446 if let Some(account) = self.accounts.get(index) { 447 let pubkey = account.pubkey.bytes(); 448 if let Some(account_data) = self.account_data.get(pubkey) { 449 let muted = Arc::clone(&account_data.muted.muted); 450 return Box::new(move |note: &Note| muted.is_muted(note)); 451 } 452 } 453 } 454 Box::new(|_: &Note| false) 455 } 456 457 pub fn send_initial_filters(&mut self, pool: &mut RelayPool, relay_url: &str) { 458 for data in self.account_data.values() { 459 pool.send_to( 460 &ClientMessage::req(data.relay.subid.clone(), vec![data.relay.filter.clone()]), 461 relay_url, 462 ); 463 pool.send_to( 464 &ClientMessage::req(data.muted.subid.clone(), vec![data.muted.filter.clone()]), 465 relay_url, 466 ); 467 } 468 } 469 470 // Returns added and removed accounts 471 fn delta_accounts(&self) -> (Vec<[u8; 32]>, Vec<[u8; 32]>) { 472 let mut added = Vec::new(); 473 for pubkey in self.accounts.iter().map(|a| a.pubkey.bytes()) { 474 if !self.account_data.contains_key(pubkey) { 475 added.push(*pubkey); 476 } 477 } 478 let mut removed = Vec::new(); 479 for pubkey in self.account_data.keys() { 480 if self.contains_account(pubkey).is_none() { 481 removed.push(*pubkey); 482 } 483 } 484 (added, removed) 485 } 486 487 fn handle_added_account(&mut self, ndb: &Ndb, pool: &mut RelayPool, pubkey: &[u8; 32]) { 488 debug!("handle_added_account {}", hex::encode(pubkey)); 489 490 // Create the user account data 491 let new_account_data = AccountData { 492 relay: AccountRelayData::new(ndb, pool, pubkey), 493 muted: AccountMutedData::new(ndb, pool, pubkey), 494 }; 495 self.account_data.insert(*pubkey, new_account_data); 496 } 497 498 fn handle_removed_account(&mut self, pubkey: &[u8; 32]) { 499 debug!("handle_removed_account {}", hex::encode(pubkey)); 500 // FIXME - we need to unsubscribe here 501 self.account_data.remove(pubkey); 502 } 503 504 fn poll_for_updates(&mut self, ndb: &Ndb) -> bool { 505 let mut changed = false; 506 for (pubkey, data) in &mut self.account_data { 507 if let Some(sub) = data.relay.sub { 508 let nks = ndb.poll_for_notes(sub, 1); 509 if !nks.is_empty() { 510 let txn = Transaction::new(ndb).expect("txn"); 511 let relays = AccountRelayData::harvest_nip65_relays(ndb, &txn, &nks); 512 debug!( 513 "pubkey {}: updated relays {:?}", 514 hex::encode(pubkey), 515 relays 516 ); 517 data.relay.advertised = relays.into_iter().collect(); 518 changed = true; 519 } 520 } 521 if let Some(sub) = data.muted.sub { 522 let nks = ndb.poll_for_notes(sub, 1); 523 if !nks.is_empty() { 524 let txn = Transaction::new(ndb).expect("txn"); 525 let muted = AccountMutedData::harvest_nip51_muted(ndb, &txn, &nks); 526 debug!("pubkey {}: updated muted {:?}", hex::encode(pubkey), muted); 527 data.muted.muted = Arc::new(muted); 528 changed = true; 529 } 530 } 531 } 532 changed 533 } 534 535 fn update_relay_configuration( 536 &mut self, 537 pool: &mut RelayPool, 538 wakeup: impl Fn() + Send + Sync + Clone + 'static, 539 ) { 540 // If forced relays are set use them only 541 let mut desired_relays = self.forced_relays.clone(); 542 543 // Compose the desired relay lists from the accounts 544 if desired_relays.is_empty() { 545 for data in self.account_data.values() { 546 desired_relays.extend(data.relay.local.iter().cloned()); 547 desired_relays.extend(data.relay.advertised.iter().cloned()); 548 } 549 } 550 551 // If no relays are specified at this point use the bootstrap list 552 if desired_relays.is_empty() { 553 desired_relays = self.bootstrap_relays.clone(); 554 } 555 556 debug!("current relays: {:?}", pool.urls()); 557 debug!("desired relays: {:?}", desired_relays); 558 559 let add: BTreeSet<String> = desired_relays.difference(&pool.urls()).cloned().collect(); 560 let sub: BTreeSet<String> = pool.urls().difference(&desired_relays).cloned().collect(); 561 if !add.is_empty() { 562 debug!("configuring added relays: {:?}", add); 563 let _ = pool.add_urls(add, wakeup); 564 } 565 if !sub.is_empty() { 566 debug!("removing unwanted relays: {:?}", sub); 567 pool.remove_urls(&sub); 568 } 569 570 debug!("current relays: {:?}", pool.urls()); 571 } 572 573 pub fn update(&mut self, ndb: &Ndb, pool: &mut RelayPool, ctx: &egui::Context) { 574 // IMPORTANT - This function is called in the UI update loop, 575 // make sure it is fast when idle 576 577 // On the initial update the relays need config even if nothing changes below 578 let mut relays_changed = self.needs_relay_config; 579 580 let ctx2 = ctx.clone(); 581 let wakeup = move || { 582 ctx2.request_repaint(); 583 }; 584 585 // Were any accounts added or removed? 586 let (added, removed) = self.delta_accounts(); 587 for pk in added { 588 self.handle_added_account(ndb, pool, &pk); 589 relays_changed = true; 590 } 591 for pk in removed { 592 self.handle_removed_account(&pk); 593 relays_changed = true; 594 } 595 596 // Did any accounts receive updates (ie NIP-65 relay lists) 597 relays_changed = self.poll_for_updates(ndb) || relays_changed; 598 599 // If needed, update the relay configuration 600 if relays_changed { 601 self.update_relay_configuration(pool, wakeup); 602 self.needs_relay_config = false; 603 } 604 } 605 } 606 607 fn get_selected_index(accounts: &[UserAccount], keystore: &KeyStorageType) -> Option<usize> { 608 match keystore.get_selected_key() { 609 KeyStorageResponse::ReceivedResult(Ok(Some(pubkey))) => { 610 return accounts.iter().position(|account| account.pubkey == pubkey); 611 } 612 613 KeyStorageResponse::ReceivedResult(Err(e)) => error!("Error getting selected key: {}", e), 614 KeyStorageResponse::Waiting | KeyStorageResponse::ReceivedResult(Ok(None)) => {} 615 }; 616 617 None 618 } 619 620 pub fn process_login_view_response( 621 manager: &mut Accounts, 622 response: AccountLoginResponse, 623 ) -> SingleUnkIdAction { 624 let login_action = match response { 625 AccountLoginResponse::CreateNew => { 626 manager.add_account(FullKeypair::generate().to_keypair()) 627 } 628 AccountLoginResponse::LoginWith(keypair) => manager.add_account(keypair), 629 }; 630 manager.select_account(login_action.switch_to_index); 631 login_action.unk 632 } 633 634 #[must_use = "You must call process_login_action on this to handle unknown ids"] 635 pub struct LoginAction { 636 unk: SingleUnkIdAction, 637 pub switch_to_index: usize, 638 } 639 640 impl LoginAction { 641 // Simple wrapper around processing the unknown action to expose too 642 // much internal logic. This allows us to have a must_use on our 643 // LoginAction type, otherwise the SingleUnkIdAction's must_use will 644 // be lost when returned in the login action 645 pub fn process_action(&mut self, ids: &mut UnknownIds, ndb: &Ndb, txn: &Transaction) { 646 self.unk.process_action(ids, ndb, txn); 647 } 648 } 649 650 #[derive(Default)] 651 struct ContainsAccount { 652 pub has_nsec: bool, 653 pub index: usize, 654 }