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