notedeck

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

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 }