Skip to main content

nostr_types/types/
metadata.rs

1use bech32::FromBase32;
2use serde::de::{Deserialize, Deserializer, MapAccess, Visitor};
3use serde::ser::{Serialize, SerializeMap, Serializer};
4use serde_json::{json, Map, Value};
5use std::fmt;
6
7/// Metadata about a user
8///
9/// Note: the value is an Option because some real-world data has been found to
10/// contain JSON nulls as values, and we don't want deserialization of those
11/// events to fail. We treat these in our get() function the same as if the key
12/// did not exist.
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct Metadata {
15    /// username
16    pub name: Option<String>,
17
18    /// about
19    pub about: Option<String>,
20
21    /// picture URL
22    pub picture: Option<String>,
23
24    /// nip05 dns id
25    pub nip05: Option<String>,
26
27    /// Additional fields not specified in NIP-01 or NIP-05
28    pub other: Map<String, Value>,
29}
30
31impl Default for Metadata {
32    fn default() -> Self {
33        Metadata {
34            name: None,
35            about: None,
36            picture: None,
37            nip05: None,
38            other: Map::new(),
39        }
40    }
41}
42
43impl Metadata {
44    /// Create new empty Metadata
45    pub fn new() -> Metadata {
46        Metadata::default()
47    }
48
49    #[allow(dead_code)]
50    pub(crate) fn mock() -> Metadata {
51        let mut map = Map::new();
52        let _ = map.insert(
53            "display_name".to_string(),
54            Value::String("William Caserin".to_string()),
55        );
56        Metadata {
57            name: Some("jb55".to_owned()),
58            about: None,
59            picture: None,
60            nip05: Some("jb55.com".to_owned()),
61            other: map,
62        }
63    }
64
65    /// Get the lnurl for the user, if available via lud06 or lud16
66    pub fn lnurl(&self) -> Option<String> {
67        if let Some(serde_json::Value::String(lud06)) = self.other.get("lud06") {
68            if let Ok(data) = bech32::decode(lud06) {
69                if data.0 == "lnurl" {
70                    if let Ok(decoded) = Vec::<u8>::from_base32(&data.1) {
71                        return Some(String::from_utf8_lossy(&decoded).to_string());
72                    }
73                }
74            }
75        }
76
77        if let Some(serde_json::Value::String(lud16)) = self.other.get("lud16") {
78            let vec: Vec<&str> = lud16.split('@').collect();
79            if vec.len() == 2 {
80                let user = &vec[0];
81                let domain = &vec[1];
82                return Some(format!("https://{domain}/.well-known/lnurlp/{user}"));
83            }
84        }
85
86        None
87    }
88}
89
90impl Serialize for Metadata {
91    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
92    where
93        S: Serializer,
94    {
95        let mut map = serializer.serialize_map(Some(4 + self.other.len()))?;
96        map.serialize_entry("name", &json!(&self.name))?;
97        map.serialize_entry("about", &json!(&self.about))?;
98        map.serialize_entry("picture", &json!(&self.picture))?;
99        map.serialize_entry("nip05", &json!(&self.nip05))?;
100        for (k, v) in &self.other {
101            map.serialize_entry(&k, &v)?;
102        }
103        map.end()
104    }
105}
106
107impl<'de> Deserialize<'de> for Metadata {
108    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
109    where
110        D: Deserializer<'de>,
111    {
112        deserializer.deserialize_map(MetadataVisitor)
113    }
114}
115
116struct MetadataVisitor;
117
118impl<'de> Visitor<'de> for MetadataVisitor {
119    type Value = Metadata;
120
121    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
122        write!(f, "A JSON object")
123    }
124
125    fn visit_map<M>(self, mut access: M) -> Result<Metadata, M::Error>
126    where
127        M: MapAccess<'de>,
128    {
129        let mut map: Map<String, Value> = Map::new();
130        while let Some((key, value)) = access.next_entry::<String, Value>()? {
131            let _ = map.insert(key, value);
132        }
133
134        let mut m: Metadata = Default::default();
135
136        if let Some(Value::String(s)) = map.remove("name") {
137            m.name = Some(s);
138        }
139        if let Some(Value::String(s)) = map.remove("about") {
140            m.about = Some(s);
141        }
142        if let Some(Value::String(s)) = map.remove("picture") {
143            m.picture = Some(s);
144        }
145        if let Some(Value::String(s)) = map.remove("nip05") {
146            m.nip05 = Some(s);
147        }
148
149        m.other = map;
150
151        Ok(m)
152    }
153}
154
155#[cfg(test)]
156mod test {
157    use super::*;
158
159    test_serde! {Metadata, test_metadata_serde}
160
161    #[test]
162    fn test_metadata_print_json() {
163        // I want to see if JSON serialized metadata is network appropriate
164        let m = Metadata::mock();
165        println!("{}", serde_json::to_string(&m).unwrap());
166    }
167
168    #[test]
169    fn test_tolerate_nulls() {
170        let json = r##"{"name":"monlovesmango","picture":"https://astral.ninja/aura/monlovesmango.svg","about":"building on nostr","nip05":"monlovesmango@astral.ninja","lud06":null,"testing":"123"}"##;
171        let m: Metadata = serde_json::from_str(&json).unwrap();
172        assert_eq!(m.name, Some("monlovesmango".to_owned()));
173        assert_eq!(m.other.get("lud06"), Some(&Value::Null));
174        assert_eq!(
175            m.other.get("testing"),
176            Some(&Value::String("123".to_owned()))
177        );
178    }
179
180    #[test]
181    fn test_metadata_lnurls() {
182        // test lud06
183        let json = r##"{"name":"mikedilger","about":"Author of Gossip client: https://github.com/mikedilger/gossip\nexpat American living in New Zealand","picture":"https://avatars.githubusercontent.com/u/1669069","nip05":"_@mikedilger.com","banner":"https://mikedilger.com/banner.jpg","display_name":"Michael Dilger","location":"New Zealand","lud06":"lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhkgetrv4h8gcn4dccnxv563ep","website":"https://mikedilger.com"}"##;
184        let m: Metadata = serde_json::from_str(&json).unwrap();
185        assert_eq!(
186            m.lnurl().as_deref(),
187            Some("https://walletofsatoshi.com/.well-known/lnurlp/decentbun13")
188        );
189
190        // test lud16
191        let json = r##"{"name":"mikedilger","about":"Author of Gossip client: https://github.com/mikedilger/gossip\nexpat American living in New Zealand","picture":"https://avatars.githubusercontent.com/u/1669069","nip05":"_@mikedilger.com","banner":"https://mikedilger.com/banner.jpg","display_name":"Michael Dilger","location":"New Zealand","lud16":"decentbun13@walletofsatoshi.com","website":"https://mikedilger.com"}"##;
192        let m: Metadata = serde_json::from_str(&json).unwrap();
193        assert_eq!(
194            m.lnurl().as_deref(),
195            Some("https://walletofsatoshi.com/.well-known/lnurlp/decentbun13")
196        );
197    }
198}