nostr_types/versioned/
metadata2.rs

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