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