1use crate::{Event, EventKind, PreEvent, PublicKey, Tag, UncheckedUrl, Unixtime};
2
3#[derive(Clone, Debug, Hash, PartialEq)]
5pub struct FileMetadata {
6 pub url: UncheckedUrl,
8
9 pub m: Option<String>,
11
12 pub x: Option<String>,
14
15 pub ox: Option<String>,
17
18 pub size: Option<u64>,
20
21 pub dim: Option<(usize, usize)>,
23
24 pub magnet: Option<UncheckedUrl>,
26
27 pub i: Option<String>,
29
30 pub blurhash: Option<String>,
32
33 pub thumb: Option<UncheckedUrl>,
35
36 pub image: Option<UncheckedUrl>,
38
39 pub summary: Option<String>,
41
42 pub alt: Option<String>,
44
45 pub fallback: Vec<UncheckedUrl>,
47
48 pub service: Option<String>,
50}
51
52impl FileMetadata {
53 pub fn new(url: UncheckedUrl) -> FileMetadata {
55 FileMetadata {
56 url,
57 m: None,
58 x: None,
59 ox: None,
60 size: None,
61 dim: None,
62 magnet: None,
63 i: None,
64 blurhash: None,
65 thumb: None,
66 image: None,
67 summary: None,
68 alt: None,
69 fallback: vec![],
70 service: None,
71 }
72 }
73
74 pub fn to_nip94_preevent(&self, pubkey: PublicKey) -> PreEvent {
76 let mut tags = vec![Tag::new(&["url", &self.url.0])];
77
78 if let Some(m) = &self.m {
79 tags.push(Tag::new(&["m", m]));
80 }
81
82 if let Some(x) = &self.x {
83 tags.push(Tag::new(&["x", x]));
84 }
85
86 if let Some(ox) = &self.ox {
87 tags.push(Tag::new(&["ox", ox]));
88 }
89
90 if let Some(size) = self.size {
91 tags.push(Tag::new(&["size", &format!("{size}")]));
92 }
93
94 if let Some(dim) = self.dim {
95 tags.push(Tag::new(&["dim", &format!("{}x{}", dim.0, dim.1)]));
96 }
97
98 if let Some(magnet) = &self.magnet {
99 tags.push(Tag::new(&["magnet", &magnet.0]));
100 }
101
102 if let Some(i) = &self.i {
103 tags.push(Tag::new(&["i", i]));
104 }
105
106 if let Some(blurhash) = &self.blurhash {
107 tags.push(Tag::new(&["blurhash", blurhash]));
108 }
109
110 if let Some(thumb) = &self.thumb {
111 tags.push(Tag::new(&["thumb", &thumb.0]));
112 }
113
114 if let Some(image) = &self.image {
115 tags.push(Tag::new(&["image", &image.0]));
116 }
117
118 if let Some(summary) = &self.summary {
119 tags.push(Tag::new(&["summary", summary]));
120 }
121
122 if let Some(alt) = &self.alt {
123 tags.push(Tag::new(&["alt", alt]));
124 }
125
126 for fallback in &self.fallback {
127 tags.push(Tag::new(&["fallback", &fallback.0]));
128 }
129
130 if let Some(service) = &self.service {
131 tags.push(Tag::new(&["service", service]));
132 }
133
134 PreEvent {
135 pubkey,
136 created_at: Unixtime::now(),
137 kind: EventKind::FileMetadata,
138 content: "".to_owned(),
139 tags,
140 }
141 }
142
143 pub fn from_nip94_event(event: &Event) -> Option<FileMetadata> {
145 if event.kind != EventKind::FileMetadata {
146 return None;
147 }
148
149 let mut fm = FileMetadata::new(UncheckedUrl("".to_owned()));
150
151 for tag in &event.tags {
152 match tag.tagname() {
153 "url" => fm.url = UncheckedUrl(tag.value().to_owned()),
154 "m" => fm.m = Some(tag.value().to_owned()),
155 "x" => fm.x = Some(tag.value().to_owned()),
156 "ox" => fm.ox = Some(tag.value().to_owned()),
157 "size" => {
158 if let Ok(u) = tag.value().parse::<u64>() {
159 fm.size = Some(u);
160 }
161 }
162 "dim" => {
163 let parts: Vec<&str> = tag.value().split('x').collect();
164 if parts.len() == 2 {
165 if let Ok(w) = parts[0].parse::<usize>() {
166 if let Ok(h) = parts[1].parse::<usize>() {
167 fm.dim = Some((w, h));
168 }
169 }
170 }
171 }
172 "magnet" => fm.magnet = Some(UncheckedUrl(tag.value().to_owned())),
173 "i" => fm.i = Some(tag.value().to_owned()),
174 "blurhash" => fm.blurhash = Some(tag.value().to_owned()),
175 "thumb" => fm.thumb = Some(UncheckedUrl(tag.value().to_owned())),
176 "image" => fm.image = Some(UncheckedUrl(tag.value().to_owned())),
177 "summary" => fm.summary = Some(tag.value().to_owned()),
178 "alt" => fm.alt = Some(tag.value().to_owned()),
179 "fallback" => fm.fallback.push(UncheckedUrl(tag.value().to_owned())),
180 "service" => fm.service = Some(tag.value().to_owned()),
181 _ => continue,
182 }
183 }
184
185 if !fm.url.0.is_empty() {
186 Some(fm)
187 } else {
188 None
189 }
190 }
191
192 pub fn to_imeta_tag(&self) -> Tag {
194 let mut tag = Tag::new(&["imeta"]);
195
196 tag.push_value(format!("url {}", self.url));
197
198 if let Some(m) = &self.m {
199 tag.push_value(format!("m {}", m));
200 }
201
202 if let Some(x) = &self.x {
203 tag.push_value(format!("x {}", x));
204 }
205
206 if let Some(ox) = &self.ox {
207 tag.push_value(format!("ox {}", ox));
208 }
209
210 if let Some(size) = &self.size {
211 tag.push_value(format!("size {}", size));
212 }
213
214 if let Some(dim) = &self.dim {
215 tag.push_value(format!("dim {}x{}", dim.0, dim.1));
216 }
217
218 if let Some(magnet) = &self.magnet {
219 tag.push_value(format!("magnet {}", magnet));
220 }
221
222 if let Some(i) = &self.i {
223 tag.push_value(format!("i {}", i));
224 }
225
226 if let Some(blurhash) = &self.blurhash {
227 tag.push_value(format!("blurhash {}", blurhash));
228 }
229
230 if let Some(thumb) = &self.thumb {
231 tag.push_value(format!("thumb {}", thumb));
232 }
233
234 if let Some(image) = &self.image {
235 tag.push_value(format!("image {}", image));
236 }
237
238 if let Some(summary) = &self.summary {
239 tag.push_value(format!("summary {}", summary));
240 }
241
242 if let Some(alt) = &self.alt {
243 tag.push_value(format!("alt {}", alt));
244 }
245
246 for fallback in &self.fallback {
247 tag.push_value(format!("fallback {}", fallback));
248 }
249
250 if let Some(service) = &self.service {
251 tag.push_value(format!("service {}", service));
252 }
253
254 tag
255 }
256
257 pub fn from_imeta_tag(tag: &Tag) -> Option<FileMetadata> {
259 let mut fm = FileMetadata::new(UncheckedUrl("".to_owned()));
260
261 for i in 0..tag.len() {
262 let parts: Vec<&str> = tag.get_index(i).splitn(2, ' ').collect();
263 if parts.len() < 2 {
264 continue;
265 }
266 match parts[0] {
267 "url" => fm.url = UncheckedUrl(parts[1].to_owned()),
268 "m" => fm.m = Some(parts[1].to_owned()),
269 "x" => fm.x = Some(parts[1].to_owned()),
270 "ox" => fm.ox = Some(parts[1].to_owned()),
271 "size" => {
272 if let Ok(u) = parts[1].parse::<u64>() {
273 fm.size = Some(u);
274 }
275 }
276 "dim" => {
277 let parts: Vec<&str> = parts[1].split('x').collect();
278 if parts.len() == 2 {
279 if let Ok(w) = parts[0].parse::<usize>() {
280 if let Ok(h) = parts[1].parse::<usize>() {
281 fm.dim = Some((w, h));
282 }
283 }
284 }
285 }
286 "magnet" => fm.magnet = Some(UncheckedUrl(parts[1].to_owned())),
287 "i" => fm.i = Some(parts[1].to_owned()),
288 "blurhash" => fm.blurhash = Some(parts[1].to_owned()),
289 "thumb" => fm.thumb = Some(UncheckedUrl(parts[1].to_owned())),
290 "image" => fm.image = Some(UncheckedUrl(parts[1].to_owned())),
291 "summary" => fm.summary = Some(parts[1].to_owned()),
292 "alt" => fm.alt = Some(parts[1].to_owned()),
293 "fallback" => fm.fallback.push(UncheckedUrl(parts[1].to_owned())),
294 "service" => fm.service = Some(parts[1].to_owned()),
295 _ => continue,
296 }
297 }
298
299 if !fm.url.0.is_empty() {
300 Some(fm)
301 } else {
302 None
303 }
304 }
305}
306
307#[cfg(test)]
308mod test {
309 use super::*;
310
311 #[tokio::test]
312 async fn test_nip94_event() {
313 let mut fm = FileMetadata::new(UncheckedUrl("https://nostr.build/blahblahblah".to_owned()));
314 fm.x = Some("12345".to_owned());
315 fm.service = Some("http".to_owned());
316 fm.size = Some(10124);
317 fm.alt = Some("a crackerjack".to_owned());
318
319 use crate::{PrivateKey, Signer};
320 let private_key = PrivateKey::generate();
321 let public_key = private_key.public_key();
322
323 let pre_event = fm.to_nip94_preevent(public_key);
324 let event = private_key.sign_event(pre_event).await.unwrap();
325 let fm2 = FileMetadata::from_nip94_event(&event).unwrap();
326
327 assert_eq!(fm, fm2);
328 }
329
330 #[test]
331 fn test_imeta_tag() {
332 let mut fm = FileMetadata::new(UncheckedUrl("https://nostr.build/blahblahblah".to_owned()));
333 fm.x = Some("12345".to_owned());
334 fm.service = Some("http".to_owned());
335 fm.size = Some(10124);
336 fm.alt = Some("a crackerjack".to_owned());
337
338 let tag = fm.to_imeta_tag();
339 let fm2 = FileMetadata::from_imeta_tag(&tag).unwrap();
340 assert_eq!(fm, fm2);
341 }
342}