1use thiserror::Error;
25
26use crate::event::{
27 Alphabet, Coordinate, CoordinateError, Event, EventBuilder, EventId, EventIdError, Kind,
28 SingleLetterTag, Tag, TagKind,
29};
30use crate::key::PublicKey;
31
32pub const KIND_TAG: &str = "k";
34
35#[derive(Debug, Default, Clone, PartialEq, Eq)]
42pub struct DeletionRequest {
43 pub event_ids: Vec<EventId>,
45 pub coordinates: Vec<Coordinate>,
47 pub kinds: Vec<Kind>,
49 pub reason: String,
51}
52
53impl DeletionRequest {
54 #[must_use]
56 pub fn new() -> Self {
57 Self::default()
58 }
59
60 #[must_use]
62 pub fn delete_event(mut self, id: EventId) -> Self {
63 self.event_ids.push(id);
64 self
65 }
66
67 #[must_use]
69 pub fn delete_events(mut self, ids: impl IntoIterator<Item = EventId>) -> Self {
70 self.event_ids.extend(ids);
71 self
72 }
73
74 #[must_use]
76 pub fn delete_coordinate(mut self, coord: Coordinate) -> Self {
77 self.coordinates.push(coord);
78 self
79 }
80
81 #[must_use]
83 pub fn hint_kind(mut self, kind: Kind) -> Self {
84 self.kinds.push(kind);
85 self
86 }
87
88 #[must_use]
90 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
91 self.reason = reason.into();
92 self
93 }
94
95 #[must_use]
98 pub fn to_tags(&self) -> Vec<Tag> {
99 let mut tags =
100 Vec::with_capacity(self.event_ids.len() + self.coordinates.len() + self.kinds.len());
101 for id in &self.event_ids {
102 tags.push(Tag::e(*id));
103 }
104 for coord in &self.coordinates {
105 tags.push(Tag::a(coord));
106 }
107 for kind in &self.kinds {
108 tags.push(Tag::k(*kind));
109 }
110 tags
111 }
112
113 pub fn from_event(event: &Event) -> Result<Self, DeletionError> {
124 if event.kind != Kind::EVENT_DELETION {
125 return Err(DeletionError::UnexpectedKind(event.kind.as_u16()));
126 }
127 let mut request = Self::new().with_reason(event.content.clone());
128 let e_kind = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::E));
129 let a_kind = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::A));
130 let k_kind = TagKind::from_wire(KIND_TAG);
131 for tag in &event.tags {
132 let head = tag.kind();
133 if head == e_kind {
134 let value = tag
135 .values()
136 .get(1)
137 .ok_or(DeletionError::MissingTagValue { tag: "e" })?;
138 request.event_ids.push(value.parse::<EventId>()?);
139 } else if head == a_kind {
140 let value = tag
141 .values()
142 .get(1)
143 .ok_or(DeletionError::MissingTagValue { tag: "a" })?;
144 request.coordinates.push(value.parse::<Coordinate>()?);
145 } else if head == k_kind {
146 let value = tag
147 .values()
148 .get(1)
149 .ok_or(DeletionError::MissingTagValue { tag: "k" })?;
150 let raw: u16 = value
151 .parse()
152 .map_err(|_| DeletionError::InvalidKindHint(value.clone()))?;
153 request.kinds.push(Kind::from(raw));
154 }
155 }
156 Ok(request)
157 }
158}
159
160impl EventBuilder {
161 #[must_use]
163 pub fn deletion(request: &DeletionRequest) -> Self {
164 Self::new(Kind::EVENT_DELETION, request.reason.clone()).tags(request.to_tags())
165 }
166}
167
168#[derive(Debug, Clone, Error)]
170#[non_exhaustive]
171pub enum DeletionError {
172 #[error("expected kind 5, got {0}")]
174 UnexpectedKind(u16),
175 #[error("`{tag}` tag is missing its value")]
177 MissingTagValue {
178 tag: &'static str,
180 },
181 #[error(transparent)]
183 InvalidEventId(#[from] EventIdError),
184 #[error(transparent)]
186 InvalidCoordinate(#[from] CoordinateError),
187 #[error("invalid `k` tag hint: `{0}`")]
189 InvalidKindHint(String),
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
194#[non_exhaustive]
195pub enum AuthorityError {
196 #[error("deletion author does not match target author")]
199 AuthorMismatch,
200}
201
202pub fn validate_target_authority(
211 deletion: &Event,
212 target_author: &PublicKey,
213) -> Result<(), AuthorityError> {
214 if deletion.pubkey == *target_author {
215 Ok(())
216 } else {
217 Err(AuthorityError::AuthorMismatch)
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::Keys;
225 use crate::types::Timestamp;
226
227 fn keys() -> Keys {
228 Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
229 }
230
231 fn other_keys() -> Keys {
232 Keys::parse("0000000000000000000000000000000000000000000000000000000000000005").unwrap()
233 }
234
235 #[test]
236 fn round_trip_simple() {
237 let id = EventId::from_byte_array([0xab; 32]);
238 let request = DeletionRequest::new().delete_event(id).with_reason("typo");
239
240 let deletion = EventBuilder::deletion(&request)
241 .created_at(Timestamp::from_secs(1))
242 .sign_with_keys(&keys())
243 .unwrap();
244 deletion.verify().unwrap();
245 assert_eq!(deletion.kind, Kind::EVENT_DELETION);
246 assert_eq!(deletion.content, "typo");
247
248 let parsed = DeletionRequest::from_event(&deletion).unwrap();
249 assert_eq!(parsed, request);
250 }
251
252 #[test]
253 fn round_trip_with_coordinate_and_kind_hint() {
254 let id = EventId::from_byte_array([0x01; 32]);
255 let coord = Coordinate::new(Kind::from(30_023_u16), *keys().public_key(), "long-form-1");
256 let request = DeletionRequest::new()
257 .delete_event(id)
258 .delete_coordinate(coord)
259 .hint_kind(Kind::from(30_023_u16))
260 .with_reason("retract draft");
261
262 let deletion = EventBuilder::deletion(&request)
263 .created_at(Timestamp::from_secs(2))
264 .sign_with_keys(&keys())
265 .unwrap();
266 let parsed = DeletionRequest::from_event(&deletion).unwrap();
267 assert_eq!(parsed, request);
268 }
269
270 #[test]
271 fn empty_request_round_trips() {
272 let request = DeletionRequest::new();
273 let deletion = EventBuilder::deletion(&request)
274 .created_at(Timestamp::from_secs(3))
275 .sign_with_keys(&keys())
276 .unwrap();
277 let parsed = DeletionRequest::from_event(&deletion).unwrap();
278 assert_eq!(parsed, request);
279 }
280
281 #[test]
282 fn rejects_wrong_kind() {
283 let event = EventBuilder::text_note("not a deletion")
284 .created_at(Timestamp::from_secs(4))
285 .sign_with_keys(&keys())
286 .unwrap();
287 let err = DeletionRequest::from_event(&event).unwrap_err();
288 assert!(matches!(err, DeletionError::UnexpectedKind(1)));
289 }
290
291 #[test]
292 fn rejects_missing_e_value() {
293 let event = EventBuilder::new(Kind::EVENT_DELETION, "")
294 .created_at(Timestamp::from_secs(5))
295 .tag(Tag::new(["e"]).unwrap())
296 .sign_with_keys(&keys())
297 .unwrap();
298 let err = DeletionRequest::from_event(&event).unwrap_err();
299 assert!(matches!(err, DeletionError::MissingTagValue { tag: "e" }));
300 }
301
302 #[test]
303 fn validate_authority_accepts_matching_author() {
304 let request = DeletionRequest::new();
305 let deletion = EventBuilder::deletion(&request)
306 .created_at(Timestamp::from_secs(6))
307 .sign_with_keys(&keys())
308 .unwrap();
309 validate_target_authority(&deletion, keys().public_key()).unwrap();
310 }
311
312 #[test]
313 fn validate_authority_rejects_mismatching_author() {
314 let request = DeletionRequest::new();
315 let deletion = EventBuilder::deletion(&request)
316 .created_at(Timestamp::from_secs(7))
317 .sign_with_keys(&keys())
318 .unwrap();
319 let err = validate_target_authority(&deletion, other_keys().public_key()).unwrap_err();
320 assert_eq!(err, AuthorityError::AuthorMismatch);
321 }
322}