#![allow(rustdoc::redundant_explicit_links)]
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use core::time::Duration;
#[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::GiftWrapBuilder;
use super::util::{missing_tag_kind, take_relay_url, unknown_tag};
use crate::error::Error;
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
use crate::event::{
AsyncSignEvent, EventBuilder, FinalizeEvent, FinalizeEventAsync, FinalizeUnsignedEvent, Kind,
SignEvent, UnsignedEvent,
};
use crate::event::{Event, Tag, TagCodec, impl_tag_codec_conversions};
use crate::key::PublicKey;
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
use crate::key::{AsyncGetPublicKey, GetPublicKey};
use crate::types::url::RelayUrl;
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
use crate::util::BoxedFuture;
const RELAY: &str = "relay";
#[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>,
pub expiration: Option<Duration>,
}
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(),
expiration: None,
}
}
#[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
}
#[inline]
pub fn expiration(mut self, duration: Duration) -> Self {
self.expiration = Some(duration);
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().map_err(Error::other)?;
let rumor: UnsignedEvent = make_rumor(
public_key,
self.receiver,
self.message,
self.rumor_extra_tags,
);
let mut builder = GiftWrapBuilder::new(self.receiver, rumor).extra_tags(self.extra_tags);
if let Some(duration) = self.expiration {
builder = builder.expiration(duration);
}
builder.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.map_err(Error::other)?;
let rumor: UnsignedEvent = make_rumor(
public_key,
self.receiver,
self.message,
self.rumor_extra_tags,
);
let mut builder =
GiftWrapBuilder::new(self.receiver, rumor).extra_tags(self.extra_tags);
if let Some(duration) = self.expiration {
builder = builder.expiration(duration);
}
builder.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(missing_tag_kind())?;
match kind.as_ref() {
RELAY => {
let url: RelayUrl = take_relay_url(&mut iter)?;
Ok(Self::Relay(url))
}
_ => Err(unknown_tag()),
}
}
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::*;
use crate::error::ErrorKind;
#[test]
fn test_parse_empty_tag() {
let tag: Vec<String> = Vec::new();
let err = Nip17Tag::parse(&tag).unwrap_err();
assert_eq!(err.kind(), ErrorKind::Missing);
}
#[test]
fn test_non_existing_tag() {
let tag = vec!["p"];
let err = Nip17Tag::parse(&tag).unwrap_err();
assert_eq!(err.kind(), ErrorKind::Malformed);
}
#[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.kind(), ErrorKind::Missing);
}
}
#[cfg(test)]
#[cfg(all(feature = "std", feature = "os-rng", feature = "nip59"))]
mod expiration_tests {
use core::time::Duration;
use super::*;
use crate::key::Keys;
use crate::nips::nip59::extract_rumor;
#[test]
fn test_private_dm_expiration_on_gift_wrap() {
let sender = Keys::generate();
let receiver = Keys::generate();
let duration: Duration = Duration::from_secs(7 * 24 * 3600);
let gift_wrap: Event = PrivateDirectMessageBuilder::new(receiver.public_key(), "hello")
.expiration(duration)
.finalize(&sender)
.unwrap();
assert_eq!(gift_wrap.kind, Kind::GiftWrap);
let expiration = gift_wrap.tags.expiration().expect("missing expiration tag");
assert_eq!(
expiration.as_secs() - gift_wrap.created_at.as_secs(),
duration.as_secs()
);
let unwrapped = extract_rumor(&receiver, &gift_wrap).unwrap();
assert_eq!(unwrapped.sender, sender.public_key());
assert_eq!(unwrapped.rumor.kind, Kind::PrivateDirectMessage);
assert!(unwrapped.rumor.tags.expiration().is_none());
}
#[test]
fn test_private_dm_without_expiration() {
let sender = Keys::generate();
let receiver = Keys::generate();
let gift_wrap: Event = PrivateDirectMessageBuilder::new(receiver.public_key(), "hello")
.finalize(&sender)
.unwrap();
assert!(gift_wrap.tags.expiration().is_none());
}
#[tokio::test]
async fn test_private_dm_expiration_async() {
let sender = Keys::generate();
let receiver = Keys::generate();
let duration: Duration = Duration::from_secs(7 * 24 * 3600);
let gift_wrap: Event = PrivateDirectMessageBuilder::new(receiver.public_key(), "hello")
.expiration(duration)
.finalize_async(&sender)
.await
.unwrap();
let expiration = gift_wrap.tags.expiration().expect("missing expiration tag");
assert_eq!(
expiration.as_secs() - gift_wrap.created_at.as_secs(),
duration.as_secs()
);
}
}