nostr_types/types/
nevent.rs

1use super::{EventKind, Id, PublicKey, UncheckedUrl};
2use crate::Error;
3use serde::{Deserialize, Serialize};
4#[cfg(feature = "speedy")]
5use speedy::{Readable, Writable};
6
7/// An 'nevent': event id along with some relays in which that event may be found.
8#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
9#[cfg_attr(feature = "speedy", derive(Readable, Writable))]
10pub struct NEvent {
11    /// Event id
12    pub id: Id,
13
14    /// Some of the relays where this could be in
15    pub relays: Vec<UncheckedUrl>,
16
17    /// Kind (optional)
18    #[serde(skip_serializing_if = "Option::is_none")]
19    #[serde(default)]
20    pub kind: Option<EventKind>,
21
22    /// Author (optional)
23    #[serde(skip_serializing_if = "Option::is_none")]
24    #[serde(default)]
25    pub author: Option<PublicKey>,
26}
27
28impl NEvent {
29    /// Export as a bech32 encoded string ("nevent")
30    pub fn as_bech32_string(&self) -> String {
31        // Compose
32        let mut tlv: Vec<u8> = Vec::new();
33
34        // Push Id
35        tlv.push(0); // the special value, in this case the id
36        tlv.push(32); // the length of the value (always 32 for id)
37        tlv.extend(self.id.0);
38
39        // Push relays
40        for relay in &self.relays {
41            tlv.push(1); // type 'relay'
42            let len = relay.0.len() as u8;
43            tlv.push(len); // the length of the string
44            tlv.extend(&relay.0.as_bytes()[..len as usize]);
45        }
46
47        // Maybe Push kind
48        if let Some(kind) = self.kind {
49            let kindnum: u32 = From::from(kind);
50            let bytes = kindnum.to_be_bytes();
51            tlv.push(3); // type 'kind'
52            tlv.push(bytes.len() as u8); // '4'
53            tlv.extend(bytes);
54        }
55
56        // Maybe Push author
57        if let Some(pubkey) = self.author {
58            tlv.push(2); // type 'author'
59            tlv.push(32); // the length of the value (always 32 for public key)
60            tlv.extend(pubkey.as_bytes());
61        }
62
63        bech32::encode::<bech32::Bech32>(*crate::HRP_NEVENT, &tlv).unwrap()
64    }
65
66    /// Import from a bech32 encoded string ("nevent")
67    pub fn try_from_bech32_string(s: &str) -> Result<NEvent, Error> {
68        let data = bech32::decode(s)?;
69        if data.0 != *crate::HRP_NEVENT {
70            Err(Error::WrongBech32(
71                crate::HRP_NEVENT.to_lowercase(),
72                data.0.to_lowercase(),
73            ))
74        } else {
75            let mut relays: Vec<UncheckedUrl> = Vec::new();
76            let mut id: Option<Id> = None;
77            let mut kind: Option<EventKind> = None;
78            let mut author: Option<PublicKey> = None;
79
80            let tlv = data.1;
81            let mut pos = 0;
82            loop {
83                // we need at least 2 more characters for anything meaningful
84                if pos > tlv.len() - 2 {
85                    break;
86                }
87                let ty = tlv[pos];
88                let len = tlv[pos + 1] as usize;
89                pos += 2;
90                if pos + len > tlv.len() {
91                    return Err(Error::InvalidProfile);
92                }
93                let raw = &tlv[pos..pos + len];
94                match ty {
95                    0 => {
96                        // special (32 bytes of id)
97                        if len != 32 {
98                            return Err(Error::InvalidNEvent);
99                        }
100                        id = Some(Id(raw
101                            .try_into()
102                            .map_err(|_| Error::WrongLengthHexString)?));
103                    }
104                    1 => {
105                        // relay
106                        let relay_str = std::str::from_utf8(raw)?;
107                        let relay = UncheckedUrl::from_str(relay_str);
108                        relays.push(relay);
109                    }
110                    2 => {
111                        // author
112                        //
113                        // Don't fail if the pubkey is bad, just don't include it.
114                        // Some client is generating these, and we want to tolerate it
115                        // as much as we can.
116                        if let Ok(pk) = PublicKey::from_bytes(raw, true) {
117                            author = Some(pk);
118                        }
119                    }
120                    3 => {
121                        // kind
122                        let kindnum = u32::from_be_bytes(
123                            raw.try_into().map_err(|_| Error::WrongLengthKindBytes)?,
124                        );
125                        kind = Some(kindnum.into());
126                    }
127                    _ => {} // unhandled type for nprofile
128                }
129                pos += len;
130            }
131            if let Some(id) = id {
132                Ok(NEvent {
133                    id,
134                    relays,
135                    kind,
136                    author,
137                })
138            } else {
139                Err(Error::InvalidNEvent)
140            }
141        }
142    }
143
144    // Mock data for testing
145    #[allow(dead_code)]
146    pub(crate) fn mock() -> NEvent {
147        let id = Id::try_from_hex_string(
148            "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9",
149        )
150        .unwrap();
151
152        NEvent {
153            id,
154            relays: vec![
155                UncheckedUrl::from_str("wss://relay.example.com"),
156                UncheckedUrl::from_str("wss://relay2.example.com"),
157            ],
158            kind: None,
159            author: None,
160        }
161    }
162}
163
164#[cfg(test)]
165mod test {
166    use super::*;
167
168    test_serde! {NEvent, test_nevent_serde}
169
170    #[test]
171    fn test_profile_bech32() {
172        let bech32 = NEvent::mock().as_bech32_string();
173        println!("{bech32}");
174        assert_eq!(
175            NEvent::mock(),
176            NEvent::try_from_bech32_string(&bech32).unwrap()
177        );
178    }
179
180    #[test]
181    fn test_nip19_example() {
182        let nevent = NEvent {
183            id: Id::try_from_hex_string(
184                "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
185            )
186            .unwrap(),
187            relays: vec![
188                UncheckedUrl::from_str("wss://r.x.com"),
189                UncheckedUrl::from_str("wss://djbas.sadkb.com"),
190            ],
191            kind: None,
192            author: None,
193        };
194
195        // As serialized by us (not necessarily in the order others would do it)
196        let bech32 = "nevent1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaks343fay";
197
198        // Try converting profile to bech32
199        assert_eq!(nevent.as_bech32_string(), bech32);
200
201        // Try converting bech32 to profile
202        assert_eq!(nevent, NEvent::try_from_bech32_string(bech32).unwrap());
203
204        // Try this one that used to fail
205        let bech32 =
206            "nevent1qqstxx3lk7zqfyn8cyyptvujfxq9w6mad4205x54772tdkmyqaay9scrqsqqqpp8x4vwhf";
207        let _ = NEvent::try_from_bech32_string(bech32).unwrap();
208        // it won't be equal, but should have the basics and should not error.
209    }
210
211    #[test]
212    fn test_nevent_alt_fields() {
213        let nevent = NEvent {
214            id: Id::try_from_hex_string(
215                "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
216            )
217            .unwrap(),
218            relays: vec![
219                UncheckedUrl::from_str("wss://r.x.com"),
220                UncheckedUrl::from_str("wss://djbas.sadkb.com"),
221            ],
222            kind: Some(EventKind::TextNote),
223            author: Some(
224                PublicKey::try_from_hex_string(
225                    "000000000332c7831d9c5a99f183afc2813a6f69a16edda7f6fc0ed8110566e6",
226                    true,
227                )
228                .unwrap(),
229            ),
230        };
231
232        // As serialized by us (not necessarily in the order others would do it)
233        let bech32 = "nevent1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksxpqqqqqqzq3qqqqqqqqrxtrcx8vut2vlrqa0c2qn5mmf59hdmflkls8dsyg9vmnqu25v0j";
234
235        // Try converting profile to bech32
236        assert_eq!(nevent.as_bech32_string(), bech32);
237
238        // Try converting bech32 to profile
239        assert_eq!(nevent, NEvent::try_from_bech32_string(bech32).unwrap());
240    }
241
242    #[test]
243    fn test_ones_that_were_failing() {
244        let bech32 = "nevent1qqswrqr63ddwk8l3zfqrgdxh2lxh2jlcxl36k3h33g25gtchzchx8agpp4mhxue69uhkummn9ekx7mqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3yamnwvaz7tm0venxx6rpd9hzuur4vgpyqdmyxs6rzdmyx4jxvdpnx4snjdmz8pnr2dtr8pnryefhv5ex2e34xvek2v3nxuckxef4v5ckxenxvs6njdtrxymnjcfnv4skvvekvs6qfe99uy";
245
246        let _ne = NEvent::try_from_bech32_string(bech32).unwrap();
247    }
248}