notedeck

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

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 }