From 8c6b2a1c731c93782963f21540ce48584d4eda91 Mon Sep 17 00:00:00 2001 From: reya Date: Fri, 8 Nov 2024 15:34:28 +0700 Subject: [PATCH] nostr: add `EventBuilder::comment` * Add support to uppercase `TagStandard::Kind` * Add `comment` example to `nostr-sdk` crate Closes https://github.com/rust-nostr/nostr/issues/611 Closes https://github.com/rust-nostr/nostr/pull/612 Co-authored-by: Yuki Kishimoto Signed-off-by: Yuki Kishimoto --- CHANGELOG.md | 1 + .../src/protocol/event/builder.rs | 22 ++++ .../src/protocol/event/tag/standard.rs | 18 ++- .../src/protocol/event/builder.rs | 22 ++++ crates/nostr-sdk/Cargo.toml | 7 +- crates/nostr-sdk/examples/comment.rs | 32 +++++ crates/nostr/README.md | 1 + crates/nostr/src/event/builder.rs | 117 +++++++++++++++++- crates/nostr/src/event/tag/standard.rs | 41 ++++-- crates/nostr/src/nips/nip34.rs | 1 + 10 files changed, 245 insertions(+), 17 deletions(-) create mode 100644 crates/nostr-sdk/examples/comment.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ee7678b07..98d3a011b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ * nostr: add `SingleLetterTag::as_str` and `TagKind::as_str` ([Yuki Kishimoto]) * nostr: add `Kind::Comment` ([reyamir]) +* nostr: add `EventBuilder::comment` ([reyamir]) ### Fixed diff --git a/bindings/nostr-sdk-ffi/src/protocol/event/builder.rs b/bindings/nostr-sdk-ffi/src/protocol/event/builder.rs index c5374d067..131946e3f 100644 --- a/bindings/nostr-sdk-ffi/src/protocol/event/builder.rs +++ b/bindings/nostr-sdk-ffi/src/protocol/event/builder.rs @@ -162,6 +162,28 @@ impl EventBuilder { } } + /// Comment + /// + /// If no `root` is passed, the `rely_to` will be used for root `e` tag. + /// + /// + #[uniffi::constructor(default(root = None, relay_url = None))] + pub fn comment( + content: String, + comment_to: &Event, + root: Option>, + relay_url: Option, + ) -> Self { + Self { + inner: nostr::EventBuilder::comment( + content, + comment_to.deref(), + root.as_ref().map(|e| e.as_ref().deref()), + relay_url.map(UncheckedUrl::from), + ), + } + } + /// Long-form text note (generally referred to as "articles" or "blog posts"). /// /// diff --git a/bindings/nostr-sdk-ffi/src/protocol/event/tag/standard.rs b/bindings/nostr-sdk-ffi/src/protocol/event/tag/standard.rs index 56c25278b..bb371c288 100644 --- a/bindings/nostr-sdk-ffi/src/protocol/event/tag/standard.rs +++ b/bindings/nostr-sdk-ffi/src/protocol/event/tag/standard.rs @@ -37,6 +37,8 @@ pub enum TagStandard { marker: Option, /// Should be the public key of the author of the referenced event public_key: Option>, + /// Whether the e tag is an uppercase E or not + uppercase: bool, }, /// Git clone (`clone` tag) /// @@ -108,6 +110,8 @@ pub enum TagStandard { }, Kind { kind: KindEnum, + /// Whether the k tag is an uppercase K or not + uppercase: bool, }, RelayUrl { relay_url: String, @@ -279,11 +283,13 @@ impl From for TagStandard { relay_url, marker, public_key, + uppercase, } => Self::EventTag { event_id: Arc::new(event_id.into()), relay_url: relay_url.map(|u| u.to_string()), marker: marker.map(|m| m.into()), public_key: public_key.map(|p| Arc::new(p.into())), + uppercase, }, tag::TagStandard::GitClone(urls) => Self::GitClone { urls: urls.into_iter().map(|r| r.to_string()).collect(), @@ -351,7 +357,10 @@ impl From for TagStandard { tag::TagStandard::ExternalIdentity(identity) => Self::ExternalIdentity { identity: identity.into(), }, - tag::TagStandard::Kind(kind) => Self::Kind { kind: kind.into() }, + tag::TagStandard::Kind { kind, uppercase } => Self::Kind { + kind: kind.into(), + uppercase, + }, tag::TagStandard::Relay(url) => Self::RelayUrl { relay_url: url.to_string(), }, @@ -475,11 +484,13 @@ impl TryFrom for tag::TagStandard { relay_url, marker, public_key, + uppercase, } => Ok(Self::Event { event_id: **event_id, relay_url: relay_url.map(UncheckedUrl::from), marker: marker.map(nip10::Marker::from), public_key: public_key.map(|p| **p), + uppercase, }), TagStandard::GitClone { urls } => { let mut parsed_urls: Vec = Vec::with_capacity(urls.len()); @@ -544,7 +555,10 @@ impl TryFrom for tag::TagStandard { coordinate: coordinate.as_ref().deref().clone(), relay_url: relay_url.map(UncheckedUrl::from), }), - TagStandard::Kind { kind } => Ok(Self::Kind(kind.into())), + TagStandard::Kind { kind, uppercase } => Ok(Self::Kind { + kind: kind.into(), + uppercase, + }), TagStandard::RelayUrl { relay_url } => Ok(Self::Relay(UncheckedUrl::from(relay_url))), TagStandard::POW { nonce, difficulty } => Ok(Self::POW { nonce: nonce.parse()?, diff --git a/bindings/nostr-sdk-js/src/protocol/event/builder.rs b/bindings/nostr-sdk-js/src/protocol/event/builder.rs index 73de7b254..886576661 100644 --- a/bindings/nostr-sdk-js/src/protocol/event/builder.rs +++ b/bindings/nostr-sdk-js/src/protocol/event/builder.rs @@ -160,6 +160,28 @@ impl JsEventBuilder { } } + /// Comment + /// + /// If no `root` is passed, the `rely_to` will be used for root `e` tag. + /// + /// + #[wasm_bindgen(js_name = comment)] + pub fn comment( + content: &str, + comment_to: &JsEvent, + root: Option, + relay_url: Option, + ) -> Self { + Self { + inner: EventBuilder::comment( + content, + comment_to.deref(), + root.as_deref(), + relay_url.map(UncheckedUrl::from), + ), + } + } + /// Long-form text note (generally referred to as "articles" or "blog posts"). /// /// diff --git a/crates/nostr-sdk/Cargo.toml b/crates/nostr-sdk/Cargo.toml index 5544dfc86..8cc839c84 100644 --- a/crates/nostr-sdk/Cargo.toml +++ b/crates/nostr-sdk/Cargo.toml @@ -69,13 +69,16 @@ name = "blacklist" required-features = ["all-nips"] [[example]] -name = "client-with-opts" +name = "client" required-features = ["all-nips"] [[example]] -name = "client" +name = "client-with-opts" required-features = ["all-nips"] +[[example]] +name = "comment" + [[example]] name = "fetch-events" required-features = ["all-nips"] diff --git a/crates/nostr-sdk/examples/comment.rs b/crates/nostr-sdk/examples/comment.rs new file mode 100644 index 000000000..ec5085318 --- /dev/null +++ b/crates/nostr-sdk/examples/comment.rs @@ -0,0 +1,32 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +use nostr_sdk::prelude::*; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + + let keys = Keys::generate(); + let client = Client::builder().signer(keys).build(); + + client.add_relay("wss://relay.damus.io/").await?; + client.add_relay("wss://relay.primal.net/").await?; + + client.connect().await; + + let event_id = + EventId::from_bech32("note1hrrgx2309my3wgeecx2tt6fl2nl8hcwl0myr3xvkcqpnq24pxg2q06armr")?; + let events = client + .fetch_events(vec![Filter::new().id(event_id)], None) + .await?; + + let comment_to = events.first().unwrap(); + let builder = EventBuilder::comment("This is a reply", comment_to, None, None); + + let output = client.send_event_builder(builder).await?; + println!("Output: {:?}", output); + + Ok(()) +} diff --git a/crates/nostr/README.md b/crates/nostr/README.md index 9c208c07a..8dd9436d5 100644 --- a/crates/nostr/README.md +++ b/crates/nostr/README.md @@ -122,6 +122,7 @@ The following crate feature flags are available: | ✅ | [18 - Reposts](https://github.com/nostr-protocol/nips/blob/master/18.md) | | ✅ | [19 - bech32-encoded entities](https://github.com/nostr-protocol/nips/blob/master/19.md) | | ✅ | [21 - URI scheme](https://github.com/nostr-protocol/nips/blob/master/21.md) | +| ✅ | [22 - Comment](https://github.com/nostr-protocol/nips/blob/master/22.md) | | ✅ | [23 - Long-form Content](https://github.com/nostr-protocol/nips/blob/master/23.md) | | ✅ | [24 - Extra metadata fields and tags](https://github.com/nostr-protocol/nips/blob/master/24.md) | | ✅ | [25 - Reactions](https://github.com/nostr-protocol/nips/blob/master/25.md) | diff --git a/crates/nostr/src/event/builder.rs b/crates/nostr/src/event/builder.rs index 631d833a8..3e36c12c8 100644 --- a/crates/nostr/src/event/builder.rs +++ b/crates/nostr/src/event/builder.rs @@ -440,6 +440,7 @@ impl EventBuilder { relay_url: relay_url.clone(), marker: Some(Marker::Root), public_key: Some(root.pubkey), + uppercase: false, })); tags.push(Tag::public_key(root.pubkey)); @@ -464,6 +465,7 @@ impl EventBuilder { relay_url: relay_url.clone(), marker: Some(Marker::Root), public_key: Some(reply_to.pubkey), + uppercase: false, })); } } @@ -474,6 +476,7 @@ impl EventBuilder { relay_url, marker: Some(Marker::Reply), public_key: Some(reply_to.pubkey), + uppercase: false, })); tags.push(Tag::public_key(reply_to.pubkey)); @@ -496,6 +499,104 @@ impl EventBuilder { Self::new(Kind::TextNote, content, tags) } + /// Comment + /// + /// If no `root` is passed, the `comment_to` will be used for root `e` tag. + /// + /// + pub fn comment( + content: S, + comment_to: &Event, + root: Option<&Event>, + relay_url: Option, + ) -> Self + where + S: Into, + { + // The added tags will be at least 4 + let mut tags: Vec = Vec::with_capacity(4); + + // Add `E` and `K` tag of **root** event + if let Some(root) = root { + // ID and author + tags.push(Tag::from_standardized_without_cell(TagStandard::Event { + event_id: root.id, + relay_url: relay_url.clone(), + marker: None, + public_key: Some(root.pubkey), + uppercase: true, + })); + + // Kind + tags.push(Tag::from_standardized_without_cell(TagStandard::Kind { + kind: root.kind, + uppercase: true, + })); + + // Add others `p` tags + tags.extend( + root.tags + .iter() + .filter(|t| { + t.kind() + == TagKind::SingleLetter(SingleLetterTag { + character: Alphabet::P, + uppercase: false, + }) + }) + .cloned(), + ); + } else { + // ID and author + tags.push(Tag::from_standardized_without_cell(TagStandard::Event { + event_id: comment_to.id, + relay_url: relay_url.clone(), + marker: None, + public_key: Some(comment_to.pubkey), + uppercase: true, + })); + + // Kind + tags.push(Tag::from_standardized_without_cell(TagStandard::Kind { + kind: comment_to.kind, + uppercase: true, + })); + } + + // Add `e` tag of event author + tags.push(Tag::from_standardized_without_cell(TagStandard::Event { + event_id: comment_to.id, + relay_url, + marker: None, + public_key: Some(comment_to.pubkey), + uppercase: false, + })); + + // Add `k` tag of event kind + tags.push(Tag::from_standardized_without_cell(TagStandard::Kind { + kind: comment_to.kind, + uppercase: false, + })); + + // Add others `p` tags of comment_to event + tags.extend( + comment_to + .tags + .iter() + .filter(|t| { + t.kind() + == TagKind::SingleLetter(SingleLetterTag { + character: Alphabet::P, + uppercase: false, + }) + }) + .cloned(), + ); + + // Compose event + Self::new(Kind::Comment, content, tags) + } + /// Long-form text note (generally referred to as "articles" or "blog posts"). /// /// @@ -561,6 +662,7 @@ impl EventBuilder { relay_url, marker: None, public_key: None, + uppercase: false, })], )) } @@ -580,6 +682,7 @@ impl EventBuilder { marker: None, // NOTE: not add public key since it's already included as `p` tag public_key: None, + uppercase: false, }), Tag::public_key(event.pubkey), ], @@ -595,9 +698,13 @@ impl EventBuilder { marker: None, // NOTE: not add public key since it's already included as `p` tag public_key: None, + uppercase: false, }), Tag::public_key(event.pubkey), - Tag::from_standardized_without_cell(TagStandard::Kind(event.kind)), + Tag::from_standardized_without_cell(TagStandard::Kind { + kind: event.kind, + uppercase: false, + }), ], ) } @@ -660,7 +767,10 @@ impl EventBuilder { tags.push(Tag::public_key(public_key)); if let Some(kind) = kind { - tags.push(Tag::from_standardized_without_cell(TagStandard::Kind(kind))); + tags.push(Tag::from_standardized_without_cell(TagStandard::Kind { + kind, + uppercase: false, + })); } Self::new(Kind::Reaction, reaction, tags) @@ -691,6 +801,7 @@ impl EventBuilder { relay_url: relay_url.map(|u| u.into()), marker: None, public_key: None, + uppercase: false, })], ) } @@ -711,6 +822,7 @@ impl EventBuilder { relay_url: Some(relay_url.into()), marker: Some(Marker::Root), public_key: None, + uppercase: false, })], ) } @@ -1133,6 +1245,7 @@ impl EventBuilder { relay_url: relay_url.clone(), marker: None, public_key: None, + uppercase: false, }); tags.extend_from_slice(&[a_tag.clone(), badge_award_event_tag]); } diff --git a/crates/nostr/src/event/tag/standard.rs b/crates/nostr/src/event/tag/standard.rs index 5db619bb0..0bf1b2f26 100644 --- a/crates/nostr/src/event/tag/standard.rs +++ b/crates/nostr/src/event/tag/standard.rs @@ -44,6 +44,8 @@ pub enum TagStandard { marker: Option, /// Should be the public key of the author of the referenced event public_key: Option, + /// Whether the e tag is an uppercase E or not + uppercase: bool, }, /// Report event /// @@ -101,7 +103,11 @@ pub enum TagStandard { coordinate: Coordinate, relay_url: Option, }, - Kind(Kind), + Kind { + kind: Kind, + /// Whether the k tag is an uppercase K or not + uppercase: bool, + }, Relay(UncheckedUrl), /// Proof of Work /// @@ -313,10 +319,6 @@ impl TagStandard { character: Alphabet::D, uppercase: false, }) => Ok(Self::Identifier(tag_1.to_string())), - TagKind::SingleLetter(SingleLetterTag { - character: Alphabet::K, - uppercase: false, - }) => Ok(Self::Kind(Kind::from_str(tag_1)?)), TagKind::SingleLetter(SingleLetterTag { character: Alphabet::M, uppercase: false, @@ -431,6 +433,7 @@ impl TagStandard { relay_url: None, marker: None, public_key: None, + uppercase: false, } } @@ -462,9 +465,11 @@ impl TagStandard { /// Get tag kind pub fn kind(&self) -> TagKind { match self { - Self::Event { .. } | Self::EventReport(..) => { - TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::E)) - } + Self::Event { uppercase, .. } => TagKind::SingleLetter(SingleLetterTag { + character: Alphabet::E, + uppercase: *uppercase, + }), + Self::EventReport(..) => TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::E)), Self::GitClone(..) => TagKind::Clone, Self::GitCommit(..) => TagKind::Commit, Self::GitEarliestUniqueCommitId(..) => { @@ -507,9 +512,9 @@ impl TagStandard { character: Alphabet::A, uppercase: false, }), - Self::Kind(..) => TagKind::SingleLetter(SingleLetterTag { + Self::Kind { uppercase, .. } => TagKind::SingleLetter(SingleLetterTag { character: Alphabet::K, - uppercase: false, + uppercase: *uppercase, }), Self::Relay(..) => TagKind::Relay, Self::POW { .. } => TagKind::Nonce, @@ -593,6 +598,7 @@ impl From for Vec { relay_url, marker, public_key, + .. } => { // ["e", , , , ] // , and are optional @@ -702,7 +708,7 @@ impl From for Vec { TagStandard::ExternalIdentity(identity) => { vec![tag_kind, identity.tag_platform_identity(), identity.proof] } - TagStandard::Kind(kind) => vec![tag_kind, kind.to_string()], + TagStandard::Kind { kind, .. } => vec![tag_kind, kind.to_string()], TagStandard::Relay(url) => vec![tag_kind, url.to_string()], TagStandard::POW { nonce, difficulty } => { vec![tag_kind, nonce.to_string(), difficulty.to_string()] @@ -927,6 +933,7 @@ where relay_url: (!tag_2.is_empty()).then_some(UncheckedUrl::from(tag_2)), marker, public_key, + uppercase: false, }) } }; @@ -1135,6 +1142,7 @@ mod tests { relay_url: None, marker: Some(Marker::Reply), public_key: None, + uppercase: false, }; assert!(tag.is_reply()); @@ -1146,6 +1154,7 @@ mod tests { relay_url: None, marker: Some(Marker::Root), public_key: None, + uppercase: false, }; assert!(!tag.is_reply()); } @@ -1247,6 +1256,7 @@ mod tests { relay_url: Some(UncheckedUrl::empty()), marker: None, public_key: None, + uppercase: false, } .to_vec() ); @@ -1265,6 +1275,7 @@ mod tests { relay_url: Some(UncheckedUrl::from("wss://relay.damus.io")), marker: None, public_key: None, + uppercase: false, } .to_vec() ); @@ -1421,6 +1432,7 @@ mod tests { relay_url: None, marker: Some(Marker::Reply), public_key: None, + uppercase: false, } .to_vec() ); @@ -1446,6 +1458,7 @@ mod tests { ) .unwrap() ), + uppercase: false, } .to_vec() ); @@ -1470,6 +1483,7 @@ mod tests { ) .unwrap() ), + uppercase: false, } .to_vec() ); @@ -1712,6 +1726,7 @@ mod tests { relay_url: None, marker: None, public_key: None, + uppercase: false, } ); @@ -1730,6 +1745,7 @@ mod tests { relay_url: Some(UncheckedUrl::from("wss://relay.damus.io")), marker: None, public_key: None, + uppercase: false, } ); @@ -1884,6 +1900,7 @@ mod tests { relay_url: None, marker: Some(Marker::Reply), public_key: None, + uppercase: false, } ); @@ -1909,6 +1926,7 @@ mod tests { ) .unwrap() ), + uppercase: false, } ); @@ -1933,6 +1951,7 @@ mod tests { ) .unwrap() ), + uppercase: false, } ); diff --git a/crates/nostr/src/nips/nip34.rs b/crates/nostr/src/nips/nip34.rs index f1934bdb7..9d13b1bf8 100644 --- a/crates/nostr/src/nips/nip34.rs +++ b/crates/nostr/src/nips/nip34.rs @@ -304,6 +304,7 @@ impl GitPatch { relay_url: None, marker: Some(Marker::Reply), public_key: None, + uppercase: false, })); } None => tags.push(Tag::hashtag("root")),