#![allow(rustdoc::redundant_explicit_links)]
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use core::fmt;
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
use super::nip44::{AsyncNip44, Nip44};
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
use super::nip59::{self, GiftWrapBuilder};
use super::util::take_relay_url;
use crate::event::Event;
use crate::event::tag::{Tag, TagCodec, TagCodecError, impl_tag_codec_conversions};
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
use crate::event::unsigned::FinalizeUnsignedEvent;
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
use crate::event::{EventBuilder, FinalizeEvent, FinalizeEventAsync, Kind, UnsignedEvent};
use crate::key::PublicKey;
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
use crate::signer::{AsyncGetPublicKey, AsyncSignEvent, GetPublicKey, SignEvent, SignerError};
use crate::types::url::{self, RelayUrl};
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
use crate::util::BoxedFuture;
const RELAY: &str = "relay";
#[derive(Debug, PartialEq)]
pub enum Error {
Url(url::Error),
Codec(TagCodecError),
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
Signer(SignerError),
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
NIP59(nip59::Error),
}
impl core::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Url(e) => e.fmt(f),
Self::Codec(e) => e.fmt(f),
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
Self::Signer(e) => e.fmt(f),
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
Self::NIP59(e) => e.fmt(f),
}
}
}
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)
}
}
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
impl From<SignerError> for Error {
fn from(e: SignerError) -> Self {
Self::Signer(e)
}
}
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
impl From<nip59::Error> for Error {
fn from(e: nip59::Error) -> Self {
Self::NIP59(e)
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct PrivateDirectMessageBuilder {
pub receiver: PublicKey,
pub message: String,
pub rumor_extra_tags: Vec<Tag>,
pub extra_tags: Vec<Tag>,
}
impl PrivateDirectMessageBuilder {
#[inline]
pub fn new<M>(receiver: PublicKey, message: M) -> Self
where
M: Into<String>,
{
Self {
receiver,
message: message.into(),
rumor_extra_tags: Vec::new(),
extra_tags: Vec::new(),
}
}
#[inline]
pub fn rumor_extra_tags<T>(mut self, tags: T) -> Self
where
T: IntoIterator<Item = Tag>,
{
self.rumor_extra_tags.extend(tags);
self
}
#[inline]
pub fn extra_tags<T>(mut self, tags: T) -> Self
where
T: IntoIterator<Item = Tag>,
{
self.extra_tags.extend(tags);
self
}
}
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
impl<S> FinalizeEvent<S> for PrivateDirectMessageBuilder
where
S: GetPublicKey + SignEvent + Nip44,
{
type Error = Error;
fn finalize(self, signer: &S) -> Result<Event, Self::Error> {
let public_key: PublicKey = signer.get_public_key()?;
let rumor: UnsignedEvent = make_rumor(
public_key,
self.receiver,
self.message,
self.rumor_extra_tags,
);
Ok(GiftWrapBuilder::new(self.receiver, rumor)
.extra_tags(self.extra_tags)
.finalize(signer)?)
}
}
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
impl<S> FinalizeEventAsync<S> for PrivateDirectMessageBuilder
where
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44,
{
type Error = Error;
fn finalize_async<'a>(self, signer: &'a S) -> BoxedFuture<'a, Result<Event, Self::Error>>
where
Self: 'a,
S: 'a,
{
Box::pin(async move {
let public_key: PublicKey = signer.get_public_key_async().await?;
let rumor: UnsignedEvent = make_rumor(
public_key,
self.receiver,
self.message,
self.rumor_extra_tags,
);
Ok(GiftWrapBuilder::new(self.receiver, rumor)
.extra_tags(self.extra_tags)
.finalize_async(signer)
.await?)
})
}
}
#[inline]
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
fn make_rumor(
sender: PublicKey,
receiver: PublicKey,
message: String,
extra_tags: Vec<Tag>,
) -> UnsignedEvent {
EventBuilder::new(Kind::PrivateDirectMessage, message)
.tag(Tag::public_key(receiver))
.tags(extra_tags)
.finalize_unsigned(sender)
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Nip17Tag {
Relay(RelayUrl),
}
impl TagCodec for Nip17Tag {
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() {
RELAY => {
let url: RelayUrl = take_relay_url::<_, _, Error>(&mut iter)?;
Ok(Self::Relay(url))
}
_ => Err(TagCodecError::Unknown.into()),
}
}
fn to_tag(&self) -> Tag {
let Self::Relay(url) = self;
let tag: Vec<String> = vec![String::from(RELAY), url.to_string()];
Tag::new(tag)
}
}
impl_tag_codec_conversions!(Nip17Tag);
pub fn extract_relay_list(event: &Event) -> impl Iterator<Item = RelayUrl> + '_ {
event
.tags
.iter()
.filter_map(|tag| match Nip17Tag::parse(tag.as_slice()) {
Ok(Nip17Tag::Relay(url)) => Some(url),
_ => None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_empty_tag() {
let tag: Vec<String> = Vec::new();
let err = Nip17Tag::parse(&tag).unwrap_err();
assert_eq!(err, Error::Codec(TagCodecError::missing_tag_kind()));
}
#[test]
fn test_non_existing_tag() {
let tag = vec!["p"];
let err = Nip17Tag::parse(&tag).unwrap_err();
assert_eq!(err, Error::Codec(TagCodecError::Unknown));
}
#[test]
fn test_standardized_relay_tag() {
let relay = RelayUrl::parse("wss://relay.damus.io").unwrap();
let tag = vec!["relay".to_string(), relay.to_string()];
let parsed = Nip17Tag::parse(&tag).unwrap();
assert_eq!(parsed, Nip17Tag::Relay(relay.clone()));
assert_eq!(parsed.to_tag(), Tag::parse(tag).unwrap());
}
#[test]
fn test_missing_relay_url() {
let tag = vec!["relay"];
let err = Nip17Tag::parse(&tag).unwrap_err();
assert_eq!(err, Error::Codec(TagCodecError::Missing("relay URL")));
}
}