nostr_types/types/
metadata.rs1use 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#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct Metadata {
15 pub name: Option<String>,
17
18 pub about: Option<String>,
20
21 pub picture: Option<String>,
23
24 pub nip05: Option<String>,
26
27 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 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 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 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 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 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}