nostr_types/versioned/
metadata1.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 MetadataV1 {
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    /// Additional fields not specified in NIP-01 or NIP-05
27    pub other: Map<String, Value>,
28}
29
30impl Default for MetadataV1 {
31    fn default() -> Self {
32        MetadataV1 {
33            name: None,
34            about: None,
35            picture: None,
36            nip05: None,
37            other: Map::new(),
38        }
39    }
40}
41
42impl MetadataV1 {
43    /// Create new empty Metadata
44    pub fn new() -> MetadataV1 {
45        MetadataV1::default()
46    }
47
48    #[allow(dead_code)]
49    pub(crate) fn mock() -> MetadataV1 {
50        let mut map = Map::new();
51        let _ = map.insert(
52            "display_name".to_string(),
53            Value::String("William Caserin".to_string()),
54        );
55        MetadataV1 {
56            name: Some("jb55".to_owned()),
57            about: None,
58            picture: None,
59            nip05: Some("jb55.com".to_owned()),
60            other: map,
61        }
62    }
63
64    /// Get the lnurl for the user, if available via lud06 or lud16
65    pub fn lnurl(&self) -> Option<String> {
66        if let Some(Value::String(lud06)) = self.other.get("lud06") {
67            if let Ok(data) = bech32::decode(lud06) {
68                if data.0 == *crate::HRP_LNURL {
69                    return Some(String::from_utf8_lossy(&data.1).to_string());
70                }
71            }
72        }
73
74        if let Some(Value::String(lud16)) = self.other.get("lud16") {
75            let vec: Vec<&str> = lud16.split('@').collect();
76            if vec.len() == 2 {
77                let user = &vec[0];
78                let domain = &vec[1];
79                return Some(format!("https://{domain}/.well-known/lnurlp/{user}"));
80            }
81        }
82
83        None
84    }
85}
86
87impl Serialize for MetadataV1 {
88    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
89    where
90        S: Serializer,
91    {
92        let mut map = serializer.serialize_map(Some(4 + self.other.len()))?;
93        map.serialize_entry("name", &json!(&self.name))?;
94        map.serialize_entry("about", &json!(&self.about))?;
95        map.serialize_entry("picture", &json!(&self.picture))?;
96        map.serialize_entry("nip05", &json!(&self.nip05))?;
97        for (k, v) in &self.other {
98            map.serialize_entry(&k, &v)?;
99        }
100        map.end()
101    }
102}
103
104impl<'de> Deserialize<'de> for MetadataV1 {
105    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
106    where
107        D: Deserializer<'de>,
108    {
109        deserializer.deserialize_map(MetadataV1Visitor)
110    }
111}
112
113struct MetadataV1Visitor;
114
115impl<'de> Visitor<'de> for MetadataV1Visitor {
116    type Value = MetadataV1;
117
118    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
119        write!(f, "A JSON object")
120    }
121
122    fn visit_map<M>(self, mut access: M) -> Result<MetadataV1, M::Error>
123    where
124        M: MapAccess<'de>,
125    {
126        let mut map: Map<String, Value> = Map::new();
127        while let Some((key, value)) = access.next_entry::<String, Value>()? {
128            let _ = map.insert(key, value);
129        }
130
131        let mut m: MetadataV1 = Default::default();
132
133        if let Some(Value::String(s)) = map.remove("name") {
134            m.name = Some(s);
135        }
136        if let Some(Value::String(s)) = map.remove("about") {
137            m.about = Some(s);
138        }
139        if let Some(Value::String(s)) = map.remove("picture") {
140            m.picture = Some(s);
141        }
142        if let Some(Value::String(s)) = map.remove("nip05") {
143            m.nip05 = Some(s);
144        }
145
146        m.other = map;
147
148        Ok(m)
149    }
150}
151
152#[cfg(test)]
153mod test {
154    use super::*;
155
156    test_serde! {MetadataV1, test_metadata_serde}
157
158    #[test]
159    fn test_metadata_print_json() {
160        // I want to see if JSON serialized metadata is network appropriate
161        let m = MetadataV1::mock();
162        println!("{}", serde_json::to_string(&m).unwrap());
163    }
164
165    #[test]
166    fn test_tolerate_nulls() {
167        let json = r##"{"name":"monlovesmango","picture":"https://astral.ninja/aura/monlovesmango.svg","about":"building on nostr","nip05":"monlovesmango@astral.ninja","lud06":null,"testing":"123"}"##;
168        let m: MetadataV1 = serde_json::from_str(json).unwrap();
169        assert_eq!(m.name, Some("monlovesmango".to_owned()));
170        assert_eq!(m.other.get("lud06"), Some(&Value::Null));
171        assert_eq!(
172            m.other.get("testing"),
173            Some(&Value::String("123".to_owned()))
174        );
175    }
176
177    #[test]
178    fn test_metadata_lnurls() {
179        // test lud06
180        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"}"##;
181        let m: MetadataV1 = serde_json::from_str(json).unwrap();
182        assert_eq!(
183            m.lnurl().as_deref(),
184            Some("https://walletofsatoshi.com/.well-known/lnurlp/decentbun13")
185        );
186
187        // test lud16
188        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"}"##;
189        let m: MetadataV1 = serde_json::from_str(json).unwrap();
190        assert_eq!(
191            m.lnurl().as_deref(),
192            Some("https://walletofsatoshi.com/.well-known/lnurlp/decentbun13")
193        );
194    }
195}