From b0f61dcf45d7c2abb11f1acc0d2a282e30c7df9e Mon Sep 17 00:00:00 2001 From: Yuki Kishimoto Date: Tue, 17 Dec 2024 10:41:37 +0100 Subject: [PATCH] nostr: add `nip22::extract_root` and `nip22:extract_parent` Closes https://github.com/rust-nostr/nostr/issues/677 Signed-off-by: Yuki Kishimoto --- CHANGELOG.md | 1 + crates/nostr/src/event/tag/list.rs | 2 +- crates/nostr/src/nips/mod.rs | 1 + crates/nostr/src/nips/nip22.rs | 160 +++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 crates/nostr/src/nips/nip22.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 70c4e57ad..9af13e322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,7 @@ * nostr: add `HttpData::to_authorization` ([Yuki Kishimoto]) * nostr: add `CoordinateBorrow` struct ([Yuki Kishimoto]) * nostr: add `Filter::custom_tags` ([Yuki Kishimoto]) +* nostr: add `nip22::extract_root` and `nip22:extract_parent` ([Yuki Kishimoto]) * database: add `Events::first_owned` and `Events::last_owned` ([Yuki Kishimoto]) * database: impl `FlatBufferDecodeBorrowed` for `EventBorrow` ([Yuki Kishimoto]) * database: add `NostrDatabaseWipe` trait ([Yuki Kishimoto]) diff --git a/crates/nostr/src/event/tag/list.rs b/crates/nostr/src/event/tag/list.rs index caab97207..71528d036 100644 --- a/crates/nostr/src/event/tag/list.rs +++ b/crates/nostr/src/event/tag/list.rs @@ -123,7 +123,7 @@ impl Tags { self.find(kind).and_then(|t| t.as_standardized()) } - /// Get first tag that match [`TagKind`]. + /// Filter tags that match [`TagKind`]. #[inline] pub fn filter<'a>(&'a self, kind: TagKind<'a>) -> impl Iterator { self.list.iter().filter(move |t| t.kind() == kind) diff --git a/crates/nostr/src/nips/mod.rs b/crates/nostr/src/nips/mod.rs index fb257ac99..a714e9abd 100644 --- a/crates/nostr/src/nips/mod.rs +++ b/crates/nostr/src/nips/mod.rs @@ -23,6 +23,7 @@ pub mod nip15; pub mod nip17; pub mod nip19; pub mod nip21; +pub mod nip22; pub mod nip26; pub mod nip34; pub mod nip35; diff --git a/crates/nostr/src/nips/nip22.rs b/crates/nostr/src/nips/nip22.rs new file mode 100644 index 000000000..96391d5f1 --- /dev/null +++ b/crates/nostr/src/nips/nip22.rs @@ -0,0 +1,160 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +//! NIP22: Comment +//! +//! + +use crate::nips::nip01::Coordinate; +use crate::nips::nip73::ExternalContentId; +use crate::{Event, EventId, Kind, PublicKey, RelayUrl, TagKind, TagStandard, Url}; + +/// Borrowed comment extracted data +pub enum Comment<'a> { + /// Event + Event { + /// Event ID + id: &'a EventId, + /// Relay hint + relay_hint: Option<&'a RelayUrl>, + /// Public key hint + pubkey_hint: Option<&'a PublicKey>, + /// Kind + kind: Option<&'a Kind>, + }, + /// Coordinate + Coordinate { + /// Coordinate + address: &'a Coordinate, + /// Relay hint + relay_hint: Option<&'a RelayUrl>, + /// Kind + kind: Option<&'a Kind>, + }, + /// External content + External { + /// Content + content: &'a ExternalContentId, + /// Web hint + hint: Option<&'a Url>, + /// Kind + /// + /// This isn't properly defined yet but it will surely be needed in the future. It could i.e. be a root domain + kind: Option<&'a Kind>, + }, +} + +/// Extract NIP22 root data +pub fn extract_root(event: &Event) -> Option { + extract_data(event, true) +} + +/// Extract NIP22 parent data +pub fn extract_parent(event: &Event) -> Option { + extract_data(event, false) +} + +fn extract_data(event: &Event, is_root: bool) -> Option { + if event.kind != Kind::Comment { + return None; + } + + let kind: Option<&Kind> = extract_kind(event, is_root); + let pubkey_hint: Option<&PublicKey> = extract_public_key(event, is_root); + + // Try to extract event + if let Some((event_id, relay_hint)) = extract_event(event, is_root) { + return Some(Comment::Event { + id: event_id, + relay_hint, + pubkey_hint, + kind, + }); + } + + // Try to extract coordinate + if let Some((address, relay_hint)) = extract_coordinate(event, is_root) { + return Some(Comment::Coordinate { + address, + relay_hint, + kind, + }); + } + + if let Some((content, hint)) = extract_external(event, is_root) { + return Some(Comment::External { + content, + hint, + kind, + }); + } + + None +} + +fn check_return(val: T, is_root: bool, uppercase: bool) -> Option { + if (is_root && uppercase) || (!is_root && !uppercase) { + return Some(val); + } + + None +} + +fn extract_kind(event: &Event, is_root: bool) -> Option<&Kind> { + let tag = event.tags.find_standardized(TagKind::k())?; + match tag { + TagStandard::Kind { kind, uppercase } => check_return(kind, is_root, *uppercase), + _ => None, + } +} + +fn extract_public_key(event: &Event, is_root: bool) -> Option<&PublicKey> { + let tag = event.tags.find_standardized(TagKind::p())?; + match tag { + TagStandard::PublicKey { + public_key, + uppercase, + .. + } => check_return(public_key, is_root, *uppercase), + _ => None, + } +} + +fn extract_event(event: &Event, is_root: bool) -> Option<(&EventId, Option<&RelayUrl>)> { + let tag = event.tags.find_standardized(TagKind::e())?; + match tag { + TagStandard::Event { + event_id, + relay_url, + uppercase, + .. + } => check_return((event_id, relay_url.as_ref()), is_root, *uppercase), + _ => None, + } +} + +fn extract_coordinate(event: &Event, is_root: bool) -> Option<(&Coordinate, Option<&RelayUrl>)> { + let tag = event.tags.find_standardized(TagKind::a())?; + match tag { + TagStandard::Coordinate { + coordinate, + relay_url, + uppercase, + .. + } => check_return((coordinate, relay_url.as_ref()), is_root, *uppercase), + _ => None, + } +} + +fn extract_external(event: &Event, is_root: bool) -> Option<(&ExternalContentId, Option<&Url>)> { + let tag = event.tags.find_standardized(TagKind::i())?; + match tag { + TagStandard::ExternalContent { + content, + hint, + uppercase, + } => check_return((content, hint.as_ref()), is_root, *uppercase), + _ => None, + } +}