use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use core::str::FromStr;
use super::util::{
missing_tag_kind, take_and_parse_optional_public_key, take_and_parse_optional_relay_url,
take_event_id, unknown_tag,
};
use crate::error::{Error, ErrorKind};
use crate::event::{
Event, EventBuilder, EventId, IntoEventBuilder, Kind, Tag, TagCodec, impl_tag_codec_conversions,
};
use crate::key::PublicKey;
use crate::types::url::RelayUrl;
const EVENT: &str = "e";
#[derive(Debug, Clone)]
pub struct TextNoteReplyBuilder<'a> {
content: String,
reply_to: &'a Event,
root: Option<&'a Event>,
relay_hint: Option<RelayUrl>,
}
impl<'a> TextNoteReplyBuilder<'a> {
pub fn new<S>(content: S, reply_to: &'a Event) -> Self
where
S: Into<String>,
{
Self {
content: content.into(),
reply_to,
root: None,
relay_hint: None,
}
}
#[inline]
pub fn root(mut self, root: &'a Event) -> Self {
self.root = Some(root);
self
}
#[inline]
pub fn relay_hint(mut self, relay_hint: RelayUrl) -> Self {
self.relay_hint = Some(relay_hint);
self
}
}
impl IntoEventBuilder for TextNoteReplyBuilder<'_> {
fn into_event_builder(self) -> EventBuilder {
let mut tags: Vec<Tag> = Vec::with_capacity(4);
match self.root {
Some(root) => {
if root.id != self.reply_to.id {
tags.push(
Nip10Tag::Event {
id: self.reply_to.id,
relay_hint: self.relay_hint.clone(),
marker: Some(Marker::Reply),
public_key: Some(self.reply_to.pubkey),
}
.to_tag(),
);
tags.push(Tag::public_key(self.reply_to.pubkey));
}
tags.push(
Nip10Tag::Event {
id: root.id,
relay_hint: self.relay_hint,
marker: Some(Marker::Root),
public_key: Some(root.pubkey),
}
.to_tag(),
);
tags.push(Tag::public_key(root.pubkey));
}
None => {
tags.push(
Nip10Tag::Event {
id: self.reply_to.id,
relay_hint: self.relay_hint,
marker: Some(Marker::Reply),
public_key: Some(self.reply_to.pubkey),
}
.to_tag(),
);
tags.push(Tag::public_key(self.reply_to.pubkey));
}
}
EventBuilder::new(Kind::TextNote, self.content).tags(tags)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Marker {
Root,
Reply,
}
impl fmt::Display for Marker {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Root => f.write_str("root"),
Self::Reply => f.write_str("reply"),
}
}
}
impl FromStr for Marker {
type Err = Error;
fn from_str(marker: &str) -> Result<Self, Self::Err> {
match marker {
"root" => Ok(Self::Root),
"reply" => Ok(Self::Reply),
_ => Err(Error::with_static_message(
ErrorKind::Invalid,
"invalid marker",
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Nip10Tag {
Event {
id: EventId,
relay_hint: Option<RelayUrl>,
marker: Option<Marker>,
public_key: Option<PublicKey>,
},
}
impl Nip10Tag {
#[inline]
pub fn is_root(&self) -> bool {
matches!(
self,
Self::Event {
marker: Some(Marker::Root),
..
}
)
}
#[inline]
pub fn is_reply(&self) -> bool {
matches!(
self,
Self::Event {
marker: Some(Marker::Reply),
..
}
)
}
}
impl TagCodec for Nip10Tag {
type Error = Error;
fn parse<I, S>(tag: I) -> Result<Self, Self::Error>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut iter = tag.into_iter();
let kind: S = iter.next().ok_or(missing_tag_kind())?;
match kind.as_ref() {
EVENT => {
let (id, relay_hint, marker, public_key) = parse_e_tag(iter)?;
Ok(Self::Event {
id,
relay_hint,
marker,
public_key,
})
}
_ => Err(unknown_tag()),
}
}
fn to_tag(&self) -> Tag {
match self {
Self::Event {
id,
relay_hint,
marker,
public_key,
} => {
let mut tag: Vec<String> = Vec::with_capacity(
2 + relay_hint.is_some() as usize
+ marker.is_some() as usize
+ public_key.is_some() as usize,
);
tag.push(String::from(EVENT));
tag.push(id.to_hex());
match (relay_hint, marker.is_some() || public_key.is_some()) {
(Some(relay_hint), ..) => tag.push(relay_hint.to_string()),
(None, true) => tag.push(String::new()),
(None, false) => {}
}
match (marker, public_key.is_some()) {
(Some(marker), _) => tag.push(marker.to_string()),
(None, true) => tag.push(String::new()),
(None, false) => {}
}
if let Some(public_key) = public_key {
tag.push(public_key.to_hex());
}
Tag::new(tag)
}
}
}
}
impl_tag_codec_conversions!(Nip10Tag);
#[allow(clippy::type_complexity)]
fn parse_e_tag<T, S>(
mut iter: T,
) -> Result<(EventId, Option<RelayUrl>, Option<Marker>, Option<PublicKey>), Error>
where
T: Iterator<Item = S>,
S: AsRef<str>,
{
let id: EventId = take_event_id(&mut iter)?;
let relay_hint: Option<RelayUrl> = take_and_parse_optional_relay_url(&mut iter)?;
let marker: Option<Marker> = match iter.next() {
Some(marker) => {
let marker: &str = marker.as_ref();
if !marker.is_empty() && marker != "mention" {
Some(Marker::from_str(marker)?)
} else {
None
}
}
_ => None,
};
let public_key: Option<PublicKey> = take_and_parse_optional_public_key(&mut iter)?;
Ok((id, relay_hint, marker, public_key))
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(all(feature = "std", feature = "os-rng"))]
use crate::event::FinalizeEvent;
#[cfg(all(feature = "std", feature = "os-rng"))]
use crate::key::Keys;
use crate::key::PublicKey;
#[test]
fn test_parse_empty_tag() {
let tag: Vec<String> = Vec::new();
let err = Nip10Tag::parse(&tag).unwrap_err();
assert_eq!(err.kind(), ErrorKind::Missing);
}
#[test]
fn test_non_existing_tag() {
let tag = vec!["p"];
let err = Nip10Tag::parse(&tag).unwrap_err();
assert_eq!(err.kind(), ErrorKind::Malformed);
}
#[test]
fn test_standardized_e_tag() {
let relay_hint = RelayUrl::parse("wss://relay.example.com").unwrap();
let public_key =
PublicKey::from_hex("aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4")
.unwrap();
let tag = vec![
String::from("e"),
EventId::all_zeros().to_hex(),
relay_hint.to_string(),
String::from("root"),
public_key.to_hex(),
];
let parsed = Nip10Tag::parse(&tag).unwrap();
assert_eq!(
parsed,
Nip10Tag::Event {
id: EventId::all_zeros(),
relay_hint: Some(relay_hint),
marker: Some(Marker::Root),
public_key: Some(public_key),
}
);
assert!(parsed.is_root());
assert!(!parsed.is_reply());
assert_eq!(parsed.to_tag(), Tag::parse(tag).unwrap());
}
#[test]
fn test_positional_e_tag() {
let tag = vec![String::from("e"), EventId::all_zeros().to_hex()];
let parsed = Nip10Tag::parse(&tag).unwrap();
assert_eq!(
parsed,
Nip10Tag::Event {
id: EventId::all_zeros(),
relay_hint: None,
marker: None,
public_key: None,
}
);
assert_eq!(parsed.to_tag(), Tag::parse(tag).unwrap());
}
#[test]
fn test_standardized_e_tag_with_empty_marker() {
let public_key =
PublicKey::from_hex("aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4")
.unwrap();
let tag = vec![
String::from("e"),
EventId::all_zeros().to_hex(),
String::from("wss://relay.example.com"),
String::new(),
public_key.to_hex(),
];
let parsed = Nip10Tag::parse(&tag).unwrap();
assert_eq!(
parsed,
Nip10Tag::Event {
id: EventId::all_zeros(),
relay_hint: Some(RelayUrl::parse("wss://relay.example.com").unwrap()),
marker: None,
public_key: Some(public_key),
}
);
assert_eq!(parsed.to_tag(), Tag::parse(tag).unwrap());
}
#[test]
fn test_standardized_e_tag_without_marker_is_invalid() {
let public_key =
PublicKey::from_hex("aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4")
.unwrap();
let tag = vec![
String::from("e"),
EventId::all_zeros().to_hex(),
String::from("wss://relay.example.com"),
public_key.to_hex(),
];
let err = Nip10Tag::parse(&tag).unwrap_err();
assert_eq!(err.kind(), ErrorKind::Invalid);
}
#[test]
fn test_e_tag_with_mention_marker() {
let hex = "19bb195b83fd26db217b6feebb444de4808d90eb4375c31c75ba5bb5c5c10cfc";
let id = EventId::from_hex(hex).unwrap();
let result = Nip10Tag::parse(["e", hex, "", "mention"]).unwrap();
assert_eq!(
result,
Nip10Tag::Event {
id,
relay_hint: None,
marker: None,
public_key: None,
}
);
}
#[test]
#[cfg(all(feature = "std", feature = "os-rng"))]
fn text_note_reply_builder() {
let root_keys = Keys::generate();
let root = EventBuilder::new(Kind::TextNote, "root")
.finalize(&root_keys)
.unwrap();
let reply_keys = Keys::generate();
let reply = TextNoteReplyBuilder::new("reply", &root)
.finalize(&reply_keys)
.unwrap();
assert_eq!(reply.tags.public_keys().collect::<Vec<_>>(), [root.pubkey]);
assert_eq!(reply.tags.event_ids().collect::<Vec<_>>(), [root.id]);
let nested_reply = TextNoteReplyBuilder::new("nested reply", &reply)
.root(&root)
.finalize(&Keys::generate())
.unwrap();
assert_eq!(
nested_reply.tags.public_keys().collect::<Vec<_>>(),
[reply.pubkey, root.pubkey]
);
assert_eq!(
nested_reply.tags.event_ids().collect::<Vec<_>>(),
[reply.id, root.id]
);
}
}