|
2 | 2 | //!
|
3 | 3 | //! <https://github.com/nostr-protocol/nips/blob/master/58.md>
|
4 | 4 |
|
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}; |
6 | 9 |
|
7 | 10 | #[derive(Debug, thiserror::Error)]
|
8 | 11 | /// [`BadgeAward`] error
|
@@ -164,3 +167,159 @@ impl BadgeAward {
|
164 | 167 | Ok(BadgeAward(event))
|
165 | 168 | }
|
166 | 169 | }
|
| 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