1use thiserror::Error;
18
19use crate::event::{Event, EventBuilder, Kind, Tag, TagKind};
20use crate::key::{PublicKey, SecretKey};
21use crate::nips::nip44::{self, Nip44Error};
22use crate::types::{RelayUrl, RelayUrlError, Timestamp, TimestampError};
23
24pub const KIND_DRAFT_WRAP: Kind = Kind::DRAFT_WRAP;
26
27pub const KIND_PRIVATE_STORAGE_RELAYS: Kind = Kind::PRIVATE_STORAGE_RELAYS;
29
30const KIND_TAG: &str = "k";
31const EXPIRATION_TAG: &str = "expiration";
32const RELAY_TAG: &str = "relay";
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct DraftWrap {
37 pub identifier: String,
39 pub draft_kind: Kind,
41 pub expiration: Option<Timestamp>,
43 pub ciphertext: String,
46 pub extra_tags: Vec<Tag>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Default)]
52pub struct PrivateStorageRelays {
53 pub ciphertext: String,
55 pub extra_tags: Vec<Tag>,
57}
58
59#[derive(Debug, Error)]
61#[non_exhaustive]
62pub enum DraftError {
63 #[error("unexpected kind for NIP-37 event: {}", .0.as_u16())]
65 WrongKind(Kind),
66 #[error("draft wrap missing `d` identifier")]
68 MissingIdentifier,
69 #[error("draft wrap missing `k` tag (wrapped draft kind)")]
71 MissingDraftKind,
72 #[error("draft wrap `k` tag value `{0}` is not a valid kind")]
74 InvalidDraftKind(String),
75 #[error(transparent)]
77 InvalidTimestamp(#[from] TimestampError),
78 #[error(transparent)]
80 Encryption(#[from] Nip44Error),
81 #[error(transparent)]
83 Json(#[from] serde_json::Error),
84 #[error(transparent)]
86 InvalidRelayUrl(#[from] RelayUrlError),
87}
88
89impl DraftWrap {
90 #[must_use]
92 pub fn new(
93 identifier: impl Into<String>,
94 draft_kind: Kind,
95 ciphertext: impl Into<String>,
96 ) -> Self {
97 Self {
98 identifier: identifier.into(),
99 draft_kind,
100 expiration: None,
101 ciphertext: ciphertext.into(),
102 extra_tags: Vec::new(),
103 }
104 }
105
106 pub fn encrypt(
113 identifier: impl Into<String>,
114 draft_kind: Kind,
115 plaintext_event_json: &str,
116 secret: &SecretKey,
117 public_key: &PublicKey,
118 ) -> Result<Self, DraftError> {
119 let ciphertext = nip44::encrypt(secret, public_key, plaintext_event_json)?;
120 Ok(Self::new(identifier, draft_kind, ciphertext))
121 }
122
123 pub fn decrypt(
132 &self,
133 secret: &SecretKey,
134 public_key: &PublicKey,
135 ) -> Result<Option<String>, DraftError> {
136 if self.ciphertext.is_empty() {
137 return Ok(None);
138 }
139 Ok(Some(nip44::decrypt(secret, public_key, &self.ciphertext)?))
140 }
141
142 #[must_use]
144 pub const fn is_tombstone(&self) -> bool {
145 self.ciphertext.is_empty()
146 }
147
148 pub fn from_event(event: &Event) -> Result<Self, DraftError> {
154 if event.kind != KIND_DRAFT_WRAP {
155 return Err(DraftError::WrongKind(event.kind));
156 }
157 let mut identifier: Option<String> = None;
158 let mut draft_kind: Option<Kind> = None;
159 let mut expiration: Option<Timestamp> = None;
160 let mut extra_tags: Vec<Tag> = Vec::new();
161 for tag in &event.tags {
162 absorb_draft_tag(
163 tag,
164 &mut identifier,
165 &mut draft_kind,
166 &mut expiration,
167 &mut extra_tags,
168 )?;
169 }
170 Ok(Self {
171 identifier: identifier.ok_or(DraftError::MissingIdentifier)?,
172 draft_kind: draft_kind.ok_or(DraftError::MissingDraftKind)?,
173 expiration,
174 ciphertext: event.content.clone(),
175 extra_tags,
176 })
177 }
178}
179
180fn absorb_draft_tag(
181 tag: &Tag,
182 identifier: &mut Option<String>,
183 draft_kind: &mut Option<Kind>,
184 expiration: &mut Option<Timestamp>,
185 extra_tags: &mut Vec<Tag>,
186) -> Result<(), DraftError> {
187 match tag.kind() {
188 TagKind::SingleLetter(s)
189 if !s.uppercase && s.character == crate::event::Alphabet::D && identifier.is_none() =>
190 {
191 *identifier = tag.get(1).map(str::to_owned);
192 }
193 _ if tag.name() == KIND_TAG && draft_kind.is_none() => {
194 let raw = tag.get(1).ok_or(DraftError::MissingDraftKind)?;
195 let value = raw
196 .parse::<u16>()
197 .map_err(|_| DraftError::InvalidDraftKind(raw.to_owned()))?;
198 *draft_kind = Some(Kind::new(value));
199 }
200 _ if tag.name() == EXPIRATION_TAG => {
201 if let Some(raw) = tag.get(1) {
202 *expiration = Some(raw.parse::<Timestamp>()?);
203 }
204 }
205 _ => extra_tags.push(tag.clone()),
206 }
207 Ok(())
208}
209
210impl PrivateStorageRelays {
211 #[must_use]
213 pub fn new(ciphertext: impl Into<String>) -> Self {
214 Self {
215 ciphertext: ciphertext.into(),
216 extra_tags: Vec::new(),
217 }
218 }
219
220 pub fn encrypt(
226 relays: &[RelayUrl],
227 secret: &SecretKey,
228 public_key: &PublicKey,
229 ) -> Result<Self, DraftError> {
230 let payload: Vec<Vec<String>> = relays
231 .iter()
232 .map(|relay| vec![RELAY_TAG.to_owned(), relay.as_str().to_owned()])
233 .collect();
234 let plaintext = serde_json::to_string(&payload)?;
235 let ciphertext = nip44::encrypt(secret, public_key, &plaintext)?;
236 Ok(Self::new(ciphertext))
237 }
238
239 pub fn decrypt(
246 &self,
247 secret: &SecretKey,
248 public_key: &PublicKey,
249 ) -> Result<Vec<RelayUrl>, DraftError> {
250 if self.ciphertext.is_empty() {
251 return Ok(Vec::new());
252 }
253 let plaintext = nip44::decrypt(secret, public_key, &self.ciphertext)?;
254 let rows: Vec<Vec<String>> = serde_json::from_str(&plaintext)?;
255 let mut relays: Vec<RelayUrl> = Vec::new();
256 for row in rows {
257 let mut iter = row.into_iter();
258 let head = iter.next();
259 let value = iter.next();
260 if head.as_deref() != Some(RELAY_TAG) {
261 continue;
262 }
263 if let Some(raw) = value {
264 relays.push(RelayUrl::parse(&raw)?);
265 }
266 }
267 Ok(relays)
268 }
269
270 pub fn from_event(event: &Event) -> Result<Self, DraftError> {
276 if event.kind != KIND_PRIVATE_STORAGE_RELAYS {
277 return Err(DraftError::WrongKind(event.kind));
278 }
279 Ok(Self {
280 ciphertext: event.content.clone(),
281 extra_tags: event.tags.iter().cloned().collect(),
282 })
283 }
284}
285
286impl EventBuilder {
287 #[must_use]
289 pub fn draft_wrap(draft: &DraftWrap) -> Self {
290 let mut builder = Self::new(KIND_DRAFT_WRAP, draft.ciphertext.clone());
291 builder = builder.tag(Tag::d(&draft.identifier)).tag(Tag::with(
292 &TagKind::from_wire(KIND_TAG),
293 [draft.draft_kind.as_u16().to_string()],
294 ));
295 if let Some(ts) = draft.expiration {
296 builder = builder.tag(Tag::with(
297 &TagKind::from_wire(EXPIRATION_TAG),
298 [ts.as_secs().to_string()],
299 ));
300 }
301 for tag in &draft.extra_tags {
302 builder = builder.tag(tag.clone());
303 }
304 builder
305 }
306
307 #[must_use]
309 pub fn private_storage_relays(list: &PrivateStorageRelays) -> Self {
310 let mut builder = Self::new(KIND_PRIVATE_STORAGE_RELAYS, list.ciphertext.clone());
311 for tag in &list.extra_tags {
312 builder = builder.tag(tag.clone());
313 }
314 builder
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 use crate::Keys;
322
323 fn keys() -> Keys {
324 Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
325 }
326
327 #[test]
328 fn draft_round_trip() {
329 let plaintext = r#"{"kind":1,"content":"hi"}"#;
330 let draft = DraftWrap::encrypt(
331 "draft-1",
332 Kind::TEXT_NOTE,
333 plaintext,
334 keys().secret_key(),
335 keys().public_key(),
336 )
337 .unwrap();
338 let event = EventBuilder::draft_wrap(&draft)
339 .sign_with_keys(&keys())
340 .unwrap();
341 let parsed = DraftWrap::from_event(&event).unwrap();
342 assert_eq!(parsed.identifier, draft.identifier);
343 assert_eq!(parsed.draft_kind, Kind::TEXT_NOTE);
344 let decrypted = parsed
345 .decrypt(keys().secret_key(), keys().public_key())
346 .unwrap();
347 assert_eq!(decrypted.as_deref(), Some(plaintext));
348 }
349
350 #[test]
351 fn tombstone_decrypts_as_none() {
352 let draft = DraftWrap::new("d", Kind::TEXT_NOTE, "");
353 assert!(draft.is_tombstone());
354 let decrypted = draft
355 .decrypt(keys().secret_key(), keys().public_key())
356 .unwrap();
357 assert!(decrypted.is_none());
358 }
359
360 #[test]
361 fn private_storage_relays_round_trip() {
362 let relays = vec![
363 RelayUrl::parse("wss://private.example/").unwrap(),
364 RelayUrl::parse("wss://other.example/").unwrap(),
365 ];
366 let list = PrivateStorageRelays::encrypt(&relays, keys().secret_key(), keys().public_key())
367 .unwrap();
368 let event = EventBuilder::private_storage_relays(&list)
369 .sign_with_keys(&keys())
370 .unwrap();
371 let parsed = PrivateStorageRelays::from_event(&event).unwrap();
372 let decrypted = parsed
373 .decrypt(keys().secret_key(), keys().public_key())
374 .unwrap();
375 assert_eq!(decrypted, relays);
376 }
377
378 #[test]
379 fn missing_kind_is_rejected() {
380 let event = EventBuilder::new(KIND_DRAFT_WRAP, "ct")
381 .tag(Tag::d("foo"))
382 .sign_with_keys(&keys())
383 .unwrap();
384 assert!(matches!(
385 DraftWrap::from_event(&event),
386 Err(DraftError::MissingDraftKind)
387 ));
388 }
389
390 #[test]
391 fn wrong_kind_is_rejected() {
392 let event = EventBuilder::text_note("not a draft")
394 .sign_with_keys(&keys())
395 .unwrap();
396 assert!(matches!(
397 DraftWrap::from_event(&event),
398 Err(DraftError::WrongKind(_))
399 ));
400 assert!(matches!(
402 PrivateStorageRelays::from_event(&event),
403 Err(DraftError::WrongKind(_))
404 ));
405 }
406
407 #[test]
408 fn invalid_k_tag_is_rejected() {
409 let event = EventBuilder::new(KIND_DRAFT_WRAP, "")
412 .tag(Tag::d("foo"))
413 .tag(Tag::with(
414 &TagKind::from_wire(KIND_TAG),
415 ["not-a-number".to_owned()],
416 ))
417 .sign_with_keys(&keys())
418 .unwrap();
419 assert!(matches!(
420 DraftWrap::from_event(&event),
421 Err(DraftError::InvalidDraftKind(raw)) if raw == "not-a-number"
422 ));
423 }
424}