瀏覽代碼

Merge branch 'types-improvements' of cesar/nostr-prototype into main

Cesar Rodas 2 月之前
父節點
當前提交
b07726ac92

+ 2 - 0
Cargo.lock

@@ -944,6 +944,7 @@ version = "0.1.0"
 dependencies = [
  "futures",
  "nostr-rs-client",
+ "nostr-rs-memory",
  "nostr-rs-relayer",
  "nostr-rs-storage-base",
  "nostr-rs-types",
@@ -1012,6 +1013,7 @@ dependencies = [
  "serde_json",
  "sha2",
  "thiserror",
+ "url",
 ]
 
 [[package]]

+ 1 - 0
crates/personal-relayer/Cargo.toml

@@ -5,6 +5,7 @@ edition = "2021"
 
 [dependencies]
 nostr-rs-types = { path = "../types" }
+nostr-rs-memory = { path = "../storage/memory" }
 nostr-rs-storage-base = { path = "../storage/base" }
 nostr-rs-client = { path = "../client" }
 nostr-rs-relayer = { path = "../relayer" }

+ 3 - 7
crates/personal-relayer/src/lib.rs

@@ -2,7 +2,7 @@ use futures::future::join_all;
 use nostr_rs_client::Pool;
 use nostr_rs_relayer::Relayer;
 use nostr_rs_storage_base::Storage;
-use nostr_rs_types::types::{Addr, Filter};
+use nostr_rs_types::types::{Addr, Filter, Id};
 use tokio::{net::TcpListener, task::JoinHandle};
 use url::Url;
 
@@ -35,15 +35,11 @@ pub enum Error {
 
 pub struct PersonalRelayer<T: Storage + Send + Sync + 'static> {
     relayer: Relayer<T>,
-    accounts: Vec<Addr>,
+    accounts: Vec<Id>,
 }
 
 impl<T: Storage + Send + Sync + 'static> PersonalRelayer<T> {
-    pub async fn new(
-        storage: T,
-        accounts: Vec<Addr>,
-        client_urls: Vec<Url>,
-    ) -> Result<Self, Error> {
+    pub async fn new(storage: T, accounts: Vec<Id>, client_urls: Vec<Url>) -> Result<Self, Error> {
         let pool = Pool::new_with_clients(client_urls);
 
         join_all(

+ 53 - 1
crates/relayer/src/relayer.rs

@@ -298,7 +298,11 @@ mod test {
     use futures::future::join_all;
     use nostr_rs_client::Url;
     use nostr_rs_memory::Memory;
-    use nostr_rs_types::{account::Account, types::Content, Request};
+    use nostr_rs_types::{
+        account::Account,
+        types::{Content, Tag},
+        Request,
+    };
     use serde_json::json;
     use tokio::time::sleep;
 
@@ -317,6 +321,12 @@ mod test {
         )
     }
 
+    fn get_note_with_custom_tags(tags: Vec<Tag>) -> Event {
+        let account = Account::default();
+        let content = Content::ShortTextNote("".to_owned());
+        account.sign_content(tags, content, None).expect("valid")
+    }
+
     fn get_note() -> Request {
         serde_json::from_value(json!(
             [
@@ -356,6 +366,48 @@ mod test {
         }
         db
     }
+    #[tokio::test]
+    async fn serve_listener_from_local_db_custom_tag() {
+        let request = serde_json::from_value(json!([
+          "REQ",
+          "1298169700973717",
+          {
+            "#f": [
+              "foo",
+            ],
+          },
+        ]))
+        .expect("valid object");
+        let relayer = Relayer::new(Some(get_db(true).await), None).expect("valid relayer");
+        let (connection, mut recv) = Connection::new_local_connection();
+
+        let note =
+            get_note_with_custom_tags(vec![Tag::Unknown("f".to_owned(), vec!["foo".to_owned()])]);
+
+        let _ = relayer
+            .process_request_from_client(&connection, note.clone().into())
+            .await;
+
+        sleep(Duration::from_millis(10)).await;
+
+        let _ = relayer
+            .process_request_from_client(&connection, request)
+            .await;
+        // ev1
+        assert_eq!(
+            note,
+            recv.try_recv().expect("valid").as_event().unwrap().event
+        );
+
+        // eod
+        assert!(recv
+            .try_recv()
+            .expect("valid")
+            .as_end_of_stored_events()
+            .is_some());
+
+        assert!(recv.try_recv().is_err());
+    }
 
     #[tokio::test]
     async fn serve_listener_from_local_db() {

+ 3 - 3
crates/storage/base/src/test.rs

@@ -4,7 +4,7 @@
 //! find events by their tags, kind and references.
 use super::*;
 use futures::{StreamExt, TryStreamExt};
-use nostr_rs_types::types::{Addr, Event, Filter, Kind};
+use nostr_rs_types::types::{Addr, Event, Filter, Id, Kind};
 use serde_json::json;
 use std::{
     collections::HashMap,
@@ -90,7 +90,7 @@ where
 {
     setup_db(db).await;
 
-    let pk: Addr = "7bdef7be22dd8e59f4600e044aa53a1cf975a9dc7d27df5833bc77db784a5805"
+    let pk: Id = "7bdef7be22dd8e59f4600e044aa53a1cf975a9dc7d27df5833bc77db784a5805"
         .try_into()
         .expect("pk");
 
@@ -196,7 +196,7 @@ where
 {
     setup_db(db).await;
 
-    let id: Addr = "42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5"
+    let id: Id = "42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5"
         .try_into()
         .expect("pk");
 

+ 4 - 2
crates/storage/rocksdb/src/lib.rs

@@ -208,14 +208,16 @@ impl Storage for RocksDb {
             Index::Id(ids) => (
                 None,
                 None,
-                ids.into_iter().map(|id| id.bytes).collect::<VecDeque<_>>(),
+                ids.into_iter()
+                    .map(|id| id.to_vec())
+                    .collect::<VecDeque<_>>(),
             ),
             Index::Author(authors) => (
                 Some(ReferenceType::Author),
                 None,
                 authors
                     .into_iter()
-                    .map(|author| author.bytes)
+                    .map(|author| author.to_vec())
                     .collect::<VecDeque<_>>(),
             ),
             Index::Kind(kinds) => (

+ 1 - 0
crates/types/Cargo.toml

@@ -22,3 +22,4 @@ serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 sha2 = "0.10.6"
 thiserror = "1.0.39"
+url = { version = "2.5.2", features = ["serde"] }

File diff suppressed because it is too large
+ 21 - 3
crates/types/src/response.rs


+ 7 - 7
crates/types/src/types/addr.rs

@@ -173,14 +173,14 @@ impl Addr {
 /// The first approach is try decoding the the Addr as a hex-encoded string.
 ///
 /// The second approach is decoding with bech32
-impl TryFrom<String> for Addr {
+impl TryFrom<&str> for Addr {
     type Error = Error;
 
-    fn try_from(value: String) -> Result<Self, Self::Error> {
-        if let Ok(bytes) = hex::decode(&value) {
+    fn try_from(value: &str) -> Result<Self, Self::Error> {
+        if let Ok(bytes) = hex::decode(value) {
             return Ok(Self { bytes, hrp: None });
         }
-        let (hrp, bytes, _) = bech32::decode(&value)?;
+        let (hrp, bytes, _) = bech32::decode(value)?;
         let hrp = match hrp.to_lowercase().as_str() {
             "npub" => Ok(HumanReadablePart::NPub),
             "nsec" => Ok(HumanReadablePart::NSec),
@@ -195,11 +195,11 @@ impl TryFrom<String> for Addr {
     }
 }
 
-impl TryFrom<&str> for Addr {
+impl TryFrom<String> for Addr {
     type Error = Error;
 
-    fn try_from(value: &str) -> Result<Self, Self::Error> {
-        value.to_owned().try_into()
+    fn try_from(value: String) -> Result<Self, Self::Error> {
+        value.as_str().try_into()
     }
 }
 

+ 4 - 4
crates/types/src/types/content/mod.rs

@@ -71,7 +71,7 @@ pub enum Content {
     /// React to another event
     Reaction(String),
     /// Unknown event
-    Unknown(Kind, String),
+    Unparsed(Kind, String),
 }
 
 impl Content {
@@ -102,13 +102,13 @@ impl Content {
                         iv,
                     }))
                 } else {
-                    Ok(Self::Unknown(Kind::Unknown(4), content.to_owned()))
+                    Ok(Self::Unparsed(Kind::Unknown(4), content.to_owned()))
                 }
             }
             Kind::EventDeletion => Ok(Self::EventDeletion),
             Kind::Repost => Ok(Self::Repost),
             Kind::Reaction => Ok(Self::Reaction(content.to_owned())),
-            kind => Ok(Self::Unknown(kind, content.to_owned())),
+            kind => Ok(Self::Unparsed(kind, content.to_owned())),
         }
     }
 
@@ -129,7 +129,7 @@ impl Content {
             }
             Self::ShortTextNote(t)
             | Self::RecommendRelayer(t)
-            | Self::Unknown(_, t)
+            | Self::Unparsed(_, t)
             | Self::Reaction(t) => Ok(t.to_owned()),
             Self::EventDeletion | Self::Repost => Ok("".to_owned()),
         }

+ 0 - 2
crates/types/src/types/event.rs

@@ -157,8 +157,6 @@ impl Event {
     /// Creates a new event
     ///
     /// At this stage user must provide the signature as well.
-    ///
-    /// TODO: Create a new function where the user provides the private key instead
     pub fn new(data: UnsignedEvent, signature: Signature) -> Result<Self, Error> {
         let id = data.id()?;
         Self::verify_signature(&data.public_key, &signature, &id)?;

+ 4 - 4
crates/types/src/types/filter.rs

@@ -21,7 +21,7 @@ use std::{
 /// Tag filter value
 pub enum TagValue {
     /// Tag another event
-    Address(super::Addr),
+    Id(super::Id),
     /// Any non standard tag
     String(String),
 }
@@ -32,7 +32,7 @@ impl TagValue {
     /// Convert the value into bytes
     pub fn into_bytes(self, id: &str) -> Vec<u8> {
         let value = match self {
-            TagValue::Address(addr) => addr.bytes,
+            TagValue::Id(addr) => addr.into_bytes(),
             TagValue::String(s) => s.into_bytes(),
         };
 
@@ -144,8 +144,8 @@ mod test {
 
     #[test]
     fn deserialize() {
-        let json = "{\"authors\":[\"c0f0f1\", \"1234567901\"]}";
-        let obj: Result<Filter, _> = serde_json::from_str(&json);
+        let json = "{\"authors\":[\"a42007e33cfa25673b26f46f39df039aa6003258a68dc88f1f1e0447607aedb3\", \"9984188a6578eb513fddcf658f389dbd532e54b82b628ad36666f7aa8f731b79\"]}";
+        let obj: Result<Filter, _> = serde_json::from_str(json);
         assert!(obj.is_ok());
     }
 

+ 33 - 0
crates/types/src/types/id.rs

@@ -1,4 +1,6 @@
 //! This mod wraps the event Ids
+use super::addr::Error;
+use bech32::FromBase32;
 use secp256k1::XOnlyPublicKey;
 use serde::{
     de::{self, Deserializer},
@@ -8,6 +10,7 @@ use serde::{
 use std::{
     fmt::{self, Display},
     ops::Deref,
+    str::FromStr,
 };
 
 /// Event Id
@@ -16,6 +19,36 @@ use std::{
 #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
 pub struct Id(pub [u8; 32]);
 
+impl TryFrom<&str> for Id {
+    type Error = Error;
+
+    fn try_from(value: &str) -> Result<Self, Self::Error> {
+        let mut slice = [0u8; 32];
+        if hex::decode_to_slice(value, &mut slice).is_ok() {
+            return Ok(Id(slice));
+        }
+        let (hrp, bytes, _) = bech32::decode(&value)?;
+        match hrp.to_lowercase().as_str() {
+            "npub" | "nsec" | "note" => {}
+            _ => return Err(Error::UnexpectedHrp),
+        };
+
+        let bytes: [u8; 32] = Vec::<u8>::from_base32(&bytes)?
+            .try_into()
+            .map_err(|_| bech32::Error::InvalidLength)?;
+
+        Ok(Id(bytes))
+    }
+}
+
+impl FromStr for Id {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        s.try_into()
+    }
+}
+
 impl Id {
     /// Convert the Id to a vector of bytes
     pub fn into_bytes(self) -> Vec<u8> {

+ 5 - 1
crates/types/src/types/kind.rs

@@ -46,6 +46,8 @@ pub enum Kind {
     ZapRequest,
     /// Zap
     Zap,
+    /// Relay List Metadata - NIP-65
+    RelayListMetadata,
     /// Unknown Kind
     Unknown(u32),
 }
@@ -97,6 +99,7 @@ impl From<Kind> for u32 {
             Kind::Reaction => 7,
             Kind::ZapRequest => 9734,
             Kind::Zap => 9735,
+            Kind::RelayListMetadata => 10002,
             Kind::Unknown(t) => t,
         }
     }
@@ -115,6 +118,7 @@ impl From<u32> for Kind {
             7 => Kind::Reaction,
             9734 => Kind::ZapRequest,
             9735 => Kind::Zap,
+            10002 => Kind::RelayListMetadata,
             any => Kind::Unknown(any),
         }
     }
@@ -150,7 +154,7 @@ impl From<&Content> for Kind {
             Content::EventDeletion => Kind::EventDeletion,
             Content::Repost => Kind::Repost,
             Content::Reaction(_) => Kind::Reaction,
-            Content::Unknown(kind, _) => *kind,
+            Content::Unparsed(kind, _) => *kind,
         }
     }
 }

+ 1 - 1
crates/types/src/types/subscription_id.rs

@@ -83,7 +83,7 @@ mod test {
 
     #[test]
     fn too_long() {
-        let f = format!("too_long:{}", SubscriptionId::default().to_string());
+        let f = format!("too_long:{}", SubscriptionId::default());
         let r: Result<SubscriptionId, _> = f.as_str().try_into();
         assert!(r.is_err());
     }

+ 627 - 0
crates/types/src/types/tag.rs

@@ -0,0 +1,627 @@
+//! Tag mod
+//!
+//! An event can tag/reference another public key, or another event.
+//!
+//! It can also use the tag to add all sort of metadata, useful for their own
+//! type
+use super::{filter::TagValue, Id};
+use chrono::{DateTime, Utc};
+use serde::{
+    de::{self, Deserialize, Deserializer},
+    ser::{self, SerializeSeq, Serializer},
+};
+use std::{
+    collections::VecDeque,
+    fmt::{self, Display},
+};
+use url::Url;
+
+/// Marker as defined NIP-10
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum Marker {
+    /// For top level replies (those replying directly to the root event), only
+    /// the "root" marker should be used
+    Root,
+    /// Denotes the id of the reply event being responded to
+    Reply,
+    /// Denotes a quoted or reposted event id.
+    Mention,
+    /// Unknown marker
+    Unknown(String),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+/// Url content or empty string
+pub enum UrlOrEmpty {
+    /// Url
+    Url(Url),
+    /// Empty string
+    Empty,
+}
+
+impl Display for UrlOrEmpty {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::Url(url) => write!(f, "{}", url),
+            Self::Empty => write!(f, ""),
+        }
+    }
+}
+
+impl Display for Marker {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(
+            f,
+            "{}",
+            match self {
+                Self::Root => "root",
+                Self::Reply => "reply",
+                Self::Mention => "mention",
+                Self::Unknown(x) => x,
+            }
+        )
+    }
+}
+
+impl From<&str> for Marker {
+    fn from(marker: &str) -> Self {
+        match marker.to_ascii_lowercase().as_str() {
+            "root" => Self::Root,
+            "reply" => Self::Reply,
+            "mention" => Self::Mention,
+            _ => Self::Unknown(marker.to_owned()),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+/// Relay access type
+pub enum RelayAccessType {
+    /// Read access
+    Read,
+    /// Write access
+    Write,
+    /// Both read and write access
+    Both,
+}
+
+/// Tags are how events relates to each other.
+///
+/// So far, there are two standard tags, Events and Public Key. This an event
+/// can be related to another, with extra options (like a relayer url, and a pet
+/// name).
+///
+/// Any non standard tag will be parsed as Unknown with a vector of strings as
+/// their parameters
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum Tag {
+    /// Tag another event
+    Event(Id, Option<UrlOrEmpty>, Option<Marker>),
+    /// Tag another public key
+    PubKey(Id, Option<UrlOrEmpty>, Option<String>),
+    /// Tag a relay
+    Relay(Url, RelayAccessType),
+    /// Tag a hashtag
+    Hashtag(String),
+    /// Tag with an external content id
+    ExternalContentId(String, Option<Url>),
+    /// Tag with a title
+    Title(String),
+    /// Any non standard tag
+    Unknown(String, Vec<String>),
+    /// Encrypted - NIP-90
+    Encrypted,
+    /// Expiration - NIP-40
+    Expiration(DateTime<Utc>),
+    /// Image - NIP-23, NIP-52, NIP-58
+    Image(Url, Option<String>),
+    /// Zap Goal - NIP-75
+    ZapGoal(Id, Option<UrlOrEmpty>),
+    /// Weird, supported nonetheless
+    Empty,
+}
+
+impl Tag {
+    /// Is the tag a public key?
+    pub fn is_pubkey(&self) -> bool {
+        matches!(self, Self::PubKey(_, _, _))
+    }
+
+    /// Is the tag an event?
+    pub fn is_event(&self) -> bool {
+        matches!(self, Self::Event(_, _, _))
+    }
+
+    /// Get the identifier for the tag
+    pub fn get_identifier(&self) -> &str {
+        match self {
+            Tag::Event(_, _, _) => "e",
+            Tag::PubKey(_, _, _) => "p",
+            Tag::Relay(_, _) => "r",
+            Tag::Hashtag(_) => "t",
+            Tag::Title(_) => "title",
+            Tag::Encrypted => "encrypted",
+            Tag::Image(_, _) => "image",
+            Tag::ZapGoal(_, _) => "goal",
+            Tag::Expiration(_) => "expiration",
+            Tag::ExternalContentId(_, _) => "i",
+            Tag::Unknown(u, _) => u,
+            Tag::Empty => "",
+        }
+    }
+
+    /// Get the indexable value of the tag
+    pub fn get_indexable_value(&self) -> Option<TagValue> {
+        match self {
+            Tag::Event(event, _, _) => Some(TagValue::Id(event.clone())),
+            Tag::PubKey(key, _, _) => Some(TagValue::Id(key.clone())),
+            Tag::ExternalContentId(id, _) => Some(TagValue::String(id.clone())),
+            Tag::Hashtag(content) | Tag::Title(content) => Some(TagValue::String(content.clone())),
+            Tag::Relay(url, _) => Some(TagValue::String(url.to_string())),
+            Tag::Unknown(_, args) => {
+                let value = args.get(0).cloned().unwrap_or_default();
+                Some(
+                    value
+                        .as_str()
+                        .try_into()
+                        .map(TagValue::Id)
+                        .unwrap_or_else(|_| TagValue::String(value)),
+                )
+            }
+            Tag::ZapGoal(event, _) => Some(TagValue::Id(event.clone())),
+            Tag::Image(image_url, _) => Some(TagValue::String(image_url.to_string())),
+            Tag::Empty | Tag::Encrypted | Tag::Expiration(_) => None,
+        }
+    }
+
+    /// Converts the tag into bytes if possible.
+    ///
+    /// The bytes can be used to store the tag in a database or to find it in an in-memory index for subscribers
+    pub fn into_bytes(self) -> Option<Vec<u8>> {
+        Some(
+            self.get_indexable_value()?
+                .into_bytes(self.get_identifier()),
+        )
+    }
+}
+
+impl ser::Serialize for Tag {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut seq = serializer.serialize_seq(Some(2))?;
+        seq.serialize_element(self.get_identifier())?;
+
+        match self {
+            Tag::Encrypted => {}
+            Tag::Expiration(date) => {
+                seq.serialize_element(&date.timestamp())?;
+            }
+            Tag::Event(event, relayer_url, marker) => {
+                seq.serialize_element(&event.to_string())?;
+                if let Some(relayer) = &relayer_url {
+                    seq.serialize_element(&relayer.to_string())?;
+                    if let Some(marker) = &marker {
+                        seq.serialize_element(&marker.to_string())?;
+                    }
+                }
+            }
+            Tag::ZapGoal(event, relayer_url) => {
+                seq.serialize_element(&event.to_string())?;
+                if let Some(relayer) = &relayer_url {
+                    seq.serialize_element(&relayer.to_string())?;
+                }
+            }
+            Tag::ExternalContentId(content, url) => {
+                seq.serialize_element(content)?;
+                if let Some(url) = url {
+                    seq.serialize_element(url)?;
+                }
+            }
+            Tag::Hashtag(content) | Tag::Title(content) => {
+                seq.serialize_element(content)?;
+            }
+            Tag::PubKey(key, relayer_url, pet_name) => {
+                seq.serialize_element(&key.to_string())?;
+                if let Some(relayer) = &relayer_url {
+                    seq.serialize_element(&relayer.to_string())?;
+                    if let Some(pet_name) = &pet_name {
+                        seq.serialize_element(pet_name)?;
+                    }
+                }
+            }
+            Tag::Relay(url, access) => {
+                seq.serialize_element(url)?;
+
+                if let Some(access) = match access {
+                    RelayAccessType::Read => Some("read"),
+                    RelayAccessType::Write => Some("write"),
+                    RelayAccessType::Both => None,
+                } {
+                    seq.serialize_element(access)?;
+                }
+            }
+            Tag::Unknown(_, extra) => {
+                for extra in extra.iter() {
+                    seq.serialize_element(extra)?;
+                }
+            }
+            Tag::Image(image, dimensions) => {
+                seq.serialize_element(image)?;
+                if let Some(dimensions) = dimensions {
+                    seq.serialize_element(dimensions)?;
+                }
+            }
+            Tag::Empty => unreachable!(),
+        }
+        seq.end()
+    }
+}
+
+impl<'de> Deserialize<'de> for Tag {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let mut parts: VecDeque<String> = Deserialize::deserialize(deserializer)?;
+
+        match parts.len() {
+            0 => return Ok(Tag::Empty),
+            1 => {
+                let tag_name = parts.pop_front().unwrap_or_default();
+                match tag_name.as_str() {
+                    "encrypted" => return Ok(Tag::Encrypted),
+                    _ => {}
+                }
+                return Ok(Tag::Unknown(tag_name, vec![]));
+            }
+            _ => {}
+        }
+
+        let tag_type = parts.pop_front().unwrap_or_default();
+        let default = Tag::Unknown(tag_type.clone(), parts.clone().into());
+
+        let tag: Result<_, D::Error> = match tag_type.as_str() {
+            "e" | "goal" | "p" => parts
+                .pop_front()
+                .ok_or_else::<D::Error, _>(|| de::Error::custom("missing argument"))
+                .and_then(|id| id.parse().map_err(de::Error::custom))
+                .and_then(|id| {
+                    let relayer_url = parts
+                        .pop_front()
+                        .map(|value| {
+                            if value.is_empty() {
+                                Ok(UrlOrEmpty::Empty)
+                            } else {
+                                value.parse().map(UrlOrEmpty::Url)
+                            }
+                        })
+                        .transpose()
+                        .map_err(de::Error::custom);
+
+                    relayer_url.map(|relayer_url| {
+                        let extra = parts.pop_front();
+                        match tag_type.as_str() {
+                            "e" => Tag::Event(id, relayer_url, extra.map(|x| x.as_str().into())),
+                            "goal" => Tag::ZapGoal(id, relayer_url),
+                            "p" => Tag::PubKey(id, relayer_url, extra),
+                            _ => unreachable!(),
+                        }
+                    })
+                }),
+            "expiration" => {
+                let timestamp = parts
+                    .pop_front()
+                    .ok_or_else(|| de::Error::custom("missing argument"))?
+                    .parse::<i64>()
+                    .map_err(|_| de::Error::custom("invalid timestamp"))?;
+
+                DateTime::<Utc>::from_timestamp(timestamp, 0)
+                    .map(Tag::Expiration)
+                    .ok_or_else(|| de::Error::custom("invalid timestamp"))
+            }
+            "title" => parts
+                .pop_front()
+                .map(Tag::Title)
+                .ok_or_else(|| de::Error::custom("missing argument")),
+            "t" => parts
+                .pop_front()
+                .map(Tag::Hashtag)
+                .ok_or_else(|| de::Error::custom("missing argument")),
+            "i" => {
+                let external_id = parts
+                    .pop_front()
+                    .ok_or_else::<D::Error, _>(|| de::Error::custom("missing external id"));
+                let url = parts
+                    .pop_front()
+                    .map(|url| url.parse::<Url>().map_err::<D::Error, _>(de::Error::custom))
+                    .transpose();
+
+                if let Ok(external_id) = external_id {
+                    url.map(|url| Tag::ExternalContentId(external_id, url))
+                } else {
+                    Err(de::Error::custom("invalid external id"))
+                }
+            }
+            "r" => {
+                let url = parts
+                    .pop_front()
+                    .ok_or_else::<D::Error, _>(|| de::Error::custom("missing url"))
+                    .and_then(|url| url.parse().map_err(de::Error::custom));
+
+                let access = parts
+                    .pop_front()
+                    .map(|x| match x.as_str() {
+                        "read" => Ok(RelayAccessType::Read),
+                        "write" => Ok(RelayAccessType::Write),
+                        _ => Err(de::Error::custom("invalid relay access type")),
+                    })
+                    .unwrap_or(Ok(RelayAccessType::Both));
+
+                if let Ok(url) = url {
+                    access.map(|access| Tag::Relay(url, access))
+                } else {
+                    Err(de::Error::custom("invalid url"))
+                }
+            }
+            _ => Ok(default.clone()),
+        };
+
+        if !parts.is_empty() {
+            return Ok(default);
+        }
+
+        Ok(tag.unwrap_or(default))
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use serde_json::json;
+
+    use super::*;
+    use crate::Response;
+
+    #[test]
+    fn bug_01() {
+        let json = r#"["EVENT","d45a98f898820258a3313f5cb14f5fe8a9263437931ac6309f23ae0324833f39",{"content":"Will fight under the light of lightnings! No darkness on nostr! 🐶🐾😂","created_at":1678685477,"id":"5462822032c16d32267ba40536409fd51ea188b20e7dd5f9e7a0aa5561346f79","kind":1,"pubkey":"8fb140b4e8ddef97ce4b821d247278a1a4353362623f64021484b372f948000c","sig":"62c8e09a0fedd8096a313ef12436b0f0bcad56e9058d4bc12f61ae6094c099bb966dce47efa2d721c5bfdf923614db46d1d1c248105cb99c4ec495292cc875b1","tags":[["e","5f63f9e7d37673e76ddf7448cd67c3d74f9be96c240a40199b59a30db32d7f43"],["e","c70894331986c66a2baf6fc12dd5c86280e4616cee0e57bbee90972ebbb4b735"],["p","ac3fb436a663b25893657f4b6a3d9d2f02d1974bb5ced603f4d0c8ee32d7e0a2"]]}]"#;
+        let message: Result<Response, _> = serde_json::from_str(json);
+        assert!(message.is_ok());
+    }
+
+    #[test]
+    fn parse_unexpected_tag() {
+        let json = r#"["EVENT","f4e62282fda9c7d93a6e3b03fd1f1a3a34936f74584b0e021edf9659fd7da9d6",{"content":"1678682368","created_at":1678682368,"id":"7506ccd8ce4de835c61c04fd16f8489b2acb8cc052ed6730cba203188dfedf57","kind":30000,"pubkey":"228cc1e37a8fec2eee3dda3a3dbd04a60968086d8f42751a7632499d938eda8f","sig":"73ec91ee5d23a93287700f31274a57d84f46c0aa06523138c8f5b0cf2f20bd8a7db72346ecae9c717d9642cea81d72c35ea95ec615146b1640f62de5e3fbbb69","tags":[["d","chats/null/lastOpened"]]}]"#;
+        let message: Result<Response, _> = serde_json::from_str(json);
+        assert!(message.is_ok());
+    }
+
+    #[test]
+    fn serialize_deserialize() {
+        let json = vec![
+            json!(["p", "d45a98f8988"]),
+            json!([
+                "p",
+                "d45a98f898820258a3313f5cb14f5fe8a9263437931ac6309f23ae0324833f39"
+            ]),
+            json!([
+                "p",
+                "8fe53b37518e3dbe9bab26d912292001d8b882de9456b7b08b615f912dc8bf4a",
+                "",
+                "mention"
+            ]),
+            json!([
+                "e",
+                "eb278e983fcedbb0d143c4250c879d078d037586c5dca8e1cf1a104f9846a460",
+            ]),
+            json!([
+                "p",
+                "2bda4f03446bc1c6ff00594e350a1e7ec57fb9cdc4a7b52694223d56ce0599e9",
+            ]),
+        ]
+        .into_iter()
+        .map(|x| serde_json::from_value(x).unwrap())
+        .collect::<Vec<Tag>>();
+
+        let expected = vec![
+            Tag::Unknown("p".to_owned(), vec![String::from("d45a98f8988")]),
+            Tag::PubKey(
+                "d45a98f898820258a3313f5cb14f5fe8a9263437931ac6309f23ae0324833f39"
+                    .parse()
+                    .unwrap(),
+                None,
+                None,
+            ),
+            Tag::PubKey(
+                "8fe53b37518e3dbe9bab26d912292001d8b882de9456b7b08b615f912dc8bf4a"
+                    .parse()
+                    .unwrap(),
+                Some(UrlOrEmpty::Empty),
+                Some("mention".to_owned()),
+            ),
+            Tag::Event(
+                "eb278e983fcedbb0d143c4250c879d078d037586c5dca8e1cf1a104f9846a460"
+                    .parse()
+                    .unwrap(),
+                None,
+                None,
+            ),
+            Tag::PubKey(
+                "2bda4f03446bc1c6ff00594e350a1e7ec57fb9cdc4a7b52694223d56ce0599e9"
+                    .parse()
+                    .unwrap(),
+                None,
+                None,
+            ),
+        ];
+
+        assert_eq!(json, expected);
+    }
+
+    #[test]
+    fn test_relay() {
+        let json = json!(["r", "https://example.com", "read"]);
+        assert_eq!(
+            Tag::Relay(
+                "https://example.com".parse().expect("valid url"),
+                RelayAccessType::Read
+            ),
+            serde_json::from_value(json).expect("valid json"),
+        );
+
+        let json = json!(["r", "https://example.com", "write"]);
+        assert_eq!(
+            Tag::Relay(
+                "https://example.com".parse().expect("valid url"),
+                RelayAccessType::Write
+            ),
+            serde_json::from_value(json).expect("valid json"),
+        );
+
+        let json = json!(["r", "https://example.com"]);
+        assert_eq!(
+            Tag::Relay(
+                "https://example.com".parse().expect("valid url"),
+                RelayAccessType::Both,
+            ),
+            serde_json::from_value(json).expect("valid json"),
+        );
+    }
+
+    #[test]
+    fn test_relay_invalid_url() {
+        let json = json!(["r", "example.com", "read"]);
+        assert_eq!(
+            Tag::Unknown(
+                "r".to_string(),
+                vec!["example.com".to_string(), "read".to_string()],
+            ),
+            serde_json::from_value(json).expect("valid json"),
+        );
+    }
+
+    #[test]
+    fn malformed_tag() {
+        let json = vec![
+            json!(["r"]),
+            json!(["t"]),
+            json!(["p"]),
+            json!(["e"]),
+            json!(["i"]),
+            json!(["title"]),
+        ];
+
+        for json in json {
+            let tag: Tag = serde_json::from_value(json).expect("valid");
+            assert!(matches!(tag, Tag::Unknown(_, _)));
+        }
+    }
+
+    #[test]
+    fn test_relay_invalid_access() {
+        let json = json!(["r", "https://example.com", "invalid"]);
+        assert_eq!(
+            Tag::Unknown(
+                "r".to_string(),
+                vec!["https://example.com".to_string(), "invalid".to_string()],
+            ),
+            serde_json::from_value(json).expect("valid json"),
+        );
+    }
+
+    #[test]
+    fn hashtag() {
+        let json = json!(["t", "rust"]);
+        assert_eq!(
+            Tag::Hashtag("rust".to_string()),
+            serde_json::from_value(json).expect("valid json"),
+        );
+    }
+
+    #[test]
+    fn title() {
+        let json = json!(["title", "Rust"]);
+        assert_eq!(
+            Tag::Title("Rust".to_string()),
+            serde_json::from_value(json).expect("valid json"),
+        );
+    }
+
+    #[test]
+    fn external_content_id() {
+        let json = json!(["i", "123"]);
+        assert_eq!(
+            serde_json::from_value::<Tag>(json).expect("valid"),
+            Tag::ExternalContentId("123".to_string(), None,),
+        );
+        let json = json!(["i", "123", "https://example.com"]);
+        assert_eq!(
+            serde_json::from_value::<Tag>(json).expect("valid"),
+            Tag::ExternalContentId(
+                "123".to_string(),
+                Some("https://example.com".parse().expect("valid url"))
+            ),
+        );
+    }
+
+    #[test]
+    fn external_content_id_invalid_args() {
+        let json = json!(["i", "123", "https://example.com", "unexpected"]);
+        assert_eq!(
+            serde_json::from_value::<Tag>(json).expect("valid"),
+            Tag::Unknown(
+                "i".to_owned(),
+                vec![
+                    "123".to_owned(),
+                    "https://example.com".to_owned(),
+                    "unexpected".to_owned()
+                ]
+            ),
+        );
+    }
+
+    #[test]
+    fn encrypted_tag() {
+        let json = json!(["encrypted"]);
+        assert_eq!(Tag::Encrypted, serde_json::from_value(json).expect("valid"));
+    }
+
+    #[test]
+    fn external_content_id_invalid_url() {
+        let json = json!(["i", "123", "facebook.com"]);
+        assert_eq!(
+            serde_json::from_value::<Tag>(json).expect("valid"),
+            Tag::Unknown(
+                "i".to_owned(),
+                vec!["123".to_owned(), "facebook.com".to_owned()]
+            ),
+        );
+    }
+
+    #[test]
+    fn goal() {
+        let json = json!([
+            "goal",
+            "d45a98f898820258a3313f5cb14f5fe8a9263437931ac6309f23ae0324833f39"
+        ]);
+        assert_eq!(
+            serde_json::from_value::<Tag>(json).expect("valid"),
+            Tag::ZapGoal(
+                "d45a98f898820258a3313f5cb14f5fe8a9263437931ac6309f23ae0324833f39"
+                    .parse()
+                    .unwrap(),
+                None
+            ),
+        );
+    }
+
+    #[test]
+    fn timestamp() {
+        let json = json!(["expiration", "1678682368"]);
+        assert_eq!(
+            serde_json::from_value::<Tag>(json).expect("valid"),
+            Tag::Expiration(DateTime::<Utc>::from_timestamp(1678682368, 0).unwrap()),
+        );
+    }
+}

+ 0 - 54
crates/types/src/types/tag/event.rs

@@ -1,54 +0,0 @@
-//! Event tag
-use super::Addr;
-use std::fmt::{self, Display};
-
-/// A tag that references another event
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct Event {
-    /// The other event to relate Id
-    pub id: Addr,
-    /// The relayer where the related content can be found
-    pub relayer_url: Option<String>,
-    /// <marker> is optional and if present is one of "reply", "root", or "mention".
-    pub marker: Option<Marker>,
-}
-
-/// Marker as defined NIP-10
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum Marker {
-    /// For top level replies (those replying directly to the root event), only
-    /// the "root" marker should be used
-    Root,
-    /// Denotes the id of the reply event being responded to
-    Reply,
-    /// Denotes a quoted or reposted event id.
-    Mention,
-    /// Unknown marker
-    Unknown(String),
-}
-
-impl Display for Marker {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(
-            f,
-            "{}",
-            match self {
-                Self::Root => "root",
-                Self::Reply => "reply",
-                Self::Mention => "mention",
-                Self::Unknown(x) => x,
-            }
-        )
-    }
-}
-
-impl From<&str> for Marker {
-    fn from(marker: &str) -> Self {
-        match marker.to_ascii_lowercase().as_str() {
-            "root" => Self::Root,
-            "reply" => Self::Reply,
-            "mention" => Self::Mention,
-            _ => Self::Unknown(marker.to_owned()),
-        }
-    }
-}

+ 0 - 184
crates/types/src/types/tag/mod.rs

@@ -1,184 +0,0 @@
-//! Tag mod
-//!
-//! An event can tag/reference another public key, or another event.
-//!
-//! It can also use the tag to add all sort of metadata, useful for their own
-//! type
-use super::{filter::TagValue, Addr};
-use serde::{
-    de::{self, Deserialize, Deserializer},
-    ser::{self, SerializeSeq, Serializer},
-};
-
-mod event;
-mod public_key;
-
-pub use self::{event::*, public_key::*};
-
-/// Tags are how events relates to each other.
-///
-/// So far, there are two standard tags, Events and Public Key. This an event
-/// can be related to another, with extra options (like a relayer url, and a pet
-/// name).
-///
-/// Any non standard tag will be parsed as Unknown with a vector of strings as
-/// their parameters
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub enum Tag {
-    /// Tag another event
-    Event(Event),
-    /// Tag another public key
-    PubKey(PubKey),
-    /// Any non standard tag
-    Unknown(String, Vec<String>),
-}
-
-impl Tag {
-    /// Is the tag a public key?
-    pub fn is_pubkey(&self) -> bool {
-        matches!(self, Self::PubKey(_))
-    }
-
-    /// Is the tag an event?
-    pub fn is_event(&self) -> bool {
-        matches!(self, Self::Event(_))
-    }
-
-    /// Get the identifier for the tag
-    pub fn get_identifier(&self) -> &str {
-        match self {
-            Tag::Event(_) => "e",
-            Tag::PubKey(_) => "p",
-            Tag::Unknown(u, _) => u,
-        }
-    }
-
-    /// Get the indexable value of the tag
-    pub fn get_indexable_value(&self) -> Option<TagValue> {
-        match self {
-            Tag::Event(event) => Some(TagValue::Address(event.id.clone())),
-            Tag::PubKey(key) => Some(TagValue::Address(key.id.clone())),
-            Tag::Unknown(_, _) => None,
-        }
-    }
-
-    /// Converts the tag into bytes if possible.
-    ///
-    /// The bytes can be used to store the tag in a database or to find it in an in-memory index for subscribers
-    pub fn into_bytes(self) -> Option<Vec<u8>> {
-        Some(
-            self.get_indexable_value()?
-                .into_bytes(self.get_identifier()),
-        )
-    }
-}
-
-impl ser::Serialize for Tag {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: Serializer,
-    {
-        let mut seq = serializer.serialize_seq(Some(2))?;
-        seq.serialize_element(self.get_identifier())?;
-
-        match self {
-            Tag::Event(event) => {
-                seq.serialize_element(&event.id.to_hex())?;
-                if let Some(relayer) = &event.relayer_url {
-                    seq.serialize_element(&relayer)?;
-                    if let Some(marker) = &event.marker {
-                        seq.serialize_element(&marker.to_string())?;
-                    }
-                }
-            }
-            Tag::PubKey(key) => {
-                seq.serialize_element(&key.id.to_hex())?;
-                if let Some(relayer) = &key.relayer_url {
-                    seq.serialize_element(relayer)?;
-                    if let Some(pet_name) = &key.pet_name {
-                        seq.serialize_element(pet_name)?;
-                    }
-                }
-            }
-            Tag::Unknown(_, extra) => {
-                for extra in extra.iter() {
-                    seq.serialize_element(extra)?;
-                }
-            }
-        }
-        seq.end()
-    }
-}
-
-impl<'de> Deserialize<'de> for Tag {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        let parts: Vec<String> = Deserialize::deserialize(deserializer)?;
-        if parts.len() < 2 {
-            return Err(de::Error::custom("expected array of two strings"));
-        }
-
-        Ok(match parts[0].as_str() {
-            "e" | "p" => {
-                let id: Addr = parts[1]
-                    .as_str()
-                    .try_into()
-                    .map_err(|e: super::addr::Error| de::Error::custom(e.to_string()))?;
-
-                let relayer_url = parts.get(2).cloned();
-                let extra = parts.get(3).cloned();
-
-                if "e" == parts[0].as_str() {
-                    Tag::Event(Event {
-                        id,
-                        relayer_url,
-                        marker: extra.map(|x| x.as_str().into()),
-                    })
-                } else {
-                    Tag::PubKey(PubKey {
-                        id,
-                        relayer_url,
-                        pet_name: extra,
-                    })
-                }
-            }
-            extra => Tag::Unknown(extra.to_owned(), parts[1..].to_owned()),
-        })
-    }
-}
-
-#[cfg(test)]
-mod test {
-    use super::*;
-    use crate::Response;
-
-    #[test]
-    fn bug_01() {
-        let json = r#"["EVENT","d45a98f898820258a3313f5cb14f5fe8a9263437931ac6309f23ae0324833f39",{"content":"Will fight under the light of lightnings! No darkness on nostr! 🐶🐾😂","created_at":1678685477,"id":"5462822032c16d32267ba40536409fd51ea188b20e7dd5f9e7a0aa5561346f79","kind":1,"pubkey":"8fb140b4e8ddef97ce4b821d247278a1a4353362623f64021484b372f948000c","sig":"62c8e09a0fedd8096a313ef12436b0f0bcad56e9058d4bc12f61ae6094c099bb966dce47efa2d721c5bfdf923614db46d1d1c248105cb99c4ec495292cc875b1","tags":[["e","5f63f9e7d37673e76ddf7448cd67c3d74f9be96c240a40199b59a30db32d7f43"],["e","c70894331986c66a2baf6fc12dd5c86280e4616cee0e57bbee90972ebbb4b735"],["p","ac3fb436a663b25893657f4b6a3d9d2f02d1974bb5ced603f4d0c8ee32d7e0a2"]]}]"#;
-        let message: Result<Response, _> = serde_json::from_str(json);
-        assert!(message.is_ok());
-    }
-
-    #[test]
-    fn parse_unexpected_tag() {
-        let json = r#"["EVENT","f4e62282fda9c7d93a6e3b03fd1f1a3a34936f74584b0e021edf9659fd7da9d6",{"content":"1678682368","created_at":1678682368,"id":"7506ccd8ce4de835c61c04fd16f8489b2acb8cc052ed6730cba203188dfedf57","kind":30000,"pubkey":"228cc1e37a8fec2eee3dda3a3dbd04a60968086d8f42751a7632499d938eda8f","sig":"73ec91ee5d23a93287700f31274a57d84f46c0aa06523138c8f5b0cf2f20bd8a7db72346ecae9c717d9642cea81d72c35ea95ec615146b1640f62de5e3fbbb69","tags":[["d","chats/null/lastOpened"]]}]"#;
-        let message: Result<Response, _> = serde_json::from_str(json);
-        assert!(message.is_ok());
-    }
-
-    #[test]
-    fn serialize_deserialize() {
-        let json = "[\"p\",\"a0b0c0d0\"]";
-        let tag: Tag = serde_json::from_str(&json).expect("valid json");
-        assert_eq!(
-            Tag::PubKey(PubKey {
-                id: Addr::try_from_public_key_str("a0b0c0d0", None).expect("valid addr"),
-                relayer_url: None,
-                pet_name: None,
-            }),
-            tag
-        );
-    }
-}

+ 0 - 14
crates/types/src/types/tag/public_key.rs

@@ -1,14 +0,0 @@
-//! Public key tag
-use super::Addr;
-
-/// Public key tag
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct PubKey {
-    /// The public key or a prefix for more privacy
-    pub id: Addr,
-    /// Optionally a relayer to connect to
-    pub relayer_url: Option<String>,
-    /// A pet name, an internal name, that this public key gives to the
-    /// related/tagged public key
-    pub pet_name: Option<String>,
-}

+ 1 - 18
src/main.rs

@@ -63,24 +63,7 @@ async fn main() {
     let db = RocksDb::new(&config.db_path).expect("db");
     let mut client_pool = Pool::new_with_clients(config.relayers);
 
-    let initial_subscription = vec![
-        Filter {
-            authors: config
-                .account
-                .iter()
-                .map(|x| x.id.clone())
-                .collect::<Vec<_>>(),
-            ..Default::default()
-        },
-        Filter {
-            references_to_public_key: config
-                .account
-                .iter()
-                .map(|x| x.id.clone())
-                .collect::<Vec<_>>(),
-            ..Default::default()
-        },
-    ];
+    let initial_subscription = vec![Filter::default()];
 
     let _ = client_pool.subscribe(initial_subscription.into()).await;
 

Some files were not shown because too many files changed in this diff