1use thiserror::Error;
32
33use crate::event::{
34 Alphabet, Coordinate, CoordinateError, Event, EventBuilder, EventId, EventIdError, Kind,
35 SingleLetterTag, Tag, TagKind, Tags,
36};
37use crate::key::{PublicKey, PublicKeyError};
38use crate::types::{RelayUrl, RelayUrlError, Url, UrlError};
39
40pub const KIND_LABEL: Kind = Kind::LABEL;
42
43pub const UGC_NAMESPACE: &str = "ugc";
46
47#[derive(Debug, Clone, PartialEq, Eq, Hash)]
52pub struct LabelTerm {
53 pub namespace: String,
56 pub value: String,
58}
59
60impl LabelTerm {
61 #[must_use]
63 pub fn new(namespace: impl Into<String>, value: impl Into<String>) -> Self {
64 Self {
65 namespace: namespace.into(),
66 value: value.into(),
67 }
68 }
69
70 #[must_use]
73 pub fn ugc(value: impl Into<String>) -> Self {
74 Self::new(UGC_NAMESPACE, value)
75 }
76
77 #[must_use]
79 pub fn is_ugc(&self) -> bool {
80 self.namespace == UGC_NAMESPACE
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum LabelTarget {
89 Event {
91 id: EventId,
93 relay_hint: Option<RelayUrl>,
96 },
97 Pubkey {
99 pubkey: PublicKey,
101 relay_hint: Option<RelayUrl>,
103 },
104 Address {
106 coordinate: Coordinate,
108 relay_hint: Option<RelayUrl>,
110 },
111 Url(Url),
113 Topic(String),
115}
116
117impl LabelTarget {
118 #[must_use]
120 pub fn to_tag(&self) -> Tag {
121 match self {
122 Self::Event { id, relay_hint } => relay_hint
123 .as_ref()
124 .map_or_else(|| Tag::e(*id), |url| Tag::e_with_relay(*id, url)),
125 Self::Pubkey { pubkey, relay_hint } => relay_hint
126 .as_ref()
127 .map_or_else(|| Tag::p(*pubkey), |url| Tag::p_with_relay(*pubkey, url)),
128 Self::Address {
129 coordinate,
130 relay_hint,
131 } => relay_hint.as_ref().map_or_else(
132 || Tag::a(coordinate),
133 |url| Tag::a_with_relay(coordinate, url),
134 ),
135 Self::Url(url) => Tag::r(url),
136 Self::Topic(topic) => Tag::t(topic),
137 }
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Default)]
146pub struct Label {
147 pub terms: Vec<LabelTerm>,
150 pub targets: Vec<LabelTarget>,
152 pub content: String,
154 pub extra_tags: Vec<Tag>,
157}
158
159impl Label {
160 #[must_use]
162 pub fn new() -> Self {
163 Self::default()
164 }
165
166 #[must_use]
168 pub fn term(mut self, term: LabelTerm) -> Self {
169 self.terms.push(term);
170 self
171 }
172
173 #[must_use]
175 pub fn target(mut self, target: LabelTarget) -> Self {
176 self.targets.push(target);
177 self
178 }
179
180 #[must_use]
182 pub fn content(mut self, content: impl Into<String>) -> Self {
183 self.content = content.into();
184 self
185 }
186
187 pub fn from_event(event: &Event) -> Result<Self, LabelError> {
201 if event.kind != KIND_LABEL {
202 return Err(LabelError::WrongKind(event.kind));
203 }
204 let terms = labels_from_tags(&event.tags)?;
205 let mut targets: Vec<LabelTarget> = Vec::new();
206 let mut extra_tags: Vec<Tag> = Vec::new();
207 for tag in &event.tags {
208 match tag.kind() {
209 TagKind::SingleLetter(s) if s.uppercase && s.character == Alphabet::L => {
210 }
212 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::L => {
213 }
215 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::E => {
216 targets.push(parse_event_target(tag)?);
217 }
218 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::P => {
219 targets.push(parse_pubkey_target(tag)?);
220 }
221 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::A => {
222 targets.push(parse_address_target(tag)?);
223 }
224 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::R => {
225 targets.push(parse_url_target(tag)?);
226 }
227 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::T => {
228 targets.push(parse_topic_target(tag)?);
229 }
230 _ => extra_tags.push(tag.clone()),
231 }
232 }
233 Ok(Self {
234 terms,
235 targets,
236 content: event.content.clone(),
237 extra_tags,
238 })
239 }
240}
241
242pub fn labels_from_tags(tags: &Tags) -> Result<Vec<LabelTerm>, LabelError> {
256 let mut terms: Vec<LabelTerm> = Vec::new();
257 for tag in tags {
258 let TagKind::SingleLetter(letter) = tag.kind() else {
259 continue;
260 };
261 if letter.character != Alphabet::L || letter.uppercase {
262 continue;
263 }
264 let value = tag.get(1).ok_or(LabelError::MalformedValue)?.to_owned();
265 let namespace = tag
266 .get(2)
267 .filter(|ns| !ns.is_empty())
268 .map_or_else(|| UGC_NAMESPACE.to_owned(), str::to_owned);
269 terms.push(LabelTerm { namespace, value });
270 }
271 Ok(terms)
272}
273
274#[must_use]
278pub fn term_to_tags(term: &LabelTerm) -> [Tag; 2] {
279 [
280 Tag::with(
281 &TagKind::single_letter(SingleLetterTag::uppercase(Alphabet::L)),
282 [term.namespace.clone()],
283 ),
284 Tag::with(
285 &TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::L)),
286 [term.value.clone(), term.namespace.clone()],
287 ),
288 ]
289}
290
291#[derive(Debug, Error)]
293#[non_exhaustive]
294pub enum LabelError {
295 #[error("expected kind 1985 (label), got kind {}", .0.as_u16())]
297 WrongKind(Kind),
298 #[error("`L` namespace tag missing namespace value")]
300 MalformedNamespace,
301 #[error("`l` label tag missing value")]
303 MalformedValue,
304 #[error("`e` target tag missing event id")]
306 MalformedEventTarget,
307 #[error("`p` target tag missing pubkey")]
309 MalformedPubkeyTarget,
310 #[error("`a` target tag missing coordinate")]
312 MalformedAddressTarget,
313 #[error("`r` target tag missing URL")]
315 MalformedUrlTarget,
316 #[error("`t` target tag missing topic")]
318 MalformedTopicTarget,
319 #[error(transparent)]
321 InvalidEventId(#[from] EventIdError),
322 #[error(transparent)]
324 InvalidPublicKey(#[from] PublicKeyError),
325 #[error(transparent)]
327 InvalidCoordinate(#[from] CoordinateError),
328 #[error(transparent)]
330 InvalidUrl(#[from] UrlError),
331 #[error(transparent)]
333 InvalidRelayUrl(#[from] RelayUrlError),
334}
335
336fn parse_event_target(tag: &Tag) -> Result<LabelTarget, LabelError> {
337 let id_hex = tag.get(1).ok_or(LabelError::MalformedEventTarget)?;
338 let id = EventId::parse(id_hex)?;
339 let relay_hint = parse_optional_relay(tag.get(2))?;
340 Ok(LabelTarget::Event { id, relay_hint })
341}
342
343fn parse_pubkey_target(tag: &Tag) -> Result<LabelTarget, LabelError> {
344 let pk_hex = tag.get(1).ok_or(LabelError::MalformedPubkeyTarget)?;
345 let pubkey = PublicKey::parse(pk_hex)?;
346 let relay_hint = parse_optional_relay(tag.get(2))?;
347 Ok(LabelTarget::Pubkey { pubkey, relay_hint })
348}
349
350fn parse_address_target(tag: &Tag) -> Result<LabelTarget, LabelError> {
351 let coord_str = tag.get(1).ok_or(LabelError::MalformedAddressTarget)?;
352 let coordinate = Coordinate::parse(coord_str)?;
353 let relay_hint = parse_optional_relay(tag.get(2))?;
354 Ok(LabelTarget::Address {
355 coordinate,
356 relay_hint,
357 })
358}
359
360fn parse_url_target(tag: &Tag) -> Result<LabelTarget, LabelError> {
361 let url_str = tag.get(1).ok_or(LabelError::MalformedUrlTarget)?;
362 let url = Url::parse(url_str)?;
363 Ok(LabelTarget::Url(url))
364}
365
366fn parse_topic_target(tag: &Tag) -> Result<LabelTarget, LabelError> {
367 let topic = tag
368 .get(1)
369 .ok_or(LabelError::MalformedTopicTarget)?
370 .to_owned();
371 Ok(LabelTarget::Topic(topic))
372}
373
374fn parse_optional_relay(value: Option<&str>) -> Result<Option<RelayUrl>, LabelError> {
375 match value {
376 Some(s) if !s.is_empty() => Ok(Some(RelayUrl::parse(s)?)),
377 _ => Ok(None),
378 }
379}
380
381impl EventBuilder {
382 #[must_use]
388 pub fn label(label: &Label) -> Self {
389 let mut builder = Self::new(KIND_LABEL, label.content.clone());
390 for term in &label.terms {
391 for tag in term_to_tags(term) {
392 builder = builder.tag(tag);
393 }
394 }
395 for target in &label.targets {
396 builder = builder.tag(target.to_tag());
397 }
398 for tag in &label.extra_tags {
399 builder = builder.tag(tag.clone());
400 }
401 builder
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408 use crate::Keys;
409
410 fn keys() -> Keys {
411 Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
412 }
413
414 fn relay() -> RelayUrl {
415 RelayUrl::parse("wss://relay.example/").unwrap()
416 }
417
418 #[test]
419 fn round_trip_topic_label() {
420 let label = Label::new()
421 .term(LabelTerm::new("ISO-639-1", "en"))
422 .target(LabelTarget::Topic("nostr".to_owned()));
423 let event = EventBuilder::label(&label).sign_with_keys(&keys()).unwrap();
424 assert_eq!(event.kind, KIND_LABEL);
425 let parsed = Label::from_event(&event).unwrap();
426 assert_eq!(parsed, label);
427 }
428
429 #[test]
430 fn round_trip_pubkey_label_with_relay() {
431 let target = LabelTarget::Pubkey {
432 pubkey: *keys().public_key(),
433 relay_hint: Some(relay()),
434 };
435 let label = Label::new()
436 .term(LabelTerm::new("com.example.ontology", "VI-hum"))
437 .target(target);
438 let event = EventBuilder::label(&label).sign_with_keys(&keys()).unwrap();
439 let parsed = Label::from_event(&event).unwrap();
440 assert_eq!(parsed, label);
441 }
442
443 #[test]
444 fn round_trip_multiple_terms_and_targets() {
445 let id = EventId::from_byte_array([0x11; 32]);
446 let coord = Coordinate::new(Kind::new(30_023), *keys().public_key(), "post-1".to_owned());
447 let label = Label::new()
448 .term(LabelTerm::new("license", "MIT"))
449 .term(LabelTerm::new("nip28.moderation", "approve"))
450 .target(LabelTarget::Event {
451 id,
452 relay_hint: Some(relay()),
453 })
454 .target(LabelTarget::Address {
455 coordinate: coord,
456 relay_hint: None,
457 })
458 .target(LabelTarget::Url(Url::parse("https://example.com").unwrap()))
459 .content("ok");
460 let event = EventBuilder::label(&label).sign_with_keys(&keys()).unwrap();
461 let parsed = Label::from_event(&event).unwrap();
462 assert_eq!(parsed, label);
463 }
464
465 #[test]
466 fn self_reporting_labels_are_readable_from_kind_one_event() {
467 let event = EventBuilder::text_note("It's beautiful here in Milan!")
468 .tag(Tag::with(
469 &TagKind::single_letter(SingleLetterTag::uppercase(Alphabet::L)),
470 ["ISO-3166-2"],
471 ))
472 .tag(Tag::with(
473 &TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::L)),
474 ["IT-MI", "ISO-3166-2"],
475 ))
476 .sign_with_keys(&keys())
477 .unwrap();
478 let terms = labels_from_tags(&event.tags).unwrap();
479 assert_eq!(terms, vec![LabelTerm::new("ISO-3166-2", "IT-MI")]);
480 }
481
482 #[test]
483 fn unmarked_label_falls_back_to_ugc() {
484 let event = EventBuilder::text_note("note")
485 .tag(Tag::with(
486 &TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::L)),
487 ["spam"],
488 ))
489 .sign_with_keys(&keys())
490 .unwrap();
491 let terms = labels_from_tags(&event.tags).unwrap();
492 assert_eq!(terms, vec![LabelTerm::ugc("spam")]);
493 assert!(terms[0].is_ugc());
494 }
495
496 #[test]
497 fn wrong_kind_is_rejected() {
498 let event = EventBuilder::text_note("nope")
499 .sign_with_keys(&keys())
500 .unwrap();
501 assert!(matches!(
502 Label::from_event(&event),
503 Err(LabelError::WrongKind(_))
504 ));
505 }
506
507 #[test]
508 fn invalid_event_target_propagates() {
509 let event = EventBuilder::new(KIND_LABEL, "")
510 .tag(Tag::with(
511 &TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::E)),
512 ["not-a-hex"],
513 ))
514 .sign_with_keys(&keys())
515 .unwrap();
516 assert!(matches!(
517 Label::from_event(&event),
518 Err(LabelError::InvalidEventId(_))
519 ));
520 }
521
522 #[test]
523 fn extra_unknown_tags_are_preserved() {
524 let custom = Tag::with(&TagKind::Custom("foo".to_owned()), ["bar"]);
525 let label = Label::new()
526 .term(LabelTerm::new("license", "MIT"))
527 .target(LabelTarget::Topic("rust".to_owned()));
528 let mut event_builder = EventBuilder::label(&label);
529 event_builder = event_builder.tag(custom.clone());
530 let event = event_builder.sign_with_keys(&keys()).unwrap();
531 let parsed = Label::from_event(&event).unwrap();
532 assert_eq!(parsed.extra_tags, vec![custom]);
533 }
534}