use thiserror::Error;
use crate::event::{
Alphabet, Coordinate, CoordinateError, Event, EventBuilder, EventId, EventIdError, Kind,
SingleLetterTag, Tag, TagKind,
};
use crate::key::PublicKey;
pub const KIND_TAG: &str = "k";
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct DeletionRequest {
pub event_ids: Vec<EventId>,
pub coordinates: Vec<Coordinate>,
pub kinds: Vec<Kind>,
pub reason: String,
}
impl DeletionRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn delete_event(mut self, id: EventId) -> Self {
self.event_ids.push(id);
self
}
#[must_use]
pub fn delete_events(mut self, ids: impl IntoIterator<Item = EventId>) -> Self {
self.event_ids.extend(ids);
self
}
#[must_use]
pub fn delete_coordinate(mut self, coord: Coordinate) -> Self {
self.coordinates.push(coord);
self
}
#[must_use]
pub fn hint_kind(mut self, kind: Kind) -> Self {
self.kinds.push(kind);
self
}
#[must_use]
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = reason.into();
self
}
#[must_use]
pub fn to_tags(&self) -> Vec<Tag> {
let mut tags =
Vec::with_capacity(self.event_ids.len() + self.coordinates.len() + self.kinds.len());
for id in &self.event_ids {
tags.push(Tag::e(*id));
}
for coord in &self.coordinates {
tags.push(Tag::a(coord));
}
for kind in &self.kinds {
tags.push(Tag::k(*kind));
}
tags
}
pub fn from_event(event: &Event) -> Result<Self, DeletionError> {
if event.kind != Kind::EVENT_DELETION {
return Err(DeletionError::UnexpectedKind(event.kind.as_u16()));
}
let mut request = Self::new().with_reason(event.content.clone());
let e_kind = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::E));
let a_kind = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::A));
let k_kind = TagKind::from_wire(KIND_TAG);
for tag in &event.tags {
let head = tag.kind();
if head == e_kind {
let value = tag
.values()
.get(1)
.ok_or(DeletionError::MissingTagValue { tag: "e" })?;
request.event_ids.push(value.parse::<EventId>()?);
} else if head == a_kind {
let value = tag
.values()
.get(1)
.ok_or(DeletionError::MissingTagValue { tag: "a" })?;
request.coordinates.push(value.parse::<Coordinate>()?);
} else if head == k_kind {
let value = tag
.values()
.get(1)
.ok_or(DeletionError::MissingTagValue { tag: "k" })?;
let raw: u16 = value
.parse()
.map_err(|_| DeletionError::InvalidKindHint(value.clone()))?;
request.kinds.push(Kind::from(raw));
}
}
Ok(request)
}
}
impl EventBuilder {
#[must_use]
pub fn deletion(request: &DeletionRequest) -> Self {
Self::new(Kind::EVENT_DELETION, request.reason.clone()).tags(request.to_tags())
}
}
#[derive(Debug, Clone, Error)]
#[non_exhaustive]
pub enum DeletionError {
#[error("expected kind 5, got {0}")]
UnexpectedKind(u16),
#[error("`{tag}` tag is missing its value")]
MissingTagValue {
tag: &'static str,
},
#[error(transparent)]
InvalidEventId(#[from] EventIdError),
#[error(transparent)]
InvalidCoordinate(#[from] CoordinateError),
#[error("invalid `k` tag hint: `{0}`")]
InvalidKindHint(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum AuthorityError {
#[error("deletion author does not match target author")]
AuthorMismatch,
}
pub fn validate_target_authority(
deletion: &Event,
target_author: &PublicKey,
) -> Result<(), AuthorityError> {
if deletion.pubkey == *target_author {
Ok(())
} else {
Err(AuthorityError::AuthorMismatch)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Keys;
use crate::types::Timestamp;
fn keys() -> Keys {
Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
}
fn other_keys() -> Keys {
Keys::parse("0000000000000000000000000000000000000000000000000000000000000005").unwrap()
}
#[test]
fn round_trip_simple() {
let id = EventId::from_byte_array([0xab; 32]);
let request = DeletionRequest::new().delete_event(id).with_reason("typo");
let deletion = EventBuilder::deletion(&request)
.created_at(Timestamp::from_secs(1))
.sign_with_keys(&keys())
.unwrap();
deletion.verify().unwrap();
assert_eq!(deletion.kind, Kind::EVENT_DELETION);
assert_eq!(deletion.content, "typo");
let parsed = DeletionRequest::from_event(&deletion).unwrap();
assert_eq!(parsed, request);
}
#[test]
fn round_trip_with_coordinate_and_kind_hint() {
let id = EventId::from_byte_array([0x01; 32]);
let coord = Coordinate::new(Kind::from(30_023_u16), *keys().public_key(), "long-form-1");
let request = DeletionRequest::new()
.delete_event(id)
.delete_coordinate(coord)
.hint_kind(Kind::from(30_023_u16))
.with_reason("retract draft");
let deletion = EventBuilder::deletion(&request)
.created_at(Timestamp::from_secs(2))
.sign_with_keys(&keys())
.unwrap();
let parsed = DeletionRequest::from_event(&deletion).unwrap();
assert_eq!(parsed, request);
}
#[test]
fn empty_request_round_trips() {
let request = DeletionRequest::new();
let deletion = EventBuilder::deletion(&request)
.created_at(Timestamp::from_secs(3))
.sign_with_keys(&keys())
.unwrap();
let parsed = DeletionRequest::from_event(&deletion).unwrap();
assert_eq!(parsed, request);
}
#[test]
fn rejects_wrong_kind() {
let event = EventBuilder::text_note("not a deletion")
.created_at(Timestamp::from_secs(4))
.sign_with_keys(&keys())
.unwrap();
let err = DeletionRequest::from_event(&event).unwrap_err();
assert!(matches!(err, DeletionError::UnexpectedKind(1)));
}
#[test]
fn rejects_missing_e_value() {
let event = EventBuilder::new(Kind::EVENT_DELETION, "")
.created_at(Timestamp::from_secs(5))
.tag(Tag::new(["e"]).unwrap())
.sign_with_keys(&keys())
.unwrap();
let err = DeletionRequest::from_event(&event).unwrap_err();
assert!(matches!(err, DeletionError::MissingTagValue { tag: "e" }));
}
#[test]
fn validate_authority_accepts_matching_author() {
let request = DeletionRequest::new();
let deletion = EventBuilder::deletion(&request)
.created_at(Timestamp::from_secs(6))
.sign_with_keys(&keys())
.unwrap();
validate_target_authority(&deletion, keys().public_key()).unwrap();
}
#[test]
fn validate_authority_rejects_mismatching_author() {
let request = DeletionRequest::new();
let deletion = EventBuilder::deletion(&request)
.created_at(Timestamp::from_secs(7))
.sign_with_keys(&keys())
.unwrap();
let err = validate_target_authority(&deletion, other_keys().public_key()).unwrap_err();
assert_eq!(err, AuthorityError::AuthorMismatch);
}
}