notedeck

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

accounts.rs (16544B)


      1 use uuid::Uuid;
      2 
      3 use crate::account::cache::AccountCache;
      4 use crate::account::contacts::Contacts;
      5 use crate::account::mute::AccountMutedData;
      6 use crate::account::relay::{
      7     modify_advertised_relays, update_relay_configuration, AccountRelayData, RelayAction,
      8     RelayDefaults,
      9 };
     10 use crate::storage::AccountStorageWriter;
     11 use crate::user_account::UserAccountSerializable;
     12 use crate::{
     13     AccountStorage, MuteFun, SingleUnkIdAction, UnifiedSubscription, UnknownIds, UserAccount,
     14     ZapWallet,
     15 };
     16 use enostr::{ClientMessage, FilledKeypair, Keypair, Pubkey, RelayPool};
     17 use nostrdb::{Ndb, Note, Transaction};
     18 
     19 // TODO: remove this
     20 use std::sync::Arc;
     21 
     22 /// The interface for managing the user's accounts.
     23 /// Represents all user-facing operations related to account management.
     24 pub struct Accounts {
     25     pub cache: AccountCache,
     26     storage_writer: Option<AccountStorageWriter>,
     27     relay_defaults: RelayDefaults,
     28     subs: AccountSubs,
     29 }
     30 
     31 impl Accounts {
     32     #[allow(clippy::too_many_arguments)]
     33     pub fn new(
     34         key_store: Option<AccountStorage>,
     35         forced_relays: Vec<String>,
     36         fallback: Pubkey,
     37         ndb: &mut Ndb,
     38         txn: &Transaction,
     39         pool: &mut RelayPool,
     40         ctx: &egui::Context,
     41         unknown_ids: &mut UnknownIds,
     42     ) -> Self {
     43         let (mut cache, unknown_id) = AccountCache::new(UserAccount::new(
     44             Keypair::only_pubkey(fallback),
     45             AccountData::new(fallback.bytes()),
     46         ));
     47 
     48         unknown_id.process_action(unknown_ids, ndb, txn);
     49 
     50         let mut storage_writer = None;
     51         if let Some(keystore) = key_store {
     52             let (reader, writer) = keystore.rw();
     53             match reader.get_accounts() {
     54                 Ok(accounts) => {
     55                     for account in accounts {
     56                         add_account_from_storage(&mut cache, account).process_action(
     57                             unknown_ids,
     58                             ndb,
     59                             txn,
     60                         )
     61                     }
     62                 }
     63                 Err(e) => {
     64                     tracing::error!("could not get keys: {e}");
     65                 }
     66             }
     67             if let Some(selected) = reader.get_selected_key().ok().flatten() {
     68                 cache.select(selected);
     69             }
     70 
     71             storage_writer = Some(writer);
     72         };
     73 
     74         let relay_defaults = RelayDefaults::new(forced_relays);
     75 
     76         let selected = cache.selected_mut();
     77         let selected_data = &mut selected.data;
     78 
     79         selected_data.query(ndb, txn);
     80 
     81         let subs = {
     82             AccountSubs::new(
     83                 ndb,
     84                 pool,
     85                 &relay_defaults,
     86                 &selected.key.pubkey,
     87                 selected_data,
     88                 create_wakeup(ctx),
     89             )
     90         };
     91 
     92         Accounts {
     93             cache,
     94             storage_writer,
     95             relay_defaults,
     96             subs,
     97         }
     98     }
     99 
    100     pub fn remove_account(
    101         &mut self,
    102         pk: &Pubkey,
    103         ndb: &mut Ndb,
    104         pool: &mut RelayPool,
    105         ctx: &egui::Context,
    106     ) -> bool {
    107         let Some(resp) = self.cache.remove(pk) else {
    108             return false;
    109         };
    110 
    111         if pk != self.cache.fallback() {
    112             if let Some(key_store) = &self.storage_writer {
    113                 if let Err(e) = key_store.remove_key(&resp.deleted) {
    114                     tracing::error!("Could not remove account {pk}: {e}");
    115                 }
    116             }
    117         }
    118 
    119         if let Some(swap_to) = resp.swap_to {
    120             let txn = Transaction::new(ndb).expect("txn");
    121             self.select_account_internal(&swap_to, ndb, &txn, pool, ctx);
    122         }
    123 
    124         true
    125     }
    126 
    127     pub fn contains_full_kp(&self, pubkey: &enostr::Pubkey) -> bool {
    128         self.cache
    129             .get(pubkey)
    130             .is_some_and(|u| u.key.secret_key.is_some())
    131     }
    132 
    133     #[must_use = "UnknownIdAction's must be handled. Use .process_unknown_id_action()"]
    134     pub fn add_account(&mut self, kp: Keypair) -> Option<AddAccountResponse> {
    135         let acc = if let Some(acc) = self.cache.get_mut(&kp.pubkey) {
    136             if kp.secret_key.is_none() || acc.key.secret_key.is_some() {
    137                 tracing::info!("Already have account, not adding");
    138                 return None;
    139             }
    140 
    141             acc.key = kp.clone();
    142             AccType::Acc(&*acc)
    143         } else {
    144             let new_account_data = AccountData::new(kp.pubkey.bytes());
    145             AccType::Entry(
    146                 self.cache
    147                     .add(UserAccount::new(kp.clone(), new_account_data)),
    148             )
    149         };
    150 
    151         if let Some(key_store) = &self.storage_writer {
    152             if let Err(e) = key_store.write_account(&acc.get_acc().into()) {
    153                 tracing::error!("Could not add key for {:?}: {e}", kp.pubkey);
    154             }
    155         }
    156 
    157         Some(AddAccountResponse {
    158             switch_to: kp.pubkey,
    159             unk_id_action: SingleUnkIdAction::pubkey(kp.pubkey),
    160         })
    161     }
    162 
    163     /// Update the `UserAccount` via callback and save the result to disk.
    164     /// return true if the update was successful
    165     pub fn update_current_account(&mut self, update: impl FnOnce(&mut UserAccount)) -> bool {
    166         let cur_account = self.get_selected_account_mut();
    167 
    168         update(cur_account);
    169 
    170         let cur_acc = self.get_selected_account();
    171 
    172         let Some(key_store) = &self.storage_writer else {
    173             return false;
    174         };
    175 
    176         if let Err(err) = key_store.write_account(&cur_acc.into()) {
    177             tracing::error!("Could not add account {:?} to storage: {err}", cur_acc.key);
    178             return false;
    179         }
    180 
    181         true
    182     }
    183 
    184     pub fn selected_filled(&self) -> Option<FilledKeypair<'_>> {
    185         self.get_selected_account().key.to_full()
    186     }
    187 
    188     /// Get the selected account's pubkey as bytes. Common operation so
    189     /// we make it a helper here.
    190     pub fn selected_account_pubkey_bytes(&self) -> &[u8; 32] {
    191         self.get_selected_account().key.pubkey.bytes()
    192     }
    193 
    194     pub fn selected_account_pubkey(&self) -> &Pubkey {
    195         &self.get_selected_account().key.pubkey
    196     }
    197 
    198     pub fn get_selected_account(&self) -> &UserAccount {
    199         self.cache.selected()
    200     }
    201 
    202     pub fn selected_account_has_wallet(&self) -> bool {
    203         self.get_selected_account().wallet.is_some()
    204     }
    205 
    206     fn get_selected_account_mut(&mut self) -> &mut UserAccount {
    207         self.cache.selected_mut()
    208     }
    209 
    210     pub fn get_selected_wallet(&self) -> Option<&ZapWallet> {
    211         self.cache.selected().wallet.as_ref()
    212     }
    213 
    214     pub fn get_selected_wallet_mut(&mut self) -> Option<&mut ZapWallet> {
    215         self.cache.selected_mut().wallet.as_mut()
    216     }
    217 
    218     fn get_selected_account_data(&self) -> &AccountData {
    219         &self.cache.selected().data
    220     }
    221 
    222     pub fn select_account(
    223         &mut self,
    224         pk_to_select: &Pubkey,
    225         ndb: &mut Ndb,
    226         txn: &Transaction,
    227         pool: &mut RelayPool,
    228         ctx: &egui::Context,
    229     ) {
    230         if !self.cache.select(*pk_to_select) {
    231             return;
    232         }
    233 
    234         self.select_account_internal(pk_to_select, ndb, txn, pool, ctx);
    235     }
    236 
    237     /// Have already selected in `AccountCache`, updating other things
    238     fn select_account_internal(
    239         &mut self,
    240         pk_to_select: &Pubkey,
    241         ndb: &mut Ndb,
    242         txn: &Transaction,
    243         pool: &mut RelayPool,
    244         ctx: &egui::Context,
    245     ) {
    246         if let Some(key_store) = &self.storage_writer {
    247             if let Err(e) = key_store.select_key(Some(*pk_to_select)) {
    248                 tracing::error!("Could not select key {:?}: {e}", pk_to_select);
    249             }
    250         }
    251 
    252         self.get_selected_account_mut().data.query(ndb, txn);
    253         self.subs.swap_to(
    254             ndb,
    255             pool,
    256             &self.relay_defaults,
    257             pk_to_select,
    258             &self.cache.selected().data,
    259             create_wakeup(ctx),
    260         );
    261     }
    262 
    263     pub fn mutefun(&self) -> Box<MuteFun> {
    264         let account_data = self.get_selected_account_data();
    265 
    266         let muted = Arc::clone(&account_data.muted.muted);
    267         Box::new(move |note: &Note, thread: &[u8; 32]| muted.is_muted(note, thread))
    268     }
    269 
    270     pub fn mute(&self) -> Box<Arc<crate::Muted>> {
    271         let account_data = self.get_selected_account_data();
    272         Box::new(Arc::clone(&account_data.muted.muted))
    273     }
    274 
    275     pub fn update_max_hashtags_per_note(&mut self, max_hashtags: usize) {
    276         for account in self.cache.accounts_mut() {
    277             account.data.muted.update_max_hashtags(max_hashtags);
    278         }
    279     }
    280 
    281     pub fn send_initial_filters(&mut self, pool: &mut RelayPool, relay_url: &str) {
    282         let data = &self.get_selected_account().data;
    283         // send the active account's relay list subscription
    284         pool.send_to(
    285             &ClientMessage::req(
    286                 self.subs.relay.remote.clone(),
    287                 vec![data.relay.filter.clone()],
    288             ),
    289             relay_url,
    290         );
    291         // send the active account's muted subscription
    292         pool.send_to(
    293             &ClientMessage::req(
    294                 self.subs.mute.remote.clone(),
    295                 vec![data.muted.filter.clone()],
    296             ),
    297             relay_url,
    298         );
    299         pool.send_to(
    300             &ClientMessage::req(
    301                 self.subs.contacts.remote.clone(),
    302                 vec![data.contacts.filter.clone()],
    303             ),
    304             relay_url,
    305         );
    306         if let Some(cur_pk) = self.selected_filled().map(|s| s.pubkey) {
    307             let giftwraps_filter = nostrdb::Filter::new()
    308                 .kinds([1059])
    309                 .pubkeys([cur_pk.bytes()])
    310                 .build();
    311             pool.send_to(
    312                 &ClientMessage::req(self.subs.giftwraps.remote.clone(), vec![giftwraps_filter]),
    313                 relay_url,
    314             );
    315         }
    316     }
    317 
    318     pub fn update(&mut self, ndb: &mut Ndb, pool: &mut RelayPool, ctx: &egui::Context) {
    319         // IMPORTANT - This function is called in the UI update loop,
    320         // make sure it is fast when idle
    321 
    322         let Some(update) = self
    323             .cache
    324             .selected_mut()
    325             .data
    326             .poll_for_updates(ndb, &self.subs)
    327         else {
    328             return;
    329         };
    330 
    331         match update {
    332             // If needed, update the relay configuration
    333             AccountDataUpdate::Relay => {
    334                 let acc = self.cache.selected();
    335                 update_relay_configuration(
    336                     pool,
    337                     &self.relay_defaults,
    338                     &acc.key.pubkey,
    339                     &acc.data.relay,
    340                     create_wakeup(ctx),
    341                 );
    342             }
    343         }
    344     }
    345 
    346     pub fn get_full<'a>(&'a self, pubkey: &Pubkey) -> Option<FilledKeypair<'a>> {
    347         self.cache.get(pubkey).and_then(|r| r.key.to_full())
    348     }
    349 
    350     pub fn process_relay_action(
    351         &mut self,
    352         ctx: &egui::Context,
    353         pool: &mut RelayPool,
    354         action: RelayAction,
    355     ) {
    356         let acc = self.cache.selected_mut();
    357         modify_advertised_relays(&acc.key, action, pool, &self.relay_defaults, &mut acc.data);
    358 
    359         update_relay_configuration(
    360             pool,
    361             &self.relay_defaults,
    362             &acc.key.pubkey,
    363             &acc.data.relay,
    364             create_wakeup(ctx),
    365         );
    366     }
    367 
    368     pub fn get_subs(&self) -> &AccountSubs {
    369         &self.subs
    370     }
    371 }
    372 
    373 enum AccType<'a> {
    374     Entry(hashbrown::hash_map::OccupiedEntry<'a, Pubkey, UserAccount>),
    375     Acc(&'a UserAccount),
    376 }
    377 
    378 impl<'a> AccType<'a> {
    379     fn get_acc(&'a self) -> &'a UserAccount {
    380         match self {
    381             AccType::Entry(occupied_entry) => occupied_entry.get(),
    382             AccType::Acc(user_account) => user_account,
    383         }
    384     }
    385 }
    386 
    387 fn create_wakeup(ctx: &egui::Context) -> impl Fn() + Send + Sync + Clone + 'static {
    388     let ctx = ctx.clone();
    389     move || {
    390         ctx.request_repaint();
    391     }
    392 }
    393 
    394 fn add_account_from_storage(
    395     cache: &mut AccountCache,
    396     user_account_serializable: UserAccountSerializable,
    397 ) -> SingleUnkIdAction {
    398     let Some(acc) = get_acc_from_storage(user_account_serializable) else {
    399         return SingleUnkIdAction::NoAction;
    400     };
    401 
    402     let pk = acc.key.pubkey;
    403     cache.add(acc);
    404 
    405     SingleUnkIdAction::pubkey(pk)
    406 }
    407 
    408 fn get_acc_from_storage(user_account_serializable: UserAccountSerializable) -> Option<UserAccount> {
    409     let keypair = user_account_serializable.key;
    410     let new_account_data = AccountData::new(keypair.pubkey.bytes());
    411 
    412     let mut wallet = None;
    413     if let Some(wallet_s) = user_account_serializable.wallet {
    414         let m_wallet: Result<crate::ZapWallet, crate::Error> = wallet_s.into();
    415         match m_wallet {
    416             Ok(w) => wallet = Some(w),
    417             Err(e) => {
    418                 tracing::error!("Problem creating wallet from disk: {e}");
    419             }
    420         };
    421     }
    422 
    423     Some(UserAccount {
    424         key: keypair,
    425         wallet,
    426         data: new_account_data,
    427     })
    428 }
    429 
    430 #[derive(Clone)]
    431 pub struct AccountData {
    432     pub(crate) relay: AccountRelayData,
    433     pub(crate) muted: AccountMutedData,
    434     pub contacts: Contacts,
    435 }
    436 
    437 impl AccountData {
    438     pub fn new(pubkey: &[u8; 32]) -> Self {
    439         Self {
    440             relay: AccountRelayData::new(pubkey),
    441             muted: AccountMutedData::new(pubkey),
    442             contacts: Contacts::new(pubkey),
    443         }
    444     }
    445 
    446     pub(super) fn poll_for_updates(
    447         &mut self,
    448         ndb: &Ndb,
    449         subs: &AccountSubs,
    450     ) -> Option<AccountDataUpdate> {
    451         let txn = Transaction::new(ndb).expect("txn");
    452         let mut resp = None;
    453         if self.relay.poll_for_updates(ndb, &txn, subs.relay.local) {
    454             resp = Some(AccountDataUpdate::Relay);
    455         }
    456 
    457         self.muted.poll_for_updates(ndb, &txn, subs.mute.local);
    458         self.contacts
    459             .poll_for_updates(ndb, &txn, subs.contacts.local);
    460 
    461         resp
    462     }
    463 
    464     /// Note: query should be called as close to the subscription as possible
    465     pub(super) fn query(&mut self, ndb: &Ndb, txn: &Transaction) {
    466         self.relay.query(ndb, txn);
    467         self.muted.query(ndb, txn);
    468         self.contacts.query(ndb, txn);
    469     }
    470 }
    471 
    472 pub(super) enum AccountDataUpdate {
    473     Relay,
    474 }
    475 
    476 pub struct AddAccountResponse {
    477     pub switch_to: Pubkey,
    478     pub unk_id_action: SingleUnkIdAction,
    479 }
    480 
    481 pub struct AccountSubs {
    482     relay: UnifiedSubscription,
    483     giftwraps: UnifiedSubscription,
    484     mute: UnifiedSubscription,
    485     pub contacts: UnifiedSubscription,
    486 }
    487 
    488 impl AccountSubs {
    489     pub(super) fn new(
    490         ndb: &mut Ndb,
    491         pool: &mut RelayPool,
    492         relay_defaults: &RelayDefaults,
    493         pk: &Pubkey,
    494         data: &AccountData,
    495         wakeup: impl Fn() + Send + Sync + Clone + 'static,
    496     ) -> Self {
    497         // TODO: since optimize
    498         let giftwraps_filter = nostrdb::Filter::new()
    499             .kinds([1059])
    500             .pubkeys([pk.bytes()])
    501             .build();
    502 
    503         update_relay_configuration(pool, relay_defaults, pk, &data.relay, wakeup);
    504 
    505         let relay = subscribe(ndb, pool, &data.relay.filter);
    506         let giftwraps = subscribe(ndb, pool, &giftwraps_filter);
    507         let mute = subscribe(ndb, pool, &data.muted.filter);
    508         let contacts = subscribe(ndb, pool, &data.contacts.filter);
    509 
    510         Self {
    511             relay,
    512             mute,
    513             contacts,
    514             giftwraps,
    515         }
    516     }
    517 
    518     pub(super) fn swap_to(
    519         &mut self,
    520         ndb: &mut Ndb,
    521         pool: &mut RelayPool,
    522         relay_defaults: &RelayDefaults,
    523         pk: &Pubkey,
    524         new_selection_data: &AccountData,
    525         wakeup: impl Fn() + Send + Sync + Clone + 'static,
    526     ) {
    527         unsubscribe(ndb, pool, &self.relay);
    528         unsubscribe(ndb, pool, &self.mute);
    529         unsubscribe(ndb, pool, &self.contacts);
    530         unsubscribe(ndb, pool, &self.giftwraps);
    531 
    532         *self = AccountSubs::new(ndb, pool, relay_defaults, pk, new_selection_data, wakeup);
    533     }
    534 }
    535 
    536 fn subscribe(ndb: &Ndb, pool: &mut RelayPool, filter: &nostrdb::Filter) -> UnifiedSubscription {
    537     let filters = vec![filter.clone()];
    538     let sub = ndb
    539         .subscribe(&filters)
    540         .expect("ndb relay list subscription");
    541 
    542     // remote subscription
    543     let subid = Uuid::new_v4().to_string();
    544     pool.subscribe(subid.clone(), filters);
    545 
    546     UnifiedSubscription {
    547         local: sub,
    548         remote: subid,
    549     }
    550 }
    551 
    552 fn unsubscribe(ndb: &mut Ndb, pool: &mut RelayPool, sub: &UnifiedSubscription) {
    553     pool.unsubscribe(sub.remote.clone());
    554 
    555     // local subscription
    556     ndb.unsubscribe(sub.local)
    557         .expect("ndb relay list unsubscribe");
    558 }