use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use super::util::{take_and_parse_optional_relay_url, take_optional_string, take_public_key};
use crate::event::Kind;
use crate::event::builder::{EventBuilder, EventBuilderTemplate};
use crate::event::tag::{Tag, TagCodec, TagCodecError, impl_tag_codec_conversions};
use crate::key::{self, PublicKey};
use crate::types::url::{self, RelayUrl};
#[derive(Debug, PartialEq)]
pub enum Error {
Keys(key::Error),
Url(url::Error),
Codec(TagCodecError),
}
impl core::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Keys(e) => e.fmt(f),
Self::Url(e) => e.fmt(f),
Self::Codec(e) => e.fmt(f),
}
}
}
impl From<key::Error> for Error {
fn from(e: key::Error) -> Self {
Self::Keys(e)
}
}
impl From<url::Error> for Error {
fn from(e: url::Error) -> Self {
Self::Url(e)
}
}
impl From<TagCodecError> for Error {
fn from(e: TagCodecError) -> Self {
Self::Codec(e)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Contact {
pub public_key: PublicKey,
pub relay_url: Option<RelayUrl>,
pub alias: Option<String>,
}
impl Contact {
#[inline]
pub fn new(public_key: PublicKey) -> Self {
Self {
public_key,
relay_url: None,
alias: None,
}
}
}
impl From<Contact> for Nip02Tag {
fn from(contact: Contact) -> Self {
Self::PublicKey {
public_key: contact.public_key,
relay_hint: contact.relay_url,
alias: contact.alias,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Nip02Tag {
PublicKey {
public_key: PublicKey,
relay_hint: Option<RelayUrl>,
alias: Option<String>,
},
}
impl TagCodec for Nip02Tag {
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(TagCodecError::missing_tag_kind())?;
match kind.as_ref() {
"p" => {
let (public_key, relay_hint, alias) = parse_p_tag(iter)?;
Ok(Self::PublicKey {
public_key,
relay_hint,
alias,
})
}
_ => Err(TagCodecError::Unknown.into()),
}
}
fn to_tag(&self) -> Tag {
let Self::PublicKey {
public_key,
relay_hint,
alias,
} = self;
let mut tag: Vec<String> = Vec::with_capacity(2 + relay_hint.is_some() as usize);
tag.push(String::from("p"));
tag.push(public_key.to_hex());
if let Some(relay_hint) = relay_hint {
tag.push(relay_hint.to_string());
} else if alias.is_some() {
tag.push(String::new());
}
if let Some(alias) = alias {
tag.push(alias.to_string());
}
Tag::new(tag)
}
}
impl_tag_codec_conversions!(Nip02Tag);
fn parse_p_tag<T, S>(mut iter: T) -> Result<(PublicKey, Option<RelayUrl>, Option<String>), Error>
where
T: Iterator<Item = S>,
S: AsRef<str>,
{
let public_key: PublicKey = take_public_key::<_, _, Error>(&mut iter)?;
let relay_hint: Option<RelayUrl> = take_and_parse_optional_relay_url(&mut iter)?;
let alias: Option<String> = take_optional_string(&mut iter);
Ok((public_key, relay_hint, alias))
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct ContactListBuilder {
pub contacts: Vec<Contact>,
}
impl ContactListBuilder {
#[inline]
pub fn new<I>(contacts: I) -> Self
where
I: IntoIterator<Item = Contact>,
{
Self {
contacts: contacts.into_iter().collect(),
}
}
}
impl EventBuilderTemplate for ContactListBuilder {
fn build(self) -> EventBuilder {
let tags = self.contacts.into_iter().map(|contact| {
Nip02Tag::PublicKey {
public_key: contact.public_key,
relay_hint: contact.relay_url,
alias: contact.alias,
}
.to_tag()
});
EventBuilder::new(Kind::ContactList, "").tags(tags)
}
}
#[cfg(test)]
mod tests {
use super::{Error, *};
use crate::prelude::*;
#[test]
fn test_standardized_p_tag() {
let raw = "00000001505e7e48927046e9bbaa728b1f3b511227e2200c578d6e6bb0c77eb9";
let public_key = PublicKey::from_hex(raw).unwrap();
let tag = vec!["p", raw];
let parsed = Nip02Tag::parse(&tag).unwrap();
assert_eq!(
parsed,
Nip02Tag::PublicKey {
public_key,
relay_hint: None,
alias: None
}
);
assert_eq!(parsed.to_tag(), Tag::parse(tag).unwrap());
let tag = vec!["p", raw, "wss://relay.damus.io/"];
let parsed = Nip02Tag::parse(&tag).unwrap();
assert_eq!(
parsed,
Nip02Tag::PublicKey {
public_key,
relay_hint: Some(RelayUrl::parse("wss://relay.damus.io/").unwrap()),
alias: None
}
);
assert_eq!(parsed.to_tag(), Tag::parse(tag).unwrap());
let tag = vec!["p", raw, "wss://relay.damus.io/", "alice"];
let parsed = Nip02Tag::parse(&tag).unwrap();
assert_eq!(
parsed,
Nip02Tag::PublicKey {
public_key,
relay_hint: Some(RelayUrl::parse("wss://relay.damus.io/").unwrap()),
alias: Some(String::from("alice"))
}
);
assert_eq!(parsed.to_tag(), Tag::parse(tag).unwrap());
let tag = vec!["p", raw, "", "alice"];
let parsed = Nip02Tag::parse(&tag).unwrap();
assert_eq!(
parsed,
Nip02Tag::PublicKey {
public_key,
relay_hint: None,
alias: Some(String::from("alice"))
}
);
assert_eq!(parsed.to_tag(), Tag::parse(tag).unwrap());
let tag = vec!["p", "hello"];
let err = Nip02Tag::parse(&tag).unwrap_err();
assert!(matches!(err, Error::Keys(key::Error::Hex(_))));
let tag = vec!["p"];
let err = Nip02Tag::parse(&tag).unwrap_err();
assert_eq!(err, Error::Codec(TagCodecError::Missing("public key")));
}
#[test]
#[cfg(all(feature = "std", feature = "os-rng"))]
fn test_make_contact_list_event() {
let keys = Keys::generate();
let contact = Contact {
public_key: PublicKey::from_hex(
"0000000000000000000000000000000000000000000000000000000000000000",
)
.unwrap(),
relay_url: None,
alias: None,
};
let event = ContactListBuilder::new([contact.clone()])
.finalize(&keys)
.unwrap();
assert_eq!(event.kind, Kind::ContactList);
assert_eq!(event.pubkey, keys.public_key());
assert_eq!(event.tags.len(), 1);
assert_eq!(
event.tags[0],
Nip02Tag::PublicKey {
public_key: contact.public_key,
relay_hint: contact.relay_url,
alias: contact.alias,
}
.to_tag()
);
}
}