1use serde::de::{Deserialize, Deserializer, MapAccess, Visitor};
2use serde::ser::{Serialize, SerializeMap, Serializer};
3use serde_json::{json, Map, Value};
4use std::fmt;
5
6#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct MetadataV2 {
14 pub name: Option<String>,
16
17 pub about: Option<String>,
19
20 pub picture: Option<String>,
22
23 pub nip05: Option<String>,
25
26 pub fields: Vec<(String, String)>,
28
29 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 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 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 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 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 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}