|
@@ -4,18 +4,57 @@
|
|
//!
|
|
//!
|
|
//! It can also use the tag to add all sort of metadata, useful for their own
|
|
//! It can also use the tag to add all sort of metadata, useful for their own
|
|
//! type
|
|
//! type
|
|
-use super::{filter::TagValue, Addr};
|
|
|
|
|
|
+use super::{filter::TagValue, Id};
|
|
|
|
+use chrono::{DateTime, Utc};
|
|
use serde::{
|
|
use serde::{
|
|
de::{self, Deserialize, Deserializer},
|
|
de::{self, Deserialize, Deserializer},
|
|
ser::{self, SerializeSeq, Serializer},
|
|
ser::{self, SerializeSeq, Serializer},
|
|
};
|
|
};
|
|
-use std::collections::VecDeque;
|
|
|
|
|
|
+use std::{
|
|
|
|
+ collections::VecDeque,
|
|
|
|
+ fmt::{self, Display},
|
|
|
|
+};
|
|
use url::Url;
|
|
use url::Url;
|
|
|
|
|
|
-mod event;
|
|
|
|
-mod public_key;
|
|
|
|
|
|
+/// 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),
|
|
|
|
+}
|
|
|
|
|
|
-pub use self::{event::*, public_key::*};
|
|
|
|
|
|
+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)]
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
/// Relay access type
|
|
/// Relay access type
|
|
@@ -39,9 +78,9 @@ pub enum RelayAccessType {
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
pub enum Tag {
|
|
pub enum Tag {
|
|
/// Tag another event
|
|
/// Tag another event
|
|
- Event(Event),
|
|
|
|
|
|
+ Event(Id, Option<Url>, Option<Marker>),
|
|
/// Tag another public key
|
|
/// Tag another public key
|
|
- PubKey(PubKey),
|
|
|
|
|
|
+ PubKey(Id, Option<Url>, Option<String>),
|
|
/// Tag a relay
|
|
/// Tag a relay
|
|
Relay(Url, RelayAccessType),
|
|
Relay(Url, RelayAccessType),
|
|
/// Tag a hashtag
|
|
/// Tag a hashtag
|
|
@@ -52,6 +91,14 @@ pub enum Tag {
|
|
Title(String),
|
|
Title(String),
|
|
/// Any non standard tag
|
|
/// Any non standard tag
|
|
Unknown(String, Vec<String>),
|
|
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<Url>),
|
|
/// Weird, supported nonetheless
|
|
/// Weird, supported nonetheless
|
|
Empty,
|
|
Empty,
|
|
}
|
|
}
|
|
@@ -59,22 +106,26 @@ pub enum Tag {
|
|
impl Tag {
|
|
impl Tag {
|
|
/// Is the tag a public key?
|
|
/// Is the tag a public key?
|
|
pub fn is_pubkey(&self) -> bool {
|
|
pub fn is_pubkey(&self) -> bool {
|
|
- matches!(self, Self::PubKey(_))
|
|
|
|
|
|
+ matches!(self, Self::PubKey(_, _, _))
|
|
}
|
|
}
|
|
|
|
|
|
/// Is the tag an event?
|
|
/// Is the tag an event?
|
|
pub fn is_event(&self) -> bool {
|
|
pub fn is_event(&self) -> bool {
|
|
- matches!(self, Self::Event(_))
|
|
|
|
|
|
+ matches!(self, Self::Event(_, _, _))
|
|
}
|
|
}
|
|
|
|
|
|
/// Get the identifier for the tag
|
|
/// Get the identifier for the tag
|
|
pub fn get_identifier(&self) -> &str {
|
|
pub fn get_identifier(&self) -> &str {
|
|
match self {
|
|
match self {
|
|
- Tag::Event(_) => "e",
|
|
|
|
- Tag::PubKey(_) => "p",
|
|
|
|
|
|
+ Tag::Event(_, _, _) => "e",
|
|
|
|
+ Tag::PubKey(_, _, _) => "p",
|
|
Tag::Relay(_, _) => "r",
|
|
Tag::Relay(_, _) => "r",
|
|
Tag::Hashtag(_) => "t",
|
|
Tag::Hashtag(_) => "t",
|
|
Tag::Title(_) => "title",
|
|
Tag::Title(_) => "title",
|
|
|
|
+ Tag::Encrypted => "encrypted",
|
|
|
|
+ Tag::Image(_, _) => "image",
|
|
|
|
+ Tag::ZapGoal(_, _) => "goal",
|
|
|
|
+ Tag::Expiration(_) => "expiration",
|
|
Tag::ExternalContentId(_, _) => "i",
|
|
Tag::ExternalContentId(_, _) => "i",
|
|
Tag::Unknown(u, _) => u,
|
|
Tag::Unknown(u, _) => u,
|
|
Tag::Empty => "",
|
|
Tag::Empty => "",
|
|
@@ -84,13 +135,24 @@ impl Tag {
|
|
/// Get the indexable value of the tag
|
|
/// Get the indexable value of the tag
|
|
pub fn get_indexable_value(&self) -> Option<TagValue> {
|
|
pub fn get_indexable_value(&self) -> Option<TagValue> {
|
|
match self {
|
|
match self {
|
|
- Tag::Event(event) => Some(TagValue::Address(event.id.clone())),
|
|
|
|
- Tag::PubKey(key) => Some(TagValue::Address(key.id.clone())),
|
|
|
|
|
|
+ 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::ExternalContentId(id, _) => Some(TagValue::String(id.clone())),
|
|
Tag::Hashtag(content) | Tag::Title(content) => Some(TagValue::String(content.clone())),
|
|
Tag::Hashtag(content) | Tag::Title(content) => Some(TagValue::String(content.clone())),
|
|
Tag::Relay(url, _) => Some(TagValue::String(url.to_string())),
|
|
Tag::Relay(url, _) => Some(TagValue::String(url.to_string())),
|
|
- Tag::Unknown(_, args) => args.get(0).cloned().map(TagValue::String),
|
|
|
|
- Tag::Empty => None,
|
|
|
|
|
|
+ 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,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@@ -114,15 +176,25 @@ impl ser::Serialize for Tag {
|
|
seq.serialize_element(self.get_identifier())?;
|
|
seq.serialize_element(self.get_identifier())?;
|
|
|
|
|
|
match self {
|
|
match self {
|
|
- Tag::Event(event) => {
|
|
|
|
- seq.serialize_element(&event.id.to_hex())?;
|
|
|
|
- if let Some(relayer) = &event.relayer_url {
|
|
|
|
|
|
+ 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)?;
|
|
seq.serialize_element(&relayer)?;
|
|
- if let Some(marker) = &event.marker {
|
|
|
|
|
|
+ if let Some(marker) = &marker {
|
|
seq.serialize_element(&marker.to_string())?;
|
|
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)?;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
Tag::ExternalContentId(content, url) => {
|
|
Tag::ExternalContentId(content, url) => {
|
|
seq.serialize_element(content)?;
|
|
seq.serialize_element(content)?;
|
|
if let Some(url) = url {
|
|
if let Some(url) = url {
|
|
@@ -132,11 +204,11 @@ impl ser::Serialize for Tag {
|
|
Tag::Hashtag(content) | Tag::Title(content) => {
|
|
Tag::Hashtag(content) | Tag::Title(content) => {
|
|
seq.serialize_element(content)?;
|
|
seq.serialize_element(content)?;
|
|
}
|
|
}
|
|
- Tag::PubKey(key) => {
|
|
|
|
- seq.serialize_element(&key.id.to_hex())?;
|
|
|
|
- if let Some(relayer) = &key.relayer_url {
|
|
|
|
|
|
+ Tag::PubKey(key, relayer_url, pet_name) => {
|
|
|
|
+ seq.serialize_element(&key.to_string())?;
|
|
|
|
+ if let Some(relayer) = &relayer_url {
|
|
seq.serialize_element(relayer)?;
|
|
seq.serialize_element(relayer)?;
|
|
- if let Some(pet_name) = &key.pet_name {
|
|
|
|
|
|
+ if let Some(pet_name) = &pet_name {
|
|
seq.serialize_element(pet_name)?;
|
|
seq.serialize_element(pet_name)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@@ -157,6 +229,12 @@ impl ser::Serialize for Tag {
|
|
seq.serialize_element(extra)?;
|
|
seq.serialize_element(extra)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+ Tag::Image(image, dimensions) => {
|
|
|
|
+ seq.serialize_element(image)?;
|
|
|
|
+ if let Some(dimensions) = dimensions {
|
|
|
|
+ seq.serialize_element(dimensions)?;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
Tag::Empty => unreachable!(),
|
|
Tag::Empty => unreachable!(),
|
|
}
|
|
}
|
|
seq.end()
|
|
seq.end()
|
|
@@ -173,7 +251,12 @@ impl<'de> Deserialize<'de> for Tag {
|
|
match parts.len() {
|
|
match parts.len() {
|
|
0 => return Ok(Tag::Empty),
|
|
0 => return Ok(Tag::Empty),
|
|
1 => {
|
|
1 => {
|
|
- return Ok(Tag::Unknown(parts.pop_front().unwrap_or_default(), vec![]));
|
|
|
|
|
|
+ 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![]));
|
|
}
|
|
}
|
|
_ => {}
|
|
_ => {}
|
|
}
|
|
}
|
|
@@ -182,31 +265,38 @@ impl<'de> Deserialize<'de> for Tag {
|
|
let default = Tag::Unknown(tag_type.clone(), parts.clone().into());
|
|
let default = Tag::Unknown(tag_type.clone(), parts.clone().into());
|
|
|
|
|
|
let tag: Result<_, D::Error> = match tag_type.as_str() {
|
|
let tag: Result<_, D::Error> = match tag_type.as_str() {
|
|
- "e" | "p" => parts
|
|
|
|
|
|
+ "e" | "goal" | "p" => parts
|
|
.pop_front()
|
|
.pop_front()
|
|
.ok_or_else::<D::Error, _>(|| de::Error::custom("missing argument"))
|
|
.ok_or_else::<D::Error, _>(|| de::Error::custom("missing argument"))
|
|
|
|
+ .and_then(|id| id.parse().map_err(de::Error::custom))
|
|
.and_then(|id| {
|
|
.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)
|
|
|
|
|
|
+ let relayer_url = parts
|
|
|
|
+ .pop_front()
|
|
|
|
+ .map(|x| x.parse())
|
|
|
|
+ .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
|
|
"title" => parts
|
|
.pop_front()
|
|
.pop_front()
|
|
.map(Tag::Title)
|
|
.map(Tag::Title)
|
|
@@ -285,15 +375,65 @@ mod test {
|
|
|
|
|
|
#[test]
|
|
#[test]
|
|
fn serialize_deserialize() {
|
|
fn serialize_deserialize() {
|
|
- 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,
|
|
|
|
- }),
|
|
|
|
- serde_json::from_value(json).expect("valid json"),
|
|
|
|
- );
|
|
|
|
|
|
+ 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::Unknown(
|
|
|
|
+ "p".to_owned(),
|
|
|
|
+ vec![
|
|
|
|
+ "8fe53b37518e3dbe9bab26d912292001d8b882de9456b7b08b615f912dc8bf4a".to_owned(),
|
|
|
|
+ "".to_owned(),
|
|
|
|
+ "mention".to_owned(),
|
|
|
|
+ ],
|
|
|
|
+ ),
|
|
|
|
+ Tag::Event(
|
|
|
|
+ "eb278e983fcedbb0d143c4250c879d078d037586c5dca8e1cf1a104f9846a460"
|
|
|
|
+ .parse()
|
|
|
|
+ .unwrap(),
|
|
|
|
+ None,
|
|
|
|
+ None,
|
|
|
|
+ ),
|
|
|
|
+ Tag::PubKey(
|
|
|
|
+ "2bda4f03446bc1c6ff00594e350a1e7ec57fb9cdc4a7b52694223d56ce0599e9"
|
|
|
|
+ .parse()
|
|
|
|
+ .unwrap(),
|
|
|
|
+ None,
|
|
|
|
+ None,
|
|
|
|
+ ),
|
|
|
|
+ ];
|
|
|
|
+
|
|
|
|
+ assert_eq!(json, expected);
|
|
}
|
|
}
|
|
|
|
|
|
#[test]
|
|
#[test]
|
|
@@ -419,6 +559,12 @@ mod test {
|
|
}
|
|
}
|
|
|
|
|
|
#[test]
|
|
#[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() {
|
|
fn external_content_id_invalid_url() {
|
|
let json = json!(["i", "123", "facebook.com"]);
|
|
let json = json!(["i", "123", "facebook.com"]);
|
|
assert_eq!(
|
|
assert_eq!(
|
|
@@ -429,4 +575,30 @@ mod test {
|
|
),
|
|
),
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ #[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()),
|
|
|
|
+ );
|
|
|
|
+ }
|
|
}
|
|
}
|