diff --git a/README.md b/README.md index c183437f9..ea6cce1c9 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Learn more about `rust-nostr` at . | ✅ | [98 - HTTP Auth](https://github.com/nostr-protocol/nips/blob/master/98.md) | | ❌ | [99 - Classified Listings](https://github.com/nostr-protocol/nips/blob/master/99.md) | | ✅ | [A0 - Voice Messages](https://github.com/nostr-protocol/nips/blob/master/A0.md) | -| ❌ | [B0 - Web Bookmarks](https://github.com/nostr-protocol/nips/blob/master/B0.md) | +| ✅ | [B0 - Web Bookmarks](https://github.com/nostr-protocol/nips/blob/master/B0.md) | | ✅ | [B7 - Blossom](https://github.com/nostr-protocol/nips/blob/master/B7.md) | | ✅ | [C0 - Code Snippets](https://github.com/nostr-protocol/nips/blob/master/C0.md) | | ✅ | [C7 - Chats](https://github.com/nostr-protocol/nips/blob/master/C7.md) | diff --git a/crates/nostr/CHANGELOG.md b/crates/nostr/CHANGELOG.md index 448a3fb6c..292d12594 100644 --- a/crates/nostr/CHANGELOG.md +++ b/crates/nostr/CHANGELOG.md @@ -35,6 +35,7 @@ ### Added +- Add NIP-B0 support (https://github.com/rust-nostr/nostr/pull/1077) - Add NIP-7D support (https://github.com/rust-nostr/nostr/pull/1071) - Add NIP-C7 support (https://github.com/rust-nostr/nostr/pull/1067) - Implement `ToBech32` trait for `Nip21` diff --git a/crates/nostr/src/event/builder.rs b/crates/nostr/src/event/builder.rs index 29aa3f94a..898645689 100644 --- a/crates/nostr/src/event/builder.rs +++ b/crates/nostr/src/event/builder.rs @@ -1945,6 +1945,14 @@ impl EventBuilder { Self::new(Kind::Comment, content).tags(tags) } + + /// Web Bookmark + /// + /// + #[inline] + pub fn web_bookmark(web_bookmark: WebBookmark) -> Self { + web_bookmark.to_event_builder() + } } fn has_nostr_event_uri(content: &str, event_id: &EventId) -> bool { diff --git a/crates/nostr/src/event/kind.rs b/crates/nostr/src/event/kind.rs index 030c12d8e..48ecb03ca 100644 --- a/crates/nostr/src/event/kind.rs +++ b/crates/nostr/src/event/kind.rs @@ -167,6 +167,7 @@ kind_variants! { PollResponse => 1018, "Poll response", "", ChatMessage => 9, "Chat Message", "", Thread => 11, "Thread", "", + WebBookmark => 39701, "Web Bookmark", "", } impl PartialEq for Kind { diff --git a/crates/nostr/src/nips/mod.rs b/crates/nostr/src/nips/mod.rs index 7ce5bcf87..2276f976e 100644 --- a/crates/nostr/src/nips/mod.rs +++ b/crates/nostr/src/nips/mod.rs @@ -55,4 +55,5 @@ pub mod nip94; pub mod nip96; #[cfg(feature = "nip98")] pub mod nip98; +pub mod nipb0; pub mod nipc0; diff --git a/crates/nostr/src/nips/nipb0.rs b/crates/nostr/src/nips/nipb0.rs new file mode 100644 index 000000000..8699f86bb --- /dev/null +++ b/crates/nostr/src/nips/nipb0.rs @@ -0,0 +1,94 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2025 Rust Nostr Developers +// Distributed under the MIT software license + +//! NIPB0: Web Bookmarks +//! +//! + +use alloc::string::String; +use alloc::vec::Vec; + +use crate::{EventBuilder, Kind, Tag, TagStandard, Timestamp}; + +/// Web Bookmark +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WebBookmark { + /// Description of the web bookmark. + pub description: String, + /// URL of the web bookmark. + pub url: String, + /// Timestamp when the web bookmark was first published. + pub published_at: Option, + /// Title of the web bookmark. + pub title: Option, + /// Hashtags for the web bookmark. + pub hashtags: Vec, +} + +impl WebBookmark { + /// Create a new web bookmark + #[inline] + pub fn new(description: T, url: T) -> Self + where + T: Into, + { + Self { + description: description.into(), + url: url.into(), + published_at: None, + title: None, + hashtags: Vec::new(), + } + } + + /// Set the title. + #[inline] + pub fn title(mut self, title: T) -> Self + where + T: Into, + { + self.title = Some(title.into()); + self + } + + /// Set the timestamp at which the web bookmark was published. + #[inline] + pub fn published_at(mut self, timestamp: Timestamp) -> Self { + self.published_at = Some(timestamp); + self + } + + /// Add a hashtag/tag. + pub fn hashtags(mut self, hashtag: T) -> Self + where + T: Into, + { + let hashtag = hashtag.into().to_lowercase(); + if !self.hashtags.contains(&hashtag) { + self.hashtags.push(hashtag); + } + self + } + + /// Convert the web bookmark to an event builder + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_event_builder(self) -> EventBuilder { + let mut tags: Vec = vec![TagStandard::Identifier(self.url).into()]; + + let mut add_if_some = |tag: Option| { + if let Some(tag) = tag { + tags.push(tag.into()); + } + }; + + add_if_some(self.published_at.map(TagStandard::PublishedAt)); + add_if_some(self.title.map(TagStandard::Title)); + + for hashtag in self.hashtags.into_iter() { + tags.push(TagStandard::Hashtag(hashtag).into()); + } + + EventBuilder::new(Kind::WebBookmark, self.description).tags(tags) + } +} diff --git a/crates/nostr/src/prelude.rs b/crates/nostr/src/prelude.rs index 27e0c9dd8..8cac6b497 100644 --- a/crates/nostr/src/prelude.rs +++ b/crates/nostr/src/prelude.rs @@ -77,6 +77,7 @@ pub use crate::nips::nip94::{self, *}; pub use crate::nips::nip96::{self, *}; #[cfg(feature = "nip98")] pub use crate::nips::nip98::{self, *}; +pub use crate::nips::nipb0::{self, *}; pub use crate::nips::nipc0::{self, *}; pub use crate::parser::{self, *}; pub use crate::signer::{self, *};