notedeck

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

profile.rs (9459B)


      1 use enostr::{FilledKeypair, FullKeypair, ProfileState, Pubkey, RelayPool};
      2 use nostrdb::{Ndb, Note, NoteBuildOptions, NoteBuilder, Transaction};
      3 
      4 use notedeck::{Accounts, ContactState, DataPath, Localization, ProfileContext};
      5 use tracing::info;
      6 
      7 use crate::{column::Column, nav::RouterAction, route::Route, storage, Damus};
      8 
      9 pub struct SaveProfileChanges {
     10     pub kp: FullKeypair,
     11     pub state: ProfileState,
     12 }
     13 
     14 impl SaveProfileChanges {
     15     pub fn new(kp: FullKeypair, state: ProfileState) -> Self {
     16         Self { kp, state }
     17     }
     18     pub fn to_note(&self) -> Note<'_> {
     19         let sec = &self.kp.secret_key.to_secret_bytes();
     20         add_client_tag(NoteBuilder::new())
     21             .kind(0)
     22             .content(&self.state.to_json())
     23             .options(NoteBuildOptions::default().created_at(true).sign(sec))
     24             .build()
     25             .expect("should build")
     26     }
     27 }
     28 
     29 fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> {
     30     builder
     31         .start_tag()
     32         .tag_str("client")
     33         .tag_str("Damus Notedeck")
     34 }
     35 
     36 pub enum ProfileAction {
     37     Edit(FullKeypair),
     38     SaveChanges(SaveProfileChanges),
     39     Follow(Pubkey),
     40     Unfollow(Pubkey),
     41     Context(ProfileContext),
     42 }
     43 
     44 impl ProfileAction {
     45     #[allow(clippy::too_many_arguments)]
     46     pub fn process_profile_action(
     47         &self,
     48         app: &mut Damus,
     49         path: &DataPath,
     50         i18n: &mut Localization,
     51         ctx: &egui::Context,
     52         ndb: &Ndb,
     53         pool: &mut RelayPool,
     54         accounts: &Accounts,
     55     ) -> Option<RouterAction> {
     56         match self {
     57             ProfileAction::Edit(kp) => Some(RouterAction::route_to(Route::EditProfile(kp.pubkey))),
     58             ProfileAction::SaveChanges(changes) => {
     59                 let note = changes.to_note();
     60                 let Ok(event) = enostr::ClientMessage::event(&note) else {
     61                     tracing::error!("could not serialize profile note?");
     62                     return None;
     63                 };
     64 
     65                 let Ok(json) = event.to_json() else {
     66                     tracing::error!("could not serialize profile note?");
     67                     return None;
     68                 };
     69 
     70                 // TODO(jb55): do this in a more centralized place
     71                 let _ = ndb.process_event_with(&json, nostrdb::IngestMetadata::new().client(true));
     72 
     73                 info!("sending {}", &json);
     74                 pool.send(&event);
     75 
     76                 Some(RouterAction::GoBack)
     77             }
     78             ProfileAction::Follow(target_key) => {
     79                 Self::send_follow_user_event(ndb, pool, accounts, target_key);
     80                 None
     81             }
     82             ProfileAction::Unfollow(target_key) => {
     83                 Self::send_unfollow_user_event(ndb, pool, accounts, target_key);
     84                 None
     85             }
     86             ProfileAction::Context(profile_context) => {
     87                 use notedeck::ProfileContextSelection;
     88                 match &profile_context.selection {
     89                     ProfileContextSelection::ViewAs => {
     90                         Some(RouterAction::SwitchAccount(profile_context.profile))
     91                     }
     92                     ProfileContextSelection::AddProfileColumn => {
     93                         let timeline_route = Route::Timeline(
     94                             crate::timeline::TimelineKind::Profile(profile_context.profile),
     95                         );
     96 
     97                         let missing_column = {
     98                             let deck_columns = app.columns(accounts).columns();
     99                             let router_head = &[timeline_route.clone()];
    100                             !deck_columns
    101                                 .iter()
    102                                 .any(|column| column.router.routes().starts_with(router_head))
    103                         };
    104 
    105                         if missing_column {
    106                             let column = Column::new(vec![timeline_route]);
    107 
    108                             app.columns_mut(i18n, accounts).add_column(column);
    109 
    110                             storage::save_decks_cache(path, &app.decks_cache);
    111                         }
    112 
    113                         None
    114                     }
    115                     _ => {
    116                         profile_context
    117                             .selection
    118                             .process(ctx, &profile_context.profile);
    119                         None
    120                     }
    121                 }
    122             }
    123         }
    124     }
    125 
    126     fn send_follow_user_event(
    127         ndb: &Ndb,
    128         pool: &mut RelayPool,
    129         accounts: &Accounts,
    130         target_key: &Pubkey,
    131     ) {
    132         send_kind_3_event(ndb, pool, accounts, FollowAction::Follow(target_key));
    133     }
    134 
    135     fn send_unfollow_user_event(
    136         ndb: &Ndb,
    137         pool: &mut RelayPool,
    138         accounts: &Accounts,
    139         target_key: &Pubkey,
    140     ) {
    141         send_kind_3_event(ndb, pool, accounts, FollowAction::Unfollow(target_key));
    142     }
    143 }
    144 
    145 pub fn builder_from_note<F>(note: Note<'_>, skip_tag: Option<F>) -> NoteBuilder<'_>
    146 where
    147     F: Fn(&nostrdb::Tag<'_>) -> bool,
    148 {
    149     let mut builder = NoteBuilder::new();
    150 
    151     builder = builder.content(note.content());
    152     builder = builder.options(NoteBuildOptions::default());
    153     builder = builder.kind(note.kind());
    154     builder = builder.pubkey(note.pubkey());
    155 
    156     for tag in note.tags() {
    157         if let Some(skip) = &skip_tag {
    158             if skip(&tag) {
    159                 continue;
    160             }
    161         }
    162 
    163         builder = builder.start_tag();
    164         for tag_item in tag {
    165             builder = match tag_item.variant() {
    166                 nostrdb::NdbStrVariant::Id(i) => builder.tag_id(i),
    167                 nostrdb::NdbStrVariant::Str(s) => builder.tag_str(s),
    168             };
    169         }
    170     }
    171 
    172     builder
    173 }
    174 
    175 enum FollowAction<'a> {
    176     Follow(&'a Pubkey),
    177     Unfollow(&'a Pubkey),
    178 }
    179 
    180 fn send_kind_3_event(ndb: &Ndb, pool: &mut RelayPool, accounts: &Accounts, action: FollowAction) {
    181     let Some(kp) = accounts.get_selected_account().key.to_full() else {
    182         return;
    183     };
    184 
    185     let txn = Transaction::new(ndb).expect("txn");
    186 
    187     let ContactState::Received {
    188         contacts: _,
    189         note_key,
    190         timestamp: _,
    191     } = accounts.get_selected_account().data.contacts.get_state()
    192     else {
    193         return;
    194     };
    195 
    196     let contact_note = match ndb.get_note_by_key(&txn, *note_key).ok() {
    197         Some(n) => n,
    198         None => {
    199             tracing::error!(
    200                 "Somehow we are in state ContactState::Received but the contact note key doesn't exist"
    201             );
    202             return;
    203         }
    204     };
    205 
    206     if contact_note.kind() != 3 {
    207         tracing::error!(
    208             "Something very wrong just occured. The key for the supposed contact note yielded a note which was not a contact..."
    209         );
    210         return;
    211     }
    212 
    213     let builder = match action {
    214         FollowAction::Follow(pubkey) => {
    215             builder_from_note(contact_note, None::<fn(&nostrdb::Tag<'_>) -> bool>)
    216                 .start_tag()
    217                 .tag_str("p")
    218                 .tag_str(&pubkey.hex())
    219         }
    220         FollowAction::Unfollow(pubkey) => builder_from_note(
    221             contact_note,
    222             Some(|tag: &nostrdb::Tag<'_>| {
    223                 if tag.count() < 2 {
    224                     return false;
    225                 }
    226 
    227                 let Some("p") = tag.get_str(0) else {
    228                     return false;
    229                 };
    230 
    231                 let Some(cur_val) = tag.get_id(1) else {
    232                     return false;
    233                 };
    234 
    235                 cur_val == pubkey.bytes()
    236             }),
    237         ),
    238     };
    239 
    240     send_note_builder(builder, ndb, pool, kp);
    241 }
    242 
    243 fn send_note_builder(builder: NoteBuilder, ndb: &Ndb, pool: &mut RelayPool, kp: FilledKeypair) {
    244     let note = builder
    245         .sign(&kp.secret_key.secret_bytes())
    246         .build()
    247         .expect("build note");
    248 
    249     let Ok(event) = &enostr::ClientMessage::event(&note) else {
    250         tracing::error!("send_note_builder: failed to build json");
    251         return;
    252     };
    253 
    254     let Ok(json) = event.to_json() else {
    255         tracing::error!("send_note_builder: failed to build json");
    256         return;
    257     };
    258 
    259     let _ = ndb.process_event_with(&json, nostrdb::IngestMetadata::new().client(true));
    260     info!("sending {}", &json);
    261     pool.send(event);
    262 }
    263 
    264 pub fn send_new_contact_list(
    265     kp: FilledKeypair,
    266     ndb: &Ndb,
    267     pool: &mut RelayPool,
    268     mut pks_to_follow: Vec<Pubkey>,
    269 ) {
    270     if !pks_to_follow.contains(kp.pubkey) {
    271         pks_to_follow.push(*kp.pubkey);
    272     }
    273 
    274     let builder = construct_new_contact_list(pks_to_follow);
    275 
    276     send_note_builder(builder, ndb, pool, kp);
    277 }
    278 
    279 fn construct_new_contact_list<'a>(pks: Vec<Pubkey>) -> NoteBuilder<'a> {
    280     let mut builder = NoteBuilder::new()
    281         .content("")
    282         .kind(3)
    283         .options(NoteBuildOptions::default());
    284 
    285     for pk in pks {
    286         builder = builder.start_tag().tag_str("p").tag_str(&pk.hex());
    287     }
    288 
    289     builder
    290 }
    291 
    292 pub fn send_default_dms_relay_list(kp: FilledKeypair<'_>, ndb: &Ndb, pool: &mut RelayPool) {
    293     send_note_builder(construct_default_dms_relay_list(), ndb, pool, kp);
    294 }
    295 
    296 fn construct_default_dms_relay_list<'a>() -> NoteBuilder<'a> {
    297     let mut builder = NoteBuilder::new()
    298         .content("")
    299         .kind(10050)
    300         .options(NoteBuildOptions::default());
    301 
    302     for relay in default_dms_relays() {
    303         builder = builder.start_tag().tag_str("relay").tag_str(relay);
    304     }
    305 
    306     builder
    307 }
    308 
    309 fn default_dms_relays() -> Vec<&'static str> {
    310     vec!["wss://relay.damus.io", "wss://nos.lol"]
    311 }