mod.rs (5799B)
1 pub mod message; 2 3 use enostr::{FullKeypair, Pubkey, SecretKey}; 4 pub use message::send_conversation_message; 5 pub use nostr::secp256k1::rand::rngs::OsRng; 6 use nostr::secp256k1::rand::Rng; 7 use nostr::{ 8 event::{EventBuilder, Kind, Tag}, 9 key::PublicKey, 10 nips::nip44, 11 util::JsonUtil, 12 }; 13 use nostrdb::{Filter, FilterBuilder, Note, NoteBuilder}; 14 use notedeck::get_p_tags; 15 16 fn build_rumor_json( 17 message: &str, 18 participants: &[Pubkey], 19 sender_pubkey: &Pubkey, 20 ) -> Option<String> { 21 let sender = nostrcrate_pk(sender_pubkey)?; 22 let mut tags = Vec::new(); 23 for participant in participants { 24 if let Some(pk) = nostrcrate_pk(participant) { 25 tags.push(Tag::public_key(pk)); 26 } else { 27 tracing::warn!("invalid participant {}", participant); 28 } 29 } 30 31 let builder = EventBuilder::new(Kind::PrivateDirectMessage, message).tags(tags); 32 Some(builder.build(sender).as_json()) 33 } 34 35 pub fn giftwrap_message( 36 rng: &mut OsRng, 37 sender_secret: &SecretKey, 38 recipient: &Pubkey, 39 rumor_json: &str, 40 ) -> Option<String> { 41 let Some(recipient_pk) = nostrcrate_pk(recipient) else { 42 tracing::warn!("failed to convert recipient pubkey {}", recipient); 43 return None; 44 }; 45 46 let encrypted_rumor = match nip44::encrypt_with_rng( 47 rng, 48 sender_secret, 49 &recipient_pk, 50 rumor_json, 51 nip44::Version::V2, 52 ) { 53 Ok(payload) => payload, 54 Err(err) => { 55 tracing::error!("failed to encrypt rumor for {recipient}: {err}"); 56 return None; 57 } 58 }; 59 60 let seal_created = randomized_timestamp(rng); 61 let Some(seal_json) = build_seal_json(&encrypted_rumor, sender_secret, seal_created) else { 62 tracing::error!("failed to build seal for recipient {}", recipient); 63 return None; 64 }; 65 66 let wrap_keys = FullKeypair::generate(); 67 let encrypted_seal = match nip44::encrypt_with_rng( 68 rng, 69 &wrap_keys.secret_key, 70 &recipient_pk, 71 &seal_json, 72 nip44::Version::V2, 73 ) { 74 Ok(payload) => payload, 75 Err(err) => { 76 tracing::error!("failed to encrypt seal for wrap: {err}"); 77 return None; 78 } 79 }; 80 81 let wrap_created = randomized_timestamp(rng); 82 build_giftwrap_json(&encrypted_seal, &wrap_keys, recipient, wrap_created) 83 } 84 85 fn build_seal_json( 86 content_ciphertext: &str, 87 sender_secret: &SecretKey, 88 created_at: u64, 89 ) -> Option<String> { 90 let builder = NoteBuilder::new() 91 .kind(13) 92 .content(content_ciphertext) 93 .created_at(created_at); 94 95 builder 96 .sign(&sender_secret.secret_bytes()) 97 .build()? 98 .json() 99 .ok() 100 } 101 102 fn build_giftwrap_json( 103 content: &str, 104 wrap_keys: &FullKeypair, 105 recipient: &Pubkey, 106 created_at: u64, 107 ) -> Option<String> { 108 let builder = NoteBuilder::new() 109 .kind(1059) 110 .content(content) 111 .created_at(created_at) 112 .start_tag() 113 .tag_str("p") 114 .tag_str(&recipient.hex()); 115 116 builder 117 .sign(&wrap_keys.secret_key.secret_bytes()) 118 .build()? 119 .json() 120 .ok() 121 } 122 123 fn nostrcrate_pk(pk: &Pubkey) -> Option<PublicKey> { 124 PublicKey::from_slice(pk.bytes()).ok() 125 } 126 127 fn current_timestamp() -> u64 { 128 use std::time::{SystemTime, UNIX_EPOCH}; 129 130 SystemTime::now() 131 .duration_since(UNIX_EPOCH) 132 .unwrap_or_default() 133 .as_secs() 134 } 135 136 fn randomized_timestamp(rng: &mut OsRng) -> u64 { 137 const MAX_SKEW_SECS: u64 = 2 * 24 * 60 * 60; 138 let now = current_timestamp(); 139 let tweak = rng.gen_range(0..=MAX_SKEW_SECS); 140 now.saturating_sub(tweak) 141 } 142 143 #[profiling::function] 144 pub fn get_participants<'a>(note: &Note<'a>) -> Vec<&'a [u8; 32]> { 145 let mut participants = get_p_tags(note); 146 let chat_message_sender = note.pubkey(); 147 if !participants.contains(&chat_message_sender) { 148 // the chat message sender must be in the participants set 149 participants.push(chat_message_sender); 150 } 151 participants 152 } 153 154 pub fn conversation_filter(cur_acc: &Pubkey) -> Vec<Filter> { 155 vec![ 156 FilterBuilder::new() 157 .kinds([14]) 158 .pubkey([cur_acc.bytes()]) 159 .build(), 160 FilterBuilder::new() 161 .kinds([14]) 162 .authors([cur_acc.bytes()]) 163 .build(), 164 ] 165 } 166 167 /// Unfortunately this gives an OR across participants 168 pub fn chatroom_filter(participants: Vec<&[u8; 32]>, me: &[u8; 32]) -> Vec<Filter> { 169 vec![FilterBuilder::new() 170 .kinds([14]) 171 .authors(participants.clone()) 172 .pubkey([me]) 173 .build()] 174 } 175 176 // easily retrievable from Note<'a> 177 pub struct Nip17ChatMessage<'a> { 178 pub sender: &'a [u8; 32], 179 pub p_tags: Vec<&'a [u8; 32]>, 180 pub subject: Option<&'a str>, 181 pub reply_to: Option<&'a [u8; 32]>, // NoteId 182 pub message: &'a str, 183 pub created_at: u64, 184 } 185 186 pub fn parse_chat_message<'a>(note: &Note<'a>) -> Option<Nip17ChatMessage<'a>> { 187 if note.kind() != 14 { 188 return None; 189 } 190 191 let mut p_tags = Vec::new(); 192 let mut subject = None; 193 let mut reply_to = None; 194 195 for tag in note.tags() { 196 if tag.count() < 2 { 197 continue; 198 } 199 let Some(first) = tag.get_str(0) else { 200 continue; 201 }; 202 203 if first == "p" { 204 if let Some(id) = tag.get_id(1) { 205 p_tags.push(id); 206 } 207 } else if first == "subject" { 208 subject = tag.get_str(1); 209 } else if first == "e" { 210 reply_to = tag.get_id(1); 211 } 212 } 213 214 Some(Nip17ChatMessage { 215 sender: note.pubkey(), 216 p_tags, 217 subject, 218 reply_to, 219 message: note.content(), 220 created_at: note.created_at(), 221 }) 222 }