nostr_types/types/
tag.rs

1use crate::{DelegationConditions, Id, PublicKeyHex, SignatureHex, UncheckedUrl, Unixtime};
2use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor};
3use serde::ser::{Serialize, SerializeSeq, Serializer};
4use std::fmt;
5
6/// A tag on an Event
7#[derive(Clone, Debug, Eq, PartialEq)]
8pub enum Tag {
9    /// Content Warning to alert client to hide content until user approves
10    ContentWarning(String),
11
12    /// Delegation (Delegated Event Signing)
13    Delegation {
14        /// Public key of the delegator
15        pubkey: PublicKeyHex,
16
17        /// Conditions query string
18        conditions: DelegationConditions,
19
20        /// 64-byte schnorr signature of the sha256 hash of the delegation token
21        sig: SignatureHex,
22    },
23
24    /// This is a reference to an event, where the first string is the event Id.
25    /// The second string is defined in NIP-01 as an optional URL, but subsequent
26    /// 'e' NIPs define more data and interpretations.
27    Event {
28        /// The Id of some other event that this event refers to
29        id: Id,
30
31        /// A recommended relay URL to find that other event
32        recommended_relay_url: Option<UncheckedUrl>,
33
34        /// A marker (commonly things like 'reply')
35        marker: Option<String>,
36    },
37
38    /// A time when the event should be considered expired
39    Expiration(Unixtime),
40
41    /// 'p' This is a reference to a user by public key, where the first string is
42    /// the PublicKey. The second string is defined in NIP-01 as an optional URL,
43    /// but subsqeuent NIPs define more data and interpretations.
44    Pubkey {
45        /// The public key of the identity that this event refers to
46        pubkey: PublicKeyHex,
47
48        /// A recommended relay URL to find information on that public key
49        recommended_relay_url: Option<UncheckedUrl>,
50
51        /// A petname given to this identity by the event author
52        petname: Option<String>,
53    },
54
55    /// 't' A hashtag
56    Hashtag(String),
57
58    /// 'r' A reference to a URL
59    Reference {
60        /// A relay url
61        url: UncheckedUrl,
62
63        /// An optional marker
64        marker: Option<String>,
65    },
66
67    /// 'g' A geohash
68    Geohash(String),
69
70    /// A subject. The first string is the subject. Should only be in TextNote events.
71    Subject(String),
72
73    /// A nonce tag for Proof of Work
74    Nonce {
75        /// A random number that makes the event hash meet the proof of work required
76        nonce: String,
77
78        /// The target number of bits for the proof of work
79        target: Option<String>,
80    },
81
82    /// Parameter of a parameterized replaceable event
83    Parameter(String),
84
85    /// Any other tag
86    Other {
87        /// The tag name
88        tag: String,
89
90        /// The subsequent fields
91        data: Vec<String>,
92    },
93
94    /// An empty array (kept so signature remains valid across ser/de)
95    Empty,
96}
97
98impl Tag {
99    /// Get the tag name for the tag (the first string in the array)a
100    pub fn tagname(&self) -> String {
101        match self {
102            Tag::ContentWarning(_) => "content-warning".to_string(),
103            Tag::Delegation { .. } => "delegation".to_string(),
104            Tag::Event { .. } => "e".to_string(),
105            Tag::Expiration(_) => "expiration".to_string(),
106            Tag::Pubkey { .. } => "p".to_string(),
107            Tag::Hashtag(_) => "t".to_string(),
108            Tag::Reference { .. } => "r".to_string(),
109            Tag::Geohash(_) => "g".to_string(),
110            Tag::Subject(_) => "subject".to_string(),
111            Tag::Nonce { .. } => "nonce".to_string(),
112            Tag::Parameter(_) => "parameter".to_string(),
113            Tag::Other { tag, .. } => tag.clone(),
114            Tag::Empty => panic!("empty tags have no tagname"),
115        }
116    }
117
118    // Mock data for testing
119    #[allow(dead_code)]
120    pub(crate) fn mock() -> Tag {
121        Tag::Event {
122            id: Id::mock(),
123            recommended_relay_url: Some(UncheckedUrl::mock()),
124            marker: None,
125        }
126    }
127}
128
129impl Serialize for Tag {
130    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
131    where
132        S: Serializer,
133    {
134        match self {
135            Tag::ContentWarning(msg) => {
136                let mut seq = serializer.serialize_seq(None)?;
137                seq.serialize_element("content-warning")?;
138                seq.serialize_element(msg)?;
139                seq.end()
140            }
141            Tag::Delegation {
142                pubkey,
143                conditions,
144                sig,
145            } => {
146                let mut seq = serializer.serialize_seq(None)?;
147                seq.serialize_element("delegation")?;
148                seq.serialize_element(pubkey)?;
149                seq.serialize_element(conditions)?;
150                seq.serialize_element(sig)?;
151                seq.end()
152            }
153            Tag::Event {
154                id,
155                recommended_relay_url,
156                marker,
157            } => {
158                let mut seq = serializer.serialize_seq(None)?;
159                seq.serialize_element("e")?;
160                seq.serialize_element(id)?;
161                if let Some(rru) = recommended_relay_url {
162                    seq.serialize_element(rru)?;
163                } else if marker.is_some() {
164                    seq.serialize_element("")?;
165                }
166                if let Some(m) = marker {
167                    seq.serialize_element(m)?;
168                }
169                seq.end()
170            }
171            Tag::Expiration(time) => {
172                let mut seq = serializer.serialize_seq(None)?;
173                seq.serialize_element("expiration")?;
174                seq.serialize_element(time)?;
175                seq.end()
176            }
177            Tag::Pubkey {
178                pubkey,
179                recommended_relay_url,
180                petname,
181            } => {
182                let mut seq = serializer.serialize_seq(None)?;
183                seq.serialize_element("p")?;
184                seq.serialize_element(pubkey)?;
185                if let Some(rru) = recommended_relay_url {
186                    seq.serialize_element(rru)?;
187                } else if petname.is_some() {
188                    seq.serialize_element("")?;
189                }
190                if let Some(pn) = petname {
191                    seq.serialize_element(pn)?;
192                }
193                seq.end()
194            }
195            Tag::Hashtag(hashtag) => {
196                let mut seq = serializer.serialize_seq(None)?;
197                seq.serialize_element("t")?;
198                seq.serialize_element(hashtag)?;
199                seq.end()
200            }
201            Tag::Reference { url, marker } => {
202                let mut seq = serializer.serialize_seq(None)?;
203                seq.serialize_element("r")?;
204                seq.serialize_element(url)?;
205                if let Some(m) = marker {
206                    seq.serialize_element(m)?
207                }
208                seq.end()
209            }
210            Tag::Geohash(geohash) => {
211                let mut seq = serializer.serialize_seq(None)?;
212                seq.serialize_element("g")?;
213                seq.serialize_element(geohash)?;
214                seq.end()
215            }
216            Tag::Subject(subject) => {
217                let mut seq = serializer.serialize_seq(None)?;
218                seq.serialize_element("subject")?;
219                seq.serialize_element(subject)?;
220                seq.end()
221            }
222            Tag::Nonce { nonce, target } => {
223                let mut seq = serializer.serialize_seq(None)?;
224                seq.serialize_element("nonce")?;
225                seq.serialize_element(nonce)?;
226                if let Some(t) = target {
227                    seq.serialize_element(t)?;
228                }
229                seq.end()
230            }
231            Tag::Parameter(parameter) => {
232                let mut seq = serializer.serialize_seq(None)?;
233                seq.serialize_element("parameter")?;
234                seq.serialize_element(parameter)?;
235                seq.end()
236            }
237            Tag::Other { tag, data } => {
238                let mut seq = serializer.serialize_seq(None)?;
239                seq.serialize_element(tag)?;
240                for s in data.iter() {
241                    seq.serialize_element(s)?;
242                }
243                seq.end()
244            }
245            Tag::Empty => {
246                let seq = serializer.serialize_seq(Some(0))?;
247                seq.end()
248            }
249        }
250    }
251}
252
253impl<'de> Deserialize<'de> for Tag {
254    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
255    where
256        D: Deserializer<'de>,
257    {
258        deserializer.deserialize_seq(TagVisitor)
259    }
260}
261
262struct TagVisitor;
263
264impl<'de> Visitor<'de> for TagVisitor {
265    type Value = Tag;
266
267    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
268        write!(f, "a sequence of strings")
269    }
270
271    fn visit_seq<A>(self, mut seq: A) -> Result<Tag, A::Error>
272    where
273        A: SeqAccess<'de>,
274    {
275        let tagname: &str = match seq.next_element()? {
276            Some(e) => e,
277            None => return Ok(Tag::Empty),
278        };
279        if tagname == "content-warning" {
280            let msg = match seq.next_element()? {
281                Some(s) => s,
282                None => {
283                    return Ok(Tag::Other {
284                        tag: tagname.to_string(),
285                        data: vec![],
286                    });
287                }
288            };
289            Ok(Tag::ContentWarning(msg))
290        } else if tagname == "delegation" {
291            let pubkey: PublicKeyHex = match seq.next_element()? {
292                Some(pk) => pk,
293                None => {
294                    return Ok(Tag::Other {
295                        tag: tagname.to_string(),
296                        data: vec![],
297                    });
298                }
299            };
300            let conditions: DelegationConditions = match seq.next_element()? {
301                Some(c) => c,
302                None => {
303                    return Ok(Tag::Other {
304                        tag: tagname.to_string(),
305                        data: vec![pubkey.into_string()],
306                    });
307                }
308            };
309            let sig: SignatureHex = match seq.next_element()? {
310                Some(s) => s,
311                None => {
312                    return Ok(Tag::Other {
313                        tag: tagname.to_string(),
314                        data: vec![pubkey.into_string(), conditions.as_string()],
315                    });
316                }
317            };
318            Ok(Tag::Delegation {
319                pubkey,
320                conditions,
321                sig,
322            })
323        } else if tagname == "e" {
324            let id: Id = match seq.next_element()? {
325                Some(id) => id,
326                None => {
327                    return Ok(Tag::Other {
328                        tag: tagname.to_string(),
329                        data: vec![],
330                    });
331                }
332            };
333            let recommended_relay_url: Option<UncheckedUrl> = seq.next_element()?;
334            let marker: Option<String> = seq.next_element()?;
335            Ok(Tag::Event {
336                id,
337                recommended_relay_url,
338                marker,
339            })
340        } else if tagname == "expiration" {
341            let time = match seq.next_element()? {
342                Some(t) => t,
343                None => {
344                    return Ok(Tag::Other {
345                        tag: tagname.to_string(),
346                        data: vec![],
347                    });
348                }
349            };
350            Ok(Tag::Expiration(time))
351        } else if tagname == "p" {
352            let pubkey: PublicKeyHex = match seq.next_element()? {
353                Some(p) => p,
354                None => {
355                    return Ok(Tag::Other {
356                        tag: tagname.to_string(),
357                        data: vec![],
358                    });
359                }
360            };
361            let recommended_relay_url: Option<UncheckedUrl> = seq.next_element()?;
362            let petname: Option<String> = seq.next_element()?;
363            Ok(Tag::Pubkey {
364                pubkey,
365                recommended_relay_url,
366                petname,
367            })
368        } else if tagname == "t" {
369            let tag = match seq.next_element()? {
370                Some(t) => t,
371                None => {
372                    return Ok(Tag::Other {
373                        tag: tagname.to_string(),
374                        data: vec![],
375                    });
376                }
377            };
378            Ok(Tag::Hashtag(tag))
379        } else if tagname == "r" {
380            let refr: UncheckedUrl = match seq.next_element()? {
381                Some(r) => r,
382                None => {
383                    return Ok(Tag::Other {
384                        tag: tagname.to_string(),
385                        data: vec![],
386                    });
387                }
388            };
389            let marker: Option<String> = seq.next_element()?;
390            Ok(Tag::Reference { url: refr, marker })
391        } else if tagname == "g" {
392            let geo = match seq.next_element()? {
393                Some(g) => g,
394                None => {
395                    return Ok(Tag::Other {
396                        tag: tagname.to_string(),
397                        data: vec![],
398                    });
399                }
400            };
401            Ok(Tag::Geohash(geo))
402        } else if tagname == "subject" {
403            let sub = match seq.next_element()? {
404                Some(s) => s,
405                None => {
406                    return Ok(Tag::Other {
407                        tag: tagname.to_string(),
408                        data: vec![],
409                    });
410                }
411            };
412            Ok(Tag::Subject(sub))
413        } else if tagname == "nonce" {
414            let nonce = match seq.next_element()? {
415                Some(n) => n,
416                None => {
417                    return Ok(Tag::Other {
418                        tag: tagname.to_string(),
419                        data: vec![],
420                    });
421                }
422            };
423            let target: Option<String> = seq.next_element()?;
424            Ok(Tag::Nonce { nonce, target })
425        } else if tagname == "parameter" {
426            let param = match seq.next_element()? {
427                Some(s) => s,
428                None => "".to_owned(), // implicit parameter
429            };
430            Ok(Tag::Parameter(param))
431        } else {
432            let mut data = Vec::new();
433            loop {
434                match seq.next_element()? {
435                    None => {
436                        return Ok(Tag::Other {
437                            tag: tagname.to_string(),
438                            data,
439                        })
440                    }
441                    Some(s) => data.push(s),
442                }
443            }
444        }
445    }
446}
447
448#[cfg(test)]
449mod test {
450    use super::*;
451
452    test_serde! {Tag, test_tag_serde}
453}