Skip to content

Commit de6e989

Browse files
committed
nostr/nips: add ProfileBadgesEvent to NIP58
1 parent df6755c commit de6e989

File tree

1 file changed

+160
-1
lines changed

1 file changed

+160
-1
lines changed

crates/nostr/src/nips/nip58.rs

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
//!
33
//! <https://github.com/nostr-protocol/nips/blob/master/58.md>
44
5-
use crate::{event::builder::Error as BuilderError, Event, EventBuilder, Keys, Kind, Tag};
5+
use secp256k1::XOnlyPublicKey;
6+
7+
use crate::event::builder::Error as BuilderError;
8+
use crate::{Event, EventBuilder, Keys, Kind, Tag, UncheckedUrl};
69

710
#[derive(Debug, thiserror::Error)]
811
/// [`BadgeAward`] error
@@ -164,3 +167,159 @@ impl BadgeAward {
164167
Ok(BadgeAward(event))
165168
}
166169
}
170+
171+
/// Profile Badges event as specified in NIP-58
172+
pub struct ProfileBadgesEvent(Event);
173+
174+
/// [`ProfileBadgesEvent`] errors
175+
#[derive(Debug, thiserror::Error)]
176+
pub enum ProfileBadgesEventError {
177+
/// Invalid length
178+
#[error("invalid length")]
179+
InvalidLength,
180+
/// Invalid kind
181+
#[error("invalid kind")]
182+
InvalidKind,
183+
/// Mismatched badge definition or award
184+
#[error("mismatched badge definition/award")]
185+
MismatchedBadgeDefinitionOrAward,
186+
/// Badge awards lack the awarded public key
187+
#[error("badge award events lack the awarded public key")]
188+
BadgeAwardsLackAwardedPublicKey,
189+
/// Badge awards lack the awarded public key
190+
#[error("badge award event lacks `a` tag")]
191+
BadgeAwardMissingATag,
192+
/// Badge Definition Event error
193+
#[error(transparent)]
194+
BadgeDefinitionError(#[from] Error),
195+
/// Event builder Error
196+
#[error(transparent)]
197+
EventBuilder(#[from] crate::event::builder::Error),
198+
}
199+
200+
impl ProfileBadgesEvent {
201+
/// Helper function to filter events for a specific [`Kind`]
202+
pub(crate) fn filter_for_kind(events: Vec<Event>, kind_needed: &Kind) -> Vec<Event> {
203+
events
204+
.into_iter()
205+
.filter(|e| e.kind == *kind_needed)
206+
.collect()
207+
}
208+
209+
fn extract_identifier(tags: Vec<Tag>) -> Option<Tag> {
210+
tags.iter()
211+
.find(|tag| matches!(tag, Tag::Identifier(_)))
212+
.cloned()
213+
}
214+
215+
fn extract_awarded_public_key(
216+
tags: &[Tag],
217+
awarded_public_key: &XOnlyPublicKey,
218+
) -> Option<(XOnlyPublicKey, Option<UncheckedUrl>)> {
219+
tags.iter().find_map(|t| match t {
220+
Tag::PubKey(pub_key, unchecked_url) if pub_key == awarded_public_key => {
221+
Some((*pub_key, unchecked_url.clone()))
222+
}
223+
_ => None,
224+
})
225+
}
226+
227+
/// Create a new [`ProfileBadgesEvent`] from badge definition and awards events
228+
/// [`badge_definitions`] and [`badge_awards`] must be ordered, so on the same position they refer to the same badge
229+
pub fn new(
230+
badge_definitions: Vec<Event>,
231+
badge_awards: Vec<Event>,
232+
pubkey_awarded: &XOnlyPublicKey,
233+
keys: &Keys,
234+
) -> Result<ProfileBadgesEvent, ProfileBadgesEventError> {
235+
if badge_definitions.len() != badge_awards.len() {
236+
return Err(ProfileBadgesEventError::InvalidLength);
237+
}
238+
239+
let mut badge_awards = ProfileBadgesEvent::filter_for_kind(badge_awards, &Kind::BadgeAward);
240+
if badge_awards.is_empty() {
241+
return Err(ProfileBadgesEventError::InvalidKind);
242+
}
243+
244+
for award in &badge_awards {
245+
if !award.tags.iter().any(|t| match t {
246+
Tag::PubKey(pub_key, _) => pub_key == pubkey_awarded,
247+
_ => false,
248+
}) {
249+
return Err(ProfileBadgesEventError::BadgeAwardsLackAwardedPublicKey);
250+
}
251+
}
252+
253+
let mut badge_definitions =
254+
ProfileBadgesEvent::filter_for_kind(badge_definitions, &Kind::BadgeDefinition);
255+
if badge_definitions.is_empty() {
256+
return Err(ProfileBadgesEventError::InvalidKind);
257+
}
258+
259+
// Add identifier `d` tag
260+
let id_tag = Tag::Identifier("profile_badges".to_owned());
261+
let mut tags: Vec<Tag> = vec![id_tag];
262+
263+
let badge_definitions_identifiers = badge_definitions
264+
.iter_mut()
265+
.map(|event| {
266+
let tags = core::mem::take(&mut event.tags);
267+
let id = Self::extract_identifier(tags).ok_or(
268+
ProfileBadgesEventError::BadgeDefinitionError(Error::IdentifierTagNotFound),
269+
)?;
270+
271+
Ok((event.clone(), id))
272+
})
273+
.collect::<Result<Vec<(Event, Tag)>, ProfileBadgesEventError>>();
274+
let badge_definitions_identifiers = badge_definitions_identifiers.map_err(|_| {
275+
ProfileBadgesEventError::BadgeDefinitionError(Error::IdentifierTagNotFound)
276+
})?;
277+
278+
let badge_awards_identifiers = badge_awards
279+
.iter_mut()
280+
.map(|event| {
281+
let tags = core::mem::take(&mut event.tags);
282+
let (_, relay_url) = Self::extract_awarded_public_key(&tags, pubkey_awarded)
283+
.ok_or(ProfileBadgesEventError::BadgeAwardsLackAwardedPublicKey)?;
284+
let (id, a_tag) = tags
285+
.iter()
286+
.find_map(|t| match t {
287+
Tag::A { identifier, .. } => Some((identifier.clone(), t.clone())),
288+
_ => None,
289+
})
290+
.ok_or(ProfileBadgesEventError::BadgeAwardMissingATag)?;
291+
Ok((event.clone(), id, a_tag, relay_url))
292+
})
293+
.collect::<Result<Vec<(Event, String, Tag, Option<UncheckedUrl>)>, ProfileBadgesEventError>>();
294+
let badge_awards_identifiers = badge_awards_identifiers?;
295+
296+
// This collection has been filtered for the needed tags
297+
let users_badges: Vec<(_, _)> =
298+
core::iter::zip(badge_definitions_identifiers, badge_awards_identifiers).collect();
299+
300+
for (badge_definition, badge_award) in users_badges {
301+
match (&badge_definition, &badge_award) {
302+
((_, Tag::Identifier(identifier)), (_, badge_id, ..)) if badge_id != identifier => {
303+
return Err(ProfileBadgesEventError::MismatchedBadgeDefinitionOrAward);
304+
}
305+
(
306+
(_, Tag::Identifier(identifier)),
307+
(badge_award_event, badge_id, a_tag, relay_url),
308+
) if badge_id == identifier => {
309+
let badge_definition_event_tag = a_tag.clone().to_owned();
310+
let badge_award_event_tag =
311+
Tag::Event(badge_award_event.id, relay_url.clone(), None);
312+
tags.extend_from_slice(&[badge_definition_event_tag, badge_award_event_tag]);
313+
}
314+
_ => {}
315+
}
316+
}
317+
318+
// Badge definitions and awards have been validated
319+
320+
let event_builder = EventBuilder::new(Kind::ProfileBadges, String::new(), &tags);
321+
let event = event_builder.to_event(keys)?;
322+
323+
Ok(ProfileBadgesEvent(event))
324+
}
325+
}

0 commit comments

Comments
 (0)