nostr_bot/
nostr.rs

1use secp256k1::Secp256k1;
2use std::str::FromStr;
3
4use log::debug;
5use serde::{Deserialize, Serialize};
6use std::fmt::Write;
7
8#[derive(Serialize, Deserialize)]
9pub struct Message {
10    pub msg_type: String,
11    pub subscription_id: String,
12    pub content: Event,
13}
14
15/// Holds non signed nostr event, for more info about events see
16/// <https://github.com/nostr-protocol/nips/blob/master/01.md#events-and-signatures>.
17pub struct EventNonSigned {
18    pub created_at: u64,
19    pub kind: u64,
20    pub tags: Vec<Vec<String>>,
21    pub content: String,
22}
23
24impl EventNonSigned {
25    /// Returns event signed by `keypair`.
26    pub fn sign(self, keypair: &secp256k1::KeyPair) -> Event {
27        Event::new(keypair, self.created_at, self.kind, self.tags, self.content)
28    }
29}
30
31/// Holds nostr event, see
32/// <https://github.com/nostr-protocol/nips/blob/master/01.md#events-and-signatures>.
33#[derive(Serialize, Deserialize)]
34pub struct Event {
35    pub id: String,
36    pub pubkey: String,
37    pub created_at: u64,
38    pub kind: u64,
39    pub tags: Vec<Vec<String>>,
40    pub content: String,
41    pub sig: String,
42}
43
44impl Event {
45    /// Creates a new nostr event and signs it using `keypair`.
46    pub fn new(
47        keypair: &secp256k1::KeyPair,
48        created_at: u64,
49        kind: u64,
50        tags: Vec<Vec<String>>,
51        content: String,
52    ) -> Self {
53        let content = escape(content);
54
55        let secp = Secp256k1::new();
56
57        let (pubkey, _parity) = keypair.x_only_public_key();
58
59        let id = Self::get_id(pubkey, created_at, kind, &tags, &content);
60        let signature = secp.sign_schnorr(&id, keypair);
61
62        Event {
63            id: id.to_string(),
64            pubkey: pubkey.to_string(),
65            created_at,
66            kind,
67            content,
68            sig: signature.to_string(),
69            tags,
70        }
71    }
72
73    /// Formats an event into string which can send to relays.
74    pub fn format(&self) -> String {
75        format!(
76            r#"["EVENT",{{"id": "{}", "pubkey": "{}", "created_at": {}, "kind": {}, "tags": [{}], "content": "{}", "sig": "{}"}}]"#,
77            self.id,
78            self.pubkey,
79            self.created_at,
80            self.kind,
81            Self::format_tags(&self.tags),
82            &self.content,
83            self.sig
84        )
85    }
86
87    /// Checks whether the signature for the event is valid.
88    pub fn has_valid_sig(&self) -> bool {
89        let secp = secp256k1::Secp256k1::verification_only();
90        // TODO: Hold secp256k1::Message/Signature/XOnlyPublicKey in the Event so it don't have to be recreated
91        let pubkey = match secp256k1::XOnlyPublicKey::from_str(&self.pubkey) {
92            Ok(pubkey) => pubkey,
93            Err(_) => return false,
94        };
95
96        let signature = match secp256k1::schnorr::Signature::from_str(&self.sig) {
97            Ok(signature) => signature,
98            Err(_) => return false,
99        };
100
101        let message = Event::get_id(
102            pubkey,
103            self.created_at,
104            self.kind,
105            &self.tags,
106            &self.content,
107        );
108
109        if message.to_string() != self.id {
110            return false;
111        }
112
113        secp.verify_schnorr(&signature, &message, &pubkey).is_ok()
114    }
115
116    fn format_tags(tags: &Vec<Vec<String>>) -> String {
117        let mut formatted = String::new();
118
119        for i in 0..tags.len() {
120            let tag = &tags[i];
121            write!(formatted, r#"["{}"]"#, tag.join(r#"",""#)).unwrap();
122            if i + 1 < tags.len() {
123                formatted.push(',');
124            }
125        }
126        formatted
127    }
128
129    fn get_id(
130        pubkey: secp256k1::XOnlyPublicKey,
131        created_at: u64,
132        kind: u64,
133        tags: &Vec<Vec<String>>,
134        content: &String,
135    ) -> secp256k1::Message {
136        let formatted_tags = Self::format_tags(tags);
137
138        let msg = format!(r#"[0,"{pubkey}",{created_at},{kind},[{formatted_tags}],"{content}"]"#);
139        secp256k1::Message::from_hashed_data::<secp256k1::hashes::sha256::Hash>(msg.as_bytes())
140    }
141}
142
143/// Returns tags that can be used to form event that is a reply to `event`.
144///
145/// This takes first "e" tag from the `event` (=root of the thread) and `event`s `id` and alo adds
146/// pubkey of the `event` author to the tags.
147pub fn tags_for_reply(event: Event) -> Vec<Vec<String>> {
148    let mut e_tags = vec![];
149    let mut p_tags = vec![];
150
151    for t in event.tags {
152        if t[0] == "e" {
153            e_tags.push(t);
154        } else if t[0] == "p" {
155            p_tags.push(t);
156        }
157    }
158
159    // Mention only author of the event
160    let mut tags = vec![vec!["p".to_string(), event.pubkey]];
161
162    // First event and event I'm going to reply to
163    if !e_tags.is_empty() {
164        tags.push(e_tags[0].clone());
165    }
166    tags.push(vec!["e".to_string(), event.id]);
167
168    debug!("Returning these tags: {:?}", tags);
169    tags
170}
171
172pub fn get_profile_event(
173    name: Option<String>,
174    about: Option<String>,
175    picture_url: Option<String>,
176) -> EventNonSigned {
177    let name = if let Some(name) = name {
178        name
179    } else {
180        "".to_string()
181    };
182    let about = if let Some(about) = about {
183        about
184    } else {
185        "".to_string()
186    };
187    let picture_url = if let Some(picture_url) = picture_url {
188        picture_url
189    } else {
190        "".to_string()
191    };
192
193    EventNonSigned {
194        created_at: crate::utils::unix_timestamp(),
195        kind: 0,
196        tags: vec![],
197        content: format!(
198            r#"{{"name":"{}","about":"{}","picture":"{}"}}"#,
199            name,
200            escape(about),
201            escape(picture_url)
202        ),
203    }
204}
205
206/// Returns [EventNonSigned] that is a reply to given `reply_to` event with content set to `content`.
207pub fn get_reply(reply_to: Event, content: String) -> EventNonSigned {
208    EventNonSigned {
209        content,
210        created_at: crate::utils::unix_timestamp(),
211        kind: 1,
212        tags: tags_for_reply(reply_to),
213    }
214}
215
216fn escape(text: String) -> String {
217    // See https://github.com/jb55/nostril/blob/master/nostril.c.
218    let mut escaped = String::new();
219    for c in text.chars() {
220        match c {
221            '"' => escaped.push_str("\\\""),
222            '\\' => escaped.push_str("\\\\"),
223            '\n' => escaped.push_str("\\n"),
224            '\r' => escaped.push_str("\\r"),
225            '\t' => escaped.push_str("\\t"),
226            _ => escaped.push(c),
227        }
228    }
229
230    escaped
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    const TEST_SECRET: &str = "67c497012395ded1448b06f4bc55abaa74e1fe8d60c3f635c980547171fb24f9";
237
238    // Valid signature for "nope" message with timestamp 1660659551
239    // Generated with TEST_SECRET, commitment '[0,"1640facabe8bcf73f3b3f85ad60b55154276806cd65226b8cbda680fc9149995",1660659551,1,[],"nope"]'
240    const NOPE_SIG: &str = "af372c8cb342d27d0c2363040b676aa7d68a0d028f09e73b567675f1beb15c29f07c970c7b65e3451f4fcc31e9c855534ad3d4e1a3af0d6bcdbab85c340df8bd";
241
242    fn test_keypair(secret: &str) -> secp256k1::KeyPair {
243        let secp = secp256k1::Secp256k1::new();
244        secp256k1::KeyPair::from_seckey_str(&secp, secret).unwrap()
245    }
246
247    fn get_test_event(keypair: secp256k1::KeyPair) -> Event {
248        Event::new(
249            &keypair,
250            1660656918,
251            1,
252            vec![
253                vec![
254                    "e".to_string(),
255                    "012df9baa5377d7f2b29922249fe36e2fde5daab3060342b93235c9b6db444dc".to_string(),
256                ],
257                vec![
258                    "p".to_string(),
259                    "8b5a3bd6143fc0ad19c80886628097140b8dafd7adc1302444dff8d8645540f8".to_string(),
260                ],
261                vec!["random_tag".to_string(), "just testing here".to_string()],
262            ],
263            "Testing nostr-bot.".to_string(),
264        )
265    }
266
267    #[test]
268    fn new_event() {
269        let keypair = test_keypair(TEST_SECRET);
270        let event = get_test_event(keypair);
271
272        // Check correct id was generated
273        assert_eq!(
274            event.id,
275            "90ce185727fed0c5695e9e20d9c2130bd1d4b8776078016423420d6110d353fe"
276        );
277
278        let signature = secp256k1::schnorr::Signature::from_str(&event.sig).unwrap();
279        let (x_only_public_key, _parity) = keypair.x_only_public_key();
280        let message = Event::get_id(
281            x_only_public_key,
282            event.created_at,
283            event.kind,
284            &event.tags,
285            &event.content,
286        );
287
288        // Check Event::get_id is generating correct ID
289        assert_eq!(message.to_string(), event.id);
290
291        // Verify the ID was signed with the TEST_SECRET
292        let secp = secp256k1::Secp256k1::verification_only();
293        assert!(secp
294            .verify_schnorr(&signature, &message, &x_only_public_key)
295            .is_ok());
296
297        // Now let's put signature (valid but for different message) and see if it's revoked
298        let mut event = event;
299        event.sig = NOPE_SIG.to_string();
300        let signature = secp256k1::schnorr::Signature::from_str(&event.sig).unwrap();
301
302        // This should fail
303        assert!(secp
304            .verify_schnorr(&signature, &message, &x_only_public_key)
305            .is_err());
306    }
307
308    #[test]
309    fn valid_event() {
310        let keypair = test_keypair(TEST_SECRET);
311        let event = get_test_event(keypair);
312
313        assert!(event.has_valid_sig());
314
315        // Changing signature should make the event invalid
316        let mut event = get_test_event(keypair);
317        event.sig = NOPE_SIG.to_string();
318        assert!(!event.has_valid_sig());
319
320        // Now let's put some random values into the original Event and check that it becomes
321        // invalid
322
323        // Changing id
324        let mut event = get_test_event(keypair);
325        event.id = "9892364de48ac0e02cf2fc3c4ddb58f29721bd0024db06495a8f9396710dbe36".to_string();
326        assert!(!event.has_valid_sig());
327
328        let mut event = get_test_event(keypair);
329        event.created_at = 123456789;
330        assert!(!event.has_valid_sig());
331
332        // Changing tags
333        let mut event = get_test_event(keypair);
334        event.tags = vec![vec!["random_tag".to_string(), "hohoho".to_string()]];
335        assert!(!event.has_valid_sig());
336
337        // Changing tags
338        let mut event = get_test_event(keypair);
339        event.tags = vec![vec!["random_tag".to_string(), "hohoho".to_string()]];
340        assert!(!event.has_valid_sig());
341
342        // Changing tags
343        let mut event = get_test_event(keypair);
344        event.content = "Difference content".to_string();
345        assert!(!event.has_valid_sig());
346    }
347}