From 0802f2992cf63169b7116382f9d9eea1e4dacc55 Mon Sep 17 00:00:00 2001 From: Yuki Kishimoto Date: Thu, 25 Apr 2024 20:24:52 +0200 Subject: [PATCH] nostr: rewrite tag * Rename `Tag` enum to `TagStandard` * Add `Tag` struct * Remove `GenericTagValue` Signed-off-by: Yuki Kishimoto --- CHANGELOG.md | 3 + bindings/nostr-ffi/src/event/builder.rs | 15 +- bindings/nostr-ffi/src/event/mod.rs | 2 +- bindings/nostr-ffi/src/event/tag.rs | 578 +++++---- bindings/nostr-ffi/src/lib.rs | 3 +- bindings/nostr-ffi/src/nips/nip65.rs | 2 +- bindings/nostr-ffi/src/types/filter.rs | 12 +- bindings/nostr-js/src/event/builder.rs | 12 +- bindings/nostr-js/src/event/tag.rs | 173 ++- bindings/nostr-js/src/nips/nip65.rs | 12 +- bindings/nostr-js/src/types/filter.rs | 6 + crates/nostr-database/examples/indexes.rs | 2 +- crates/nostr-database/examples/memory.rs | 2 +- crates/nostr-database/src/index.rs | 8 +- crates/nostr-database/src/tag_indexes.rs | 11 +- crates/nostr-sdk/src/client/mod.rs | 10 +- crates/nostr/Cargo.toml | 3 +- crates/nostr/src/event/builder.rs | 253 ++-- crates/nostr/src/event/id.rs | 7 + crates/nostr/src/event/mod.rs | 41 +- crates/nostr/src/event/tag.rs | 1326 ++++++++++++--------- crates/nostr/src/key/mod.rs | 2 +- crates/nostr/src/key/public_key.rs | 7 + crates/nostr/src/lib.rs | 6 +- crates/nostr/src/nips/nip01.rs | 14 +- crates/nostr/src/nips/nip07.rs | 4 +- crates/nostr/src/nips/nip15.rs | 35 +- crates/nostr/src/nips/nip48.rs | 9 +- crates/nostr/src/nips/nip51.rs | 29 +- crates/nostr/src/nips/nip53.rs | 94 +- crates/nostr/src/nips/nip57.rs | 24 +- crates/nostr/src/nips/nip58.rs | 14 +- crates/nostr/src/nips/nip65.rs | 12 +- crates/nostr/src/nips/nip94.rs | 134 ++- crates/nostr/src/nips/nip98.rs | 23 +- crates/nostr/src/types/filter.rs | 130 +- crates/nostr/src/types/mod.rs | 2 +- 37 files changed, 1785 insertions(+), 1235 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 517427c75..74aca0c9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * nostr: adj. kind to be `u16` instead of `u64` according to NIP01 ([Yuki Kishimoto]) * nostr: improve NIP19 serialization performance ([Yuki Kishimoto]) * nostr: improve `EventId::from_hex` performance ([Yuki Kishimoto]) +* nostr: rename `Tag` enum to `TagStandard` ([Yuki Kishimoto]) * database: small improvements to flatbuffers `Event::encode` ([Yuki Kishimoto]) * pool: inline `RelayPool` methods ([Yuki Kishimoto]) * sdk: inline `Client`, `ClientBuilder` and `Options` methods ([Yuki Kishimoto]) @@ -40,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * nostr: add tags indexes to `Event` ([Yuki Kishimoto]) * nostr: add `hex::decode_to_slice` ([Yuki Kishimoto]) * nostr: add `SecretKey::generate` ([Yuki Kishimoto]) +* nostr: add `Tag` struct ([Yuki Kishimoto]) * pool: add `RelayPool::start` ([Yuki Kishimoto]) * pool: add `NegentropyDirection` default ([Yuki Kishimoto]) * sdk: add `Client::builder()` ([Yuki Kishimoto]) @@ -56,6 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +* nostr: remove `GenericTagValue` ([Yuki Kishimoto]) * ffi(nostr): remove `Kind::match*` methods ([Yuki Kishimoto]) ## [v0.30.0] diff --git a/bindings/nostr-ffi/src/event/builder.rs b/bindings/nostr-ffi/src/event/builder.rs index edc6b6b7c..e806375f0 100644 --- a/bindings/nostr-ffi/src/event/builder.rs +++ b/bindings/nostr-ffi/src/event/builder.rs @@ -95,13 +95,16 @@ impl EventBuilder { } #[uniffi::constructor] - pub fn relay_list(list: HashMap>) -> Self { - let iter = list - .into_iter() - .map(|(url, r)| (UncheckedUrl::from(url), r.map(|r| r.into()))); - Self { - inner: nostr::EventBuilder::relay_list(iter), + pub fn relay_list(map: HashMap>) -> Result { + let mut list = Vec::with_capacity(map.len()); + for (url, metadata) in map.into_iter() { + let relay_url: Url = Url::parse(&url)?; + let metadata = metadata.map(|m| m.into()); + list.push((relay_url, metadata)) } + Ok(Self { + inner: nostr::EventBuilder::relay_list(list), + }) } #[uniffi::constructor] diff --git a/bindings/nostr-ffi/src/event/mod.rs b/bindings/nostr-ffi/src/event/mod.rs index 7dec32495..0f7371cc2 100644 --- a/bindings/nostr-ffi/src/event/mod.rs +++ b/bindings/nostr-ffi/src/event/mod.rs @@ -18,7 +18,7 @@ pub mod unsigned; pub use self::builder::EventBuilder; pub use self::id::EventId; pub use self::kind::{Kind, KindEnum}; -pub use self::tag::{RelayMetadata, Tag, TagEnum, TagKind}; +pub use self::tag::{RelayMetadata, Tag, TagKind}; pub use self::unsigned::UnsignedEvent; use crate::error::Result; use crate::nips::nip01::Coordinate; diff --git a/bindings/nostr-ffi/src/event/tag.rs b/bindings/nostr-ffi/src/event/tag.rs index effdb4059..e52fcfcd4 100644 --- a/bindings/nostr-ffi/src/event/tag.rs +++ b/bindings/nostr-ffi/src/event/tag.rs @@ -2,8 +2,7 @@ // Copyright (c) 2023-2024 Rust Nostr Developers // Distributed under the MIT software license -#![allow(non_camel_case_types)] - +use std::borrow::Cow; use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; @@ -245,7 +244,7 @@ pub enum TagKind { }, } -impl From for TagKind { +impl<'a> From> for TagKind { fn from(value: tag::TagKind) -> Self { match value { tag::TagKind::SingleLetter(single_letter) => Self::SingleLetter { @@ -291,12 +290,14 @@ impl From for TagKind { tag::TagKind::Encrypted => Self::Encrypted, tag::TagKind::Request => Self::Request, tag::TagKind::Word => Self::Word, - tag::TagKind::Custom(unknown) => Self::Unknown { unknown }, + tag::TagKind::Custom(unknown) => Self::Unknown { + unknown: unknown.to_string(), + }, } } } -impl From for tag::TagKind { +impl<'a> From for tag::TagKind<'a> { fn from(value: TagKind) -> Self { match value { TagKind::SingleLetter { single_letter } => Self::SingleLetter(**single_letter), @@ -340,23 +341,218 @@ impl From for tag::TagKind { TagKind::Encrypted => Self::Encrypted, TagKind::Request => Self::Request, TagKind::Word => Self::Word, - TagKind::Unknown { unknown } => Self::Custom(unknown), + TagKind::Unknown { unknown } => Self::Custom(Cow::Owned(unknown)), } } } +/// Tag +#[derive(Debug, PartialEq, Eq, Hash, Object)] +#[uniffi::export(Debug, Eq, Hash)] +pub struct Tag { + inner: tag::Tag, +} + +impl Deref for Tag { + type Target = tag::Tag; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl From for Tag { + fn from(inner: tag::Tag) -> Self { + Self { inner } + } +} + +#[uniffi::export] +impl Tag { + /// Parse tag + /// + /// Return error if the tag is empty! + #[inline] + #[uniffi::constructor] + pub fn parse(data: &[String]) -> Result { + Ok(Self { + inner: tag::Tag::parse(data)?, + }) + } + + /// Construct from standardized tag + #[inline] + #[uniffi::constructor] + pub fn from_standardized(standardized: TagStandard) -> Result { + let standardized: tag::TagStandard = tag::TagStandard::try_from(standardized)?; + Ok(Self { + inner: tag::Tag::from_standardized(standardized), + }) + } + + /// Get tag kind + #[inline] + pub fn kind(&self) -> TagKind { + self.inner.kind().into() + } + + /// Return the **first** tag value (index `1`), if exists. + #[inline] + pub fn content(&self) -> Option { + self.inner.content().map(|c| c.to_string()) + } + + /// Get `SingleLetterTag` + #[inline] + pub fn single_letter_tag(&self) -> Option> { + self.inner.single_letter_tag().map(|s| Arc::new(s.into())) + } + + /// Get standardized tag + pub fn as_standardized(&self) -> Option { + self.inner.as_standardized().cloned().map(|t| t.into()) + } + + /// Get array of strings + pub fn as_vec(&self) -> Vec { + self.inner.as_vec().to_vec() + } + + /// Compose `["e", "]` + /// + /// + #[inline] + #[uniffi::constructor] + pub fn event(event_id: &EventId) -> Self { + Self { + inner: tag::Tag::event(**event_id), + } + } + + /// Compose `["p", ""]` tag + /// + /// + #[inline] + #[uniffi::constructor] + pub fn public_key(public_key: &PublicKey) -> Self { + Self { + inner: tag::Tag::public_key(**public_key), + } + } + + /// Compose `["d", ""]` tag + /// + /// + #[inline] + #[uniffi::constructor] + pub fn identifier(identifier: &str) -> Self { + Self { + inner: tag::Tag::identifier(identifier), + } + } + + /// Compose `["a", ""]` tag + /// + /// + #[inline] + #[uniffi::constructor] + pub fn coordinate(coordinate: &Coordinate) -> Self { + Self { + inner: tag::Tag::coordinate(coordinate.deref().clone()), + } + } + + /// Compose `["nonce", "", ""]` tag + /// + /// + #[inline] + #[uniffi::constructor] + pub fn pow(nonce: u64, difficulty: u8) -> Self { + Self { + inner: tag::Tag::pow(nonce as u128, difficulty), + } + } + + /// Compose `["expiration", ""]` tag + /// + /// + #[inline] + #[uniffi::constructor] + pub fn expiration(timestamp: &Timestamp) -> Self { + Self { + inner: tag::Tag::expiration(**timestamp), + } + } + + /// Compose `["e", "", ""]` tag + /// + /// + #[inline] + #[uniffi::constructor] + pub fn event_report(event_id: &EventId, report: Report) -> Self { + Self { + inner: tag::Tag::event_report(**event_id, report.into()), + } + } + + /// Compose `["p", "", ""]` tag + /// + /// + #[inline] + #[uniffi::constructor] + pub fn public_key_report(public_key: &PublicKey, report: Report) -> Self { + Self { + inner: tag::Tag::public_key_report(**public_key, report.into()), + } + } + + /// Compose `["r", "", ""]` tag + /// + /// + #[inline] + #[uniffi::constructor] + pub fn relay_metadata(relay_url: &str, metadata: Option) -> Result { + let relay_url: Url = Url::from_str(relay_url)?; + Ok(Self { + inner: tag::Tag::relay_metadata(relay_url, metadata.map(|m| m.into())), + }) + } + + /// Compose `["t", ""]` tag + #[inline] + #[uniffi::constructor] + pub fn hashtag(hashtag: &str) -> Self { + Self { + inner: tag::Tag::hashtag(hashtag), + } + } + + /// Compose custom tag + /// + /// JSON: `["", "", "", ...]` + #[inline] + #[uniffi::constructor] + pub fn custom(kind: TagKind, values: &[String]) -> Self { + Self { + inner: tag::Tag::custom(kind.into(), values), + } + } + + /// Check if `Tag` is an event `reply` + pub fn is_reply(&self) -> bool { + self.inner.is_reply() + } +} + +/// Standardized tag #[derive(Enum)] -pub enum TagEnum { - Unknown { - kind: TagKind, - data: Vec, - }, - EventTag { +pub enum TagStandard { + Event { event_id: Arc, relay_url: Option, marker: Option, }, - PublicKeyTag { + PublicKey { public_key: Arc, relay_url: Option, alias: Option, @@ -380,7 +576,7 @@ pub enum TagEnum { Reference { reference: String, }, - RelayMetadataTag { + RelayMetadata { relay_url: String, rw: Option, }, @@ -393,10 +589,10 @@ pub enum TagEnum { Identifier { identifier: String, }, - ExternalIdentityTag { + ExternalIdentity { identity: Identity, }, - A { + Coordinate { coordinate: Arc, relay_url: Option, }, @@ -466,7 +662,7 @@ pub enum TagEnum { PublishedAt { timestamp: Arc, }, - UrlTag { + Url { url: String, }, MimeType { @@ -504,7 +700,7 @@ pub enum TagEnum { Ends { timestamp: Arc, }, - LiveEventStatusTag { + LiveEventStatus { status: LiveEventStatus, }, CurrentParticipants { @@ -537,7 +733,7 @@ pub enum TagEnum { Request { event: Arc, }, - DataVendingMachineStatusTag { + DataVendingMachineStatus { status: DataVendingMachineStatus, extra_info: Option, }, @@ -552,42 +748,38 @@ pub enum TagEnum { }, } -impl From for TagEnum { - fn from(value: tag::Tag) -> Self { +impl From for TagStandard { + fn from(value: tag::TagStandard) -> Self { match value { - tag::Tag::Generic(kind, data) => Self::Unknown { - kind: kind.into(), - data, - }, - tag::Tag::Event { + tag::TagStandard::Event { event_id, relay_url, marker, - } => Self::EventTag { + } => Self::Event { event_id: Arc::new(event_id.into()), relay_url: relay_url.map(|u| u.to_string()), marker: marker.map(|m| m.into()), }, - tag::Tag::PublicKey { + tag::TagStandard::PublicKey { public_key, relay_url, alias, uppercase, - } => Self::PublicKeyTag { + } => Self::PublicKey { public_key: Arc::new(public_key.into()), relay_url: relay_url.map(|u| u.to_string()), alias, uppercase, }, - tag::Tag::EventReport(id, report) => Self::EventReport { + tag::TagStandard::EventReport(id, report) => Self::EventReport { event_id: Arc::new(id.into()), report: report.into(), }, - tag::Tag::PubKeyReport(pk, report) => Self::PubKeyReport { + tag::TagStandard::PublicKeyReport(pk, report) => Self::PubKeyReport { public_key: Arc::new(pk.into()), report: report.into(), }, - tag::Tag::PubKeyLiveEvent { + tag::TagStandard::PubKeyLiveEvent { public_key, relay_url, marker, @@ -598,33 +790,36 @@ impl From for TagEnum { marker: marker.into(), proof: proof.map(|p| p.to_string()), }, - tag::Tag::Reference(r) => Self::Reference { reference: r }, - tag::Tag::RelayMetadata(url, rw) => Self::RelayMetadataTag { - relay_url: url.to_string(), - rw: rw.map(|rw| rw.into()), + tag::TagStandard::Reference(r) => Self::Reference { reference: r }, + tag::TagStandard::RelayMetadata { + relay_url, + metadata, + } => Self::RelayMetadata { + relay_url: relay_url.to_string(), + rw: metadata.map(|rw| rw.into()), }, - tag::Tag::Hashtag(t) => Self::Hashtag { hashtag: t }, - tag::Tag::Geohash(g) => Self::Geohash { geohash: g }, - tag::Tag::Identifier(d) => Self::Identifier { identifier: d }, - tag::Tag::A { + tag::TagStandard::Hashtag(t) => Self::Hashtag { hashtag: t }, + tag::TagStandard::Geohash(g) => Self::Geohash { geohash: g }, + tag::TagStandard::Identifier(d) => Self::Identifier { identifier: d }, + tag::TagStandard::Coordinate { coordinate, relay_url, - } => Self::A { + } => Self::Coordinate { coordinate: Arc::new(coordinate.into()), relay_url: relay_url.map(|u| u.to_string()), }, - tag::Tag::ExternalIdentity(identity) => Self::ExternalIdentityTag { + tag::TagStandard::ExternalIdentity(identity) => Self::ExternalIdentity { identity: identity.into(), }, - tag::Tag::Kind(kind) => Self::Kind { kind: kind.into() }, - tag::Tag::Relay(url) => Self::RelayUrl { + tag::TagStandard::Kind(kind) => Self::Kind { kind: kind.into() }, + tag::TagStandard::Relay(url) => Self::RelayUrl { relay_url: url.to_string(), }, - tag::Tag::POW { nonce, difficulty } => Self::POW { + tag::TagStandard::POW { nonce, difficulty } => Self::POW { nonce: nonce.to_string(), difficulty, }, - tag::Tag::Delegation { + tag::TagStandard::Delegation { delegator, conditions, sig, @@ -633,105 +828,104 @@ impl From for TagEnum { conditions: conditions.to_string(), sig: sig.to_string(), }, - tag::Tag::ContentWarning { reason } => Self::ContentWarning { reason }, - tag::Tag::Expiration(timestamp) => Self::Expiration { + tag::TagStandard::ContentWarning { reason } => Self::ContentWarning { reason }, + tag::TagStandard::Expiration(timestamp) => Self::Expiration { timestamp: Arc::new(timestamp.into()), }, - tag::Tag::Subject(sub) => Self::Subject { subject: sub }, - tag::Tag::Challenge(challenge) => Self::Challenge { challenge }, - tag::Tag::Title(title) => Self::Title { title }, - tag::Tag::Image(image, dimensions) => Self::Image { + tag::TagStandard::Subject(sub) => Self::Subject { subject: sub }, + tag::TagStandard::Challenge(challenge) => Self::Challenge { challenge }, + tag::TagStandard::Title(title) => Self::Title { title }, + tag::TagStandard::Image(image, dimensions) => Self::Image { url: image.to_string(), dimensions: dimensions.map(|d| Arc::new(d.into())), }, - tag::Tag::Thumb(thumb, dimensions) => Self::Thumb { + tag::TagStandard::Thumb(thumb, dimensions) => Self::Thumb { url: thumb.to_string(), dimensions: dimensions.map(|d| Arc::new(d.into())), }, - tag::Tag::Summary(summary) => Self::Summary { summary }, - tag::Tag::PublishedAt(timestamp) => Self::PublishedAt { + tag::TagStandard::Summary(summary) => Self::Summary { summary }, + tag::TagStandard::PublishedAt(timestamp) => Self::PublishedAt { timestamp: Arc::new(timestamp.into()), }, - tag::Tag::Description(description) => Self::Description { desc: description }, - tag::Tag::Bolt11(bolt11) => Self::Bolt11 { bolt11 }, - tag::Tag::Preimage(preimage) => Self::Preimage { preimage }, - tag::Tag::Relays(relays) => Self::Relays { + tag::TagStandard::Description(description) => Self::Description { desc: description }, + tag::TagStandard::Bolt11(bolt11) => Self::Bolt11 { bolt11 }, + tag::TagStandard::Preimage(preimage) => Self::Preimage { preimage }, + tag::TagStandard::Relays(relays) => Self::Relays { urls: relays.into_iter().map(|r| r.to_string()).collect(), }, - tag::Tag::Amount { millisats, bolt11 } => Self::Amount { millisats, bolt11 }, - tag::Tag::Name(name) => Self::Name { name }, - tag::Tag::Lnurl(lnurl) => Self::Lnurl { lnurl }, - tag::Tag::Url(url) => Self::UrlTag { + tag::TagStandard::Amount { millisats, bolt11 } => Self::Amount { millisats, bolt11 }, + tag::TagStandard::Name(name) => Self::Name { name }, + tag::TagStandard::Lnurl(lnurl) => Self::Lnurl { lnurl }, + tag::TagStandard::Url(url) => Self::Url { url: url.to_string(), }, - tag::Tag::MimeType(mime) => Self::MimeType { mime }, - tag::Tag::Aes256Gcm { key, iv } => Self::Aes256Gcm { key, iv }, - tag::Tag::Sha256(hash) => Self::Sha256 { + tag::TagStandard::MimeType(mime) => Self::MimeType { mime }, + tag::TagStandard::Aes256Gcm { key, iv } => Self::Aes256Gcm { key, iv }, + tag::TagStandard::Sha256(hash) => Self::Sha256 { hash: hash.to_string(), }, - tag::Tag::Size(bytes) => Self::Size { size: bytes as u64 }, - tag::Tag::Dim(dim) => Self::Dim { + tag::TagStandard::Size(bytes) => Self::Size { size: bytes as u64 }, + tag::TagStandard::Dim(dim) => Self::Dim { dimensions: Arc::new(dim.into()), }, - tag::Tag::Magnet(uri) => Self::Magnet { uri }, - tag::Tag::Blurhash(data) => Self::Blurhash { blurhash: data }, - tag::Tag::Streaming(url) => Self::Streaming { + tag::TagStandard::Magnet(uri) => Self::Magnet { uri }, + tag::TagStandard::Blurhash(data) => Self::Blurhash { blurhash: data }, + tag::TagStandard::Streaming(url) => Self::Streaming { url: url.to_string(), }, - tag::Tag::Recording(url) => Self::Recording { + tag::TagStandard::Recording(url) => Self::Recording { url: url.to_string(), }, - tag::Tag::Starts(timestamp) => Self::Starts { + tag::TagStandard::Starts(timestamp) => Self::Starts { timestamp: Arc::new(timestamp.into()), }, - tag::Tag::Ends(timestamp) => Self::Ends { + tag::TagStandard::Ends(timestamp) => Self::Ends { timestamp: Arc::new(timestamp.into()), }, - tag::Tag::LiveEventStatus(s) => Self::LiveEventStatusTag { status: s.into() }, - tag::Tag::CurrentParticipants(num) => Self::CurrentParticipants { num }, - tag::Tag::TotalParticipants(num) => Self::TotalParticipants { num }, - tag::Tag::AbsoluteURL(url) => Self::AbsoluteURL { + tag::TagStandard::LiveEventStatus(s) => Self::LiveEventStatus { status: s.into() }, + tag::TagStandard::CurrentParticipants(num) => Self::CurrentParticipants { num }, + tag::TagStandard::TotalParticipants(num) => Self::TotalParticipants { num }, + tag::TagStandard::AbsoluteURL(url) => Self::AbsoluteURL { url: url.to_string(), }, - tag::Tag::Method(method) => Self::Method { + tag::TagStandard::Method(method) => Self::Method { method: method.into(), }, - tag::Tag::Payload(p) => Self::Payload { + tag::TagStandard::Payload(p) => Self::Payload { hash: p.to_string(), }, - tag::Tag::Anon { msg } => Self::Anon { msg }, - tag::Tag::Proxy { id, protocol } => Self::Proxy { + tag::TagStandard::Anon { msg } => Self::Anon { msg }, + tag::TagStandard::Proxy { id, protocol } => Self::Proxy { id, protocol: protocol.into(), }, - tag::Tag::Emoji { shortcode, url } => Self::Emoji { + tag::TagStandard::Emoji { shortcode, url } => Self::Emoji { shortcode, url: url.to_string(), }, - tag::Tag::Encrypted => Self::Encrypted, - tag::Tag::Request(event) => Self::Request { + tag::TagStandard::Encrypted => Self::Encrypted, + tag::TagStandard::Request(event) => Self::Request { event: Arc::new(event.into()), }, - tag::Tag::DataVendingMachineStatus { status, extra_info } => { - Self::DataVendingMachineStatusTag { + tag::TagStandard::DataVendingMachineStatus { status, extra_info } => { + Self::DataVendingMachineStatus { status: status.into(), extra_info, } } - tag::Tag::Word(word) => Self::Word { word }, - tag::Tag::LabelNamespace(label) => Self::LabelNamespace { namespace: label }, - tag::Tag::Label(labels) => Self::Label { label: labels }, + tag::TagStandard::Word(word) => Self::Word { word }, + tag::TagStandard::LabelNamespace(label) => Self::LabelNamespace { namespace: label }, + tag::TagStandard::Label(labels) => Self::Label { label: labels }, } } } -impl TryFrom for tag::Tag { +impl TryFrom for tag::TagStandard { type Error = NostrError; - fn try_from(value: TagEnum) -> Result { + fn try_from(value: TagStandard) -> Result { match value { - TagEnum::Unknown { kind, data } => Ok(Self::Generic(kind.into(), data)), - TagEnum::EventTag { + TagStandard::Event { event_id, relay_url, marker, @@ -740,7 +934,7 @@ impl TryFrom for tag::Tag { relay_url: relay_url.map(UncheckedUrl::from), marker: marker.map(tag::Marker::from), }), - TagEnum::PublicKeyTag { + TagStandard::PublicKey { public_key, relay_url, alias, @@ -751,13 +945,13 @@ impl TryFrom for tag::Tag { alias, uppercase, }), - TagEnum::EventReport { event_id, report } => { + TagStandard::EventReport { event_id, report } => { Ok(Self::EventReport(**event_id, report.into())) } - TagEnum::PubKeyReport { public_key, report } => { - Ok(Self::PubKeyReport(**public_key, report.into())) + TagStandard::PubKeyReport { public_key, report } => { + Ok(Self::PublicKeyReport(**public_key, report.into())) } - TagEnum::PubKeyLiveEvent { + TagStandard::PubKeyLiveEvent { public_key, relay_url, marker, @@ -771,31 +965,31 @@ impl TryFrom for tag::Tag { None => None, }, }), - TagEnum::Reference { reference } => Ok(Self::Reference(reference)), - TagEnum::RelayMetadataTag { relay_url, rw } => Ok(Self::RelayMetadata( - UncheckedUrl::from(relay_url), - rw.map(|rw| rw.into()), - )), - TagEnum::Hashtag { hashtag } => Ok(Self::Hashtag(hashtag)), - TagEnum::Geohash { geohash } => Ok(Self::Geohash(geohash)), - TagEnum::Identifier { identifier } => Ok(Self::Identifier(identifier)), - TagEnum::ExternalIdentityTag { identity } => { + TagStandard::Reference { reference } => Ok(Self::Reference(reference)), + TagStandard::RelayMetadata { relay_url, rw } => Ok(Self::RelayMetadata { + relay_url: Url::from_str(&relay_url)?, + metadata: rw.map(|rw| rw.into()), + }), + TagStandard::Hashtag { hashtag } => Ok(Self::Hashtag(hashtag)), + TagStandard::Geohash { geohash } => Ok(Self::Geohash(geohash)), + TagStandard::Identifier { identifier } => Ok(Self::Identifier(identifier)), + TagStandard::ExternalIdentity { identity } => { Ok(Self::ExternalIdentity(identity.into())) } - TagEnum::A { + TagStandard::Coordinate { coordinate, relay_url, - } => Ok(Self::A { + } => Ok(Self::Coordinate { coordinate: coordinate.as_ref().deref().clone(), relay_url: relay_url.map(UncheckedUrl::from), }), - TagEnum::Kind { kind } => Ok(Self::Kind(kind.into())), - TagEnum::RelayUrl { relay_url } => Ok(Self::Relay(UncheckedUrl::from(relay_url))), - TagEnum::POW { nonce, difficulty } => Ok(Self::POW { + TagStandard::Kind { kind } => Ok(Self::Kind(kind.into())), + TagStandard::RelayUrl { relay_url } => Ok(Self::Relay(UncheckedUrl::from(relay_url))), + TagStandard::POW { nonce, difficulty } => Ok(Self::POW { nonce: nonce.parse()?, difficulty, }), - TagEnum::Delegation { + TagStandard::Delegation { delegator, conditions, sig, @@ -804,140 +998,70 @@ impl TryFrom for tag::Tag { conditions: Conditions::from_str(&conditions)?, sig: Signature::from_str(&sig)?, }), - TagEnum::ContentWarning { reason } => Ok(Self::ContentWarning { reason }), - TagEnum::Expiration { timestamp } => Ok(Self::Expiration(**timestamp)), - TagEnum::Subject { subject } => Ok(Self::Subject(subject)), - TagEnum::Challenge { challenge } => Ok(Self::Challenge(challenge)), - TagEnum::Title { title } => Ok(Self::Title(title)), - TagEnum::Image { url, dimensions } => Ok(Self::Image( + TagStandard::ContentWarning { reason } => Ok(Self::ContentWarning { reason }), + TagStandard::Expiration { timestamp } => Ok(Self::Expiration(**timestamp)), + TagStandard::Subject { subject } => Ok(Self::Subject(subject)), + TagStandard::Challenge { challenge } => Ok(Self::Challenge(challenge)), + TagStandard::Title { title } => Ok(Self::Title(title)), + TagStandard::Image { url, dimensions } => Ok(Self::Image( UncheckedUrl::from(url), dimensions.map(|d| **d), )), - TagEnum::Thumb { url, dimensions } => Ok(Self::Thumb( + TagStandard::Thumb { url, dimensions } => Ok(Self::Thumb( UncheckedUrl::from(url), dimensions.map(|d| **d), )), - TagEnum::Summary { summary } => Ok(Self::Summary(summary)), - TagEnum::Description { desc } => Ok(Self::Description(desc)), - TagEnum::Bolt11 { bolt11 } => Ok(Self::Bolt11(bolt11)), - TagEnum::Preimage { preimage } => Ok(Self::Preimage(preimage)), - TagEnum::Relays { urls } => Ok(Self::Relays( + TagStandard::Summary { summary } => Ok(Self::Summary(summary)), + TagStandard::Description { desc } => Ok(Self::Description(desc)), + TagStandard::Bolt11 { bolt11 } => Ok(Self::Bolt11(bolt11)), + TagStandard::Preimage { preimage } => Ok(Self::Preimage(preimage)), + TagStandard::Relays { urls } => Ok(Self::Relays( urls.into_iter().map(UncheckedUrl::from).collect(), )), - TagEnum::Amount { millisats, bolt11 } => Ok(Self::Amount { millisats, bolt11 }), - TagEnum::Lnurl { lnurl } => Ok(Self::Lnurl(lnurl)), - TagEnum::Name { name } => Ok(Self::Name(name)), - TagEnum::PublishedAt { timestamp } => Ok(Self::PublishedAt(**timestamp)), - TagEnum::UrlTag { url } => Ok(Self::Url(Url::parse(&url)?)), - TagEnum::MimeType { mime } => Ok(Self::MimeType(mime)), - TagEnum::Aes256Gcm { key, iv } => Ok(Self::Aes256Gcm { key, iv }), - TagEnum::Sha256 { hash } => Ok(Self::Sha256(Sha256Hash::from_str(&hash)?)), - TagEnum::Size { size } => Ok(Self::Size(size as usize)), - TagEnum::Dim { dimensions } => Ok(Self::Dim(**dimensions)), - TagEnum::Magnet { uri } => Ok(Self::Magnet(uri)), - TagEnum::Blurhash { blurhash } => Ok(Self::Blurhash(blurhash)), - TagEnum::Streaming { url } => Ok(Self::Streaming(UncheckedUrl::from(url))), - TagEnum::Recording { url } => Ok(Self::Recording(UncheckedUrl::from(url))), - TagEnum::Starts { timestamp } => Ok(Self::Starts(**timestamp)), - TagEnum::Ends { timestamp } => Ok(Self::Ends(**timestamp)), - TagEnum::LiveEventStatusTag { status } => Ok(Self::LiveEventStatus(status.into())), - TagEnum::CurrentParticipants { num } => Ok(Self::CurrentParticipants(num)), - TagEnum::TotalParticipants { num } => Ok(Self::CurrentParticipants(num)), - TagEnum::AbsoluteURL { url } => Ok(Self::AbsoluteURL(UncheckedUrl::from(url))), - TagEnum::Method { method } => Ok(Self::Method(method.into())), - TagEnum::Payload { hash } => Ok(Self::Payload(Sha256Hash::from_str(&hash)?)), - TagEnum::Anon { msg } => Ok(Self::Anon { msg }), - TagEnum::Proxy { id, protocol } => Ok(Self::Proxy { + TagStandard::Amount { millisats, bolt11 } => Ok(Self::Amount { millisats, bolt11 }), + TagStandard::Lnurl { lnurl } => Ok(Self::Lnurl(lnurl)), + TagStandard::Name { name } => Ok(Self::Name(name)), + TagStandard::PublishedAt { timestamp } => Ok(Self::PublishedAt(**timestamp)), + TagStandard::Url { url } => Ok(Self::Url(Url::parse(&url)?)), + TagStandard::MimeType { mime } => Ok(Self::MimeType(mime)), + TagStandard::Aes256Gcm { key, iv } => Ok(Self::Aes256Gcm { key, iv }), + TagStandard::Sha256 { hash } => Ok(Self::Sha256(Sha256Hash::from_str(&hash)?)), + TagStandard::Size { size } => Ok(Self::Size(size as usize)), + TagStandard::Dim { dimensions } => Ok(Self::Dim(**dimensions)), + TagStandard::Magnet { uri } => Ok(Self::Magnet(uri)), + TagStandard::Blurhash { blurhash } => Ok(Self::Blurhash(blurhash)), + TagStandard::Streaming { url } => Ok(Self::Streaming(UncheckedUrl::from(url))), + TagStandard::Recording { url } => Ok(Self::Recording(UncheckedUrl::from(url))), + TagStandard::Starts { timestamp } => Ok(Self::Starts(**timestamp)), + TagStandard::Ends { timestamp } => Ok(Self::Ends(**timestamp)), + TagStandard::LiveEventStatus { status } => Ok(Self::LiveEventStatus(status.into())), + TagStandard::CurrentParticipants { num } => Ok(Self::CurrentParticipants(num)), + TagStandard::TotalParticipants { num } => Ok(Self::CurrentParticipants(num)), + TagStandard::AbsoluteURL { url } => Ok(Self::AbsoluteURL(UncheckedUrl::from(url))), + TagStandard::Method { method } => Ok(Self::Method(method.into())), + TagStandard::Payload { hash } => Ok(Self::Payload(Sha256Hash::from_str(&hash)?)), + TagStandard::Anon { msg } => Ok(Self::Anon { msg }), + TagStandard::Proxy { id, protocol } => Ok(Self::Proxy { id, protocol: protocol.into(), }), - TagEnum::Emoji { shortcode, url } => Ok(Self::Emoji { + TagStandard::Emoji { shortcode, url } => Ok(Self::Emoji { shortcode, url: UncheckedUrl::from(url), }), - TagEnum::Encrypted => Ok(Self::Encrypted), - TagEnum::Request { event } => Ok(Self::Request(event.as_ref().deref().clone())), - TagEnum::DataVendingMachineStatusTag { status, extra_info } => { + TagStandard::Encrypted => Ok(Self::Encrypted), + TagStandard::Request { event } => Ok(Self::Request(event.as_ref().deref().clone())), + TagStandard::DataVendingMachineStatus { status, extra_info } => { Ok(Self::DataVendingMachineStatus { status: status.into(), extra_info, }) } - TagEnum::Word { word } => Ok(Self::Word(word)), - TagEnum::LabelNamespace { namespace } => Ok(Self::LabelNamespace(namespace)), - TagEnum::Label { label } => Ok(Self::Label(label)), - } - } -} - -#[derive(Debug, PartialEq, Eq, Hash, Object)] -#[uniffi::export(Debug, Eq, Hash)] -pub struct Tag { - inner: tag::Tag, -} - -impl Deref for Tag { - type Target = tag::Tag; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl From for Tag { - fn from(inner: tag::Tag) -> Self { - Self { inner } - } -} - -#[uniffi::export] -impl Tag { - #[uniffi::constructor] - pub fn parse(data: &[String]) -> Result { - Ok(Self { - inner: tag::Tag::parse(data)?, - }) - } - - #[uniffi::constructor] - pub fn from_enum(e: TagEnum) -> Result { - Ok(Self { - inner: tag::Tag::try_from(e)?, - }) - } - - /// Compose `["e", ""]` tag - #[uniffi::constructor] - pub fn event(event_id: &EventId) -> Self { - Self { - inner: tag::Tag::event(**event_id), - } - } - - /// Compose `["p", ""]` tag - #[uniffi::constructor] - pub fn public_key(public_key: &PublicKey) -> Self { - Self { - inner: tag::Tag::public_key(**public_key), + TagStandard::Word { word } => Ok(Self::Word(word)), + TagStandard::LabelNamespace { namespace } => Ok(Self::LabelNamespace(namespace)), + TagStandard::Label { label } => Ok(Self::Label(label)), } } - - /// Check if `Tag` is an event `reply` - pub fn is_reply(&self) -> bool { - self.inner.is_reply() - } - - pub fn as_enum(&self) -> TagEnum { - self.inner.clone().into() - } - - pub fn as_vec(&self) -> Vec { - self.inner.as_vec() - } - - pub fn kind(&self) -> TagKind { - self.inner.kind().into() - } } /// Supported external identity providers diff --git a/bindings/nostr-ffi/src/lib.rs b/bindings/nostr-ffi/src/lib.rs index 51c87e150..255d8fb48 100644 --- a/bindings/nostr-ffi/src/lib.rs +++ b/bindings/nostr-ffi/src/lib.rs @@ -19,8 +19,7 @@ pub mod util; pub use crate::error::NostrError; pub use crate::event::{ - Event, EventBuilder, EventId, Kind, KindEnum, RelayMetadata, Tag, TagEnum, TagKind, - UnsignedEvent, + Event, EventBuilder, EventId, Kind, KindEnum, RelayMetadata, Tag, TagKind, UnsignedEvent, }; pub use crate::key::{Keys, PublicKey, SecretKey}; pub use crate::message::{ClientMessage, ClientMessageEnum, RelayMessage, RelayMessageEnum}; diff --git a/bindings/nostr-ffi/src/nips/nip65.rs b/bindings/nostr-ffi/src/nips/nip65.rs index c3e3f0a34..88e6fe3b3 100644 --- a/bindings/nostr-ffi/src/nips/nip65.rs +++ b/bindings/nostr-ffi/src/nips/nip65.rs @@ -13,6 +13,6 @@ use crate::{Event, RelayMetadata}; pub fn extract_relay_list(event: &Event) -> HashMap> { nip65::extract_relay_list(event.deref()) .into_iter() - .map(|(s, r)| (s.to_string(), r.map(|r| r.into()))) + .map(|(s, r)| (s.to_string(), r.clone().map(|r| r.into()))) .collect() } diff --git a/bindings/nostr-ffi/src/types/filter.rs b/bindings/nostr-ffi/src/types/filter.rs index 479d78b5a..8ff43e8b0 100644 --- a/bindings/nostr-ffi/src/types/filter.rs +++ b/bindings/nostr-ffi/src/types/filter.rs @@ -5,7 +5,7 @@ use std::ops::Deref; use std::sync::Arc; -use nostr::types::filter::{self, IntoGenericTagValue}; +use nostr::types::filter; use nostr::JsonUtil; use uniffi::{Enum, Object, Record}; @@ -467,15 +467,7 @@ impl From for nostr::Filter { generic_tags: f .generic_tags .into_iter() - .map(|GenericTag { key, value }| { - ( - **key, - value - .into_iter() - .map(|v| v.into_generic_tag_value()) - .collect(), - ) - }) + .map(|GenericTag { key, value }| (**key, value.into_iter().collect())) .collect(), } } diff --git a/bindings/nostr-js/src/event/builder.rs b/bindings/nostr-js/src/event/builder.rs index 2b078b789..f95b58076 100644 --- a/bindings/nostr-js/src/event/builder.rs +++ b/bindings/nostr-js/src/event/builder.rs @@ -109,10 +109,16 @@ impl JsEventBuilder { } #[wasm_bindgen(js_name = relayList)] - pub fn relay_list(relays: Vec) -> Self { - Self { - inner: EventBuilder::relay_list(relays.into_iter().map(|r| r.into())), + pub fn relay_list(relays: Vec) -> Result { + let mut list = Vec::with_capacity(relays.len()); + for JsRelayListItem { url, metadata } in relays.into_iter() { + let relay_url: Url = Url::parse(&url).map_err(into_err)?; + let metadata = metadata.map(|m| m.into()); + list.push((relay_url, metadata)) } + Ok(Self { + inner: EventBuilder::relay_list(list), + }) } #[wasm_bindgen(js_name = textNote)] diff --git a/bindings/nostr-js/src/event/tag.rs b/bindings/nostr-js/src/event/tag.rs index a7e8fd474..d1707e78a 100644 --- a/bindings/nostr-js/src/event/tag.rs +++ b/bindings/nostr-js/src/event/tag.rs @@ -2,12 +2,49 @@ // Copyright (c) 2023-2024 Rust Nostr Developers // Distributed under the MIT software license +use std::ops::Deref; + use nostr::prelude::*; use wasm_bindgen::prelude::*; use crate::error::{into_err, Result}; use crate::event::JsEventId; use crate::key::JsPublicKey; +use crate::nips::nip01::JsCoordinate; +use crate::types::filter::JsSingleLetterTag; +use crate::types::JsTimestamp; + +/// Report +/// +/// +#[wasm_bindgen(js_name = Report)] +pub enum JsReport { + /// Depictions of nudity, porn, etc + Nudity, + /// Profanity, hateful speech, etc. + Profanity, + /// Something which may be illegal in some jurisdiction + Illegal, + /// Spam + Spam, + /// Someone pretending to be someone else + Impersonation, + /// Reports that don't fit in the above categories + Other, +} + +impl From for Report { + fn from(value: JsReport) -> Self { + match value { + JsReport::Nudity => Self::Nudity, + JsReport::Profanity => Self::Profanity, + JsReport::Illegal => Self::Illegal, + JsReport::Spam => Self::Spam, + JsReport::Impersonation => Self::Impersonation, + JsReport::Other => Self::Other, + } + } +} #[wasm_bindgen(js_name = HttpMethod)] pub enum JsHttpMethod { @@ -113,6 +150,7 @@ impl From for RelayMetadata { } } +/// Tag #[wasm_bindgen(js_name = Tag)] pub struct JsTag { inner: Tag, @@ -132,6 +170,10 @@ impl From for Tag { #[wasm_bindgen(js_class = Tag)] impl JsTag { + /// Parse tag + /// + /// Return error if the tag is empty! + #[inline] #[wasm_bindgen] pub fn parse(tag: Vec) -> Result { Ok(Self { @@ -139,7 +181,42 @@ impl JsTag { }) } - /// Compose `["e", ""]` tag + /// Get tag kind + #[inline] + pub fn kind(&self) -> String { + self.inner.kind().to_string() + } + + /// Return the **first** tag value (index `1`), if exists. + #[inline] + pub fn content(&self) -> Option { + self.inner.content().map(|c| c.to_string()) + } + + /// Get `SingleLetterTag` + #[inline] + pub fn single_letter_tag(&self) -> Option { + self.inner.single_letter_tag().map(|s| s.into()) + } + + /// Get array of strings + #[inline] + #[wasm_bindgen(js_name = asVec)] + pub fn as_vec(&self) -> Vec { + self.inner.as_vec().to_vec() + } + + /// Consume tag and return array of strings + #[inline] + #[wasm_bindgen(js_name = toVec)] + pub fn to_vec(self) -> Vec { + self.inner.to_vec() + } + + /// Compose `["e", "]` + /// + /// + #[inline] pub fn event(event_id: &JsEventId) -> Self { Self { inner: Tag::event(**event_id), @@ -147,32 +224,98 @@ impl JsTag { } /// Compose `["p", ""]` tag + /// + /// + #[inline] + #[wasm_bindgen(js_name = publicKey)] pub fn public_key(public_key: &JsPublicKey) -> Self { Self { inner: Tag::public_key(**public_key), } } - /// Check if `Tag` is an event `reply` - pub fn is_reply(&self) -> bool { - self.inner.is_reply() + /// Compose `["d", ""]` tag + /// + /// + #[inline] + pub fn identifier(identifier: &str) -> Self { + Self { + inner: Tag::identifier(identifier), + } } - pub fn kind(&self) -> String { - self.inner.kind().to_string() + /// Compose `["a", ""]` tag + /// + /// + #[inline] + pub fn coordinate(coordinate: &JsCoordinate) -> Self { + Self { + inner: Tag::coordinate(coordinate.deref().clone()), + } } - /// Get tag as vector of string + /// Compose `["nonce", "", ""]` tag /// - /// Internally clone tag and convert it to `Vec`. To avoid tag clone, use `toVec()`. - #[wasm_bindgen(js_name = asVec)] - pub fn as_vec(&self) -> Vec { - self.inner.as_vec() + /// + #[inline] + pub fn pow(nonce: u64, difficulty: u8) -> Self { + Self { + inner: Tag::pow(nonce as u128, difficulty), + } } - /// Consume the tag and return vector of string - #[wasm_bindgen(js_name = toVec)] - pub fn to_vec(self) -> Vec { - self.inner.to_vec() + /// Compose `["expiration", ""]` tag + /// + /// + #[inline] + pub fn expiration(timestamp: &JsTimestamp) -> Self { + Self { + inner: Tag::expiration(**timestamp), + } + } + + /// Compose `["e", "", ""]` tag + /// + /// + #[inline] + pub fn event_report(event_id: &JsEventId, report: JsReport) -> Self { + Self { + inner: Tag::event_report(**event_id, report.into()), + } + } + + /// Compose `["p", "", ""]` tag + /// + /// + #[inline] + pub fn public_key_report(public_key: &JsPublicKey, report: JsReport) -> Self { + Self { + inner: Tag::public_key_report(**public_key, report.into()), + } + } + + /// Compose `["r", "", ""]` tag + /// + /// + #[inline] + pub fn relay_metadata(relay_url: &str, metadata: Option) -> Result { + let relay_url: Url = Url::parse(relay_url).map_err(into_err)?; + Ok(Self { + inner: Tag::relay_metadata(relay_url, metadata.map(|m| m.into())), + }) + } + + /// Compose `["t", ""]` tag + #[inline] + pub fn hashtag(hashtag: &str) -> Self { + Self { + inner: Tag::hashtag(hashtag), + } + } + + /// Check if `Tag` is an event `reply` + #[inline] + pub fn is_reply(&self) -> bool { + self.inner.is_reply() } } diff --git a/bindings/nostr-js/src/nips/nip65.rs b/bindings/nostr-js/src/nips/nip65.rs index 7d18c8a96..c0ba0db80 100644 --- a/bindings/nostr-js/src/nips/nip65.rs +++ b/bindings/nostr-js/src/nips/nip65.rs @@ -4,7 +4,6 @@ use std::ops::Deref; use nostr::nips::nip65; -use nostr::{RelayMetadata, UncheckedUrl}; use wasm_bindgen::prelude::*; use crate::event::tag::JsRelayMetadata; @@ -17,15 +16,6 @@ pub struct JsRelayListItem { pub metadata: Option, } -impl From for (UncheckedUrl, Option) { - fn from(value: JsRelayListItem) -> Self { - ( - UncheckedUrl::from(value.url), - value.metadata.map(|r| r.into()), - ) - } -} - #[wasm_bindgen(js_class = RelayListItem)] impl JsRelayListItem { #[wasm_bindgen(constructor)] @@ -40,7 +30,7 @@ pub fn extract_relay_list(event: &JsEvent) -> Vec { .into_iter() .map(|(s, r)| JsRelayListItem { url: s.to_string(), - metadata: r.map(|r| r.into()), + metadata: r.clone().map(|r| r.into()), }) .collect() } diff --git a/bindings/nostr-js/src/types/filter.rs b/bindings/nostr-js/src/types/filter.rs index 3a0ba45f3..bd256c65c 100644 --- a/bindings/nostr-js/src/types/filter.rs +++ b/bindings/nostr-js/src/types/filter.rs @@ -88,6 +88,12 @@ impl Deref for JsSingleLetterTag { } } +impl From for JsSingleLetterTag { + fn from(inner: SingleLetterTag) -> Self { + Self { inner } + } +} + #[wasm_bindgen(js_class = SingleLetterTag)] impl JsSingleLetterTag { pub fn lowercase(character: JsAlphabet) -> Self { diff --git a/crates/nostr-database/examples/indexes.rs b/crates/nostr-database/examples/indexes.rs index 10692d626..98c76baa9 100644 --- a/crates/nostr-database/examples/indexes.rs +++ b/crates/nostr-database/examples/indexes.rs @@ -49,7 +49,7 @@ async fn main() { let event = EventBuilder::new( Kind::Custom(123), "Custom with d tag", - [Tag::Identifier(format!("myid{i}"))], + [Tag::identifier(format!("myid{i}"))], ) .to_event(&keys_a) .unwrap(); diff --git a/crates/nostr-database/examples/memory.rs b/crates/nostr-database/examples/memory.rs index f27664fe4..0e1ea5fcf 100644 --- a/crates/nostr-database/examples/memory.rs +++ b/crates/nostr-database/examples/memory.rs @@ -53,7 +53,7 @@ async fn main() { let event = EventBuilder::new( Kind::Custom(123), "Custom with d tag", - [Tag::Identifier(format!("myid{i}"))], + [Tag::identifier(format!("myid{i}"))], ) .to_event(&keys_a) .unwrap(); diff --git a/crates/nostr-database/src/index.rs b/crates/nostr-database/src/index.rs index 519e6abd1..e7a79e5bf 100644 --- a/crates/nostr-database/src/index.rs +++ b/crates/nostr-database/src/index.rs @@ -11,9 +11,7 @@ use std::sync::Arc; use nostr::event::id; use nostr::nips::nip01::Coordinate; -use nostr::{ - Alphabet, Event, EventId, Filter, GenericTagValue, Kind, PublicKey, SingleLetterTag, Timestamp, -}; +use nostr::{Alphabet, Event, EventId, Filter, Kind, PublicKey, SingleLetterTag, Timestamp}; use thiserror::Error; use tokio::sync::RwLock; @@ -108,7 +106,7 @@ struct FilterIndex { kinds: HashSet, since: Option, until: Option, - generic_tags: HashMap>, + generic_tags: HashMap>, } impl FilterIndex { @@ -346,7 +344,7 @@ impl From for QueryPattern { let identifier = filter .generic_tags .get(&SingleLetterTag::lowercase(Alphabet::D)) - .and_then(|v| v.iter().next().map(|v| hash(v.to_string()))); + .and_then(|v| v.iter().next().map(hash)); match ( kinds_len, diff --git a/crates/nostr-database/src/tag_indexes.rs b/crates/nostr-database/src/tag_indexes.rs index 6151e38c2..e397a97f5 100644 --- a/crates/nostr-database/src/tag_indexes.rs +++ b/crates/nostr-database/src/tag_indexes.rs @@ -11,7 +11,7 @@ use std::ops::{Deref, DerefMut}; use flatbuffers::{ForwardsUOffset, Vector}; use nostr::hashes::siphash24::Hash as SipHash24; use nostr::hashes::Hash; -use nostr::{Alphabet, GenericTagValue, SingleLetterTag, Tag}; +use nostr::{Alphabet, SingleLetterTag, Tag}; #[cfg(feature = "flatbuf")] use crate::flatbuffers::StringVector; @@ -75,7 +75,7 @@ where for (single_letter_tag, content) in iter.filter_map(|t| Some((t.single_letter_tag()?, t.content()?))) { - let inner = hash(content.to_string()); + let inner = hash(content); tag_index .entry(single_letter_tag) .or_default() @@ -128,11 +128,8 @@ impl DerefMut for TagIndexValues { impl TagIndexValues { pub fn iter<'a, I>(iter: I) -> impl Iterator + 'a where - I: Iterator + 'a, + I: Iterator + 'a, { - iter.map(|value| { - let s: String = value.to_string(); - hash(s) - }) + iter.map(hash) } } diff --git a/crates/nostr-sdk/src/client/mod.rs b/crates/nostr-sdk/src/client/mod.rs index 9d3fcc6c1..e525db1a5 100644 --- a/crates/nostr-sdk/src/client/mod.rs +++ b/crates/nostr-sdk/src/client/mod.rs @@ -903,7 +903,7 @@ impl Client { #[inline] pub async fn set_relay_list(&self, relays: I) -> Result where - I: IntoIterator)>, + I: IntoIterator)>, { let builder = EventBuilder::relay_list(relays); self.send_event_builder(builder).await @@ -922,7 +922,7 @@ impl Client { /// # let my_keys = Keys::generate(); /// # let client = Client::new(&my_keys); /// client - /// .publish_text_note("My first text note from Nostr SDK!", []) + /// .publish_text_note("My first text note from rust-nostr!", []) /// .await /// .unwrap(); /// # } @@ -984,12 +984,12 @@ impl Client { for event in events.into_iter() { for tag in event.into_iter_tags() { - if let Tag::PublicKey { + if let Some(TagStandard::PublicKey { public_key, relay_url, alias, uppercase: false, - } = tag + }) = tag.to_standardized() { contact_list.push(Contact::new(public_key, relay_url, alias)) } @@ -1066,7 +1066,7 @@ impl Client { /// .unwrap(); /// /// client - /// .send_direct_msg(alice_pubkey, "My first DM fro Nostr SDK!", None) + /// .send_direct_msg(alice_pubkey, "My first DM from rust-nostr!", None) /// .await /// .unwrap(); /// # } diff --git a/crates/nostr/Cargo.toml b/crates/nostr/Cargo.toml index 79e9eb4cd..0981a89cd 100644 --- a/crates/nostr/Cargo.toml +++ b/crates/nostr/Cargo.toml @@ -18,7 +18,6 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = ["std", "all-nips"] std = [ - "dep:once_cell", "dep:url", "base64?/std", "bitcoin/std", @@ -70,7 +69,7 @@ chacha20 = { version = "0.9", optional = true } chacha20poly1305 = { version = "0.10", default-features = false, features = ["getrandom"], optional = true } negentropy = { version = "0.3", default-features = false } nostr-ots = { version = "0.2", optional = true } -once_cell = { workspace = true, optional = true } +once_cell.workspace = true reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "socks"], optional = true } scrypt = { version = "0.11", default-features = false, optional = true } serde = { version = "1.0", default-features = false, features = ["derive"] } diff --git a/crates/nostr/src/event/builder.rs b/crates/nostr/src/event/builder.rs index 6e868d27a..45c5259b8 100644 --- a/crates/nostr/src/event/builder.rs +++ b/crates/nostr/src/event/builder.rs @@ -17,7 +17,7 @@ use serde_json::{json, Value}; use super::kind::{Kind, NIP90_JOB_REQUEST_RANGE, NIP90_JOB_RESULT_RANGE}; use super::tag::ImageDimensions; -use super::{Event, EventId, Marker, Tag, TagKind, UnsignedEvent}; +use super::{Event, EventId, Marker, Tag, TagKind, TagStandard, UnsignedEvent}; use crate::key::{self, Keys, PublicKey}; use crate::nips::nip01::Coordinate; #[cfg(feature = "nip04")] @@ -283,7 +283,7 @@ impl EventBuilder { loop { nonce += 1; - tags.push(Tag::POW { nonce, difficulty }); + tags.push(Tag::pow(nonce, difficulty)); let created_at: Timestamp = self .custom_created_at @@ -379,11 +379,11 @@ impl EventBuilder { /// pub fn relay_list(iter: I) -> Self where - I: IntoIterator)>, + I: IntoIterator)>, { let tags = iter .into_iter() - .map(|(url, metadata)| Tag::RelayMetadata(url, metadata)); + .map(|(url, metadata)| Tag::relay_metadata(url, metadata)); Self::new(Kind::RelayList, "", tags) } @@ -395,7 +395,7 @@ impl EventBuilder { /// ```rust,no_run /// use nostr::EventBuilder; /// - /// let builder = EventBuilder::text_note("My first text note from Nostr SDK!", []); + /// let builder = EventBuilder::text_note("My first text note from rust-nostr!", []); /// ``` #[inline] pub fn text_note(content: S, tags: I) -> Self @@ -426,11 +426,11 @@ impl EventBuilder { match root { Some(root) => { // ID and author - tags.push(Tag::Event { + tags.push(Tag::from_standardized_without_cell(TagStandard::Event { event_id: root.id(), relay_url: relay_url.clone(), marker: Some(Marker::Root), - }); + })); tags.push(Tag::public_key(root.author())); // Add others `p` tags @@ -448,20 +448,20 @@ impl EventBuilder { } None => { // No root event is passed, use `reply_to` event ID for `root` marker - tags.push(Tag::Event { + tags.push(Tag::from_standardized_without_cell(TagStandard::Event { event_id: reply_to.id(), relay_url: relay_url.clone(), marker: Some(Marker::Root), - }); + })); } } // Add `e` and `p` tag of event author - tags.push(Tag::Event { + tags.push(Tag::from_standardized_without_cell(TagStandard::Event { event_id: reply_to.id(), relay_url, marker: Some(Marker::Reply), - }); + })); tags.push(Tag::public_key(reply_to.author())); // Add others `p` tags of reply_to event @@ -490,18 +490,18 @@ impl EventBuilder { /// ```rust,no_run /// use std::str::FromStr; /// - /// use nostr::{EventBuilder, Tag, Timestamp, EventId, UncheckedUrl}; + /// use nostr::prelude::*; /// /// let event_id = EventId::from_hex("b3e392b11f5d4f28321cedd09303a748acfd0487aea5a7450b3481c60b6e4f87").unwrap(); /// let content: &str = "Lorem [ipsum][4] dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\nRead more at #[3]."; /// let tags = &[ - /// Tag::Identifier("lorem-ipsum".to_string()), - /// Tag::Title("Lorem Ipsum".to_string()), - /// Tag::PublishedAt(Timestamp::from(1296962229)), - /// Tag::Hashtag("placeholder".to_string()), + /// Tag::identifier("lorem-ipsum".to_string()), + /// Tag::from_standardized(TagStandard::Title("Lorem Ipsum".to_string())), + /// Tag::from_standardized(TagStandard::PublishedAt(Timestamp::from(1296962229))), + /// Tag::hashtag("placeholder".to_string()), /// Tag::event(event_id), /// ]; - /// let builder = EventBuilder::long_form_text_note("My first text note from Nostr SDK!", []); + /// let builder = EventBuilder::long_form_text_note("My first text note from rust-nostr!", []); /// ``` #[inline] pub fn long_form_text_note(content: S, tags: I) -> Self @@ -517,11 +517,13 @@ impl EventBuilder { where I: IntoIterator, { - let tags = contacts.into_iter().map(|contact| Tag::PublicKey { - public_key: contact.public_key, - relay_url: contact.relay_url, - alias: contact.alias, - uppercase: false, + let tags = contacts.into_iter().map(|contact| { + Tag::from_standardized_without_cell(TagStandard::PublicKey { + public_key: contact.public_key, + relay_url: contact.relay_url, + alias: contact.alias, + uppercase: false, + }) }); Self::new(Kind::ContactList, "", tags) } @@ -538,11 +540,11 @@ impl EventBuilder { Ok(Self::new( Kind::OpenTimestamps, ots, - [Tag::Event { + [Tag::from_standardized_without_cell(TagStandard::Event { event_id, relay_url, marker: None, - }], + })], )) } @@ -577,11 +579,11 @@ impl EventBuilder { Kind::Repost, event.as_json(), [ - Tag::Event { + Tag::from_standardized_without_cell(TagStandard::Event { event_id: event.id(), relay_url, marker: None, - }, + }), Tag::public_key(event.author()), ], ) @@ -590,13 +592,13 @@ impl EventBuilder { Kind::GenericRepost, event.as_json(), [ - Tag::Event { + Tag::from_standardized_without_cell(TagStandard::Event { event_id: event.id(), relay_url, marker: None, - }, + }), Tag::public_key(event.author()), - Tag::Kind(event.kind()), + Tag::from_standardized_without_cell(TagStandard::Kind(event.kind())), ], ) } @@ -651,7 +653,7 @@ impl EventBuilder { [ Tag::event(event_id), Tag::public_key(public_key), - Tag::Kind(kind), + Tag::from_standardized_without_cell(TagStandard::Kind(kind)), ], ) } @@ -676,11 +678,11 @@ impl EventBuilder { Self::new( Kind::ChannelMetadata, metadata.as_json(), - [Tag::Event { + [Tag::from_standardized_without_cell(TagStandard::Event { event_id: channel_id, relay_url: relay_url.map(|u| u.into()), marker: None, - }], + })], ) } @@ -695,11 +697,11 @@ impl EventBuilder { Self::new( Kind::ChannelMessage, content, - [Tag::Event { + [Tag::from_standardized_without_cell(TagStandard::Event { event_id: channel_id, relay_url: Some(relay_url.into()), marker: Some(Marker::Root), - }], + })], ) } @@ -753,7 +755,10 @@ impl EventBuilder { Self::new( Kind::Authentication, "", - [Tag::Challenge(challenge.into()), Tag::Relay(relay.into())], + [ + Tag::from_standardized_without_cell(TagStandard::Challenge(challenge.into())), + Tag::from_standardized_without_cell(TagStandard::Relay(relay.into())), + ], ) } @@ -796,10 +801,13 @@ impl EventBuilder { where S: Into, { - tags.push(Tag::A { - coordinate: Coordinate::new(Kind::LiveEvent, live_event_host).identifier(live_event_id), - relay_url: relay_url.map(|u| u.into()), - }); + tags.push(Tag::from_standardized_without_cell( + TagStandard::Coordinate { + coordinate: Coordinate::new(Kind::LiveEvent, live_event_host) + .identifier(live_event_id), + relay_url: relay_url.map(|u| u.into()), + }, + )); Self::new(Kind::LiveEventMessage, content, tags) } @@ -862,13 +870,15 @@ impl EventBuilder { S2: Into, { let mut tags: Vec = vec![ - Tag::Bolt11(bolt11.into()), - Tag::Description(zap_request.as_json()), + Tag::from_standardized_without_cell(TagStandard::Bolt11(bolt11.into())), + Tag::from_standardized_without_cell(TagStandard::Description(zap_request.as_json())), ]; // add preimage tag if provided if let Some(pre_image_tag) = preimage { - tags.push(Tag::Preimage(pre_image_tag.into())) + tags.push(Tag::from_standardized_without_cell(TagStandard::Preimage( + pre_image_tag.into(), + ))) } // add e tag @@ -902,12 +912,14 @@ impl EventBuilder { } // add P tag - tags.push(Tag::PublicKey { - public_key: zap_request.author(), - relay_url: None, - alias: None, - uppercase: true, - }); + tags.push(Tag::from_standardized_without_cell( + TagStandard::PublicKey { + public_key: zap_request.author(), + relay_url: None, + alias: None, + uppercase: true, + }, + )); Self::new(Kind::ZapReceipt, "", tags) } @@ -921,7 +933,7 @@ impl EventBuilder { /// use nostr::{EventBuilder, ImageDimensions, UncheckedUrl}; /// /// let badge_id = String::from("nostr-sdk-test-badge"); - /// let name = Some(String::from("Nostr SDK test badge")); + /// let name = Some(String::from("rust-nostr test badge")); /// let description = Some(String::from("This is a test badge")); /// let image_url = Some(UncheckedUrl::from("https://nostr.build/someimage/1337")); /// let image_size = Some(ImageDimensions::new(1024, 1024)); @@ -947,24 +959,28 @@ impl EventBuilder { let mut tags: Vec = Vec::new(); // Set identifier tag - tags.push(Tag::Identifier(badge_id.into())); + tags.push(Tag::identifier(badge_id.into())); // Set name tag if let Some(name) = name { - tags.push(Tag::Name(name.into())); + tags.push(Tag::from_standardized_without_cell(TagStandard::Name( + name.into(), + ))); } // Set description tag if let Some(description) = description { - tags.push(Tag::Description(description.into())); + tags.push(Tag::from_standardized_without_cell( + TagStandard::Description(description.into()), + )); } // Set image tag if let Some(image) = image { let image_tag = if let Some(dimensions) = image_dimensions { - Tag::Image(image, Some(dimensions)) + Tag::from_standardized_without_cell(TagStandard::Image(image, Some(dimensions))) } else { - Tag::Image(image, None) + Tag::from_standardized_without_cell(TagStandard::Image(image, None)) }; tags.push(image_tag); } @@ -972,9 +988,9 @@ impl EventBuilder { // Set thumbnail tags for (thumb, dimensions) in thumbnails.into_iter() { let thumb_tag = if let Some(dimensions) = dimensions { - Tag::Thumb(thumb, Some(dimensions)) + Tag::from_standardized_without_cell(TagStandard::Thumb(thumb, Some(dimensions))) } else { - Tag::Thumb(thumb, None) + Tag::from_standardized_without_cell(TagStandard::Thumb(thumb, None)) }; tags.push(thumb_tag); } @@ -989,27 +1005,30 @@ impl EventBuilder { where I: IntoIterator, { - let mut tags = Vec::new(); - let badge_id = badge_definition .iter_tags() - .find_map(|t| match t { - Tag::Identifier(id) => Some(id), + .find_map(|t| match t.as_standardized() { + Some(TagStandard::Identifier(id)) => Some(id), _ => None, }) .ok_or(Error::NIP58(nip58::Error::IdentifierTagNotFound))?; + // At least 1 tag + let mut tags = Vec::with_capacity(1); + // Add identity tag - tags.push(Tag::A { - coordinate: Coordinate::new(Kind::BadgeDefinition, badge_definition.author()) - .identifier(badge_id), - relay_url: None, - }); + tags.push(Tag::from_standardized_without_cell( + TagStandard::Coordinate { + coordinate: Coordinate::new(Kind::BadgeDefinition, badge_definition.author()) + .identifier(badge_id), + relay_url: None, + }, + )); // Add awarded pubkeys let ptags = awarded_pubkeys .into_iter() - .filter(|p| matches!(p, Tag::PublicKey { .. })); + .filter(|p| p.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P))); tags.extend(ptags); // Build event @@ -1034,8 +1053,8 @@ impl EventBuilder { } for award in badge_awards.iter() { - if !award.iter_tags().any(|t| match t { - Tag::PublicKey { public_key, .. } => public_key == pubkey_awarded, + if !award.iter_tags().any(|t| match t.as_standardized() { + Some(TagStandard::PublicKey { public_key, .. }) => public_key == pubkey_awarded, _ => false, }) { return Err(Error::NIP58(Nip58Error::BadgeAwardsLackAwardedPublicKey)); @@ -1049,7 +1068,7 @@ impl EventBuilder { } // Add identifier `d` tag - let id_tag: Tag = Tag::Identifier("profile_badges".to_string()); + let id_tag: Tag = Tag::identifier("profile_badges"); let mut tags: Vec = vec![id_tag]; let badge_definitions_identifiers = badge_definitions.into_iter().filter_map(|event| { @@ -1059,8 +1078,11 @@ impl EventBuilder { let badge_awards_identifiers = badge_awards.into_iter().filter_map(|event| { let (_, relay_url) = nip58::extract_awarded_public_key(event.tags(), pubkey_awarded)?; - let (id, a_tag) = event.iter_tags().find_map(|t| match t { - Tag::A { coordinate, .. } => Some((coordinate.identifier.clone(), t.clone())), + let relay_url = relay_url.clone(); + let (id, a_tag) = event.iter_tags().find_map(|t| match t.as_standardized() { + Some(TagStandard::Coordinate { coordinate, .. }) => { + Some((coordinate.identifier.clone(), t.clone())) + } _ => None, })?; Some((event, id, a_tag, relay_url)) @@ -1077,11 +1099,12 @@ impl EventBuilder { ((_, identifier), (badge_award_event, badge_id, a_tag, relay_url)) if badge_id == identifier => { - let badge_award_event_tag: Tag = Tag::Event { - event_id: badge_award_event.id(), - relay_url, - marker: None, - }; + let badge_award_event_tag: Tag = + Tag::from_standardized_without_cell(TagStandard::Event { + event_id: badge_award_event.id(), + relay_url: relay_url.clone(), + marker: None, + }); tags.extend_from_slice(&[a_tag, badge_award_event_tag]); } _ => {} @@ -1136,11 +1159,11 @@ impl EventBuilder { tags.extend_from_slice(&[ Tag::event(job_request.id()), Tag::public_key(job_request.author()), - Tag::Request(job_request), - Tag::Amount { + Tag::from_standardized_without_cell(TagStandard::Request(job_request)), + Tag::from_standardized_without_cell(TagStandard::Amount { millisats: amount_millisats, bolt11, - }, + }), ]); Ok(Self::new(kind, "", tags)) } else { @@ -1163,13 +1186,16 @@ impl EventBuilder { payload: Option, ) -> Self { let tags = [ - Tag::DataVendingMachineStatus { status, extra_info }, + Tag::from_standardized_without_cell(TagStandard::DataVendingMachineStatus { + status, + extra_info, + }), Tag::event(job_request.id()), Tag::public_key(job_request.author()), - Tag::Amount { + Tag::from_standardized_without_cell(TagStandard::Amount { millisats: amount_millisats, bolt11, - }, + }), ]; Self::new(Kind::JobFeedback, payload.unwrap_or_default(), tags) } @@ -1261,8 +1287,8 @@ impl EventBuilder { let mut tags: Vec = Vec::with_capacity(1 + usize::from(expiration.is_some())); tags.push(Tag::public_key(*receiver)); - if let Some(expiration) = expiration { - tags.push(Tag::Expiration(expiration)); + if let Some(timestamp) = expiration { + tags.push(Tag::expiration(timestamp)); } Self::new(Kind::GiftWrap, content, tags) @@ -1358,7 +1384,13 @@ impl EventBuilder { where I: IntoIterator, { - Self::new(Kind::BlockedRelays, "", relay.into_iter().map(Tag::Relay)) + Self::new( + Kind::BlockedRelays, + "", + relay + .into_iter() + .map(|r| Tag::from_standardized_without_cell(TagStandard::Relay(r))), + ) } /// Search relays @@ -1369,7 +1401,13 @@ impl EventBuilder { where I: IntoIterator, { - Self::new(Kind::SearchRelays, "", relay.into_iter().map(Tag::Relay)) + Self::new( + Kind::SearchRelays, + "", + relay + .into_iter() + .map(|r| Tag::from_standardized_without_cell(TagStandard::Relay(r))), + ) } /// Interests @@ -1413,7 +1451,13 @@ impl EventBuilder { where I: IntoIterator, { - Self::new(Kind::RelaySets, "", relay.into_iter().map(Tag::Relay)) + Self::new( + Kind::RelaySets, + "", + relay + .into_iter() + .map(|r| Tag::from_standardized_without_cell(TagStandard::Relay(r))), + ) } /// Bookmark sets @@ -1456,9 +1500,9 @@ impl EventBuilder { where I: IntoIterator, { - let tags = emoji - .into_iter() - .map(|(s, url)| Tag::Emoji { shortcode: s, url }); + let tags = emoji.into_iter().map(|(s, url)| { + Tag::from_standardized_without_cell(TagStandard::Emoji { shortcode: s, url }) + }); Self::new(Kind::EmojiSets, "", tags) } @@ -1475,7 +1519,10 @@ impl EventBuilder { Self::new( Kind::Label, "", - [Tag::LabelNamespace(namespace), Tag::Label(labels)], + [ + Tag::from_standardized_without_cell(TagStandard::LabelNamespace(namespace)), + Tag::from_standardized_without_cell(TagStandard::Label(labels)), + ], ) } } @@ -1548,7 +1595,7 @@ mod tests { .tags .clone() .iter() - .find(|t| matches!(t, Tag::Preimage(_))) + .find(|t| t.kind() == TagKind::Preimage) .is_some(); assert_eq!(true, has_preimage_tag); @@ -1568,7 +1615,7 @@ mod tests { .tags .clone() .iter() - .find(|t| matches!(t, Tag::Preimage(_))) + .find(|t| t.kind() == TagKind::Preimage) .is_some(); assert_eq!(false, has_preimage_tag); @@ -1584,7 +1631,7 @@ mod tests { .tags .clone() .iter() - .find(|t| matches!(t, Tag::Identifier(_))) + .find(|t| t.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::D))) .is_some(); assert_eq!(true, has_id); @@ -1610,7 +1657,7 @@ mod tests { .tags .clone() .iter() - .find(|t| matches!(t, Tag::Identifier(_))) + .find(|t| t.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::D))) .is_some(); assert_eq!(true, has_id); @@ -1665,7 +1712,7 @@ mod tests { // Create new event with the event builder let awarded_pubkeys = vec![ - Tag::PublicKey { + Tag::from_standardized(TagStandard::PublicKey { public_key: PublicKey::from_str( "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", ) @@ -1673,8 +1720,8 @@ mod tests { relay_url: Some(UncheckedUrl::from_str("wss://nostr.oxtr.dev").unwrap()), alias: None, uppercase: false, - }, - Tag::PublicKey { + }), + Tag::from_standardized(TagStandard::PublicKey { public_key: PublicKey::from_str( "232a4ba3df82ccc252a35abee7d87d1af8fc3cc749e4002c3691434da692b1df", ) @@ -1682,7 +1729,7 @@ mod tests { relay_url: Some(UncheckedUrl::from_str("wss://nostr.oxtr.dev").unwrap()), alias: None, uppercase: false, - }, + }), ]; let event_builder: Event = EventBuilder::award_badge(&badge_definition_event, awarded_pubkeys) @@ -1708,13 +1755,13 @@ mod tests { let relay_url = UncheckedUrl::from_str("wss://nostr.oxtr.dev").unwrap(); let awarded_pubkeys = vec![ - Tag::PublicKey { + Tag::from_standardized(TagStandard::PublicKey { public_key: pub_key.clone(), relay_url: Some(relay_url.clone()), alias: None, uppercase: false, - }, - Tag::PublicKey { + }), + Tag::from_standardized(TagStandard::PublicKey { public_key: PublicKey::from_str( "232a4ba3df82ccc252a35abee7d87d1af8fc3cc749e4002c3691434da692b1df", ) @@ -1722,7 +1769,7 @@ mod tests { relay_url: Some(UncheckedUrl::from_str("wss://nostr.oxtr.dev").unwrap()), alias: None, uppercase: false, - }, + }), ]; let bravery_badge_event = self::EventBuilder::define_badge("bravery", None, None, None, None, Vec::new()) diff --git a/crates/nostr/src/event/id.rs b/crates/nostr/src/event/id.rs index e61ce6c33..58723bd5a 100644 --- a/crates/nostr/src/event/id.rs +++ b/crates/nostr/src/event/id.rs @@ -198,6 +198,13 @@ impl fmt::Display for EventId { } } +// Required to keep clean the methods of `Filter` struct +impl From for String { + fn from(event_id: EventId) -> Self { + event_id.to_hex() + } +} + impl From for Tag { fn from(event_id: EventId) -> Self { Tag::event(event_id) diff --git a/crates/nostr/src/event/mod.rs b/crates/nostr/src/event/mod.rs index 80ef27190..60becb23c 100644 --- a/crates/nostr/src/event/mod.rs +++ b/crates/nostr/src/event/mod.rs @@ -35,7 +35,7 @@ pub use self::builder::EventBuilder; pub use self::id::EventId; pub use self::kind::Kind; pub use self::partial::{MissingPartialEvent, PartialEvent}; -pub use self::tag::{Marker, Tag, TagKind}; +pub use self::tag::{Marker, Tag, TagKind, TagStandard}; pub use self::unsigned::UnsignedEvent; use crate::nips::nip01::Coordinate; #[cfg(feature = "std")] @@ -43,10 +43,10 @@ use crate::types::time::Instant; use crate::types::time::TimeSupplier; #[cfg(feature = "std")] use crate::SECP256K1; -use crate::{GenericTagValue, JsonUtil, PublicKey, SingleLetterTag, Timestamp}; +use crate::{Alphabet, JsonUtil, PublicKey, SingleLetterTag, Timestamp}; /// Tags Indexes -pub type TagsIndexes = BTreeMap>; +pub type TagsIndexes = BTreeMap>; /// [`Event`] error #[derive(Debug, PartialEq, Eq)] @@ -311,8 +311,8 @@ impl Event { /// Get [`Timestamp`] expiration if set #[inline] pub fn expiration(&self) -> Option<&Timestamp> { - for tag in self.iter_tags() { - if let Tag::Expiration(timestamp) = tag { + for tag in self.iter_tags().filter(|t| t.kind() == TagKind::Expiration) { + if let Some(TagStandard::Expiration(timestamp)) = tag.as_standardized() { return Some(timestamp); } } @@ -406,8 +406,11 @@ impl Event { /// Extract identifier (`d` tag), if exists. #[inline] pub fn identifier(&self) -> Option<&str> { - for tag in self.iter_tags() { - if let Tag::Identifier(id) = tag { + for tag in self + .iter_tags() + .filter(|t| t.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::D))) + { + if let Some(TagStandard::Identifier(id)) = tag.as_standardized() { return Some(id); } } @@ -416,22 +419,22 @@ impl Event { /// Extract public keys from tags (`p` tag) /// - /// **This method extract ONLY `Tag::PublicKey`** + /// **This method extract ONLY `TagStandard::PublicKey`** #[inline] pub fn public_keys(&self) -> impl Iterator { - self.iter_tags().filter_map(|t| match t { - Tag::PublicKey { public_key, .. } => Some(public_key), + self.iter_tags().filter_map(|t| match t.as_standardized() { + Some(TagStandard::PublicKey { public_key, .. }) => Some(public_key), _ => None, }) } /// Extract event IDs from tags (`e` tag) /// - /// **This method extract ONLY `Tag::Event`** + /// **This method extract ONLY `TagStandard::Event`** #[inline] pub fn event_ids(&self) -> impl Iterator { - self.iter_tags().filter_map(|t| match t { - Tag::Event { event_id, .. } => Some(event_id), + self.iter_tags().filter_map(|t| match t.as_standardized() { + Some(TagStandard::Event { event_id, .. }) => Some(event_id), _ => None, }) } @@ -439,8 +442,8 @@ impl Event { /// Extract coordinates from tags (`a` tag) #[inline] pub fn coordinates(&self) -> impl Iterator { - self.iter_tags().filter_map(|t| match t { - Tag::A { coordinate, .. } => Some(coordinate), + self.iter_tags().filter_map(|t| match t.as_standardized() { + Some(TagStandard::Coordinate { coordinate, .. }) => Some(coordinate), _ => None, }) } @@ -451,7 +454,9 @@ impl Event { .iter_tags() .filter_map(|t| Some((t.single_letter_tag()?, t.content()?))) { - idx.entry(single_letter_tag).or_default().insert(content); + idx.entry(single_letter_tag) + .or_default() + .insert(content.to_string()); } idx } @@ -598,7 +603,7 @@ mod tests { fn test_event_expired() { let my_keys = Keys::generate(); let event = - EventBuilder::text_note("my content", [Tag::Expiration(Timestamp::from(1600000000))]) + EventBuilder::text_note("my content", [Tag::expiration(Timestamp::from(1600000000))]) .to_event(&my_keys) .unwrap(); @@ -614,7 +619,7 @@ mod tests { let my_keys = Keys::generate(); let event = EventBuilder::text_note( "my content", - [Tag::Expiration(Timestamp::from(expiry_date))], + [Tag::expiration(Timestamp::from(expiry_date))], ) .to_event(&my_keys) .unwrap(); diff --git a/crates/nostr/src/event/tag.rs b/crates/nostr/src/event/tag.rs index fe54dcc55..a869aeafb 100644 --- a/crates/nostr/src/event/tag.rs +++ b/crates/nostr/src/event/tag.rs @@ -4,10 +4,13 @@ //! Tag -use alloc::borrow::ToOwned; +use alloc::borrow::{Cow, ToOwned}; use alloc::string::{String, ToString}; +use alloc::sync::Arc; use alloc::vec::Vec; +use core::cmp::Ordering; use core::fmt; +use core::hash::{Hash, Hasher}; use core::num::ParseIntError; use core::str::FromStr; @@ -15,6 +18,10 @@ use bitcoin::hashes::sha256::Hash as Sha256Hash; use bitcoin::hex::HexToArrayError; use bitcoin::secp256k1; use bitcoin::secp256k1::schnorr::Signature; +#[cfg(feature = "std")] +use once_cell::sync::OnceCell; // TODO: when MSRV will be >= 1.70.0, use `std::cell::OnceLock` instead and remove `once_cell` dep. +#[cfg(not(feature = "std"))] +use once_cell::unsync::OnceCell; // TODO: when MSRV will be >= 1.70.0, use `core::cell::OnceCell` instead and remove `once_cell` dep. use serde::de::Error as DeserializerError; use serde::ser::SerializeSeq; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -25,15 +32,13 @@ use crate::nips::nip26::{Conditions, Error as Nip26Error}; use crate::nips::nip48::Protocol; use crate::nips::nip53::{self, LiveEventMarker, LiveEventStatus}; use crate::nips::nip90::DataVendingMachineStatus; -use crate::types::filter::IntoGenericTagValue; use crate::types::url::{ParseError, Url}; use crate::{ - key, Alphabet, Event, GenericTagValue, JsonUtil, Kind, PublicKey, SingleLetterTag, Timestamp, - UncheckedUrl, + key, Alphabet, Event, JsonUtil, Kind, PublicKey, SingleLetterTag, Timestamp, UncheckedUrl, }; -/// [`Tag`] error -#[derive(Debug)] +/// Tag error +#[derive(Debug, PartialEq, Eq)] pub enum Error { /// Keys Keys(key::Error), @@ -43,6 +48,8 @@ pub enum Error { UnknownReportType, /// Impossible to find tag kind KindNotFound, + /// Empty tag + EmptyTag, /// Invalid Zap Request InvalidZapRequest, /// Impossible to parse integer @@ -71,6 +78,8 @@ pub enum Error { InvalidHttpMethod(String), /// Invalid Relay Metadata InvalidRelayMetadata(String), + /// Unknown standardized tag + UnknownStardardizedTag, } #[cfg(feature = "std")] @@ -83,6 +92,7 @@ impl fmt::Display for Error { Self::MarkerParseError => write!(f, "Impossible to parse marker"), Self::UnknownReportType => write!(f, "Unknown report type"), Self::KindNotFound => write!(f, "Impossible to find tag kind"), + Self::EmptyTag => write!(f, "Empty tag"), Self::InvalidZapRequest => write!(f, "Invalid Zap request"), Self::ParseIntError(e) => write!(f, "Parse integer: {e}"), Self::Secp256k1(e) => write!(f, "Secp256k1: {e}"), @@ -97,6 +107,7 @@ impl fmt::Display for Error { Self::InvalidImageDimensions => write!(f, "Invalid image dimensions"), Self::InvalidHttpMethod(m) => write!(f, "Invalid HTTP method: {m}"), Self::InvalidRelayMetadata(s) => write!(f, "Invalid relay metadata: {s}"), + Self::UnknownStardardizedTag => write!(f, "Unknown standardized tag"), } } } @@ -201,6 +212,8 @@ where } /// Report +/// +/// #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Report { /// Depictions of nudity, porn, etc @@ -208,8 +221,6 @@ pub enum Report { /// Profanity, hateful speech, etc. Profanity, /// Something which may be illegal in some jurisdiction - /// - /// Remember: there is what is right and there is the law. Illegal, /// Spam Spam, @@ -355,7 +366,7 @@ impl FromStr for RelayMetadata { /// Tag kind #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum TagKind { +pub enum TagKind<'a> { /// Single letter SingleLetter(SingleLetterTag), /// Relay @@ -439,10 +450,10 @@ pub enum TagKind { /// Word Word, /// Custom tag kind - Custom(String), + Custom(Cow<'a, str>), } -impl fmt::Display for TagKind { +impl<'a> fmt::Display for TagKind<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::SingleLetter(s) => write!(f, "{s}"), @@ -491,12 +502,9 @@ impl fmt::Display for TagKind { } } -impl From for TagKind -where - S: AsRef, -{ - fn from(tag: S) -> Self { - match tag.as_ref() { +impl<'a> From<&'a str> for TagKind<'a> { + fn from(kind: &'a str) -> Self { + match kind { "relay" => Self::Relay, "nonce" => Self::Nonce, "delegation" => Self::Delegation, @@ -537,18 +545,298 @@ where "encrypted" => Self::Encrypted, "request" => Self::Request, "word" => Self::Word, - t => match SingleLetterTag::from_str(t) { + k => match SingleLetterTag::from_str(k) { Ok(s) => Self::SingleLetter(s), - Err(..) => Self::Custom(t.to_owned()), + Err(..) => Self::Custom(Cow::Borrowed(k)), }, } } } +/// Tag +#[derive(Debug, Clone)] +pub struct Tag { + buf: Vec, + standardized: Arc>>, +} + +impl PartialEq for Tag { + fn eq(&self, other: &Self) -> bool { + self.buf == other.buf + } +} + +impl Eq for Tag {} + +impl PartialOrd for Tag { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Tag { + fn cmp(&self, other: &Self) -> Ordering { + self.buf.cmp(&other.buf) + } +} + +impl Hash for Tag { + fn hash(&self, state: &mut H) { + self.buf.hash(state); + } +} + +impl Tag { + #[inline] + fn new(buf: Vec, standardized: Option) -> Self { + Self { + buf, + standardized: Arc::new(OnceCell::from(standardized)), + } + } + + #[inline] + fn new_with_empty_cell(buf: Vec) -> Self { + Self { + buf, + standardized: Arc::new(OnceCell::new()), + } + } + + /// Parse tag + /// + /// Return error if the tag is empty! + pub fn parse(tag: &[S]) -> Result + where + S: AsRef, + { + // Check if it's empty + if tag.is_empty() { + return Err(Error::EmptyTag); + } + + // NOT USE `Self::new`! + Ok(Self::new_with_empty_cell( + tag.iter().map(|v| v.as_ref().to_string()).collect(), + )) + } + + /// Construct from standardized tag + #[inline] + pub fn from_standardized(standardized: TagStandard) -> Self { + Self::new(standardized.clone().to_vec(), Some(standardized)) + } + + /// Construct from standardized tag without initialize cell (avoid a clone) + #[inline] + pub fn from_standardized_without_cell(standardized: TagStandard) -> Self { + Self::new_with_empty_cell(standardized.to_vec()) + } + + /// Get tag kind + #[inline] + pub fn kind(&self) -> TagKind { + // SAFETY: `buf` must not be empty, checked during parsing. + let key: &str = &self.buf[0]; + TagKind::from(key) + } + + /// Return the **first** tag value (index `1`), if exists. + #[inline] + pub fn content(&self) -> Option<&str> { + self.buf.get(1).map(|s| s.as_str()) + } + + /// Get [SingleLetterTag] + #[inline] + pub fn single_letter_tag(&self) -> Option { + match self.kind() { + TagKind::SingleLetter(s) => Some(s), + _ => None, + } + } + + /// Get reference of standardized tag + #[inline] + pub fn as_standardized(&self) -> Option<&TagStandard> { + self.standardized + .get_or_init(|| TagStandard::parse(self.as_vec()).ok()) + .as_ref() + } + + /// Consume tag and get standardized tag + pub fn to_standardized(self) -> Option { + let standardized: OnceCell> = Arc::unwrap_or_clone(self.standardized); + match standardized.into_inner() { + Some(inner) => inner, + None => TagStandard::parse(&self.buf).ok(), + } + } + + /// Get reference of array of strings + #[inline] + pub fn as_vec(&self) -> &[String] { + &self.buf + } + + /// Consume tag and return array of strings + #[inline] + pub fn to_vec(self) -> Vec { + self.buf + } + + /// Compose `["e", "]` + /// + /// + #[inline] + pub fn event(event_id: EventId) -> Self { + Self::from_standardized_without_cell(TagStandard::event(event_id)) + } + + /// Compose `["p", ""]` tag + /// + /// + #[inline] + pub fn public_key(public_key: PublicKey) -> Self { + Self::from_standardized_without_cell(TagStandard::public_key(public_key)) + } + + /// Compose `["d", ""]` tag + /// + /// + #[inline] + pub fn identifier(identifier: T) -> Self + where + T: Into, + { + Self::from_standardized_without_cell(TagStandard::Identifier(identifier.into())) + } + + /// Compose `["a", ""]` tag + /// + /// + #[inline] + pub fn coordinate(coordinate: Coordinate) -> Self { + Self::from_standardized_without_cell(TagStandard::Coordinate { + coordinate, + relay_url: None, + }) + } + + /// Compose `["nonce", "", ""]` tag + /// + /// + #[inline] + pub fn pow(nonce: u128, difficulty: u8) -> Self { + Self::from_standardized_without_cell(TagStandard::POW { nonce, difficulty }) + } + + /// Compose `["expiration", ""]` tag + /// + /// + #[inline] + pub fn expiration(timestamp: Timestamp) -> Self { + Self::from_standardized_without_cell(TagStandard::Expiration(timestamp)) + } + + /// Compose `["e", "", ""]` tag + /// + /// + #[inline] + pub fn event_report(event_id: EventId, report: Report) -> Self { + Self::from_standardized_without_cell(TagStandard::EventReport(event_id, report)) + } + + /// Compose `["p", "", ""]` tag + /// + /// + #[inline] + pub fn public_key_report(public_key: PublicKey, report: Report) -> Self { + Self::from_standardized_without_cell(TagStandard::PublicKeyReport(public_key, report)) + } + + /// Compose `["r", "", ""]` tag + /// + /// + #[inline] + pub fn relay_metadata(relay_url: Url, metadata: Option) -> Self { + Self::from_standardized_without_cell(TagStandard::RelayMetadata { + relay_url, + metadata, + }) + } + + /// Compose `["t", ""]` tag + #[inline] + pub fn hashtag(hashtag: T) -> Self + where + T: Into, + { + Self::from_standardized_without_cell(TagStandard::Hashtag(hashtag.into())) + } + + /// Compose custom tag + /// + /// JSON: `["", "", "", ...]` + #[inline] + pub fn custom(kind: TagKind, values: I) -> Self + where + I: IntoIterator, + S: Into, + { + // Collect values + let values: Vec = values.into_iter().map(|v| v.into()).collect(); + + // Compose tag + let mut buf: Vec = Vec::with_capacity(1 + values.len()); + buf.push(kind.to_string()); + buf.extend(values); + + // NOT USE `Self::new`! + Self::new_with_empty_cell(buf) + } + + /// Check if tag is an event `reply` + #[inline] + pub fn is_reply(&self) -> bool { + matches!( + self.as_standardized(), + Some(TagStandard::Event { + marker: Some(Marker::Reply), + .. + }) + ) + } +} + +impl Serialize for Tag { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut seq = serializer.serialize_seq(Some(self.buf.len()))?; + for element in self.buf.iter() { + seq.serialize_element(&element)?; + } + seq.end() + } +} + +impl<'de> Deserialize<'de> for Tag { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + type Data = Vec; + let tag: Vec = Data::deserialize(deserializer)?; + Self::parse(&tag).map_err(DeserializerError::custom) + } +} + +/// Standardized tag #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum Tag { - Generic(TagKind, Vec), +pub enum TagStandard { Event { event_id: EventId, relay_url: Option, @@ -561,8 +849,14 @@ pub enum Tag { /// Whether the p tag is an uppercase P or not uppercase: bool, }, + /// Report event + /// + /// EventReport(EventId, Report), - PubKeyReport(PublicKey, Report), + /// Report public key + /// + /// + PublicKeyReport(PublicKey, Report), PubKeyLiveEvent { public_key: PublicKey, relay_url: Option, @@ -570,17 +864,26 @@ pub enum Tag { proof: Option, }, Reference(String), - RelayMetadata(UncheckedUrl, Option), + /// Relay Metadata + /// + /// + RelayMetadata { + relay_url: Url, + metadata: Option, + }, Hashtag(String), Geohash(String), Identifier(String), ExternalIdentity(Identity), - A { + Coordinate { coordinate: Coordinate, relay_url: Option, }, Kind(Kind), Relay(UncheckedUrl), + /// Proof of Work + /// + /// POW { nonce: u128, difficulty: u8, @@ -662,7 +965,7 @@ pub enum Tag { Label(Vec), } -impl Tag { +impl TagStandard { /// Parse [`Tag`] from slice of string #[inline] pub fn parse(tag: &[S]) -> Result @@ -670,23 +973,14 @@ impl Tag { S: AsRef, { let tag_kind: TagKind = match tag.first() { - Some(kind) => TagKind::from(kind), + Some(kind) => TagKind::from(kind.as_ref()), None => return Err(Error::KindNotFound), }; - match Self::inaternal_parse(&tag_kind, tag) { - Ok(Some(tag)) => Ok(tag), - _ => Ok(Self::custom( - tag_kind, - tag.get(1..) - .unwrap_or_default() - .iter() - .map(|s| s.as_ref().to_owned()), - )), - } + Self::inaternal_parse(&tag_kind, tag) } - fn inaternal_parse(tag_kind: &TagKind, tag: &[S]) -> Result, Error> + fn inaternal_parse(tag_kind: &TagKind, tag: &[S]) -> Result where S: AsRef, { @@ -700,7 +994,7 @@ impl Tag { .skip(1) .map(|u| UncheckedUrl::from(u.as_ref())) .collect::>(); - return Ok(Some(Self::Relays(urls))); + return Ok(Self::Relays(urls)); } // Check `l` tag @@ -709,15 +1003,15 @@ impl Tag { uppercase: false, })) { let labels = tag.iter().skip(1).map(|u| u.as_ref().to_string()).collect(); - return Ok(Some(Self::Label(labels))); + return Ok(Self::Label(labels)); } if tag_len == 1 { return match tag_kind { - TagKind::ContentWarning => Ok(Some(Self::ContentWarning { reason: None })), - TagKind::Anon => Ok(Some(Self::Anon { msg: None })), - TagKind::Encrypted => Ok(Some(Self::Encrypted)), - _ => Ok(None), + TagKind::ContentWarning => Ok(Self::ContentWarning { reason: None }), + TagKind::Anon => Ok(Self::Anon { msg: None }), + TagKind::Encrypted => Ok(Self::Encrypted), + _ => Err(Error::UnknownStardardizedTag), }; } @@ -728,113 +1022,117 @@ impl Tag { TagKind::SingleLetter(SingleLetterTag { character: Alphabet::A, uppercase: false, - }) => Ok(Some(Self::A { + }) => Ok(Self::Coordinate { coordinate: Coordinate::from_str(tag_1)?, relay_url: None, - })), + }), TagKind::SingleLetter(SingleLetterTag { character: Alphabet::P, uppercase, }) => { let public_key = PublicKey::from_str(tag_1)?; - Ok(Some(Self::PublicKey { + Ok(Self::PublicKey { public_key, relay_url: None, alias: None, uppercase: *uppercase, - })) + }) } TagKind::SingleLetter(SingleLetterTag { character: Alphabet::E, uppercase: false, - }) => Ok(Some(Self::event(EventId::from_hex(tag_1)?))), + }) => Ok(Self::event(EventId::from_hex(tag_1)?)), TagKind::SingleLetter(SingleLetterTag { character: Alphabet::R, uppercase: false, }) => { if tag_1.starts_with("ws://") || tag_1.starts_with("wss://") { - Ok(Some(Self::RelayMetadata(UncheckedUrl::from(tag_1), None))) + Ok(Self::RelayMetadata { + relay_url: Url::parse(tag_1)?, + metadata: None, + }) } else { - Ok(Some(Self::Reference(tag_1.to_owned()))) + Ok(Self::Reference(tag_1.to_owned())) } } TagKind::SingleLetter(SingleLetterTag { character: Alphabet::T, uppercase: false, - }) => Ok(Some(Self::Hashtag(tag_1.to_owned()))), + }) => Ok(Self::Hashtag(tag_1.to_owned())), TagKind::SingleLetter(SingleLetterTag { character: Alphabet::G, uppercase: false, - }) => Ok(Some(Self::Geohash(tag_1.to_owned()))), + }) => Ok(Self::Geohash(tag_1.to_owned())), TagKind::SingleLetter(SingleLetterTag { character: Alphabet::D, uppercase: false, - }) => Ok(Some(Self::Identifier(tag_1.to_owned()))), + }) => Ok(Self::Identifier(tag_1.to_owned())), TagKind::SingleLetter(SingleLetterTag { character: Alphabet::K, uppercase: false, - }) => Ok(Some(Self::Kind(Kind::from_str(tag_1)?))), + }) => Ok(Self::Kind(Kind::from_str(tag_1)?)), TagKind::SingleLetter(SingleLetterTag { character: Alphabet::M, uppercase: false, - }) => Ok(Some(Self::MimeType(tag_1.to_owned()))), + }) => Ok(Self::MimeType(tag_1.to_owned())), TagKind::SingleLetter(SingleLetterTag { character: Alphabet::X, uppercase: false, - }) => Ok(Some(Self::Sha256(Sha256Hash::from_str(tag_1)?))), + }) => Ok(Self::Sha256(Sha256Hash::from_str(tag_1)?)), TagKind::SingleLetter(SingleLetterTag { character: Alphabet::U, uppercase: false, - }) => Ok(Some(Self::AbsoluteURL(UncheckedUrl::from(tag_1)))), - TagKind::Relay => Ok(Some(Self::Relay(UncheckedUrl::from(tag_1)))), - TagKind::ContentWarning => Ok(Some(Self::ContentWarning { + }) => Ok(Self::AbsoluteURL(UncheckedUrl::from(tag_1))), + TagKind::Relay => Ok(Self::Relay(UncheckedUrl::from(tag_1))), + TagKind::ContentWarning => Ok(Self::ContentWarning { reason: Some(tag_1.to_owned()), - })), - TagKind::Expiration => Ok(Some(Self::Expiration(Timestamp::from_str(tag_1)?))), - TagKind::Subject => Ok(Some(Self::Subject(tag_1.to_owned()))), - TagKind::Challenge => Ok(Some(Self::Challenge(tag_1.to_owned()))), - TagKind::Title => Ok(Some(Self::Title(tag_1.to_owned()))), - TagKind::Image => Ok(Some(Self::Image(UncheckedUrl::from(tag_1), None))), - TagKind::Thumb => Ok(Some(Self::Thumb(UncheckedUrl::from(tag_1), None))), - TagKind::Summary => Ok(Some(Self::Summary(tag_1.to_owned()))), - TagKind::PublishedAt => Ok(Some(Self::PublishedAt(Timestamp::from_str(tag_1)?))), - TagKind::Description => Ok(Some(Self::Description(tag_1.to_owned()))), - TagKind::Bolt11 => Ok(Some(Self::Bolt11(tag_1.to_owned()))), - TagKind::Preimage => Ok(Some(Self::Preimage(tag_1.to_owned()))), - TagKind::Amount => Ok(Some(Self::Amount { + }), + TagKind::Expiration => Ok(Self::Expiration(Timestamp::from_str(tag_1)?)), + TagKind::Subject => Ok(Self::Subject(tag_1.to_owned())), + TagKind::Challenge => Ok(Self::Challenge(tag_1.to_owned())), + TagKind::Title => Ok(Self::Title(tag_1.to_owned())), + TagKind::Image => Ok(Self::Image(UncheckedUrl::from(tag_1), None)), + TagKind::Thumb => Ok(Self::Thumb(UncheckedUrl::from(tag_1), None)), + TagKind::Summary => Ok(Self::Summary(tag_1.to_owned())), + TagKind::PublishedAt => Ok(Self::PublishedAt(Timestamp::from_str(tag_1)?)), + TagKind::Description => Ok(Self::Description(tag_1.to_owned())), + TagKind::Bolt11 => Ok(Self::Bolt11(tag_1.to_owned())), + TagKind::Preimage => Ok(Self::Preimage(tag_1.to_owned())), + TagKind::Amount => Ok(Self::Amount { millisats: tag_1.parse()?, bolt11: None, - })), - TagKind::Lnurl => Ok(Some(Self::Lnurl(tag_1.to_owned()))), - TagKind::Name => Ok(Some(Self::Name(tag_1.to_owned()))), - TagKind::Url => Ok(Some(Self::Url(Url::parse(tag_1)?))), - TagKind::Magnet => Ok(Some(Self::Magnet(tag_1.to_owned()))), - TagKind::Blurhash => Ok(Some(Self::Blurhash(tag_1.to_owned()))), - TagKind::Streaming => Ok(Some(Self::Streaming(UncheckedUrl::from(tag_1)))), - TagKind::Recording => Ok(Some(Self::Recording(UncheckedUrl::from(tag_1)))), - TagKind::Starts => Ok(Some(Self::Starts(Timestamp::from_str(tag_1)?))), - TagKind::Ends => Ok(Some(Self::Ends(Timestamp::from_str(tag_1)?))), + }), + TagKind::Lnurl => Ok(Self::Lnurl(tag_1.to_owned())), + TagKind::Name => Ok(Self::Name(tag_1.to_owned())), + TagKind::Url => Ok(Self::Url(Url::parse(tag_1)?)), + TagKind::Magnet => Ok(Self::Magnet(tag_1.to_owned())), + TagKind::Blurhash => Ok(Self::Blurhash(tag_1.to_owned())), + TagKind::Streaming => Ok(Self::Streaming(UncheckedUrl::from(tag_1))), + TagKind::Recording => Ok(Self::Recording(UncheckedUrl::from(tag_1))), + TagKind::Starts => Ok(Self::Starts(Timestamp::from_str(tag_1)?)), + TagKind::Ends => Ok(Self::Ends(Timestamp::from_str(tag_1)?)), TagKind::Status => match DataVendingMachineStatus::from_str(tag_1) { - Ok(status) => Ok(Some(Self::DataVendingMachineStatus { + Ok(status) => Ok(Self::DataVendingMachineStatus { status, extra_info: None, - })), - Err(_) => Ok(Some(Self::LiveEventStatus(LiveEventStatus::from(tag_1)))), /* TODO: check if unknown status error? */ + }), + Err(_) => Ok(Self::LiveEventStatus(LiveEventStatus::from(tag_1))), /* TODO: check if unknown status error? */ }, - TagKind::CurrentParticipants => Ok(Some(Self::CurrentParticipants(tag_1.parse()?))), - TagKind::TotalParticipants => Ok(Some(Self::TotalParticipants(tag_1.parse()?))), - TagKind::Method => Ok(Some(Self::Method(HttpMethod::from_str(tag_1)?))), - TagKind::Payload => Ok(Some(Self::Payload(Sha256Hash::from_str(tag_1)?))), - TagKind::Anon => Ok(Some(Self::Anon { + TagKind::CurrentParticipants => Ok(Self::CurrentParticipants(tag_1.parse()?)), + TagKind::TotalParticipants => Ok(Self::TotalParticipants(tag_1.parse()?)), + TagKind::Method => Ok(Self::Method(HttpMethod::from_str(tag_1)?)), + TagKind::Payload => Ok(Self::Payload(Sha256Hash::from_str(tag_1)?)), + TagKind::Anon => Ok(Self::Anon { msg: (!tag_1.is_empty()).then_some(tag_1.to_owned()), - })), - TagKind::Request => Ok(Some(Self::Request(Event::from_json(tag_1)?))), - TagKind::Word => Ok(Some(Self::Word(tag_1.to_string()))), + }), + TagKind::Request => Ok(Self::Request(Event::from_json(tag_1)?)), + TagKind::Word => Ok(Self::Word(tag_1.to_string())), TagKind::SingleLetter(SingleLetterTag { character: Alphabet::L, uppercase: true, - }) => Ok(Some(Self::LabelNamespace(tag_1.to_string()))), - _ => Ok(None), + }) => Ok(Self::LabelNamespace(tag_1.to_string())), + TagKind::Dim => Ok(Self::Dim(ImageDimensions::from_str(tag_1)?)), + _ => Err(Error::UnknownStardardizedTag), }; } @@ -849,21 +1147,21 @@ impl Tag { }) => { let public_key = PublicKey::from_str(tag_1)?; if tag_2.is_empty() { - Ok(Some(Self::PublicKey { + Ok(Self::PublicKey { public_key, relay_url: Some(UncheckedUrl::empty()), alias: None, uppercase: false, - })) + }) } else { match Report::from_str(tag_2) { - Ok(report) => Ok(Some(Self::PubKeyReport(public_key, report))), - Err(_) => Ok(Some(Self::PublicKey { + Ok(report) => Ok(Self::PublicKeyReport(public_key, report)), + Err(_) => Ok(Self::PublicKey { public_key, relay_url: Some(UncheckedUrl::from(tag_2)), alias: None, uppercase: false, - })), + }), } } } @@ -873,49 +1171,49 @@ impl Tag { }) => { let event_id = EventId::from_hex(tag_1)?; if tag_2.is_empty() { - Ok(Some(Self::Event { + Ok(Self::Event { event_id, relay_url: Some(UncheckedUrl::empty()), marker: None, - })) + }) } else { match Report::from_str(tag_2) { - Ok(report) => Ok(Some(Self::EventReport(event_id, report))), - Err(_) => Ok(Some(Self::Event { + Ok(report) => Ok(Self::EventReport(event_id, report)), + Err(_) => Ok(Self::Event { event_id, relay_url: Some(UncheckedUrl::from(tag_2)), marker: None, - })), + }), } } } TagKind::SingleLetter(SingleLetterTag { character: Alphabet::I, uppercase: false, - }) => Ok(Some(Self::ExternalIdentity(Identity::new(tag_1, tag_2)?))), - TagKind::Nonce => Ok(Some(Self::POW { + }) => Ok(Self::ExternalIdentity(Identity::new(tag_1, tag_2)?)), + TagKind::Nonce => Ok(Self::POW { nonce: tag_1.parse()?, difficulty: tag_2.parse()?, - })), + }), TagKind::SingleLetter(SingleLetterTag { character: Alphabet::A, uppercase: false, - }) => Ok(Some(Self::A { + }) => Ok(Self::Coordinate { coordinate: Coordinate::from_str(tag_1)?, relay_url: Some(UncheckedUrl::from(tag_2)), - })), - TagKind::Image => Ok(Some(Self::Image( + }), + TagKind::Image => Ok(Self::Image( UncheckedUrl::from(tag_1), Some(ImageDimensions::from_str(tag_2)?), - ))), - TagKind::Thumb => Ok(Some(Self::Thumb( + )), + TagKind::Thumb => Ok(Self::Thumb( UncheckedUrl::from(tag_1), Some(ImageDimensions::from_str(tag_2)?), - ))), - TagKind::Aes256Gcm => Ok(Some(Self::Aes256Gcm { + )), + TagKind::Aes256Gcm => Ok(Self::Aes256Gcm { key: tag_1.to_owned(), iv: tag_2.to_owned(), - })), + }), TagKind::SingleLetter(SingleLetterTag { character: Alphabet::R, uppercase: false, @@ -923,30 +1221,30 @@ impl Tag { if (tag_1.starts_with("ws://") || tag_1.starts_with("wss://")) && !tag_2.is_empty() { - Ok(Some(Self::RelayMetadata( - UncheckedUrl::from(tag_1), - Some(RelayMetadata::from_str(tag_2)?), - ))) + Ok(Self::RelayMetadata { + relay_url: Url::from_str(tag_1)?, + metadata: Some(RelayMetadata::from_str(tag_2)?), + }) } else { - Ok(None) + Err(Error::UnknownStardardizedTag) } } - TagKind::Proxy => Ok(Some(Self::Proxy { + TagKind::Proxy => Ok(Self::Proxy { id: tag_1.to_owned(), protocol: Protocol::from(tag_2), - })), - TagKind::Emoji => Ok(Some(Self::Emoji { + }), + TagKind::Emoji => Ok(Self::Emoji { shortcode: tag_1.to_owned(), url: UncheckedUrl::from(tag_2), - })), + }), TagKind::Status => match DataVendingMachineStatus::from_str(tag_1) { - Ok(status) => Ok(Some(Self::DataVendingMachineStatus { + Ok(status) => Ok(Self::DataVendingMachineStatus { status, extra_info: Some(tag_2.to_string()), - })), - Err(_) => Ok(None), + }), + Err(_) => Err(Error::UnknownStardardizedTag), }, - _ => Ok(None), + _ => Err(Error::UnknownStardardizedTag), }; } @@ -964,34 +1262,34 @@ impl Tag { let relay_url: Option = Some(UncheckedUrl::from(tag_2)); match LiveEventMarker::from_str(tag_3) { - Ok(marker) => Ok(Some(Self::PubKeyLiveEvent { + Ok(marker) => Ok(Self::PubKeyLiveEvent { public_key, relay_url, marker, proof: None, - })), - Err(_) => Ok(Some(Self::PublicKey { + }), + Err(_) => Ok(Self::PublicKey { public_key, relay_url, alias: Some(tag_3.to_string()), uppercase: *uppercase, - })), + }), } } TagKind::SingleLetter(SingleLetterTag { character: Alphabet::E, uppercase: false, - }) => Ok(Some(Self::Event { + }) => Ok(Self::Event { event_id: EventId::from_hex(tag_1)?, relay_url: (!tag_2.is_empty()).then_some(UncheckedUrl::from(tag_2)), marker: (!tag_3.is_empty()).then_some(Marker::from(tag_3)), - })), - TagKind::Delegation => Ok(Some(Self::Delegation { + }), + TagKind::Delegation => Ok(Self::Delegation { delegator: PublicKey::from_str(tag_1)?, conditions: Conditions::from_str(tag_2)?, sig: Signature::from_str(tag_3)?, - })), - _ => Ok(None), + }), + _ => Err(Error::UnknownStardardizedTag), }; } @@ -1005,20 +1303,20 @@ impl Tag { TagKind::SingleLetter(SingleLetterTag { character: Alphabet::P, .. - }) => Ok(Some(Self::PubKeyLiveEvent { + }) => Ok(Self::PubKeyLiveEvent { public_key: PublicKey::from_str(tag_1)?, relay_url: (!tag_2.is_empty()).then_some(UncheckedUrl::from(tag_2)), marker: LiveEventMarker::from_str(tag_3)?, proof: Signature::from_str(tag_4).ok(), - })), - _ => Ok(None), + }), + _ => Err(Error::UnknownStardardizedTag), }; } - Ok(None) + Err(Error::UnknownStardardizedTag) } - /// Compose `Tag::Event` without `relay_url` and `marker` + /// Compose `TagStandard::Event` without `relay_url` and `marker` /// /// JSON: `["e", "event-id"]` #[inline] @@ -1030,7 +1328,7 @@ impl Tag { } } - /// Compose `Tag::PublicKey` without `relay_url` and `alias` + /// Compose `TagStandard::PublicKey` without `relay_url` and `alias` /// /// JSON: `["p", ""]` #[inline] @@ -1043,48 +1341,21 @@ impl Tag { } } - /// Compose custom tag - /// - /// JSON: `["", "", "", ...]` - #[inline] - pub fn custom(kind: TagKind, values: I) -> Self - where - I: IntoIterator, - S: Into, - { - Self::Generic(kind, values.into_iter().map(|v| v.into()).collect()) - } - - /// Check if [Tag] is an event `reply` + /// Check if tag is an event `reply` #[inline] pub fn is_reply(&self) -> bool { matches!( self, - Tag::Event { + Self::Event { marker: Some(Marker::Reply), .. } ) } - /// Get [`Tag`] as string vector - /// - /// Internally clone tag and convert it to `Vec`. To avoid tag clone, use `Tag::to_vec`. - #[inline] - pub fn as_vec(&self) -> Vec { - self.clone().into() - } - - /// Consume [`Tag`] and return string vector - #[inline] - pub fn to_vec(self) -> Vec { - self.into() - } - /// Get tag kind pub fn kind(&self) -> TagKind { match self { - Self::Generic(kind, ..) => kind.clone(), Self::Event { .. } | Self::EventReport(..) => TagKind::SingleLetter(SingleLetterTag { character: Alphabet::E, uppercase: false, @@ -1093,13 +1364,13 @@ impl Tag { character: Alphabet::P, uppercase: *uppercase, }), - Self::PubKeyReport(..) | Self::PubKeyLiveEvent { .. } => { + Self::PublicKeyReport(..) | Self::PubKeyLiveEvent { .. } => { TagKind::SingleLetter(SingleLetterTag { character: Alphabet::P, uppercase: false, }) } - Self::Reference(..) | Self::RelayMetadata(..) => { + Self::Reference(..) | Self::RelayMetadata { .. } => { TagKind::SingleLetter(SingleLetterTag { character: Alphabet::R, uppercase: false, @@ -1121,7 +1392,7 @@ impl Tag { character: Alphabet::I, uppercase: false, }), - Self::A { .. } => TagKind::SingleLetter(SingleLetterTag { + Self::Coordinate { .. } => TagKind::SingleLetter(SingleLetterTag { character: Alphabet::A, uppercase: false, }), @@ -1192,99 +1463,24 @@ impl Tag { } } - /// Get [SingleLetterTag] + /// Consume [`Tag`] and return string vector #[inline] - pub fn single_letter_tag(&self) -> Option { - match self.kind() { - TagKind::SingleLetter(s) => Some(s), - _ => None, - } - } - - /// Return the **first** tag value (index `1`), if exists. - pub fn content(&self) -> Option { - match self { - Self::Generic(_, l) => l.first().map(|v| v.into_generic_tag_value()), - Self::Event { event_id, .. } | Self::EventReport(event_id, ..) => { - Some((*event_id).into_generic_tag_value()) - } - Self::PublicKey { public_key, .. } - | Self::PubKeyReport(public_key, ..) - | Self::PubKeyLiveEvent { public_key, .. } => { - Some((*public_key).into_generic_tag_value()) - } - Self::Reference(val) => Some(val.into_generic_tag_value()), - Self::RelayMetadata(url, ..) => Some(url.to_string().into_generic_tag_value()), - Self::Hashtag(val) => Some(val.into_generic_tag_value()), - Self::Geohash(val, ..) => Some(val.into_generic_tag_value()), - Self::Identifier(val, ..) => Some(val.into_generic_tag_value()), - Self::ExternalIdentity(..) => None, - Self::A { coordinate, .. } => Some(coordinate.to_string().into_generic_tag_value()), - Self::Kind(kind) => Some(kind.to_string().into_generic_tag_value()), - Self::Relay(url) => Some(url.to_string().into_generic_tag_value()), - Self::POW { nonce, .. } => Some(nonce.to_string().into_generic_tag_value()), - Self::Delegation { delegator, .. } => Some(delegator.into_generic_tag_value()), - Self::ContentWarning { reason } => reason.clone().map(GenericTagValue::String), - Self::Expiration(timestamp) => Some(timestamp.to_string().into_generic_tag_value()), - Self::Subject(val) => Some(val.into_generic_tag_value()), - Self::Challenge(val) => Some(val.into_generic_tag_value()), - Self::Title(val) => Some(val.into_generic_tag_value()), - Self::Image(url, ..) => Some(url.to_string().into_generic_tag_value()), - Self::Thumb(url, ..) => Some(url.to_string().into_generic_tag_value()), - Self::Summary(val) => Some(val.into_generic_tag_value()), - Self::PublishedAt(timestamp) => Some(timestamp.to_string().into_generic_tag_value()), - Self::Description(val) => Some(val.into_generic_tag_value()), - Self::Bolt11(val) => Some(val.into_generic_tag_value()), - Self::Preimage(val) => Some(val.into_generic_tag_value()), - Self::Relays(list) => list.first().map(|r| r.to_string().into_generic_tag_value()), - Self::Amount { millisats, .. } => Some(millisats.to_string().into_generic_tag_value()), - Self::Name(val) => Some(val.into_generic_tag_value()), - Self::Lnurl(val) => Some(val.into_generic_tag_value()), - Self::Url(url) => Some(url.to_string().into_generic_tag_value()), - Self::MimeType(val) => Some(val.into_generic_tag_value()), - Self::Aes256Gcm { key, .. } => Some(key.clone().into_generic_tag_value()), - Self::Sha256(val) => Some(val.to_string().into_generic_tag_value()), - Self::Size(val) => Some(val.to_string().into_generic_tag_value()), - Self::Dim(val) => Some(val.to_string().into_generic_tag_value()), - Self::Magnet(val) => Some(val.into_generic_tag_value()), - Self::Blurhash(val) => Some(val.into_generic_tag_value()), - Self::Streaming(url) => Some(url.to_string().into_generic_tag_value()), - Self::Recording(url) => Some(url.to_string().into_generic_tag_value()), - Self::Starts(timestamp) => Some(timestamp.to_string().into_generic_tag_value()), - Self::Ends(timestamp) => Some(timestamp.to_string().into_generic_tag_value()), - Self::LiveEventStatus(val) => Some(val.to_string().into_generic_tag_value()), - Self::DataVendingMachineStatus { status, .. } => { - Some(status.to_string().into_generic_tag_value()) - } - Self::CurrentParticipants(num) => Some(num.to_string().into_generic_tag_value()), - Self::TotalParticipants(num) => Some(num.to_string().into_generic_tag_value()), - Self::AbsoluteURL(url) => Some(url.to_string().into_generic_tag_value()), - Self::Method(val) => Some(val.to_string().into_generic_tag_value()), - Self::Payload(val) => Some(val.to_string().into_generic_tag_value()), - Self::Anon { msg } => msg.clone().map(GenericTagValue::String), - Self::Proxy { id, .. } => Some(id.into_generic_tag_value()), - Self::Emoji { shortcode, .. } => Some(shortcode.into_generic_tag_value()), - Self::Encrypted => None, - Self::Request(val) => Some(val.as_json().into_generic_tag_value()), - Self::Word(val) => Some(val.into_generic_tag_value()), - Self::LabelNamespace(val) => Some(val.into_generic_tag_value()), - Self::Label(l) => l.first().map(|v| v.into_generic_tag_value()), - } + pub fn to_vec(self) -> Vec { + self.into() } } -impl From for Vec { - fn from(tag: Tag) -> Self { - let tag_kind: TagKind = tag.kind(); +impl From for Vec { + fn from(tag: TagStandard) -> Self { + let tag_kind: String = tag.kind().to_string(); match tag { - Tag::Generic(kind, data) => [vec![kind.to_string()], data].concat(), - Tag::Event { + TagStandard::Event { event_id, relay_url, marker, } => { - let mut tag = vec![tag_kind.to_string(), event_id.to_hex()]; + let mut tag = vec![tag_kind, event_id.to_hex()]; if let Some(relay_url) = relay_url { tag.push(relay_url.to_string()); } @@ -1296,13 +1492,13 @@ impl From for Vec { } tag } - Tag::PublicKey { + TagStandard::PublicKey { public_key, relay_url, alias, .. } => { - let mut tag = vec![tag_kind.to_string(), public_key.to_string()]; + let mut tag = vec![tag_kind, public_key.to_string()]; if let Some(relay_url) = relay_url { tag.push(relay_url.to_string()); } @@ -1311,20 +1507,20 @@ impl From for Vec { } tag } - Tag::EventReport(id, report) => { - vec![tag_kind.to_string(), id.to_hex(), report.to_string()] + TagStandard::EventReport(id, report) => { + vec![tag_kind, id.to_hex(), report.to_string()] } - Tag::PubKeyReport(pk, report) => { - vec![tag_kind.to_string(), pk.to_string(), report.to_string()] + TagStandard::PublicKeyReport(pk, report) => { + vec![tag_kind, pk.to_string(), report.to_string()] } - Tag::PubKeyLiveEvent { + TagStandard::PubKeyLiveEvent { public_key, relay_url, marker, proof, } => { let mut tag = vec![ - tag_kind.to_string(), + tag_kind, public_key.to_string(), relay_url.map(|u| u.to_string()).unwrap_or_default(), marker.to_string(), @@ -1334,174 +1530,162 @@ impl From for Vec { } tag } - Tag::Reference(r) => vec![tag_kind.to_string(), r], - Tag::RelayMetadata(url, rw) => { - let mut tag = vec![tag_kind.to_string(), url.to_string()]; - if let Some(rw) = rw { - tag.push(rw.to_string()); + TagStandard::Reference(r) => vec![tag_kind, r], + TagStandard::RelayMetadata { + relay_url, + metadata, + } => { + let mut tag = vec![tag_kind, relay_url.to_string()]; + if let Some(metadata) = metadata { + tag.push(metadata.to_string()); } tag } - Tag::Hashtag(t) => vec![tag_kind.to_string(), t], - Tag::Geohash(g) => vec![tag_kind.to_string(), g], - Tag::Identifier(d) => vec![tag_kind.to_string(), d], - Tag::A { + TagStandard::Hashtag(t) => vec![tag_kind, t], + TagStandard::Geohash(g) => vec![tag_kind, g], + TagStandard::Identifier(d) => vec![tag_kind, d], + TagStandard::Coordinate { coordinate, relay_url, } => { - let mut vec = vec![tag_kind.to_string(), coordinate.to_string()]; + let mut vec = vec![tag_kind, coordinate.to_string()]; if let Some(relay) = relay_url { vec.push(relay.to_string()); } vec } - Tag::ExternalIdentity(identity) => identity.into(), - Tag::Kind(kind) => vec![tag_kind.to_string(), kind.to_string()], - Tag::Relay(url) => vec![tag_kind.to_string(), url.to_string()], - Tag::POW { nonce, difficulty } => vec![ - tag_kind.to_string(), - nonce.to_string(), - difficulty.to_string(), - ], - Tag::Delegation { + TagStandard::ExternalIdentity(identity) => identity.into(), + 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()] + } + TagStandard::Delegation { delegator, conditions, sig, } => vec![ - tag_kind.to_string(), + tag_kind, delegator.to_string(), conditions.to_string(), sig.to_string(), ], - Tag::ContentWarning { reason } => { - let mut tag = vec![tag_kind.to_string()]; + TagStandard::ContentWarning { reason } => { + let mut tag = vec![tag_kind]; if let Some(reason) = reason { tag.push(reason); } tag } - Tag::Expiration(timestamp) => { - vec![tag_kind.to_string(), timestamp.to_string()] + TagStandard::Expiration(timestamp) => { + vec![tag_kind, timestamp.to_string()] } - Tag::Subject(sub) => vec![tag_kind.to_string(), sub], - Tag::Challenge(challenge) => vec![tag_kind.to_string(), challenge], - Tag::Title(title) => vec![tag_kind.to_string(), title], - Tag::Image(image, dimensions) => { - let mut tag = vec![tag_kind.to_string(), image.to_string()]; + TagStandard::Subject(sub) => vec![tag_kind, sub], + TagStandard::Challenge(challenge) => vec![tag_kind, challenge], + TagStandard::Title(title) => vec![tag_kind, title], + TagStandard::Image(image, dimensions) => { + let mut tag = vec![tag_kind, image.to_string()]; if let Some(dim) = dimensions { tag.push(dim.to_string()); } tag } - Tag::Thumb(thumb, dimensions) => { - let mut tag = vec![tag_kind.to_string(), thumb.to_string()]; + TagStandard::Thumb(thumb, dimensions) => { + let mut tag = vec![tag_kind, thumb.to_string()]; if let Some(dim) = dimensions { tag.push(dim.to_string()); } tag } - Tag::Summary(summary) => vec![tag_kind.to_string(), summary], - Tag::PublishedAt(timestamp) => { - vec![tag_kind.to_string(), timestamp.to_string()] + TagStandard::Summary(summary) => vec![tag_kind, summary], + TagStandard::PublishedAt(timestamp) => { + vec![tag_kind, timestamp.to_string()] } - Tag::Description(description) => { - vec![tag_kind.to_string(), description] + TagStandard::Description(description) => { + vec![tag_kind, description] } - Tag::Bolt11(bolt11) => { - vec![tag_kind.to_string(), bolt11] + TagStandard::Bolt11(bolt11) => { + vec![tag_kind, bolt11] } - Tag::Preimage(preimage) => { - vec![tag_kind.to_string(), preimage] + TagStandard::Preimage(preimage) => { + vec![tag_kind, preimage] } - Tag::Relays(relays) => vec![tag_kind.to_string()] + TagStandard::Relays(relays) => vec![tag_kind] .into_iter() .chain(relays.iter().map(|relay| relay.to_string())) .collect::>(), - Tag::Amount { millisats, bolt11 } => { - let mut tag = vec![tag_kind.to_string(), millisats.to_string()]; + TagStandard::Amount { millisats, bolt11 } => { + let mut tag = vec![tag_kind, millisats.to_string()]; if let Some(bolt11) = bolt11 { tag.push(bolt11); } tag } - Tag::Name(name) => { - vec![tag_kind.to_string(), name] + TagStandard::Name(name) => { + vec![tag_kind, name] } - Tag::Lnurl(lnurl) => { - vec![tag_kind.to_string(), lnurl] + TagStandard::Lnurl(lnurl) => { + vec![tag_kind, lnurl] } - Tag::Url(url) => vec![tag_kind.to_string(), url.to_string()], - Tag::MimeType(mime) => vec![tag_kind.to_string(), mime], - Tag::Aes256Gcm { key, iv } => vec![tag_kind.to_string(), key, iv], - Tag::Sha256(hash) => vec![tag_kind.to_string(), hash.to_string()], - Tag::Size(bytes) => vec![tag_kind.to_string(), bytes.to_string()], - Tag::Dim(dim) => vec![tag_kind.to_string(), dim.to_string()], - Tag::Magnet(uri) => vec![tag_kind.to_string(), uri], - Tag::Blurhash(data) => vec![tag_kind.to_string(), data], - Tag::Streaming(url) => vec![tag_kind.to_string(), url.to_string()], - Tag::Recording(url) => vec![tag_kind.to_string(), url.to_string()], - Tag::Starts(timestamp) => { - vec![tag_kind.to_string(), timestamp.to_string()] + TagStandard::Url(url) => vec![tag_kind, url.to_string()], + TagStandard::MimeType(mime) => vec![tag_kind, mime], + TagStandard::Aes256Gcm { key, iv } => vec![tag_kind, key, iv], + TagStandard::Sha256(hash) => vec![tag_kind, hash.to_string()], + TagStandard::Size(bytes) => vec![tag_kind, bytes.to_string()], + TagStandard::Dim(dim) => vec![tag_kind, dim.to_string()], + TagStandard::Magnet(uri) => vec![tag_kind, uri], + TagStandard::Blurhash(data) => vec![tag_kind, data], + TagStandard::Streaming(url) => vec![tag_kind, url.to_string()], + TagStandard::Recording(url) => vec![tag_kind, url.to_string()], + TagStandard::Starts(timestamp) => { + vec![tag_kind, timestamp.to_string()] } - Tag::Ends(timestamp) => { - vec![tag_kind.to_string(), timestamp.to_string()] + TagStandard::Ends(timestamp) => { + vec![tag_kind, timestamp.to_string()] } - Tag::LiveEventStatus(s) => { - vec![tag_kind.to_string(), s.to_string()] + TagStandard::LiveEventStatus(s) => { + vec![tag_kind, s.to_string()] } - Tag::CurrentParticipants(num) => { - vec![tag_kind.to_string(), num.to_string()] + TagStandard::CurrentParticipants(num) => { + vec![tag_kind, num.to_string()] } - Tag::TotalParticipants(num) => { - vec![tag_kind.to_string(), num.to_string()] + TagStandard::TotalParticipants(num) => { + vec![tag_kind, num.to_string()] } - Tag::AbsoluteURL(url) => { - vec![tag_kind.to_string(), url.to_string()] + TagStandard::AbsoluteURL(url) => { + vec![tag_kind, url.to_string()] } - Tag::Method(method) => { - vec![tag_kind.to_string(), method.to_string()] + TagStandard::Method(method) => { + vec![tag_kind, method.to_string()] } - Tag::Payload(p) => vec![tag_kind.to_string(), p.to_string()], - Tag::Anon { msg } => { - let mut tag = vec![tag_kind.to_string()]; + TagStandard::Payload(p) => vec![tag_kind, p.to_string()], + TagStandard::Anon { msg } => { + let mut tag = vec![tag_kind]; if let Some(msg) = msg { tag.push(msg); } tag } - Tag::Proxy { id, protocol } => { - vec![tag_kind.to_string(), id, protocol.to_string()] + TagStandard::Proxy { id, protocol } => { + vec![tag_kind, id, protocol.to_string()] } - Tag::Emoji { shortcode, url } => { - vec![tag_kind.to_string(), shortcode, url.to_string()] + TagStandard::Emoji { shortcode, url } => { + vec![tag_kind, shortcode, url.to_string()] } - Tag::Encrypted => vec![tag_kind.to_string()], - Tag::Request(event) => vec![tag_kind.to_string(), event.as_json()], - Tag::DataVendingMachineStatus { status, extra_info } => { - let mut tag = vec![tag_kind.to_string(), status.to_string()]; + TagStandard::Encrypted => vec![tag_kind], + TagStandard::Request(event) => vec![tag_kind, event.as_json()], + TagStandard::DataVendingMachineStatus { status, extra_info } => { + let mut tag = vec![tag_kind, status.to_string()]; if let Some(extra_info) = extra_info { tag.push(extra_info); } tag } - Tag::Word(word) => vec![TagKind::Word.to_string(), word], - Tag::LabelNamespace(n) => vec![ - TagKind::SingleLetter(SingleLetterTag { - character: Alphabet::L, - uppercase: true, - }) - .to_string(), - n, - ], - Tag::Label(l) => { + TagStandard::Word(word) => vec![tag_kind, word], + TagStandard::LabelNamespace(n) => vec![tag_kind, n], + TagStandard::Label(l) => { let mut tag = Vec::with_capacity(1 + l.len()); - tag.push( - TagKind::SingleLetter(SingleLetterTag { - character: Alphabet::L, - uppercase: false, - }) - .to_string(), - ); + tag.push(tag_kind); tag.extend(l); tag } @@ -1509,31 +1693,6 @@ impl From for Vec { } } -impl Serialize for Tag { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let data: Vec = self.as_vec(); - let mut seq = serializer.serialize_seq(Some(data.len()))?; - for element in data.into_iter() { - seq.serialize_element(&element)?; - } - seq.end() - } -} - -impl<'de> Deserialize<'de> for Tag { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - type Data = Vec; - let tag: Vec = Data::deserialize(deserializer)?; - Self::parse(&tag).map_err(DeserializerError::custom) - } -} - /// Supported external identity providers #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ExternalIdentity { @@ -1601,18 +1760,18 @@ impl Identity { } } -impl TryFrom for Identity { +impl TryFrom for Identity { type Error = Error; - fn try_from(value: Tag) -> Result { + fn try_from(value: TagStandard) -> Result { match value { - Tag::ExternalIdentity(iden) => Ok(iden), + TagStandard::ExternalIdentity(iden) => Ok(iden), _ => Err(Error::InvalidIdentity), } } } -impl From for Tag { +impl From for TagStandard { fn from(value: Identity) -> Self { Self::ExternalIdentity(value) } @@ -1638,11 +1797,26 @@ mod tests { use crate::{Event, JsonUtil, Timestamp}; #[test] - fn test_tag_is_reply() { - let tag = Tag::Relay(UncheckedUrl::new("wss://relay.damus.io")); + fn test_tag_match_standardized() { + let tag: Tag = Tag::parse(&["d", "bravery"]).unwrap(); + assert_eq!( + tag.as_standardized(), + Some(&TagStandard::Identifier(String::from("bravery"))) + ); + + let tag: Tag = Tag::parse(&["d", "test"]).unwrap(); + assert_eq!( + tag.to_standardized(), + Some(TagStandard::Identifier(String::from("test"))) + ); + } + + #[test] + fn test_tag_standard_is_reply() { + let tag = TagStandard::Relay(UncheckedUrl::new("wss://relay.damus.io")); assert!(!tag.is_reply()); - let tag = Tag::Event { + let tag = TagStandard::Event { event_id: EventId::from_hex( "2be17aa3031bdcb006f0fce80c146dea9c1c0268b0af2398bb673365c6444d45", ) @@ -1652,7 +1826,7 @@ mod tests { }; assert!(tag.is_reply()); - let tag = Tag::Event { + let tag = TagStandard::Event { event_id: EventId::from_hex( "2be17aa3031bdcb006f0fce80c146dea9c1c0268b0af2398bb673365c6444d45", ) @@ -1666,10 +1840,7 @@ mod tests { #[test] fn test_extract_tag_content() { let t: Tag = Tag::parse(&["aaaaaa", "bbbbbb"]).unwrap(); - assert_eq!( - t.content(), - Some(GenericTagValue::String(String::from("bbbbbb"))) - ); + assert_eq!(t.content(), Some("bbbbbb")); // Test extract public key let t: Tag = Tag::parse(&[ @@ -1679,9 +1850,7 @@ mod tests { .unwrap(); assert_eq!( t.content(), - Some(GenericTagValue::String(String::from( - "f86c44a2de95d9149b51c6a29afeabba264c18e2fa7c49de93424a0c56947785" - ))) + Some("f86c44a2de95d9149b51c6a29afeabba264c18e2fa7c49de93424a0c56947785") ); // Test extract event ID @@ -1692,9 +1861,7 @@ mod tests { .unwrap(); assert_eq!( t.content(), - Some(GenericTagValue::String(String::from( - "2be17aa3031bdcb006f0fce80c146dea9c1c0268b0af2398bb673365c6444d45" - ))) + Some("2be17aa3031bdcb006f0fce80c146dea9c1c0268b0af2398bb673365c6444d45") ); } @@ -1741,7 +1908,8 @@ mod tests { fn test_tag_as_vec() { assert_eq!( vec!["content-warning"], - Tag::ContentWarning { reason: None }.as_vec() + Tag::from_standardized_without_cell(TagStandard::ContentWarning { reason: None }) + .to_vec() ); assert_eq!( @@ -1755,7 +1923,7 @@ mod tests { ) .unwrap() ) - .as_vec() + .to_vec() ); assert_eq!( @@ -1769,40 +1937,36 @@ mod tests { ) .unwrap() ) - .as_vec() + .to_vec() ); assert_eq!( vec!["expiration", "1600000000"], - Tag::Expiration(Timestamp::from(1600000000)).as_vec() + Tag::expiration(Timestamp::from(1600000000)).to_vec() ); assert_eq!( vec!["content-warning", "reason"], - Tag::ContentWarning { + Tag::from_standardized_without_cell(TagStandard::ContentWarning { reason: Some(String::from("reason")) - } - .as_vec() + }) + .to_vec() ); assert_eq!( vec!["subject", "textnote with subject"], - Tag::Subject(String::from("textnote with subject")).as_vec() + Tag::from_standardized_without_cell(TagStandard::Subject(String::from( + "textnote with subject" + ))) + .to_vec() ); assert_eq!( - vec!["client", "nostr-sdk"], - Tag::Generic( - TagKind::Custom("client".to_string()), - vec!["nostr-sdk".to_string()] - ) - .as_vec() + vec!["client", "rust-nostr"], + Tag::custom(TagKind::Custom(Cow::Borrowed("client")), ["rust-nostr"]).to_vec() ); - assert_eq!( - vec!["d", "test"], - Tag::Identifier("test".to_string()).as_vec() - ); + assert_eq!(vec!["d", "test"], Tag::identifier("test").to_vec()); assert_eq!( vec![ @@ -1810,7 +1974,7 @@ mod tests { "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d", "wss://relay.damus.io" ], - Tag::PublicKey { + Tag::from_standardized_without_cell(TagStandard::PublicKey { public_key: PublicKey::from_str( "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d" ) @@ -1818,8 +1982,8 @@ mod tests { relay_url: Some(UncheckedUrl::from("wss://relay.damus.io")), alias: None, uppercase: false, - } - .as_vec() + }) + .to_vec() ); assert_eq!( @@ -1828,15 +1992,15 @@ mod tests { "378f145897eea948952674269945e88612420db35791784abf0616b4fed56ef7", "" ], - Tag::Event { + Tag::from_standardized_without_cell(TagStandard::Event { event_id: EventId::from_hex( "378f145897eea948952674269945e88612420db35791784abf0616b4fed56ef7" ) .unwrap(), relay_url: Some(UncheckedUrl::empty()), marker: None - } - .as_vec() + }) + .to_vec() ); assert_eq!( @@ -1845,15 +2009,15 @@ mod tests { "378f145897eea948952674269945e88612420db35791784abf0616b4fed56ef7", "wss://relay.damus.io" ], - Tag::Event { + Tag::from_standardized_without_cell(TagStandard::Event { event_id: EventId::from_hex( "378f145897eea948952674269945e88612420db35791784abf0616b4fed56ef7" ) .unwrap(), relay_url: Some(UncheckedUrl::from("wss://relay.damus.io")), marker: None - } - .as_vec() + }) + .to_vec() ); assert_eq!( @@ -1862,14 +2026,14 @@ mod tests { "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d", "spam" ], - Tag::PubKeyReport( + Tag::public_key_report( PublicKey::from_str( "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d" ) .unwrap(), Report::Spam ) - .as_vec() + .to_vec() ); assert_eq!( @@ -1878,23 +2042,34 @@ mod tests { "378f145897eea948952674269945e88612420db35791784abf0616b4fed56ef7", "nudity" ], - Tag::EventReport( + Tag::event_report( EventId::from_hex( "378f145897eea948952674269945e88612420db35791784abf0616b4fed56ef7" ) .unwrap(), Report::Nudity, ) - .as_vec() + .to_vec() ); + assert_eq!(vec!["nonce", "1", "20"], Tag::pow(1, 20).to_vec()); + assert_eq!( - vec!["nonce", "1", "20"], - Tag::POW { - nonce: 1, - difficulty: 20 - } - .as_vec() + vec![ + "a", + "30023:a695f6b60119d9521934a691347d9f78e8770b56da16bb255ee286ddf9fda919:ipsum" + ], + Tag::coordinate( + Coordinate::new( + Kind::LongFormTextNote, + PublicKey::from_str( + "a695f6b60119d9521934a691347d9f78e8770b56da16bb255ee286ddf9fda919" + ) + .unwrap() + ) + .identifier("ipsum"), + ) + .to_vec() ); assert_eq!( @@ -1903,7 +2078,7 @@ mod tests { "30023:a695f6b60119d9521934a691347d9f78e8770b56da16bb255ee286ddf9fda919:ipsum", "wss://relay.nostr.org" ], - Tag::A { + Tag::from_standardized_without_cell(TagStandard::Coordinate { coordinate: Coordinate::new( Kind::LongFormTextNote, PublicKey::from_str( @@ -1913,8 +2088,8 @@ mod tests { ) .identifier("ipsum"), relay_url: Some(UncheckedUrl::from_str("wss://relay.nostr.org").unwrap()) - } - .as_vec() + }) + .to_vec() ); assert_eq!( @@ -1924,7 +2099,7 @@ mod tests { "wss://relay.damus.io", "Speaker", ], - Tag::PubKeyLiveEvent { + Tag::from_standardized_without_cell(TagStandard::PubKeyLiveEvent { public_key: PublicKey::from_str( "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d" ) @@ -1932,8 +2107,8 @@ mod tests { relay_url: Some(UncheckedUrl::from("wss://relay.damus.io")), marker: LiveEventMarker::Speaker, proof: None - } - .as_vec() + }) + .to_vec() ); assert_eq!( @@ -1943,7 +2118,7 @@ mod tests { "", "Participant", ], - Tag::PubKeyLiveEvent { + Tag::from_standardized_without_cell(TagStandard::PubKeyLiveEvent { public_key: PublicKey::from_str( "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d" ) @@ -1951,8 +2126,8 @@ mod tests { relay_url: None, marker: LiveEventMarker::Participant, proof: None - } - .as_vec() + }) + .to_vec() ); assert_eq!( @@ -1962,7 +2137,7 @@ mod tests { "wss://relay.damus.io", "alias", ], - Tag::PublicKey { + Tag::from_standardized_without_cell(TagStandard::PublicKey { public_key: PublicKey::from_str( "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d" ) @@ -1970,8 +2145,8 @@ mod tests { relay_url: Some(UncheckedUrl::from("wss://relay.damus.io")), alias: Some(String::from("alias")), uppercase: false, - } - .as_vec() + }) + .to_vec() ); assert_eq!( @@ -1981,15 +2156,15 @@ mod tests { "", "reply" ], - Tag::Event { + Tag::from_standardized_without_cell(TagStandard::Event { event_id: EventId::from_hex( "378f145897eea948952674269945e88612420db35791784abf0616b4fed56ef7" ) .unwrap(), relay_url: None, marker: Some(Marker::Reply) - } - .as_vec() + }) + .to_vec() ); assert_eq!( @@ -1999,15 +2174,15 @@ mod tests { "kind=1", "fd0954de564cae9923c2d8ee9ab2bf35bc19757f8e328a978958a2fcc950eaba0754148a203adec29b7b64080d0cf5a32bebedd768ea6eb421a6b751bb4584a8", ], - Tag::Delegation { delegator: PublicKey::from_str( + Tag::from_standardized_without_cell(TagStandard::Delegation { delegator: PublicKey::from_str( "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d" - ).unwrap(), conditions: Conditions::from_str("kind=1").unwrap(), sig: Signature::from_str("fd0954de564cae9923c2d8ee9ab2bf35bc19757f8e328a978958a2fcc950eaba0754148a203adec29b7b64080d0cf5a32bebedd768ea6eb421a6b751bb4584a8").unwrap() } - .as_vec() + ).unwrap(), conditions: Conditions::from_str("kind=1").unwrap(), sig: Signature::from_str("fd0954de564cae9923c2d8ee9ab2bf35bc19757f8e328a978958a2fcc950eaba0754148a203adec29b7b64080d0cf5a32bebedd768ea6eb421a6b751bb4584a8").unwrap() }) + .to_vec() ); assert_eq!( vec!["lnurl", "lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0dajx2mrv92x9xp"], - Tag::Lnurl(String::from("lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0dajx2mrv92x9xp")).as_vec(), + Tag::from_standardized_without_cell(TagStandard::Lnurl(String::from("lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0dajx2mrv92x9xp"))).to_vec(), ); assert_eq!( @@ -2018,53 +2193,59 @@ mod tests { "Host", "a5d9290ef9659083c490b303eb7ee41356d8778ff19f2f91776c8dc4443388a64ffcf336e61af4c25c05ac3ae952d1ced889ed655b67790891222aaa15b99fdd" ], - Tag::PubKeyLiveEvent { + Tag::from_standardized_without_cell(TagStandard::PubKeyLiveEvent { public_key: PublicKey::from_str( "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d" ).unwrap(), relay_url: Some(UncheckedUrl::from("wss://relay.damus.io")), marker: LiveEventMarker::Host, proof: Some(Signature::from_str("a5d9290ef9659083c490b303eb7ee41356d8778ff19f2f91776c8dc4443388a64ffcf336e61af4c25c05ac3ae952d1ced889ed655b67790891222aaa15b99fdd").unwrap()) - } - .as_vec() + }) + .to_vec() ); assert_eq!( vec!["L", "#t"], - Tag::LabelNamespace("#t".to_string()).as_vec() + Tag::from_standardized_without_cell(TagStandard::LabelNamespace("#t".to_string())) + .to_vec() ); assert_eq!( vec!["l", "IT-MI"], - Tag::Label(vec!["IT-MI".to_string()]).as_vec() + Tag::from_standardized_without_cell(TagStandard::Label(vec!["IT-MI".to_string()])) + .to_vec() ); assert_eq!( vec!["l", "IT-MI", "ISO-3166-2"], - Tag::Label(vec!["IT-MI".to_string(), "ISO-3166-2".to_string()]).as_vec() + Tag::from_standardized_without_cell(TagStandard::Label(vec![ + "IT-MI".to_string(), + "ISO-3166-2".to_string() + ])) + .to_vec() ); assert_eq!( - vec!["r", "wss://atlas.nostr.land"], - Tag::RelayMetadata(UncheckedUrl::new("wss://atlas.nostr.land"), None).as_vec() + vec!["r", "wss://atlas.nostr.land/"], + Tag::relay_metadata(Url::from_str("wss://atlas.nostr.land").unwrap(), None).to_vec() ); assert_eq!( - vec!["r", "wss://atlas.nostr.land", "read"], - Tag::RelayMetadata( - UncheckedUrl::new("wss://atlas.nostr.land"), + vec!["r", "wss://atlas.nostr.land/", "read"], + Tag::relay_metadata( + Url::from_str("wss://atlas.nostr.land").unwrap(), Some(RelayMetadata::Read) ) - .as_vec() + .to_vec() ); assert_eq!( - vec!["r", "wss://atlas.nostr.land", "write"], - Tag::RelayMetadata( - UncheckedUrl::new("wss://atlas.nostr.land"), + vec!["r", "wss://atlas.nostr.land/", "write"], + Tag::relay_metadata( + Url::from_str("wss://atlas.nostr.land").unwrap(), Some(RelayMetadata::Write) ) - .as_vec() + .to_vec() ); assert_eq!( @@ -2073,7 +2254,7 @@ mod tests { TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::R)), ["wss://atlas.nostr.land", ""] ) - .as_vec() + .to_vec() ); assert_eq!( @@ -2089,20 +2270,17 @@ mod tests { "" ] ) - .as_vec() + .to_vec() ); } #[test] fn test_tag_parser() { - match Tag::parse::(&[]) { - Err(Error::KindNotFound) => (), - _ => panic!(), - } + assert_eq!(Tag::parse::(&[]).unwrap_err(), Error::EmptyTag); assert_eq!( Tag::parse(&["content-warning"]).unwrap(), - Tag::ContentWarning { reason: None } + Tag::from_standardized_without_cell(TagStandard::ContentWarning { reason: None }) ); assert_eq!( @@ -2135,51 +2313,49 @@ mod tests { assert_eq!( Tag::parse(&["expiration", "1600000000"]).unwrap(), - Tag::Expiration(Timestamp::from(1600000000)) + Tag::expiration(Timestamp::from(1600000000)) ); assert_eq!( Tag::parse(&["content-warning", "reason"]).unwrap(), - Tag::ContentWarning { + Tag::from_standardized_without_cell(TagStandard::ContentWarning { reason: Some(String::from("reason")) - } + }) ); assert_eq!( Tag::parse(&["subject", "textnote with subject"]).unwrap(), - Tag::Subject(String::from("textnote with subject")) + Tag::from_standardized_without_cell(TagStandard::Subject(String::from( + "textnote with subject" + ))) ); assert_eq!( Tag::parse(&["client", "nostr-sdk"]).unwrap(), - Tag::Generic( - TagKind::Custom("client".to_string()), - vec!["nostr-sdk".to_string()] - ) + Tag::custom(TagKind::Custom(Cow::Borrowed("client")), ["nostr-sdk"]) ); - assert_eq!( - Tag::parse(&["d", "test"]).unwrap(), - Tag::Identifier("test".to_string()) - ); + assert_eq!(Tag::parse(&["d", "test"]).unwrap(), Tag::identifier("test")); assert_eq!( - Tag::parse(&["r", "https://example.com",]).unwrap(), - Tag::Reference(String::from("https://example.com"),) + Tag::parse(&["r", "https://example.com"]).unwrap(), + Tag::from_standardized_without_cell(TagStandard::Reference(String::from( + "https://example.com" + ))) ); assert_eq!( - Tag::parse(&["r", "wss://alicerelay.example.com",]).unwrap(), - Tag::RelayMetadata(UncheckedUrl::from("wss://alicerelay.example.com"), None) + Tag::parse(&["r", "wss://alicerelay.example.com/"]).unwrap(), + Tag::relay_metadata(Url::from_str("wss://alicerelay.example.com").unwrap(), None) ); assert_eq!( Tag::parse(&["i", "github:12345678", "abcdefghijklmnop"]).unwrap(), - Tag::ExternalIdentity(Identity { + Tag::from_standardized_without_cell(TagStandard::ExternalIdentity(Identity { platform: ExternalIdentity::GitHub, ident: "12345678".to_string(), proof: "abcdefghijklmnop".to_string() - }) + })) ); assert_eq!( @@ -2189,7 +2365,7 @@ mod tests { "wss://relay.damus.io" ]) .unwrap(), - Tag::PublicKey { + Tag::from_standardized_without_cell(TagStandard::PublicKey { public_key: PublicKey::from_str( "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d" ) @@ -2197,7 +2373,7 @@ mod tests { relay_url: Some(UncheckedUrl::from("wss://relay.damus.io")), alias: None, uppercase: false - } + }) ); assert_eq!( @@ -2207,14 +2383,14 @@ mod tests { "" ]) .unwrap(), - Tag::Event { + Tag::from_standardized_without_cell(TagStandard::Event { event_id: EventId::from_hex( "378f145897eea948952674269945e88612420db35791784abf0616b4fed56ef7" ) .unwrap(), relay_url: Some(UncheckedUrl::empty()), marker: None - } + }) ); assert_eq!( @@ -2224,14 +2400,14 @@ mod tests { "wss://relay.damus.io" ]) .unwrap(), - Tag::Event { + Tag::from_standardized_without_cell(TagStandard::Event { event_id: EventId::from_hex( "378f145897eea948952674269945e88612420db35791784abf0616b4fed56ef7" ) .unwrap(), relay_url: Some(UncheckedUrl::from("wss://relay.damus.io")), marker: None - } + }) ); assert_eq!( @@ -2241,13 +2417,13 @@ mod tests { "impersonation" ]) .unwrap(), - Tag::PubKeyReport( + Tag::from_standardized_without_cell(TagStandard::PublicKeyReport( PublicKey::from_str( "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d" ) .unwrap(), Report::Impersonation - ) + )) ); assert_eq!( @@ -2257,13 +2433,13 @@ mod tests { "other" ]) .unwrap(), - Tag::PubKeyReport( + Tag::from_standardized_without_cell(TagStandard::PublicKeyReport( PublicKey::from_str( "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d" ) .unwrap(), Report::Other - ) + )) ); assert_eq!( @@ -2273,22 +2449,16 @@ mod tests { "profanity" ]) .unwrap(), - Tag::EventReport( + Tag::from_standardized_without_cell(TagStandard::EventReport( EventId::from_hex( "378f145897eea948952674269945e88612420db35791784abf0616b4fed56ef7" ) .unwrap(), Report::Profanity - ) + )) ); - assert_eq!( - Tag::parse(&["nonce", "1", "20"]).unwrap(), - Tag::POW { - nonce: 1, - difficulty: 20 - } - ); + assert_eq!(Tag::parse(&["nonce", "1", "20"]).unwrap(), Tag::pow(1, 20)); assert_eq!( Tag::parse(&[ @@ -2297,7 +2467,7 @@ mod tests { "wss://relay.nostr.org" ]) .unwrap(), - Tag::A { + Tag::from_standardized_without_cell(TagStandard::Coordinate { coordinate: Coordinate::new( Kind::LongFormTextNote, PublicKey::from_str( @@ -2307,34 +2477,34 @@ mod tests { ) .identifier("ipsum"), relay_url: Some(UncheckedUrl::from_str("wss://relay.nostr.org").unwrap()) - } + }) ); assert_eq!( - Tag::parse(&["r", "wss://alicerelay.example.com", "read"]).unwrap(), - Tag::RelayMetadata( - UncheckedUrl::from("wss://alicerelay.example.com"), + Tag::parse(&["r", "wss://alicerelay.example.com/", "read"]).unwrap(), + Tag::relay_metadata( + Url::from_str("wss://alicerelay.example.com").unwrap(), Some(RelayMetadata::Read) ) ); assert_eq!( - Tag::parse(&["r", "wss://atlas.nostr.land"]).unwrap(), - Tag::RelayMetadata(UncheckedUrl::new("wss://atlas.nostr.land"), None) + Tag::parse(&["r", "wss://atlas.nostr.land/"]).unwrap(), + Tag::relay_metadata(Url::from_str("wss://atlas.nostr.land").unwrap(), None) ); assert_eq!( - Tag::parse(&["r", "wss://atlas.nostr.land", "read"]).unwrap(), - Tag::RelayMetadata( - UncheckedUrl::new("wss://atlas.nostr.land"), + Tag::parse(&["r", "wss://atlas.nostr.land/", "read"]).unwrap(), + Tag::relay_metadata( + Url::from_str("wss://atlas.nostr.land").unwrap(), Some(RelayMetadata::Read) ) ); assert_eq!( - Tag::parse(&["r", "wss://atlas.nostr.land", "write"]).unwrap(), - Tag::RelayMetadata( - UncheckedUrl::new("wss://atlas.nostr.land"), + Tag::parse(&["r", "wss://atlas.nostr.land/", "write"]).unwrap(), + Tag::relay_metadata( + Url::from_str("wss://atlas.nostr.land").unwrap(), Some(RelayMetadata::Write) ) ); @@ -2371,7 +2541,7 @@ mod tests { "alias", ]) .unwrap(), - Tag::PublicKey { + Tag::from_standardized_without_cell(TagStandard::PublicKey { public_key: PublicKey::from_str( "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d" ) @@ -2379,7 +2549,7 @@ mod tests { relay_url: Some(UncheckedUrl::from("wss://relay.damus.io")), alias: Some(String::from("alias")), uppercase: false, - } + }) ); assert_eq!( @@ -2390,14 +2560,14 @@ mod tests { "reply" ]) .unwrap(), - Tag::Event { + Tag::from_standardized_without_cell(TagStandard::Event { event_id: EventId::from_hex( "378f145897eea948952674269945e88612420db35791784abf0616b4fed56ef7" ) .unwrap(), relay_url: None, marker: Some(Marker::Reply) - } + }) ); assert_eq!( @@ -2407,9 +2577,9 @@ mod tests { "kind=1", "fd0954de564cae9923c2d8ee9ab2bf35bc19757f8e328a978958a2fcc950eaba0754148a203adec29b7b64080d0cf5a32bebedd768ea6eb421a6b751bb4584a8", ]).unwrap(), - Tag::Delegation { delegator: PublicKey::from_str( + Tag::from_standardized_without_cell(TagStandard::Delegation { delegator: PublicKey::from_str( "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d" - ).unwrap(), conditions: Conditions::from_str("kind=1").unwrap(), sig: Signature::from_str("fd0954de564cae9923c2d8ee9ab2bf35bc19757f8e328a978958a2fcc950eaba0754148a203adec29b7b64080d0cf5a32bebedd768ea6eb421a6b751bb4584a8").unwrap() } + ).unwrap(), conditions: Conditions::from_str("kind=1").unwrap(), sig: Signature::from_str("fd0954de564cae9923c2d8ee9ab2bf35bc19757f8e328a978958a2fcc950eaba0754148a203adec29b7b64080d0cf5a32bebedd768ea6eb421a6b751bb4584a8").unwrap() }) ); assert_eq!( @@ -2421,19 +2591,19 @@ mod tests { "wss//nostr.fmt.wiz.biz" ]) .unwrap(), - Tag::Relays(vec![ + Tag::from_standardized_without_cell(TagStandard::Relays(vec![ UncheckedUrl::from("wss://relay.damus.io/"), UncheckedUrl::from("wss://nostr-relay.wlvs.space/"), UncheckedUrl::from("wss://nostr.fmt.wiz.biz"), UncheckedUrl::from("wss//nostr.fmt.wiz.biz") - ]) + ])) ); assert_eq!( Tag::parse(&[ "bolt11", "lnbc10u1p3unwfusp5t9r3yymhpfqculx78u027lxspgxcr2n2987mx2j55nnfs95nxnzqpp5jmrh92pfld78spqs78v9euf2385t83uvpwk9ldrlvf6ch7tpascqhp5zvkrmemgth3tufcvflmzjzfvjt023nazlhljz2n9hattj4f8jq8qxqyjw5qcqpjrzjqtc4fc44feggv7065fqe5m4ytjarg3repr5j9el35xhmtfexc42yczarjuqqfzqqqqqqqqlgqqqqqqgq9q9qxpqysgq079nkq507a5tw7xgttmj4u990j7wfggtrasah5gd4ywfr2pjcn29383tphp4t48gquelz9z78p4cq7ml3nrrphw5w6eckhjwmhezhnqpy6gyf0"]).unwrap(), - Tag::Bolt11("lnbc10u1p3unwfusp5t9r3yymhpfqculx78u027lxspgxcr2n2987mx2j55nnfs95nxnzqpp5jmrh92pfld78spqs78v9euf2385t83uvpwk9ldrlvf6ch7tpascqhp5zvkrmemgth3tufcvflmzjzfvjt023nazlhljz2n9hattj4f8jq8qxqyjw5qcqpjrzjqtc4fc44feggv7065fqe5m4ytjarg3repr5j9el35xhmtfexc42yczarjuqqfzqqqqqqqqlgqqqqqqgq9q9qxpqysgq079nkq507a5tw7xgttmj4u990j7wfggtrasah5gd4ywfr2pjcn29383tphp4t48gquelz9z78p4cq7ml3nrrphw5w6eckhjwmhezhnqpy6gyf0".to_string()) + Tag::from_standardized_without_cell(TagStandard::Bolt11("lnbc10u1p3unwfusp5t9r3yymhpfqculx78u027lxspgxcr2n2987mx2j55nnfs95nxnzqpp5jmrh92pfld78spqs78v9euf2385t83uvpwk9ldrlvf6ch7tpascqhp5zvkrmemgth3tufcvflmzjzfvjt023nazlhljz2n9hattj4f8jq8qxqyjw5qcqpjrzjqtc4fc44feggv7065fqe5m4ytjarg3repr5j9el35xhmtfexc42yczarjuqqfzqqqqqqqqlgqqqqqqgq9q9qxpqysgq079nkq507a5tw7xgttmj4u990j7wfggtrasah5gd4ywfr2pjcn29383tphp4t48gquelz9z78p4cq7ml3nrrphw5w6eckhjwmhezhnqpy6gyf0".to_string())) ); assert_eq!( @@ -2442,9 +2612,9 @@ mod tests { "5d006d2cf1e73c7148e7519a4c68adc81642ce0e25a432b2434c99f97344c15f" ]) .unwrap(), - Tag::Preimage( + Tag::from_standardized_without_cell(TagStandard::Preimage( "5d006d2cf1e73c7148e7519a4c68adc81642ce0e25a432b2434c99f97344c15f".to_string() - ) + )) ); assert_eq!( @@ -2452,30 +2622,33 @@ mod tests { "description", "{\"pubkey\":\"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\",\"content\":\"\",\"id\":\"d9cc14d50fcb8c27539aacf776882942c1a11ea4472f8cdec1dea82fab66279d\",\"created_at\":1674164539,\"sig\":\"77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d\",\"kind\":9734,\"tags\":[[\"e\",\"3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8\"],[\"p\",\"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\"],[\"relays\",\"wss://relay.damus.io\",\"wss://nostr-relay.wlvs.space\",\"wss://nostr.fmt.wiz.biz\",\"wss://relay.nostr.bg\",\"wss://nostr.oxtr.dev\",\"wss://nostr.v0l.io\",\"wss://brb.io\",\"wss://nostr.bitcoiner.social\",\"ws://monad.jb55.com:8080\",\"wss://relay.snort.social\"]]}" ]).unwrap(), - Tag::Description("{\"pubkey\":\"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\",\"content\":\"\",\"id\":\"d9cc14d50fcb8c27539aacf776882942c1a11ea4472f8cdec1dea82fab66279d\",\"created_at\":1674164539,\"sig\":\"77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d\",\"kind\":9734,\"tags\":[[\"e\",\"3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8\"],[\"p\",\"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\"],[\"relays\",\"wss://relay.damus.io\",\"wss://nostr-relay.wlvs.space\",\"wss://nostr.fmt.wiz.biz\",\"wss://relay.nostr.bg\",\"wss://nostr.oxtr.dev\",\"wss://nostr.v0l.io\",\"wss://brb.io\",\"wss://nostr.bitcoiner.social\",\"ws://monad.jb55.com:8080\",\"wss://relay.snort.social\"]]}".to_string()) + Tag::from_standardized_without_cell(TagStandard::Description("{\"pubkey\":\"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\",\"content\":\"\",\"id\":\"d9cc14d50fcb8c27539aacf776882942c1a11ea4472f8cdec1dea82fab66279d\",\"created_at\":1674164539,\"sig\":\"77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d\",\"kind\":9734,\"tags\":[[\"e\",\"3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8\"],[\"p\",\"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\"],[\"relays\",\"wss://relay.damus.io\",\"wss://nostr-relay.wlvs.space\",\"wss://nostr.fmt.wiz.biz\",\"wss://relay.nostr.bg\",\"wss://nostr.oxtr.dev\",\"wss://nostr.v0l.io\",\"wss://brb.io\",\"wss://nostr.bitcoiner.social\",\"ws://monad.jb55.com:8080\",\"wss://relay.snort.social\"]]}".to_string())) ); assert_eq!( Tag::parse(&["amount", "10000"]).unwrap(), - Tag::Amount { + Tag::from_standardized_without_cell(TagStandard::Amount { millisats: 10_000, bolt11: None - } + }) ); assert_eq!( Tag::parse(&["L", "#t"]).unwrap(), - Tag::LabelNamespace("#t".to_string()) + Tag::from_standardized_without_cell(TagStandard::LabelNamespace("#t".to_string())) ); assert_eq!( Tag::parse(&["l", "IT-MI"]).unwrap(), - Tag::Label(vec!["IT-MI".to_string()]) + Tag::from_standardized_without_cell(TagStandard::Label(vec!["IT-MI".to_string()])) ); assert_eq!( Tag::parse(&["l", "IT-MI", "ISO-3166-2"]).unwrap(), - Tag::Label(vec!["IT-MI".to_string(), "ISO-3166-2".to_string()]) + Tag::from_standardized_without_cell(TagStandard::Label(vec![ + "IT-MI".to_string(), + "ISO-3166-2".to_string() + ])) ); } } @@ -2486,6 +2659,14 @@ mod benches { use super::*; + #[bench] + pub fn get_tag_kind(bh: &mut Bencher) { + let tag = Tag::identifier("id"); + bh.iter(|| { + black_box(tag.kind()); + }); + } + #[bench] pub fn parse_p_tag(bh: &mut Bencher) { let tag = &[ @@ -2497,6 +2678,17 @@ mod benches { }); } + #[bench] + pub fn parse_p_standardized_tag(bh: &mut Bencher) { + let tag = &[ + "p", + "13adc511de7e1cfcf1c6b7f6365fb5a03442d7bcacf565ea57fa7770912c023d", + ]; + bh.iter(|| { + black_box(TagStandard::parse(tag)).unwrap(); + }); + } + #[bench] pub fn parse_e_tag(bh: &mut Bencher) { let tag = &[ @@ -2509,6 +2701,18 @@ mod benches { }); } + #[bench] + pub fn parse_e_standardized_tag(bh: &mut Bencher) { + let tag = &[ + "e", + "378f145897eea948952674269945e88612420db35791784abf0616b4fed56ef7", + "wss://relay.damus.io", + ]; + bh.iter(|| { + black_box(TagStandard::parse(tag)).unwrap(); + }); + } + #[bench] pub fn parse_a_tag(bh: &mut Bencher) { let tag = &[ @@ -2520,4 +2724,16 @@ mod benches { black_box(Tag::parse(tag)).unwrap(); }); } + + #[bench] + pub fn parse_a_standardized_tag(bh: &mut Bencher) { + let tag = &[ + "a", + "30023:a695f6b60119d9521934a691347d9f78e8770b56da16bb255ee286ddf9fda919:ipsum", + "wss://relay.nostr.org", + ]; + bh.iter(|| { + black_box(TagStandard::parse(tag)).unwrap(); + }); + } } diff --git a/crates/nostr/src/key/mod.rs b/crates/nostr/src/key/mod.rs index 5e21747fd..647ae17f7 100644 --- a/crates/nostr/src/key/mod.rs +++ b/crates/nostr/src/key/mod.rs @@ -26,7 +26,7 @@ pub use self::secret_key::SecretKey; use crate::SECP256K1; /// [`Keys`] error -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum Error { /// Invalid secret key InvalidSecretKey, diff --git a/crates/nostr/src/key/public_key.rs b/crates/nostr/src/key/public_key.rs index 2d68a2ad0..1d9a57357 100644 --- a/crates/nostr/src/key/public_key.rs +++ b/crates/nostr/src/key/public_key.rs @@ -110,6 +110,13 @@ impl FromStr for PublicKey { } } +// Required to keep clean the methods of `Filter` struct +impl From for String { + fn from(public_key: PublicKey) -> Self { + public_key.to_hex() + } +} + impl Serialize for PublicKey { fn serialize(&self, serializer: S) -> Result where diff --git a/crates/nostr/src/lib.rs b/crates/nostr/src/lib.rs index 88e64e9a9..4353ee3ab 100644 --- a/crates/nostr/src/lib.rs +++ b/crates/nostr/src/lib.rs @@ -7,6 +7,7 @@ #![forbid(unsafe_code)] #![warn(missing_docs)] #![warn(rustdoc::bare_urls)] +#![allow(clippy::arc_with_non_send_sync)] #![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(bench, feature(test))] #![cfg_attr(docsrs, feature(doc_auto_cfg))] @@ -50,7 +51,7 @@ pub mod util; #[doc(hidden)] pub use self::event::tag::{ ExternalIdentity, HttpMethod, Identity, ImageDimensions, Marker, RelayMetadata, Report, Tag, - TagKind, + TagKind, TagStandard, }; #[doc(hidden)] pub use self::event::{ @@ -64,8 +65,7 @@ pub use self::message::{ClientMessage, RawRelayMessage, RelayMessage, Subscripti pub use self::nips::nip19::{FromBech32, ToBech32}; #[doc(hidden)] pub use self::types::{ - Alphabet, Contact, Filter, GenericTagValue, Metadata, SingleLetterTag, Timestamp, TryIntoUrl, - UncheckedUrl, Url, + Alphabet, Contact, Filter, Metadata, SingleLetterTag, Timestamp, TryIntoUrl, UncheckedUrl, Url, }; #[doc(hidden)] pub use self::util::JsonUtil; diff --git a/crates/nostr/src/nips/nip01.rs b/crates/nostr/src/nips/nip01.rs index 55b743ccc..a69305477 100644 --- a/crates/nostr/src/nips/nip01.rs +++ b/crates/nostr/src/nips/nip01.rs @@ -16,10 +16,10 @@ use core::str::FromStr; use super::nip19::FromBech32; use super::nip21::NostrURI; use crate::event::id; -use crate::{key, Filter, Kind, PublicKey, Tag, UncheckedUrl}; +use crate::{key, Filter, Kind, PublicKey, Tag, TagStandard, UncheckedUrl}; /// Raw Event error -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum Error { /// Keys error Keys(key::Error), @@ -150,11 +150,11 @@ impl Coordinate { } impl From for Tag { - fn from(value: Coordinate) -> Self { - Self::A { - relay_url: value.relays.first().cloned().map(UncheckedUrl::from), - coordinate: value, - } + fn from(coordinate: Coordinate) -> Self { + Self::from_standardized(TagStandard::Coordinate { + relay_url: coordinate.relays.first().cloned().map(UncheckedUrl::from), + coordinate, + }) } } diff --git a/crates/nostr/src/nips/nip07.rs b/crates/nostr/src/nips/nip07.rs index bed4856b9..623c92755 100644 --- a/crates/nostr/src/nips/nip07.rs +++ b/crates/nostr/src/nips/nip07.rs @@ -157,8 +157,8 @@ impl Nip07Signer { .iter() .map(|t| { t.as_vec() - .into_iter() - .map(|v| JsValue::from_str(&v)) + .iter() + .map(|v| JsValue::from_str(v)) .collect::() }) .collect(); diff --git a/crates/nostr/src/nips/nip15.rs b/crates/nostr/src/nips/nip15.rs index acc86f687..e910247fd 100644 --- a/crates/nostr/src/nips/nip15.rs +++ b/crates/nostr/src/nips/nip15.rs @@ -55,7 +55,7 @@ impl StallData { impl From for Vec { fn from(value: StallData) -> Self { - vec![Tag::Identifier(value.id)] + vec![Tag::identifier(value.id)] } } @@ -165,10 +165,11 @@ impl ProductData { impl From for Vec { fn from(value: ProductData) -> Self { - let mut tags = Vec::new(); - tags.push(Tag::Identifier(value.stall_id)); - value.categories.unwrap_or_default().iter().for_each(|cat| { - tags.push(Tag::Hashtag(cat.into())); + let categories: Vec = value.categories.unwrap_or_default(); + let mut tags: Vec = Vec::with_capacity(1 + categories.len()); + tags.push(Tag::identifier(value.stall_id)); + categories.iter().for_each(|cat| { + tags.push(Tag::hashtag(cat)); }); tags } @@ -363,11 +364,7 @@ mod tests { .shipping(vec![ShippingMethod::new("123", 5.0).name("default")]); let tags: Vec = stall.clone().into(); assert_eq!(tags.len(), 1); - assert_eq!( - tags[0], - Tag::Identifier("123".into()), - "tags contains stall id" - ); + assert_eq!(tags[0], Tag::identifier("123"), "tags contains stall id"); let string: String = stall.as_json(); assert_eq!( @@ -391,21 +388,9 @@ mod tests { let tags: Vec = product.clone().into(); assert_eq!(tags.len(), 3); - assert_eq!( - tags[0], - Tag::Identifier("456".into()), - "tags contains stall id" - ); - assert_eq!( - tags[1], - Tag::Hashtag("Test".into()), - "tags contains category" - ); - assert_eq!( - tags[2], - Tag::Hashtag("Product".into()), - "tags contains category" - ); + assert_eq!(tags[0], Tag::identifier("456"), "tags contains stall id"); + assert_eq!(tags[1], Tag::hashtag("Test"), "tags contains category"); + assert_eq!(tags[2], Tag::hashtag("Product"), "tags contains category"); let string: String = product.as_json(); assert_eq!( diff --git a/crates/nostr/src/nips/nip48.rs b/crates/nostr/src/nips/nip48.rs index 56c698133..2aa5bf053 100644 --- a/crates/nostr/src/nips/nip48.rs +++ b/crates/nostr/src/nips/nip48.rs @@ -6,7 +6,7 @@ //! //! -use alloc::string::String; +use alloc::string::{String, ToString}; use core::fmt; /// NIP48 Proxy Protocol @@ -38,16 +38,15 @@ impl fmt::Display for Protocol { impl From for Protocol where - S: Into, + S: AsRef, { fn from(s: S) -> Self { - let s: String = s.into(); - match s.as_str() { + match s.as_ref() { "activitypub" => Self::ActivityPub, "atproto" => Self::ATProto, "rss" => Self::Rss, "web" => Self::Web, - _ => Self::Custom(s), + s => Self::Custom(s.to_string()), } } } diff --git a/crates/nostr/src/nips/nip51.rs b/crates/nostr/src/nips/nip51.rs index 1fb8a7cec..aa67dab08 100644 --- a/crates/nostr/src/nips/nip51.rs +++ b/crates/nostr/src/nips/nip51.rs @@ -9,7 +9,7 @@ use alloc::string::String; use alloc::vec::Vec; use super::nip01::Coordinate; -use crate::{EventId, PublicKey, Tag, UncheckedUrl, Url}; +use crate::{EventId, PublicKey, Tag, TagStandard, UncheckedUrl, Url}; /// Things the user doesn't want to see in their feeds pub struct MuteList { @@ -36,9 +36,14 @@ impl From for Vec { Vec::with_capacity(public_keys.len() + hashtags.len() + event_ids.len() + words.len()); tags.extend(public_keys.into_iter().map(Tag::public_key)); - tags.extend(hashtags.into_iter().map(Tag::Hashtag)); + tags.extend(hashtags.into_iter().map(Tag::hashtag)); tags.extend(event_ids.into_iter().map(Tag::event)); - tags.extend(words.into_iter().map(Tag::Word)); + tags.extend( + words + .into_iter() + .map(TagStandard::Word) + .map(Tag::from_standardized), + ); tags } @@ -70,8 +75,12 @@ impl From for Vec { tags.extend(event_ids.into_iter().map(Tag::event)); tags.extend(coordinate.into_iter().map(Tag::from)); - tags.extend(hashtags.into_iter().map(Tag::Hashtag)); - tags.extend(urls.into_iter().map(Tag::Url)); + tags.extend(hashtags.into_iter().map(Tag::hashtag)); + tags.extend( + urls.into_iter() + .map(TagStandard::Url) + .map(Tag::from_standardized), + ); tags } @@ -94,7 +103,7 @@ impl From for Vec { ) -> Self { let mut tags = Vec::with_capacity(hashtags.len() + coordinate.len()); - tags.extend(hashtags.into_iter().map(Tag::Hashtag)); + tags.extend(hashtags.into_iter().map(Tag::hashtag)); tags.extend(coordinate.into_iter().map(Tag::from)); tags @@ -113,11 +122,9 @@ impl From for Vec { fn from(Emojis { emojis, coordinate }: Emojis) -> Self { let mut tags = Vec::with_capacity(emojis.len() + coordinate.len()); - tags.extend( - emojis - .into_iter() - .map(|(s, url)| Tag::Emoji { shortcode: s, url }), - ); + tags.extend(emojis.into_iter().map(|(s, url)| { + Tag::from_standardized_without_cell(TagStandard::Emoji { shortcode: s, url }) + })); tags.extend(coordinate.into_iter().map(Tag::from)); tags diff --git a/crates/nostr/src/nips/nip53.rs b/crates/nostr/src/nips/nip53.rs index a147e9516..a9664c3bd 100644 --- a/crates/nostr/src/nips/nip53.rs +++ b/crates/nostr/src/nips/nip53.rs @@ -13,10 +13,10 @@ use core::str::FromStr; use bitcoin::secp256k1::schnorr::Signature; -use crate::{ImageDimensions, PublicKey, Tag, Timestamp, UncheckedUrl}; +use crate::{ImageDimensions, PublicKey, Tag, TagStandard, Timestamp, UncheckedUrl}; /// NIP53 Error -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub enum Error { /// Unknown [`LiveEventMarker`] UnknownLiveEventMarker(String), @@ -174,22 +174,30 @@ impl From for Vec { let mut tags = Vec::with_capacity(1); - tags.push(Tag::Identifier(id)); + tags.push(Tag::identifier(id)); if let Some(title) = title { - tags.push(Tag::Title(title)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Title( + title, + ))); } if let Some(summary) = summary { - tags.push(Tag::Summary(summary)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Summary( + summary, + ))); } if let Some(streaming) = streaming { - tags.push(Tag::Streaming(streaming)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Streaming( + streaming, + ))); } if let Some(status) = status { - tags.push(Tag::LiveEventStatus(status)); + tags.push(Tag::from_standardized_without_cell( + TagStandard::LiveEventStatus(status), + )); } if let Some(LiveEventHost { @@ -198,62 +206,82 @@ impl From for Vec { proof, }) = host { - tags.push(Tag::PubKeyLiveEvent { - public_key, - relay_url, - marker: LiveEventMarker::Host, - proof, - }); + tags.push(Tag::from_standardized_without_cell( + TagStandard::PubKeyLiveEvent { + public_key, + relay_url, + marker: LiveEventMarker::Host, + proof, + }, + )); } for (public_key, relay_url) in speakers.into_iter() { - tags.push(Tag::PubKeyLiveEvent { - public_key, - relay_url, - marker: LiveEventMarker::Speaker, - proof: None, - }); + tags.push(Tag::from_standardized_without_cell( + TagStandard::PubKeyLiveEvent { + public_key, + relay_url, + marker: LiveEventMarker::Speaker, + proof: None, + }, + )); } for (public_key, relay_url) in participants.into_iter() { - tags.push(Tag::PubKeyLiveEvent { - public_key, - relay_url, - marker: LiveEventMarker::Participant, - proof: None, - }); + tags.push(Tag::from_standardized_without_cell( + TagStandard::PubKeyLiveEvent { + public_key, + relay_url, + marker: LiveEventMarker::Participant, + proof: None, + }, + )); } if let Some((image, dim)) = image { - tags.push(Tag::Image(image, dim)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Image( + image, dim, + ))); } for hashtag in hashtags.into_iter() { - tags.push(Tag::Hashtag(hashtag)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Hashtag( + hashtag, + ))); } if let Some(recording) = recording { - tags.push(Tag::Recording(recording)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Recording( + recording, + ))); } if let Some(starts) = starts { - tags.push(Tag::Starts(starts)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Starts( + starts, + ))); } if let Some(ends) = ends { - tags.push(Tag::Ends(ends)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Ends(ends))); } if let Some(current_participants) = current_participants { - tags.push(Tag::CurrentParticipants(current_participants)); + tags.push(Tag::from_standardized_without_cell( + TagStandard::CurrentParticipants(current_participants), + )); } if let Some(total_participants) = total_participants { - tags.push(Tag::TotalParticipants(total_participants)); + tags.push(Tag::from_standardized_without_cell( + TagStandard::TotalParticipants(total_participants), + )); } if !relays.is_empty() { - tags.push(Tag::Relays(relays)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Relays( + relays, + ))); } tags diff --git a/crates/nostr/src/nips/nip57.rs b/crates/nostr/src/nips/nip57.rs index b6cf154cb..8ee70de3d 100644 --- a/crates/nostr/src/nips/nip57.rs +++ b/crates/nostr/src/nips/nip57.rs @@ -32,7 +32,7 @@ use crate::types::time::TimeSupplier; use crate::SECP256K1; use crate::{ event, util, Event, EventBuilder, EventId, JsonUtil, Keys, Kind, PublicKey, SecretKey, Tag, - Timestamp, UncheckedUrl, + TagStandard, Timestamp, UncheckedUrl, }; type Aes256CbcEnc = Encryptor; @@ -226,7 +226,9 @@ impl From for Vec { let mut tags: Vec = vec![Tag::public_key(public_key)]; if !relays.is_empty() { - tags.push(Tag::Relays(relays)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Relays( + relays, + ))); } if let Some(event_id) = event_id { @@ -238,14 +240,16 @@ impl From for Vec { } if let Some(amount) = amount { - tags.push(Tag::Amount { + tags.push(Tag::from_standardized_without_cell(TagStandard::Amount { millisats: amount, bolt11: None, - }); + })); } if let Some(lnurl) = lnurl { - tags.push(Tag::Lnurl(lnurl)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Lnurl( + lnurl, + ))); } tags @@ -258,7 +262,9 @@ pub fn anonymous_zap_request(data: ZapRequestData) -> Result { let keys = Keys::generate(); let message: String = data.message.clone(); let mut tags: Vec = data.into(); - tags.push(Tag::Anon { msg: None }); + tags.push(Tag::from_standardized_without_cell(TagStandard::Anon { + msg: None, + })); Ok(EventBuilder::new(Kind::ZapRequest, message, tags).to_event(&keys)?) } @@ -300,7 +306,9 @@ where // Compose event let mut tags: Vec = data.into(); - tags.push(Tag::Anon { msg: Some(msg) }); + tags.push(Tag::from_standardized_without_cell(TagStandard::Anon { + msg: Some(msg), + })); let private_zap_keys: Keys = Keys::new_with_ctx(secp, secret_key); Ok(EventBuilder::new(Kind::ZapRequest, "", tags) .custom_created_at(created_at) @@ -350,7 +358,7 @@ where fn extract_anon_tag_message(event: &Event) -> Result<&String, Error> { for tag in event.iter_tags() { - if let Tag::Anon { msg } = tag { + if let Some(TagStandard::Anon { msg }) = tag.as_standardized() { return msg.as_ref().ok_or(Error::InvalidPrivateZapMessage); } } diff --git a/crates/nostr/src/nips/nip58.rs b/crates/nostr/src/nips/nip58.rs index 53abf7e46..bfcbc0d8d 100644 --- a/crates/nostr/src/nips/nip58.rs +++ b/crates/nostr/src/nips/nip58.rs @@ -9,7 +9,7 @@ use alloc::vec::Vec; use core::fmt; -use crate::{Event, Kind, PublicKey, Tag, UncheckedUrl}; +use crate::{Event, Kind, PublicKey, Tag, TagStandard, UncheckedUrl}; #[derive(Debug)] /// [`BadgeAward`](crate::event::kind::Kind#variant.BadgeAward) error @@ -54,16 +54,16 @@ pub(crate) fn filter_for_kind(events: Vec, kind_needed: &Kind) -> Vec( + tags: &'a [Tag], awarded_public_key: &PublicKey, -) -> Option<(PublicKey, Option)> { - tags.iter().find_map(|t| match t { - Tag::PublicKey { +) -> Option<(&'a PublicKey, &'a Option)> { + tags.iter().find_map(|t| match t.as_standardized() { + Some(TagStandard::PublicKey { public_key, relay_url, .. - } if public_key == awarded_public_key => Some((*public_key, relay_url.clone())), + }) if public_key == awarded_public_key => Some((public_key, relay_url)), _ => None, }) } diff --git a/crates/nostr/src/nips/nip65.rs b/crates/nostr/src/nips/nip65.rs index 344ab3337..d8f052974 100644 --- a/crates/nostr/src/nips/nip65.rs +++ b/crates/nostr/src/nips/nip65.rs @@ -8,16 +8,20 @@ use alloc::vec::Vec; -use crate::{Event, RelayMetadata, Tag, UncheckedUrl}; +use crate::{Event, RelayMetadata, TagStandard, Url}; /// Extracts the relay info (url, optional read/write flag) from the event #[inline] -pub fn extract_relay_list(event: &Event) -> Vec<(UncheckedUrl, Option)> { +pub fn extract_relay_list(event: &Event) -> Vec<(&Url, &Option)> { event .iter_tags() .filter_map(|tag| { - if let Tag::RelayMetadata(url, metadata) = tag { - Some((url.clone(), metadata.clone())) + if let Some(TagStandard::RelayMetadata { + relay_url, + metadata, + }) = tag.as_standardized() + { + Some((relay_url, metadata)) } else { None } diff --git a/crates/nostr/src/nips/nip94.rs b/crates/nostr/src/nips/nip94.rs index 0112566b4..7f0898262 100644 --- a/crates/nostr/src/nips/nip94.rs +++ b/crates/nostr/src/nips/nip94.rs @@ -11,7 +11,7 @@ use alloc::vec::Vec; use bitcoin::hashes::sha256::Hash as Sha256Hash; -use crate::{ImageDimensions, Tag, Url}; +use crate::{ImageDimensions, Tag, TagKind, TagStandard, Url}; /// Potential errors returned when parsing tags into a [FileMetadata] struct #[derive(Debug, PartialEq, Eq)] @@ -141,28 +141,38 @@ impl From for Vec { let mut tags: Vec = Vec::with_capacity(3); - tags.push(Tag::Url(url)); - tags.push(Tag::MimeType(mime_type)); - tags.push(Tag::Sha256(hash)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Url(url))); + tags.push(Tag::from_standardized_without_cell(TagStandard::MimeType( + mime_type, + ))); + tags.push(Tag::from_standardized_without_cell(TagStandard::Sha256( + hash, + ))); if let Some((key, iv)) = aes_256_gcm { - tags.push(Tag::Aes256Gcm { key, iv }); + tags.push(Tag::from_standardized_without_cell( + TagStandard::Aes256Gcm { key, iv }, + )); } if let Some(size) = size { - tags.push(Tag::Size(size)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Size(size))); } if let Some(dim) = dim { - tags.push(Tag::Dim(dim)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Dim(dim))); } if let Some(magnet) = magnet { - tags.push(Tag::Magnet(magnet)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Magnet( + magnet, + ))); } if let Some(blurhash) = blurhash { - tags.push(Tag::Blurhash(blurhash)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Blurhash( + blurhash, + ))); } tags @@ -173,39 +183,93 @@ impl TryFrom> for FileMetadata { type Error = FileMetadataError; fn try_from(value: Vec) -> Result { - let url = match value.iter().find(|t| matches!(t, Tag::Url(_))) { - Some(Tag::Url(url)) => Ok(url), + let url = match value + .iter() + .find(|t| t.kind() == TagKind::Url) + .map(|t| t.as_standardized()) + { + Some(Some(TagStandard::Url(url))) => Ok(url), _ => Err(Self::Error::MissingUrl), }?; - let mime = match value.iter().find(|t| matches!(t, Tag::MimeType(_))) { - Some(Tag::MimeType(mime)) => Ok(mime), + + let mime = match value + .iter() + .find(|t| { + let t = t.as_standardized(); + matches!(t, Some(TagStandard::MimeType(..))) + }) + .map(|t| t.as_standardized()) + { + Some(Some(TagStandard::MimeType(mime))) => Ok(mime), _ => Err(Self::Error::MissingMimeType), }?; - let sha256 = match value.iter().find(|t| matches!(t, Tag::Sha256(_))) { - Some(Tag::Sha256(sha256)) => Ok(sha256), + + let sha256 = match value + .iter() + .find(|t| { + let t = t.as_standardized(); + matches!(t, Some(TagStandard::Sha256(..))) + }) + .map(|t| t.as_standardized()) + { + Some(Some(TagStandard::Sha256(sha256))) => Ok(sha256), _ => Err(Self::Error::MissingSha), }?; + let mut metadata = FileMetadata::new(url.clone(), mime, *sha256); - if let Some(Tag::Aes256Gcm { key, iv }) = - value.iter().find(|t| matches!(t, Tag::Aes256Gcm { .. })) - { + if let Some(TagStandard::Aes256Gcm { key, iv }) = value.iter().find_map(|t| { + let t = t.as_standardized(); + if matches!(t, Some(TagStandard::Aes256Gcm { .. })) { + t + } else { + None + } + }) { metadata = metadata.aes_256_gcm(key, iv); } - if let Some(Tag::Size(size)) = value.iter().find(|t| matches!(t, Tag::Size(_))) { + if let Some(TagStandard::Size(size)) = value.iter().find_map(|t| { + let t = t.as_standardized(); + if matches!(t, Some(TagStandard::Size { .. })) { + t + } else { + None + } + }) { metadata = metadata.size(*size); } - if let Some(Tag::Dim(dim)) = value.iter().find(|t| matches!(t, Tag::Dim(_))) { + if let Some(TagStandard::Dim(dim)) = value.iter().find_map(|t| { + let t = t.as_standardized(); + if matches!(t, Some(TagStandard::Dim { .. })) { + t + } else { + None + } + }) { metadata = metadata.dimensions(*dim); } - if let Some(Tag::Magnet(magnet)) = value.iter().find(|t| matches!(t, Tag::Magnet(_))) { + if let Some(TagStandard::Magnet(magnet)) = value.iter().find_map(|t| { + let t = t.as_standardized(); + if matches!(t, Some(TagStandard::Magnet { .. })) { + t + } else { + None + } + }) { metadata = metadata.magnet(magnet); } - if let Some(Tag::Blurhash(bh)) = value.iter().find(|t| matches!(t, Tag::Blurhash(_))) { + if let Some(TagStandard::Blurhash(bh)) = value.iter().find_map(|t| { + let t = t.as_standardized(); + if matches!(t, Some(TagStandard::Blurhash { .. })) { + t + } else { + None + } + }) { metadata = metadata.blurhash(bh); } @@ -232,10 +296,10 @@ mod tests { height: 640, }; let tags = vec![ - Tag::Dim(dim.clone()), - Tag::Sha256(hash), - Tag::Url(url.clone()), - Tag::MimeType(String::from("image/jpeg")), + Tag::from_standardized_without_cell(TagStandard::Dim(dim)), + Tag::from_standardized_without_cell(TagStandard::Sha256(hash)), + Tag::from_standardized_without_cell(TagStandard::Url(url.clone())), + Tag::from_standardized_without_cell(TagStandard::MimeType(String::from("image/jpeg"))), ]; let got = FileMetadata::try_from(tags).unwrap(); let expected = FileMetadata::new(url, "image/jpeg", hash).dimensions(dim); @@ -251,9 +315,9 @@ mod tests { height: 640, }; let tags = vec![ - Tag::Dim(dim.clone()), - Tag::Sha256(hash), - Tag::MimeType(String::from("image/jpeg")), + Tag::from_standardized_without_cell(TagStandard::Dim(dim.clone())), + Tag::from_standardized_without_cell(TagStandard::Sha256(hash)), + Tag::from_standardized_without_cell(TagStandard::MimeType(String::from("image/jpeg"))), ]; let got = FileMetadata::try_from(tags).unwrap_err(); @@ -269,9 +333,9 @@ mod tests { height: 640, }; let tags = vec![ - Tag::Dim(dim.clone()), - Tag::Sha256(hash), - Tag::Url(url.clone()), + Tag::from_standardized_without_cell(TagStandard::Dim(dim.clone())), + Tag::from_standardized_without_cell(TagStandard::Sha256(hash)), + Tag::from_standardized_without_cell(TagStandard::Url(url.clone())), ]; let got = FileMetadata::try_from(tags).unwrap_err(); @@ -286,9 +350,9 @@ mod tests { height: 640, }; let tags = vec![ - Tag::Dim(dim.clone()), - Tag::Url(url.clone()), - Tag::MimeType(String::from("image/jpeg")), + Tag::from_standardized_without_cell(TagStandard::Dim(dim.clone())), + Tag::from_standardized_without_cell(TagStandard::Url(url.clone())), + Tag::from_standardized_without_cell(TagStandard::MimeType(String::from("image/jpeg"))), ]; let got = FileMetadata::try_from(tags).unwrap_err(); diff --git a/crates/nostr/src/nips/nip98.rs b/crates/nostr/src/nips/nip98.rs index a4261a406..ec05dc945 100644 --- a/crates/nostr/src/nips/nip98.rs +++ b/crates/nostr/src/nips/nip98.rs @@ -14,7 +14,7 @@ use core::fmt; use bitcoin::hashes::sha256::Hash as Sha256Hash; -use crate::{HttpMethod, Tag, UncheckedUrl}; +use crate::{HttpMethod, Tag, TagStandard, UncheckedUrl}; /// [`HttpData`] required tags #[derive(Debug)] @@ -99,9 +99,14 @@ impl From for Vec { payload, } = data; - let mut tags: Vec = vec![Tag::AbsoluteURL(url), Tag::Method(method)]; + let mut tags: Vec = vec![ + Tag::from_standardized_without_cell(TagStandard::AbsoluteURL(url)), + Tag::from_standardized_without_cell(TagStandard::Method(method)), + ]; if let Some(payload) = payload { - tags.push(Tag::Payload(payload)); + tags.push(Tag::from_standardized_without_cell(TagStandard::Payload( + payload, + ))); } tags @@ -114,24 +119,24 @@ impl TryFrom> for HttpData { fn try_from(value: Vec) -> Result { let url = value .iter() - .find_map(|t| match t { - Tag::AbsoluteURL(u) => Some(u), + .find_map(|t| match t.as_standardized() { + Some(TagStandard::AbsoluteURL(u)) => Some(u), _ => None, }) .cloned() .ok_or(Error::MissingTag(RequiredTags::AbsoluteURL))?; let method = value .iter() - .find_map(|t| match t { - Tag::Method(m) => Some(m), + .find_map(|t| match t.as_standardized() { + Some(TagStandard::Method(m)) => Some(m), _ => None, }) .cloned() .ok_or(Error::MissingTag(RequiredTags::Method))?; let payload = value .iter() - .find_map(|t| match t { - Tag::Payload(p) => Some(p), + .find_map(|t| match t.as_standardized() { + Some(TagStandard::Payload(p)) => Some(p), _ => None, }) .cloned(); diff --git a/crates/nostr/src/types/filter.rs b/crates/nostr/src/types/filter.rs index 61fc68d4e..8146bd3e1 100644 --- a/crates/nostr/src/types/filter.rs +++ b/crates/nostr/src/types/filter.rs @@ -7,8 +7,9 @@ #[cfg(not(feature = "std"))] use alloc::collections::{BTreeMap as AllocMap, BTreeSet as AllocSet}; -use alloc::string::{String, ToString}; +use alloc::string::String; use core::fmt; +use core::hash::Hash; use core::str::FromStr; #[cfg(feature = "std")] use std::collections::{HashMap as AllocMap, HashSet as AllocSet}; @@ -20,7 +21,7 @@ use serde::{Deserialize, Serialize}; use crate::event::TagsIndexes; use crate::{Event, EventId, JsonUtil, Kind, PublicKey, Timestamp}; -type GenericTags = AllocMap>; +type GenericTags = AllocMap>; /// Alphabet Error #[derive(Debug)] @@ -255,71 +256,6 @@ impl<'de> Deserialize<'de> for SingleLetterTag { } } -/// Generic Tag Value -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum GenericTagValue { - /// Public key - PublicKey(PublicKey), - /// Event ID - EventId(EventId), - /// Other (string) - String(String), -} - -impl fmt::Display for GenericTagValue { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::PublicKey(inner) => write!(f, "{inner}"), - Self::EventId(inner) => write!(f, "{inner}"), - Self::String(inner) => write!(f, "{inner}"), - } - } -} - -impl Serialize for GenericTagValue { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -#[allow(missing_docs)] -pub trait IntoGenericTagValue { - fn into_generic_tag_value(self) -> GenericTagValue; -} - -impl IntoGenericTagValue for PublicKey { - fn into_generic_tag_value(self) -> GenericTagValue { - GenericTagValue::PublicKey(self) - } -} - -impl IntoGenericTagValue for EventId { - fn into_generic_tag_value(self) -> GenericTagValue { - GenericTagValue::EventId(self) - } -} - -impl IntoGenericTagValue for String { - fn into_generic_tag_value(self) -> GenericTagValue { - GenericTagValue::String(self) - } -} - -impl IntoGenericTagValue for &String { - fn into_generic_tag_value(self) -> GenericTagValue { - GenericTagValue::String(self.clone()) - } -} - -impl IntoGenericTagValue for &str { - fn into_generic_tag_value(self) -> GenericTagValue { - GenericTagValue::String(self.to_string()) - } -} - /// Subscription filters /// /// @@ -677,15 +613,12 @@ impl Filter { } /// Add custom tag - pub fn custom_tag(mut self, tag: SingleLetterTag, values: I) -> Self + pub fn custom_tag(mut self, tag: SingleLetterTag, values: I) -> Self where - I: IntoIterator, - T: IntoGenericTagValue, + I: IntoIterator, + S: Into, { - let values: AllocSet = values - .into_iter() - .map(|v| v.into_generic_tag_value()) - .collect(); + let values: AllocSet = values.into_iter().map(|v| v.into()).collect(); self.generic_tags.entry(tag).or_default().extend(values); self } @@ -694,9 +627,9 @@ impl Filter { pub fn remove_custom_tag(mut self, tag: SingleLetterTag, values: I) -> Self where I: IntoIterator, - T: IntoGenericTagValue, + T: Into, { - let values = values.into_iter().map(|v| v.into_generic_tag_value()); + let values = values.into_iter().map(|v| v.into()); self.generic_tags.entry(tag).and_modify(|set| { for item in values { set.remove(&item); @@ -816,32 +749,7 @@ where if let (Some('#'), Some(ch), None) = (chars.next(), chars.next(), chars.next()) { let tag: SingleLetterTag = SingleLetterTag::from_char(ch).map_err(serde::de::Error::custom)?; - let temp_values: AllocSet = map.next_value()?; - - #[cfg(feature = "std")] - let mut values: AllocSet = - AllocSet::with_capacity(temp_values.len()); - #[cfg(not(feature = "std"))] - let mut values: AllocSet = AllocSet::new(); - - for v in temp_values.into_iter() { - match (tag.character, tag.uppercase) { - (Alphabet::E, false) => { - let id: EventId = - EventId::from_hex(v).map_err(serde::de::Error::custom)?; - values.insert(GenericTagValue::EventId(id)); - } - (Alphabet::P, ..) => { - let pk: PublicKey = - PublicKey::from_hex(v).map_err(serde::de::Error::custom)?; - values.insert(GenericTagValue::PublicKey(pk)); - } - _ => { - values.insert(GenericTagValue::String(v)); - } - } - } - + let values: AllocSet = map.next_value()?; generic_tags.insert(tag, values); } else { map.next_value::()?; @@ -857,7 +765,7 @@ where fn extend_or_collect(mut set: Option>, iter: I) -> Option> where I: IntoIterator, - T: Eq + Ord + core::hash::Hash, + T: Eq + Ord + Hash, { match set.as_mut() { Some(s) => { @@ -873,7 +781,7 @@ where fn remove_or_none(mut set: Option>, iter: I) -> Option> where I: IntoIterator, - T: Eq + Ord + core::hash::Hash, + T: Eq + Ord + Hash, { if let Some(s) = set.as_mut() { for item in iter.into_iter() { @@ -975,7 +883,7 @@ mod tests { ) .custom_tag(SingleLetterTag::lowercase(Alphabet::Z), ["rating"]); let json = r##"{"search":"test","#d":["identifier"],"#j":["test1"],"#p":["379e863e8357163b5bce5d2688dc4f1dcc2d505222fb8d74db600f30535dfdfe"],"#z":["rating"]}"##; - assert_eq!(filter.as_json(), json.to_string()); + assert_eq!(filter.as_json(), json); } #[test] @@ -1012,7 +920,7 @@ mod tests { uppercase: false, }) .unwrap(); - assert!(set.contains(&GenericTagValue::EventId(event_id))); + assert!(set.contains(&event_id.to_hex())); // Check #p tag let set = filter @@ -1022,7 +930,7 @@ mod tests { uppercase: false, }) .unwrap(); - assert!(set.contains(&GenericTagValue::PublicKey(pubkey))); + assert!(set.contains(&pubkey.to_hex())); // Check #a tag let set = filter @@ -1032,8 +940,8 @@ mod tests { uppercase: false, }) .unwrap(); - assert!(set.contains(&GenericTagValue::String(String::from("...")))); - assert!(set.contains(&GenericTagValue::String(String::from("test")))); + assert!(set.contains("...")); + assert!(set.contains("test")); let json = r##"{"#":["..."],"search":"test"}"##; let filter = Filter::from_json(json).unwrap(); @@ -1164,7 +1072,7 @@ mod benches { use test::{black_box, Bencher}; use super::*; - use crate::Tag; + use crate::{Tag, TagStandard}; #[bench] pub fn filter_match_event(bh: &mut Bencher) { @@ -1181,7 +1089,7 @@ mod benches { Tag::public_key(PublicKey::from_hex("b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a").unwrap()), Tag::public_key(PublicKey::from_hex("379e863e8357163b5bce5d2688dc4f1dcc2d505222fb8d74db600f30535dfdfe").unwrap()), Tag::event(EventId::from_hex("7469af3be8c8e06e1b50ef1caceba30392ddc0b6614507398b7d7daa4c218e96").unwrap()), - Tag::Kind(Kind::TextNote), + Tag::from_standardized(TagStandard::Kind(Kind::TextNote)), ], "test", Signature::from_str("273a9cd5d11455590f4359500bccb7a89428262b96b3ea87a756b770964472f8c3e87f5d5e64d8d2e859a71462a3f477b554565c4f2f326cb01dd7620db71502").unwrap(), diff --git a/crates/nostr/src/types/mod.rs b/crates/nostr/src/types/mod.rs index f70933831..2eeed3593 100644 --- a/crates/nostr/src/types/mod.rs +++ b/crates/nostr/src/types/mod.rs @@ -11,7 +11,7 @@ pub mod time; pub mod url; pub use self::contact::Contact; -pub use self::filter::{Alphabet, Filter, GenericTagValue, SingleLetterTag}; +pub use self::filter::{Alphabet, Filter, SingleLetterTag}; pub use self::metadata::Metadata; pub use self::time::Timestamp; pub use self::url::{TryIntoUrl, UncheckedUrl, Url};