Sfoglia il codice sorgente

Improved types

* Added relayer, external content, hashtags, title tags
* Improved tests
* Any invalid tag will default to Tag::Unknown, to be able to replicate signature
* Any unparsed tag argument will result into Tag::Unknown, to be able to
  replicate signature
Cesar Rodas 2 mesi fa
parent
commit
3fcaf1a9bb

+ 1 - 0
Cargo.lock

@@ -1012,6 +1012,7 @@ dependencies = [
  "serde_json",
  "sha2",
  "thiserror",
+ "url",
 ]
 
 [[package]]

+ 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
+ 1 - 2
crates/types/src/response.rs


+ 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)?;

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

@@ -145,7 +145,7 @@ mod test {
     #[test]
     fn deserialize() {
         let json = "{\"authors\":[\"c0f0f1\", \"1234567901\"]}";
-        let obj: Result<Filter, _> = serde_json::from_str(&json);
+        let obj: Result<Filter, _> = serde_json::from_str(json);
         assert!(obj.is_ok());
     }
 

+ 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());
     }

+ 277 - 29
crates/types/src/types/tag/mod.rs

@@ -9,12 +9,25 @@ use serde::{
     de::{self, Deserialize, Deserializer},
     ser::{self, SerializeSeq, Serializer},
 };
+use std::collections::VecDeque;
+use url::Url;
 
 mod event;
 mod public_key;
 
 pub use self::{event::*, public_key::*};
 
+#[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
@@ -29,8 +42,18 @@ pub enum Tag {
     Event(Event),
     /// Tag another public key
     PubKey(PubKey),
+    /// 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>),
+    /// Weird, supported nonetheless
+    Empty,
 }
 
 impl Tag {
@@ -49,7 +72,12 @@ impl Tag {
         match self {
             Tag::Event(_) => "e",
             Tag::PubKey(_) => "p",
+            Tag::Relay(_, _) => "r",
+            Tag::Hashtag(_) => "t",
+            Tag::Title(_) => "title",
+            Tag::ExternalContentId(_, _) => "i",
             Tag::Unknown(u, _) => u,
+            Tag::Empty => "",
         }
     }
 
@@ -58,7 +86,11 @@ impl Tag {
         match self {
             Tag::Event(event) => Some(TagValue::Address(event.id.clone())),
             Tag::PubKey(key) => Some(TagValue::Address(key.id.clone())),
-            Tag::Unknown(_, _) => None,
+            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) => args.get(0).cloned().map(TagValue::String),
+            Tag::Empty => None,
         }
     }
 
@@ -91,6 +123,15 @@ impl ser::Serialize for Tag {
                     }
                 }
             }
+            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) => {
                 seq.serialize_element(&key.id.to_hex())?;
                 if let Some(relayer) = &key.relayer_url {
@@ -100,11 +141,23 @@ impl ser::Serialize for Tag {
                     }
                 }
             }
+            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::Empty => unreachable!(),
         }
         seq.end()
     }
@@ -115,42 +168,104 @@ impl<'de> Deserialize<'de> for Tag {
     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"));
+        let mut parts: VecDeque<String> = Deserialize::deserialize(deserializer)?;
+
+        match parts.len() {
+            0 => return Ok(Tag::Empty),
+            1 => {
+                return Ok(Tag::Unknown(parts.pop_front().unwrap_or_default(), vec![]));
+            }
+            _ => {}
         }
 
-        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()),
-                    })
+        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" | "p" => parts
+                .pop_front()
+                .ok_or_else::<D::Error, _>(|| de::Error::custom("missing argument"))
+                .and_then(|id| {
+                    Addr::try_from_public_key_str(&id, None)
+                        .map(|id| {
+                            let relayer_url = parts.pop_front();
+                            let extra = parts.pop_front();
+
+                            if "e" == tag_type {
+                                Tag::Event(Event {
+                                    id,
+                                    relayer_url,
+                                    marker: extra.map(|x| x.as_str().into()),
+                                })
+                            } else {
+                                Tag::PubKey(PubKey {
+                                    id,
+                                    relayer_url,
+                                    pet_name: extra,
+                                })
+                            }
+                        })
+                        .map_err(de::Error::custom)
+                }),
+            "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 {
-                    Tag::PubKey(PubKey {
-                        id,
-                        relayer_url,
-                        pet_name: extra,
+                    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"))
                 }
             }
-            extra => Tag::Unknown(extra.to_owned(), parts[1..].to_owned()),
-        })
+            _ => 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;
 
@@ -170,15 +285,148 @@ mod test {
 
     #[test]
     fn serialize_deserialize() {
-        let json = "[\"p\",\"a0b0c0d0\"]";
-        let tag: Tag = serde_json::from_str(&json).expect("valid json");
+        let json = json!(["p", "a0b0c0d0"]);
         assert_eq!(
             Tag::PubKey(PubKey {
                 id: Addr::try_from_public_key_str("a0b0c0d0", None).expect("valid addr"),
                 relayer_url: None,
                 pet_name: None,
             }),
-            tag
+            serde_json::from_value(json).expect("valid json"),
+        );
+    }
+
+    #[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 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()]
+            ),
         );
     }
 }

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