nip17.rs (9999B)
1 #![forbid(unsafe_code)] 2 3 extern crate alloc; 4 5 use alloc::{string::String, vec::Vec}; 6 7 use nostr::nips::nip59; 8 use nostr::{ 9 Event, EventBuilder, Kind, NostrSigner, PublicKey, Tag, TagKind, Timestamp, UnsignedEvent, 10 }; 11 use thiserror::Error; 12 13 use radroots_events::kinds::{KIND_MESSAGE, KIND_MESSAGE_FILE}; 14 use radroots_events::message::RadrootsMessage; 15 use radroots_events::message_file::RadrootsMessageFile; 16 use radroots_events_codec::error::{EventEncodeError, EventParseError}; 17 use radroots_events_codec::message::decode as message_decode; 18 use radroots_events_codec::message::encode as message_encode; 19 use radroots_events_codec::message_file::decode as message_file_decode; 20 use radroots_events_codec::message_file::encode as message_file_encode; 21 use radroots_events_codec::parsed::RadrootsParsedData; 22 use radroots_events_codec::wire::WireEventParts; 23 24 use crate::util::created_at_u32_saturating; 25 26 #[derive(Debug, Error)] 27 pub enum RadrootsNip17Error { 28 #[error("Message encode error: {0}")] 29 MessageEncode(#[from] EventEncodeError), 30 #[error("Message decode error: {0}")] 31 MessageDecode(#[from] EventParseError), 32 #[error("NIP-59 error: {0}")] 33 Nip59(#[from] nip59::Error), 34 #[error("Event builder error: {0}")] 35 EventBuilder(#[from] nostr::event::builder::Error), 36 #[error("Signer error: {0}")] 37 Signer(#[from] nostr::signer::SignerError), 38 #[error("Key error: {0}")] 39 Key(#[from] nostr::key::Error), 40 #[error("Unsupported rumor kind: {0}")] 41 UnsupportedRumorKind(u32), 42 } 43 44 #[derive(Clone, Debug)] 45 pub enum RadrootsNip17Rumor { 46 Message(RadrootsParsedData<RadrootsMessage>), 47 MessageFile(Box<RadrootsParsedData<RadrootsMessageFile>>), 48 } 49 50 #[derive(Clone, Debug)] 51 pub struct RadrootsNip17WrapOptions { 52 pub include_sender: bool, 53 pub rumor_created_at: Option<u32>, 54 pub gift_wrap_tags: Vec<Vec<String>>, 55 } 56 57 impl Default for RadrootsNip17WrapOptions { 58 fn default() -> Self { 59 Self { 60 include_sender: true, 61 rumor_created_at: None, 62 gift_wrap_tags: Vec::new(), 63 } 64 } 65 } 66 67 fn tags_from_slices(tag_slices: &[Vec<String>]) -> Vec<Tag> { 68 let mut tags = Vec::with_capacity(tag_slices.len()); 69 for slice in tag_slices { 70 if slice.is_empty() { 71 continue; 72 } 73 let key = slice[0].clone(); 74 let values = slice[1..].to_vec(); 75 tags.push(Tag::custom(TagKind::Custom(key.into()), values)); 76 } 77 tags 78 } 79 80 fn rumor_from_parts( 81 parts: WireEventParts, 82 author: PublicKey, 83 created_at: Option<u32>, 84 ) -> UnsignedEvent { 85 let tags = tags_from_slices(&parts.tags); 86 let timestamp = match created_at { 87 Some(ts) => Timestamp::from_secs(ts as u64), 88 None => Timestamp::now(), 89 }; 90 let mut rumor = UnsignedEvent::new( 91 author, 92 timestamp, 93 Kind::Custom(parts.kind as u16), 94 tags, 95 parts.content, 96 ); 97 rumor.ensure_id(); 98 rumor 99 } 100 101 fn parse_recipients( 102 recipients: &[radroots_events::message::RadrootsMessageRecipient], 103 ) -> Result<Vec<PublicKey>, RadrootsNip17Error> { 104 let mut out = Vec::with_capacity(recipients.len()); 105 for recipient in recipients { 106 out.push(recipient.public_key.parse::<PublicKey>()?); 107 } 108 Ok(out) 109 } 110 111 fn push_unique(recipients: &mut Vec<PublicKey>, pubkey: PublicKey) { 112 if recipients.iter().any(|r| r == &pubkey) { 113 return; 114 } 115 recipients.push(pubkey); 116 } 117 118 async fn wrap_rumor<T>( 119 signer: &T, 120 rumor: UnsignedEvent, 121 mut recipients: Vec<PublicKey>, 122 options: &RadrootsNip17WrapOptions, 123 ) -> Result<Vec<Event>, RadrootsNip17Error> 124 where 125 T: NostrSigner, 126 { 127 let sender_pubkey = signer.get_public_key().await?; 128 if options.include_sender { 129 push_unique(&mut recipients, sender_pubkey); 130 } 131 let extra_tags = tags_from_slices(&options.gift_wrap_tags); 132 133 let mut out = Vec::with_capacity(recipients.len()); 134 for recipient in recipients { 135 let event = 136 EventBuilder::gift_wrap(signer, &recipient, rumor.clone(), extra_tags.clone()).await?; 137 out.push(event); 138 } 139 Ok(out) 140 } 141 142 pub async fn radroots_nostr_wrap_message<T>( 143 signer: &T, 144 message: &RadrootsMessage, 145 options: RadrootsNip17WrapOptions, 146 ) -> Result<Vec<Event>, RadrootsNip17Error> 147 where 148 T: NostrSigner, 149 { 150 let parts = message_encode::to_wire_parts(message)?; 151 let author = signer.get_public_key().await?; 152 let rumor = rumor_from_parts(parts, author, options.rumor_created_at); 153 let recipients = parse_recipients(&message.recipients)?; 154 wrap_rumor(signer, rumor, recipients, &options).await 155 } 156 157 pub async fn radroots_nostr_wrap_message_file<T>( 158 signer: &T, 159 message: &RadrootsMessageFile, 160 options: RadrootsNip17WrapOptions, 161 ) -> Result<Vec<Event>, RadrootsNip17Error> 162 where 163 T: NostrSigner, 164 { 165 let parts = message_file_encode::to_wire_parts(message)?; 166 let author = signer.get_public_key().await?; 167 let rumor = rumor_from_parts(parts, author, options.rumor_created_at); 168 let recipients = parse_recipients(&message.recipients)?; 169 wrap_rumor(signer, rumor, recipients, &options).await 170 } 171 172 pub async fn radroots_nostr_unwrap_gift_wrap<T>( 173 signer: &T, 174 gift_wrap: &Event, 175 ) -> Result<RadrootsNip17Rumor, RadrootsNip17Error> 176 where 177 T: NostrSigner, 178 { 179 let unwrapped = nip59::extract_rumor(signer, gift_wrap).await?; 180 let mut rumor = unwrapped.rumor; 181 let id = rumor.id().to_string(); 182 let author = rumor.pubkey.to_string(); 183 let published_at = created_at_u32_saturating(rumor.created_at); 184 let kind = rumor.kind.as_u16() as u32; 185 let tags: Vec<Vec<String>> = rumor 186 .tags 187 .as_slice() 188 .iter() 189 .map(|t| t.as_slice().to_vec()) 190 .collect(); 191 let content = rumor.content.clone(); 192 193 match kind { 194 KIND_MESSAGE => { 195 let metadata = 196 message_decode::data_from_event(id, author, published_at, kind, content, tags)?; 197 Ok(RadrootsNip17Rumor::Message(metadata)) 198 } 199 KIND_MESSAGE_FILE => { 200 let metadata = message_file_decode::data_from_event( 201 id, 202 author, 203 published_at, 204 kind, 205 content, 206 tags, 207 )?; 208 Ok(RadrootsNip17Rumor::MessageFile(Box::new(metadata))) 209 } 210 other => Err(RadrootsNip17Error::UnsupportedRumorKind(other)), 211 } 212 } 213 214 #[cfg(all(test, feature = "nip17"))] 215 mod tests { 216 use super::*; 217 use crate::test_fixtures::{FIXTURE_ALICE, FIXTURE_BOB}; 218 use nostr::{Keys, SecretKey}; 219 use radroots_events::message::{RadrootsMessage, RadrootsMessageRecipient}; 220 use radroots_events::message_file::{RadrootsMessageFile, RadrootsMessageFileDimensions}; 221 222 fn sender_keys() -> Keys { 223 Keys::new(SecretKey::from_hex(FIXTURE_ALICE.secret_key_hex).unwrap()) 224 } 225 226 fn receiver_keys() -> Keys { 227 Keys::new(SecretKey::from_hex(FIXTURE_BOB.secret_key_hex).unwrap()) 228 } 229 230 #[tokio::test] 231 async fn wrap_and_unwrap_message() { 232 let sender = sender_keys(); 233 let receiver = receiver_keys(); 234 let message = RadrootsMessage { 235 recipients: vec![RadrootsMessageRecipient { 236 public_key: receiver.public_key().to_string(), 237 relay_url: None, 238 }], 239 content: "hello".to_string(), 240 reply_to: None, 241 subject: None, 242 }; 243 let options = RadrootsNip17WrapOptions { 244 include_sender: false, 245 rumor_created_at: Some(1700000000), 246 gift_wrap_tags: Vec::new(), 247 }; 248 249 let events = radroots_nostr_wrap_message(&sender, &message, options) 250 .await 251 .unwrap(); 252 assert_eq!(events.len(), 1); 253 254 let rumor = radroots_nostr_unwrap_gift_wrap(&receiver, &events[0]) 255 .await 256 .unwrap(); 257 match rumor { 258 RadrootsNip17Rumor::Message(metadata) => { 259 assert_eq!(metadata.data.content, "hello"); 260 assert_eq!(metadata.data.recipients.len(), 1); 261 } 262 other => panic!("expected message rumor, got {other:?}"), 263 } 264 } 265 266 #[tokio::test] 267 async fn wrap_and_unwrap_message_file() { 268 let sender = sender_keys(); 269 let receiver = receiver_keys(); 270 let message = RadrootsMessageFile { 271 recipients: vec![RadrootsMessageRecipient { 272 public_key: receiver.public_key().to_string(), 273 relay_url: None, 274 }], 275 file_url: "https://files.example/encrypted.bin".to_string(), 276 reply_to: None, 277 subject: None, 278 file_type: "image/jpeg".to_string(), 279 encryption_algorithm: "aes-gcm".to_string(), 280 decryption_key: "key".to_string(), 281 decryption_nonce: "nonce".to_string(), 282 encrypted_hash: "hash".to_string(), 283 original_hash: None, 284 size: Some(1200), 285 dimensions: Some(RadrootsMessageFileDimensions { w: 1200, h: 800 }), 286 blurhash: None, 287 thumb: None, 288 fallbacks: Vec::new(), 289 }; 290 let options = RadrootsNip17WrapOptions { 291 include_sender: false, 292 rumor_created_at: Some(1700000001), 293 gift_wrap_tags: Vec::new(), 294 }; 295 296 let events = radroots_nostr_wrap_message_file(&sender, &message, options) 297 .await 298 .unwrap(); 299 assert_eq!(events.len(), 1); 300 301 let rumor = radroots_nostr_unwrap_gift_wrap(&receiver, &events[0]) 302 .await 303 .unwrap(); 304 match rumor { 305 RadrootsNip17Rumor::MessageFile(metadata) => { 306 assert_eq!(metadata.data.file_url, message.file_url); 307 assert_eq!(metadata.data.encrypted_hash, message.encrypted_hash); 308 } 309 other => panic!("expected message file rumor, got {other:?}"), 310 } 311 } 312 }