use thiserror::Error;
use crate::event::{Event, EventBuilder, Kind, Tag, TagKind};
use crate::types::{RelayUrl, RelayUrlError};
pub const KIND_REQUEST_TO_VANISH: Kind = Kind::REQUEST_TO_VANISH;
pub const ALL_RELAYS_SENTINEL: &str = "ALL_RELAYS";
const RELAY_TAG: &str = "relay";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum VanishTarget {
Relay(RelayUrl),
AllRelays,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequestToVanish {
pub reason: String,
pub targets: Vec<VanishTarget>,
pub extra_tags: Vec<Tag>,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum VanishError {
#[error("unexpected kind for NIP-62 request to vanish: {}", .0.as_u16())]
WrongKind(Kind),
#[error("NIP-62 event missing required `relay` tag")]
MissingTarget,
#[error("`relay` tag missing target")]
MalformedTarget,
#[error(transparent)]
InvalidRelayUrl(#[from] RelayUrlError),
}
impl VanishTarget {
fn to_tag(&self) -> Tag {
let head = TagKind::from_wire(RELAY_TAG);
match self {
Self::Relay(url) => Tag::with(&head, [url.as_str().to_owned()]),
Self::AllRelays => Tag::with(&head, [ALL_RELAYS_SENTINEL.to_owned()]),
}
}
fn from_tag(tag: &Tag) -> Result<Self, VanishError> {
let raw = tag.get(1).ok_or(VanishError::MalformedTarget)?;
if raw == ALL_RELAYS_SENTINEL {
Ok(Self::AllRelays)
} else {
Ok(Self::Relay(RelayUrl::parse(raw)?))
}
}
}
impl RequestToVanish {
#[must_use]
pub fn relay(reason: impl Into<String>, relays: Vec<RelayUrl>) -> Self {
Self {
reason: reason.into(),
targets: relays.into_iter().map(VanishTarget::Relay).collect(),
extra_tags: Vec::new(),
}
}
#[must_use]
pub fn all_relays(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
targets: vec![VanishTarget::AllRelays],
extra_tags: Vec::new(),
}
}
pub fn from_event(event: &Event) -> Result<Self, VanishError> {
if event.kind != KIND_REQUEST_TO_VANISH {
return Err(VanishError::WrongKind(event.kind));
}
let mut targets: Vec<VanishTarget> = Vec::new();
let mut extra_tags: Vec<Tag> = Vec::new();
for tag in &event.tags {
if tag.name() == RELAY_TAG {
targets.push(VanishTarget::from_tag(tag)?);
} else {
extra_tags.push(tag.clone());
}
}
if targets.is_empty() {
return Err(VanishError::MissingTarget);
}
Ok(Self {
reason: event.content.clone(),
targets,
extra_tags,
})
}
}
impl EventBuilder {
pub fn request_to_vanish(req: &RequestToVanish) -> Result<Self, VanishError> {
if req.targets.is_empty() {
return Err(VanishError::MissingTarget);
}
let mut builder = Self::new(KIND_REQUEST_TO_VANISH, req.reason.clone());
for target in &req.targets {
builder = builder.tag(target.to_tag());
}
for tag in &req.extra_tags {
builder = builder.tag(tag.clone());
}
Ok(builder)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Keys;
fn keys() -> Keys {
Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
}
#[test]
fn relay_target_round_trip() {
let req = RequestToVanish::relay(
"GDPR request",
vec![RelayUrl::parse("wss://relay.example/").unwrap()],
);
let event = EventBuilder::request_to_vanish(&req)
.unwrap()
.sign_with_keys(&keys())
.unwrap();
let parsed = RequestToVanish::from_event(&event).unwrap();
assert_eq!(parsed, req);
}
#[test]
fn all_relays_round_trip() {
let req = RequestToVanish::all_relays("legal");
let event = EventBuilder::request_to_vanish(&req)
.unwrap()
.sign_with_keys(&keys())
.unwrap();
let parsed = RequestToVanish::from_event(&event).unwrap();
assert!(matches!(parsed.targets[0], VanishTarget::AllRelays));
}
#[test]
fn missing_target_is_rejected() {
let req = RequestToVanish {
reason: "x".into(),
targets: Vec::new(),
extra_tags: Vec::new(),
};
assert!(matches!(
EventBuilder::request_to_vanish(&req),
Err(VanishError::MissingTarget)
));
}
#[test]
fn wrong_kind_is_rejected() {
let event = EventBuilder::text_note("nope")
.sign_with_keys(&keys())
.unwrap();
assert!(matches!(
RequestToVanish::from_event(&event),
Err(VanishError::WrongKind(_))
));
}
#[test]
fn parse_event_with_no_relay_tags_rejected() {
let event = EventBuilder::new(KIND_REQUEST_TO_VANISH, "no relay tag here")
.sign_with_keys(&keys())
.unwrap();
assert!(matches!(
RequestToVanish::from_event(&event),
Err(VanishError::MissingTarget)
));
}
#[test]
fn malformed_relay_tag_is_rejected() {
let event = EventBuilder::new(KIND_REQUEST_TO_VANISH, "")
.tag(Tag::with(
&TagKind::from_wire(RELAY_TAG),
Vec::<String>::new(),
))
.sign_with_keys(&keys())
.unwrap();
assert!(matches!(
RequestToVanish::from_event(&event),
Err(VanishError::MalformedTarget)
));
}
}