Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nostr: add nip22::extract_root and nip22:extract_parent #729

Merged
merged 2 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,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])
Expand Down
1 change: 1 addition & 0 deletions bindings/nostr-sdk-ffi/src/protocol/nips/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod nip15;
pub mod nip17;
pub mod nip19;
pub mod nip21;
pub mod nip22;
pub mod nip26;
pub mod nip34;
pub mod nip39;
Expand Down
91 changes: 91 additions & 0 deletions bindings/nostr-sdk-ffi/src/protocol/nips/nip22.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) 2022-2023 Yuki Kishimoto
// Copyright (c) 2023-2024 Rust Nostr Developers
// Distributed under the MIT software license

use std::ops::Deref;
use std::sync::Arc;

use nostr::nips::nip22::{self, Comment};
use uniffi::Enum;

use super::nip01::Coordinate;
use super::nip73::ExternalContentId;
use crate::protocol::event::{Event, EventId, Kind};
use crate::protocol::key::PublicKey;

/// Extracted NIP22 comment
///
/// <https://github.com/nostr-protocol/nips/blob/master/22.md>
#[derive(Enum)]
pub enum ExtractedComment {
/// Event
Event {
/// Event ID
id: Arc<EventId>,
/// Relay hint
relay_hint: Option<String>,
/// Public key hint
pubkey_hint: Option<Arc<PublicKey>>,
/// Kind
kind: Option<Arc<Kind>>,
},
/// Coordinate
// NOTE: the enum variant can't have the same name of types in inner fields, otherwise will create issues with kotlin,
// so rename this to `Address`.
Address {
/// Coordinate
address: Arc<Coordinate>,
/// Relay hint
relay_hint: Option<String>,
/// Kind
kind: Option<Arc<Kind>>,
},
/// External content
External {
/// Content
content: ExternalContentId,
/// Web hint
hint: Option<String>,
},
}

impl From<Comment<'_>> for ExtractedComment {
fn from(comment: Comment<'_>) -> Self {
match comment {
Comment::Event {
id,
relay_hint,
pubkey_hint,
kind,
} => Self::Event {
id: Arc::new((*id).into()),
relay_hint: relay_hint.map(|u| u.to_string()),
pubkey_hint: pubkey_hint.map(|p| Arc::new((*p).into())),
kind: kind.map(|k| Arc::new((*k).into())),
},
Comment::Coordinate {
address,
relay_hint,
kind,
} => Self::Address {
address: Arc::new(address.clone().into()),
relay_hint: relay_hint.map(|u| u.to_string()),
kind: kind.map(|k| Arc::new((*k).into())),
},
Comment::External { content, hint } => Self::External {
content: content.clone().into(),
hint: hint.map(|u| u.to_string()),
},
}
}
}

/// Extract NIP22 root comment data
pub fn nip22_extract_root(event: &Event) -> Option<ExtractedComment> {
nip22::extract_root(event.deref()).map(|c| c.into())
}

/// Extract NIP22 parent comment data
pub fn nip22_extract_parent(event: &Event) -> Option<ExtractedComment> {
nip22::extract_parent(event.deref()).map(|c| c.into())
}
2 changes: 1 addition & 1 deletion crates/nostr/src/event/tag/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item = &'a Tag> {
self.list.iter().filter(move |t| t.kind() == kind)
Expand Down
1 change: 1 addition & 0 deletions crates/nostr/src/nips/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
173 changes: 173 additions & 0 deletions crates/nostr/src/nips/nip22.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Copyright (c) 2022-2023 Yuki Kishimoto
// Copyright (c) 2023-2024 Rust Nostr Developers
// Distributed under the MIT software license

//! NIP22: Comment
//!
//! <https://github.com/nostr-protocol/nips/blob/master/22.md>

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>,
},
}

/// Extract NIP22 root data
pub fn extract_root(event: &Event) -> Option<Comment> {
extract_data(event, true)
}

/// Extract NIP22 parent data
pub fn extract_parent(event: &Event) -> Option<Comment> {
extract_data(event, false)
}

fn extract_data(event: &Event, is_root: bool) -> Option<Comment> {
if event.kind != Kind::Comment {
return None;
}

// Try to extract event
if let Some((event_id, relay_hint, public_key)) = extract_event(event, is_root) {
return Some(Comment::Event {
id: event_id,
relay_hint,
pubkey_hint: public_key,
kind: extract_kind(event, is_root),
});
}

// Try to extract coordinate
if let Some((address, relay_hint)) = extract_coordinate(event, is_root) {
return Some(Comment::Coordinate {
address,
relay_hint,
kind: extract_kind(event, is_root),
});
}

if let Some((content, hint)) = extract_external(event, is_root) {
return Some(Comment::External { content, hint });
}

None
}

fn check_return<T>(val: T, is_root: bool, uppercase: bool) -> Option<T> {
if (is_root && uppercase) || (!is_root && !uppercase) {
return Some(val);
}

None
}

/// Returns the first kind tag that matches the `is_root` condition.
///
/// # Example:
/// * is_root = true -> returns first `K` tag
/// * is_root = false -> returns first `k` tag
fn extract_kind(event: &Event, is_root: bool) -> Option<&Kind> {
event
.tags
.filter_standardized(TagKind::k())
.find_map(|tag| match tag {
TagStandard::Kind { kind, uppercase } => check_return(kind, is_root, *uppercase),
_ => None,
})
}

/// Returns the first event tag that matches the `is_root` condition.
///
/// # Example:
/// * is_root = true -> returns first `E` tag
/// * is_root = false -> returns first `e` tag
fn extract_event(
event: &Event,
is_root: bool,
) -> Option<(&EventId, Option<&RelayUrl>, Option<&PublicKey>)> {
event
.tags
.filter_standardized(TagKind::e())
.find_map(|tag| match tag {
TagStandard::Event {
event_id,
relay_url,
public_key,
uppercase,
..
} => check_return(
(event_id, relay_url.as_ref(), public_key.as_ref()),
is_root,
*uppercase,
),
_ => None,
})
}

/// Returns the first coordinate tag that matches the `is_root` condition.
///
/// # Example:
/// * is_root = true -> returns first `A` tag
/// * is_root = false -> returns first `a` tag
fn extract_coordinate(event: &Event, is_root: bool) -> Option<(&Coordinate, Option<&RelayUrl>)> {
event
.tags
.filter_standardized(TagKind::a())
.find_map(|tag| match tag {
TagStandard::Coordinate {
coordinate,
relay_url,
uppercase,
..
} => check_return((coordinate, relay_url.as_ref()), is_root, *uppercase),
_ => None,
})
}

/// Returns the first external content tag that matches the `is_root` condition.
///
/// # Example:
/// * is_root = true -> returns first `I` tag
/// * is_root = false -> returns first `i` tag
fn extract_external(event: &Event, is_root: bool) -> Option<(&ExternalContentId, Option<&Url>)> {
event
.tags
.filter_standardized(TagKind::i())
.find_map(|tag| match tag {
TagStandard::ExternalContent {
content,
hint,
uppercase,
} => check_return((content, hint.as_ref()), is_root, *uppercase),
_ => None,
})
}
Loading