1use thiserror::Error;
39
40use crate::event::{
41 Alphabet, Coordinate, CoordinateError, Event, EventBuilder, EventId, EventIdError, Kind,
42 SingleLetterTag, Tag, TagKind, Tags,
43};
44use crate::key::{PublicKey, PublicKeyError};
45use crate::types::{ImageDimensions, ImageError, RelayUrl, RelayUrlError};
46
47pub const KIND_BADGE_DEFINITION: Kind = Kind::BADGE_DEFINITION;
49pub const KIND_BADGE_AWARD: Kind = Kind::BADGE_AWARD;
51pub const KIND_PROFILE_BADGES: Kind = Kind::PROFILE_BADGES;
53pub const KIND_BADGE_SET: Kind = Kind::BADGE_SET;
55pub const DEPRECATED_PROFILE_BADGES_IDENTIFIER: &str = "profile_badges";
58
59#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct BadgeImage {
63 pub url: String,
65 pub dim: Option<ImageDimensions>,
67}
68
69impl BadgeImage {
70 #[must_use]
72 pub fn new(url: impl Into<String>) -> Self {
73 Self {
74 url: url.into(),
75 dim: None,
76 }
77 }
78
79 #[must_use]
81 pub const fn dim(mut self, dim: ImageDimensions) -> Self {
82 self.dim = Some(dim);
83 self
84 }
85
86 fn to_tag(&self, name: &str) -> Tag {
87 let mut values: Vec<String> = Vec::with_capacity(2);
88 values.push(self.url.clone());
89 if let Some(dim) = self.dim {
90 values.push(dim.to_string());
91 }
92 custom_tag(name, values)
93 }
94
95 fn from_tag(tag: &Tag) -> Result<Option<Self>, BadgeError> {
96 let Some(url) = tag.get(1) else {
97 return Ok(None);
98 };
99 let mut img = Self::new(url);
100 if let Some(d) = tag.get(2)
101 && !d.is_empty()
102 {
103 img.dim = Some(
104 d.parse::<ImageDimensions>()
105 .map_err(BadgeError::InvalidImageDim)?,
106 );
107 }
108 Ok(Some(img))
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct BadgeDefinition {
115 pub identifier: String,
117 pub name: Option<String>,
119 pub description: Option<String>,
121 pub image: Option<BadgeImage>,
123 pub thumbnails: Vec<BadgeImage>,
125}
126
127impl BadgeDefinition {
128 #[must_use]
130 pub fn new(identifier: impl Into<String>) -> Self {
131 Self {
132 identifier: identifier.into(),
133 name: None,
134 description: None,
135 image: None,
136 thumbnails: Vec::new(),
137 }
138 }
139
140 #[must_use]
142 pub fn name(mut self, name: impl Into<String>) -> Self {
143 self.name = Some(name.into());
144 self
145 }
146
147 #[must_use]
149 pub fn description(mut self, description: impl Into<String>) -> Self {
150 self.description = Some(description.into());
151 self
152 }
153
154 #[must_use]
156 pub fn image(mut self, image: BadgeImage) -> Self {
157 self.image = Some(image);
158 self
159 }
160
161 #[must_use]
163 pub fn thumbnail(mut self, thumb: BadgeImage) -> Self {
164 self.thumbnails.push(thumb);
165 self
166 }
167
168 #[must_use]
170 pub fn to_tags(&self) -> Vec<Tag> {
171 let mut tags: Vec<Tag> = Vec::with_capacity(4 + self.thumbnails.len());
172 tags.push(Tag::d(&self.identifier));
173 if let Some(name) = &self.name {
174 tags.push(custom_tag("name", [name.clone()]));
175 }
176 if let Some(desc) = &self.description {
177 tags.push(custom_tag("description", [desc.clone()]));
178 }
179 if let Some(img) = &self.image {
180 tags.push(img.to_tag("image"));
181 }
182 for thumb in &self.thumbnails {
183 tags.push(thumb.to_tag("thumb"));
184 }
185 tags
186 }
187
188 pub fn from_event(event: &Event) -> Result<Self, BadgeError> {
197 if event.kind != KIND_BADGE_DEFINITION {
198 return Err(BadgeError::WrongKind(event.kind));
199 }
200 let identifier = identifier_value(&event.tags)
201 .ok_or(BadgeError::MissingIdentifier)?
202 .to_owned();
203 let mut def = Self::new(identifier);
204 for tag in &event.tags {
205 match tag.kind() {
206 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::D => {}
207 TagKind::Custom(name) if name == "name" => {
208 def.name = tag.get(1).map(str::to_owned);
209 }
210 TagKind::Custom(name) if name == "description" => {
211 def.description = tag.get(1).map(str::to_owned);
212 }
213 TagKind::Custom(name) if name == "image" => {
214 def.image = BadgeImage::from_tag(tag)?;
215 }
216 TagKind::Custom(name) if name == "thumb" => {
217 push_thumbnail(tag, &mut def.thumbnails)?;
218 }
219 _ => {}
220 }
221 }
222 Ok(def)
223 }
224
225 #[must_use]
227 pub fn coordinate(&self, issuer: PublicKey) -> Coordinate {
228 Coordinate::new(KIND_BADGE_DEFINITION, issuer, self.identifier.clone())
229 }
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct BadgeAwardee {
235 pub pubkey: PublicKey,
237 pub relay_hint: Option<RelayUrl>,
239}
240
241impl BadgeAwardee {
242 #[must_use]
244 pub const fn new(pubkey: PublicKey) -> Self {
245 Self {
246 pubkey,
247 relay_hint: None,
248 }
249 }
250
251 #[must_use]
253 pub fn relay_hint(mut self, hint: RelayUrl) -> Self {
254 self.relay_hint = Some(hint);
255 self
256 }
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct BadgeAward {
262 pub definition: Coordinate,
264 pub awardees: Vec<BadgeAwardee>,
266}
267
268impl BadgeAward {
269 #[must_use]
272 pub fn new(definition: Coordinate, awardee: BadgeAwardee) -> Self {
273 Self {
274 definition,
275 awardees: vec![awardee],
276 }
277 }
278
279 #[must_use]
281 pub fn awardee(mut self, awardee: BadgeAwardee) -> Self {
282 self.awardees.push(awardee);
283 self
284 }
285
286 #[must_use]
288 pub fn to_tags(&self) -> Vec<Tag> {
289 let mut tags: Vec<Tag> = Vec::with_capacity(1 + self.awardees.len());
290 tags.push(letter_tag(Alphabet::A, [self.definition.to_wire()]));
291 for awardee in &self.awardees {
292 let mut values: Vec<String> = Vec::with_capacity(2);
293 values.push(awardee.pubkey.to_hex());
294 if let Some(relay) = &awardee.relay_hint {
295 values.push(relay.as_str().to_owned());
296 }
297 tags.push(letter_tag(Alphabet::P, values));
298 }
299 tags
300 }
301
302 pub fn from_event(event: &Event) -> Result<Self, BadgeError> {
311 if event.kind != KIND_BADGE_AWARD {
312 return Err(BadgeError::WrongKind(event.kind));
313 }
314 let mut definition: Option<Coordinate> = None;
315 let mut awardees: Vec<BadgeAwardee> = Vec::new();
316 for tag in &event.tags {
317 match tag.kind() {
318 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::A => {
319 let coord_str = tag.get(1).ok_or(BadgeError::MalformedDefinition)?;
320 definition =
321 Some(Coordinate::parse(coord_str).map_err(BadgeError::InvalidCoordinate)?);
322 }
323 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::P => {
324 let pk_hex = tag.get(1).ok_or(BadgeError::MalformedAwardee)?;
325 let pubkey = PublicKey::parse(pk_hex).map_err(BadgeError::InvalidPublicKey)?;
326 let relay_hint = parse_optional_relay(tag.get(2))?;
327 awardees.push(BadgeAwardee { pubkey, relay_hint });
328 }
329 _ => {}
330 }
331 }
332 let definition = definition.ok_or(BadgeError::MissingDefinition)?;
333 if awardees.is_empty() {
334 return Err(BadgeError::MissingAwardee);
335 }
336 Ok(Self {
337 definition,
338 awardees,
339 })
340 }
341}
342
343#[derive(Debug, Clone, PartialEq, Eq)]
345pub struct ProfileBadgeEntry {
346 pub definition: Coordinate,
348 pub definition_relay: Option<RelayUrl>,
350 pub award: EventId,
352 pub award_relay: Option<RelayUrl>,
354}
355
356impl ProfileBadgeEntry {
357 #[must_use]
359 pub const fn new(definition: Coordinate, award: EventId) -> Self {
360 Self {
361 definition,
362 definition_relay: None,
363 award,
364 award_relay: None,
365 }
366 }
367
368 #[must_use]
370 pub fn definition_relay(mut self, hint: RelayUrl) -> Self {
371 self.definition_relay = Some(hint);
372 self
373 }
374
375 #[must_use]
377 pub fn award_relay(mut self, hint: RelayUrl) -> Self {
378 self.award_relay = Some(hint);
379 self
380 }
381}
382
383#[derive(Debug, Clone, Copy, PartialEq, Eq)]
387pub enum ProfileBadgesSource {
388 Replaceable,
390 DeprecatedAddressable,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq)]
396pub struct ProfileBadges {
397 pub entries: Vec<ProfileBadgeEntry>,
399}
400
401impl ProfileBadges {
402 #[must_use]
404 pub const fn new() -> Self {
405 Self {
406 entries: Vec::new(),
407 }
408 }
409
410 #[must_use]
412 pub fn entry(mut self, entry: ProfileBadgeEntry) -> Self {
413 self.entries.push(entry);
414 self
415 }
416
417 #[must_use]
419 pub fn to_tags(&self) -> Vec<Tag> {
420 let mut tags: Vec<Tag> = Vec::with_capacity(self.entries.len() * 2);
421 for entry in &self.entries {
422 let mut a_values: Vec<String> = Vec::with_capacity(2);
423 a_values.push(entry.definition.to_wire());
424 if let Some(r) = &entry.definition_relay {
425 a_values.push(r.as_str().to_owned());
426 }
427 tags.push(letter_tag(Alphabet::A, a_values));
428 let mut e_values: Vec<String> = Vec::with_capacity(2);
429 e_values.push(entry.award.to_hex());
430 if let Some(r) = &entry.award_relay {
431 e_values.push(r.as_str().to_owned());
432 }
433 tags.push(letter_tag(Alphabet::E, e_values));
434 }
435 tags
436 }
437
438 pub fn from_event(event: &Event) -> Result<(Self, ProfileBadgesSource), BadgeError> {
451 let source = match event.kind {
452 KIND_PROFILE_BADGES => ProfileBadgesSource::Replaceable,
453 KIND_BADGE_SET
454 if event
455 .tags
456 .identifier()
457 .is_some_and(|d| d == DEPRECATED_PROFILE_BADGES_IDENTIFIER) =>
458 {
459 ProfileBadgesSource::DeprecatedAddressable
460 }
461 other => return Err(BadgeError::WrongKind(other)),
462 };
463 let mut entries: Vec<ProfileBadgeEntry> = Vec::new();
464 let mut pending_a: Option<(Coordinate, Option<RelayUrl>)> = None;
465 for tag in &event.tags {
466 match tag.kind() {
467 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::A => {
468 let coord_str = tag.get(1).ok_or(BadgeError::MalformedDefinition)?;
469 let coord =
470 Coordinate::parse(coord_str).map_err(BadgeError::InvalidCoordinate)?;
471 let relay = parse_optional_relay(tag.get(2))?;
472 pending_a = Some((coord, relay));
473 }
474 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::E => {
475 pair_award_with_pending(tag, &mut pending_a, &mut entries)?;
476 }
477 _ => {}
478 }
479 }
480 Ok((Self { entries }, source))
481 }
482}
483
484impl Default for ProfileBadges {
485 fn default() -> Self {
486 Self::new()
487 }
488}
489
490fn identifier_value(tags: &Tags) -> Option<&str> {
491 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::D));
492 tags.find_first(&head).and_then(|tag| tag.get(1))
493}
494
495fn push_thumbnail(tag: &Tag, thumbnails: &mut Vec<BadgeImage>) -> Result<(), BadgeError> {
496 if let Some(img) = BadgeImage::from_tag(tag)? {
497 thumbnails.push(img);
498 }
499 Ok(())
500}
501
502fn pair_award_with_pending(
503 tag: &Tag,
504 pending_a: &mut Option<(Coordinate, Option<RelayUrl>)>,
505 entries: &mut Vec<ProfileBadgeEntry>,
506) -> Result<(), BadgeError> {
507 let id_hex = tag.get(1).ok_or(BadgeError::MalformedAward)?;
508 let award = EventId::parse(id_hex).map_err(BadgeError::InvalidEventId)?;
509 let award_relay = parse_optional_relay(tag.get(2))?;
510 if let Some((definition, definition_relay)) = pending_a.take() {
512 entries.push(ProfileBadgeEntry {
513 definition,
514 definition_relay,
515 award,
516 award_relay,
517 });
518 }
519 Ok(())
520}
521
522fn parse_optional_relay(value: Option<&str>) -> Result<Option<RelayUrl>, BadgeError> {
523 match value {
524 Some(s) if !s.is_empty() => Ok(Some(
525 RelayUrl::parse(s).map_err(BadgeError::InvalidRelayUrl)?,
526 )),
527 _ => Ok(None),
528 }
529}
530
531fn letter_tag<I, S>(alphabet: Alphabet, args: I) -> Tag
532where
533 I: IntoIterator<Item = S>,
534 S: Into<String>,
535{
536 let head = TagKind::single_letter(SingleLetterTag::lowercase(alphabet));
537 Tag::with(&head, args)
538}
539
540fn custom_tag<I, S>(name: &str, args: I) -> Tag
541where
542 I: IntoIterator<Item = S>,
543 S: Into<String>,
544{
545 Tag::with(&TagKind::Custom(name.to_owned()), args)
546}
547
548#[derive(Debug, Error)]
550#[non_exhaustive]
551pub enum BadgeError {
552 #[error("unexpected kind {}", .0.as_u16())]
554 WrongKind(Kind),
555 #[error("badge definition is missing the `d` identifier tag")]
557 MissingIdentifier,
558 #[error("badge award must carry an `a` definition tag")]
560 MissingDefinition,
561 #[error("badge award must carry at least one `p` awardee tag")]
563 MissingAwardee,
564 #[error("malformed `a` definition tag")]
566 MalformedDefinition,
567 #[error("malformed `p` awardee tag")]
569 MalformedAwardee,
570 #[error("malformed `e` award tag")]
572 MalformedAward,
573 #[error("invalid coordinate: {0}")]
575 InvalidCoordinate(#[source] CoordinateError),
576 #[error("invalid public key: {0}")]
578 InvalidPublicKey(#[source] PublicKeyError),
579 #[error("invalid event id: {0}")]
581 InvalidEventId(#[source] EventIdError),
582 #[error("invalid relay URL: {0}")]
584 InvalidRelayUrl(#[source] RelayUrlError),
585 #[error("invalid image dimensions: {0}")]
587 InvalidImageDim(#[source] ImageError),
588}
589
590impl EventBuilder {
591 #[must_use]
593 pub fn badge_definition(definition: &BadgeDefinition) -> Self {
594 let mut builder = Self::new(KIND_BADGE_DEFINITION, "");
595 for tag in definition.to_tags() {
596 builder = builder.tag(tag);
597 }
598 builder
599 }
600
601 #[must_use]
603 pub fn badge_award(award: &BadgeAward) -> Self {
604 let mut builder = Self::new(KIND_BADGE_AWARD, "");
605 for tag in award.to_tags() {
606 builder = builder.tag(tag);
607 }
608 builder
609 }
610
611 #[must_use]
613 pub fn profile_badges(badges: &ProfileBadges) -> Self {
614 let mut builder = Self::new(KIND_PROFILE_BADGES, "");
615 for tag in badges.to_tags() {
616 builder = builder.tag(tag);
617 }
618 builder
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625 use crate::Keys;
626
627 fn keys() -> Keys {
628 Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
629 }
630
631 fn other_keys() -> Keys {
632 Keys::parse("0000000000000000000000000000000000000000000000000000000000000005").unwrap()
633 }
634
635 #[test]
636 fn badge_definition_round_trips() {
637 let def = BadgeDefinition::new("bravery")
638 .name("Medal of Bravery")
639 .description("Awarded to users demonstrating bravery")
640 .image(
641 BadgeImage::new("https://example.com/bravery.png")
642 .dim(ImageDimensions::new(1024, 1024).unwrap()),
643 )
644 .thumbnail(
645 BadgeImage::new("https://example.com/bravery_256.png")
646 .dim(ImageDimensions::new(256, 256).unwrap()),
647 )
648 .thumbnail(BadgeImage::new("https://example.com/bravery_64.png"));
649 let event = EventBuilder::badge_definition(&def)
650 .sign_with_keys(&keys())
651 .unwrap();
652 assert_eq!(event.kind, KIND_BADGE_DEFINITION);
653 let parsed = BadgeDefinition::from_event(&event).unwrap();
654 assert_eq!(parsed, def);
655 }
656
657 #[test]
658 fn badge_definition_requires_d_tag() {
659 let event = EventBuilder::new(KIND_BADGE_DEFINITION, "")
660 .sign_with_keys(&keys())
661 .unwrap();
662 assert!(matches!(
663 BadgeDefinition::from_event(&event),
664 Err(BadgeError::MissingIdentifier)
665 ));
666 }
667
668 #[test]
669 fn badge_award_round_trips_multiple_awardees() {
670 let issuer = *keys().public_key();
671 let definition = Coordinate::new(KIND_BADGE_DEFINITION, issuer, "bravery");
672 let award = BadgeAward::new(definition, BadgeAwardee::new(*other_keys().public_key()))
673 .awardee(
674 BadgeAwardee::new(issuer)
675 .relay_hint(RelayUrl::parse("wss://relay.example/").unwrap()),
676 );
677 let event = EventBuilder::badge_award(&award)
678 .sign_with_keys(&keys())
679 .unwrap();
680 let parsed = BadgeAward::from_event(&event).unwrap();
681 assert_eq!(parsed, award);
682 }
683
684 #[test]
685 fn badge_award_requires_definition_and_awardee() {
686 let event_no_a = EventBuilder::new(KIND_BADGE_AWARD, "")
687 .tag(letter_tag(Alphabet::P, [keys().public_key().to_hex()]))
688 .sign_with_keys(&keys())
689 .unwrap();
690 assert!(matches!(
691 BadgeAward::from_event(&event_no_a),
692 Err(BadgeError::MissingDefinition)
693 ));
694
695 let coord = Coordinate::new(KIND_BADGE_DEFINITION, *keys().public_key(), "x");
696 let event_no_p = EventBuilder::new(KIND_BADGE_AWARD, "")
697 .tag(letter_tag(Alphabet::A, [coord.to_wire()]))
698 .sign_with_keys(&keys())
699 .unwrap();
700 assert!(matches!(
701 BadgeAward::from_event(&event_no_p),
702 Err(BadgeError::MissingAwardee)
703 ));
704 }
705
706 #[test]
707 fn profile_badges_round_trip_pairs() {
708 let issuer = *keys().public_key();
709 let coord1 = Coordinate::new(KIND_BADGE_DEFINITION, issuer, "bravery");
710 let coord2 = Coordinate::new(KIND_BADGE_DEFINITION, issuer, "honor");
711 let id1 = EventId::from_byte_array([0x10; 32]);
712 let id2 = EventId::from_byte_array([0x20; 32]);
713 let badges = ProfileBadges::new()
714 .entry(
715 ProfileBadgeEntry::new(coord1, id1)
716 .award_relay(RelayUrl::parse("wss://nostr.academy/").unwrap()),
717 )
718 .entry(ProfileBadgeEntry::new(coord2, id2));
719 let event = EventBuilder::profile_badges(&badges)
720 .sign_with_keys(&other_keys())
721 .unwrap();
722 let (parsed, source) = ProfileBadges::from_event(&event).unwrap();
723 assert_eq!(source, ProfileBadgesSource::Replaceable);
724 assert_eq!(parsed, badges);
725 }
726
727 #[test]
728 fn profile_badges_drops_orphaned_e_tag() {
729 let issuer = *keys().public_key();
730 let coord = Coordinate::new(KIND_BADGE_DEFINITION, issuer, "bravery");
731 let id1 = EventId::from_byte_array([0x10; 32]);
732 let orphan_id = EventId::from_byte_array([0x99; 32]);
733 let event = EventBuilder::new(KIND_PROFILE_BADGES, "")
735 .tag(letter_tag(Alphabet::E, [orphan_id.to_hex()]))
736 .tag(letter_tag(Alphabet::A, [coord.to_wire()]))
737 .tag(letter_tag(Alphabet::E, [id1.to_hex()]))
738 .sign_with_keys(&other_keys())
739 .unwrap();
740 let (parsed, _) = ProfileBadges::from_event(&event).unwrap();
741 assert_eq!(parsed.entries.len(), 1);
742 assert_eq!(parsed.entries[0].award, id1);
743 }
744
745 #[test]
746 fn profile_badges_recognises_deprecated_kind() {
747 let issuer = *keys().public_key();
748 let coord = Coordinate::new(KIND_BADGE_DEFINITION, issuer, "bravery");
749 let id = EventId::from_byte_array([0xab; 32]);
750 let event = EventBuilder::new(KIND_BADGE_SET, "")
751 .tag(Tag::d(DEPRECATED_PROFILE_BADGES_IDENTIFIER))
752 .tag(letter_tag(Alphabet::A, [coord.to_wire()]))
753 .tag(letter_tag(Alphabet::E, [id.to_hex()]))
754 .sign_with_keys(&other_keys())
755 .unwrap();
756 let (parsed, source) = ProfileBadges::from_event(&event).unwrap();
757 assert_eq!(source, ProfileBadgesSource::DeprecatedAddressable);
758 assert_eq!(parsed.entries.len(), 1);
759 }
760
761 #[test]
762 fn profile_badges_rejects_addressable_30008_without_marker() {
763 let event = EventBuilder::new(KIND_BADGE_SET, "")
766 .tag(Tag::d("some-other-set"))
767 .sign_with_keys(&other_keys())
768 .unwrap();
769 assert!(matches!(
770 ProfileBadges::from_event(&event),
771 Err(BadgeError::WrongKind(_))
772 ));
773 }
774
775 #[test]
776 fn coordinate_helper_uses_issuer_pubkey() {
777 let def = BadgeDefinition::new("bravery");
778 let coord = def.coordinate(*keys().public_key());
779 assert_eq!(coord.kind, KIND_BADGE_DEFINITION);
780 assert_eq!(coord.identifier, "bravery");
781 }
782}