1use thiserror::Error;
35
36use super::nip59;
37use crate::event::{
38 Alphabet, Event, EventBuilder, Kind, SingleLetterTag, Tag, TagKind, Tags, UnsignedEvent,
39};
40use crate::key::{Keys, PublicKey};
41use crate::types::{RelayUrl, Timestamp};
42
43const SUBJECT_TAG: &str = "subject";
45const REPLY_MARKER: &str = "reply";
47
48#[derive(Debug, Error)]
50#[non_exhaustive]
51pub enum Nip17Error {
52 #[error("recipients list must not be empty")]
58 NoRecipients,
59 #[error("expected kind 10050, got {0}")]
61 UnexpectedKind(u16),
62 #[error("`relay` tag is missing the URL value")]
64 MissingRelayUrl,
65 #[error(transparent)]
67 InvalidRelayUrl(#[from] crate::types::RelayUrlError),
68 #[error(transparent)]
70 Wrap(#[from] nip59::Nip59Error),
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Hash)]
83pub struct Recipient {
84 pub public_key: PublicKey,
86 pub relay_hint: Option<RelayUrl>,
89}
90
91impl Recipient {
92 #[must_use]
94 pub const fn new(public_key: PublicKey) -> Self {
95 Self {
96 public_key,
97 relay_hint: None,
98 }
99 }
100
101 #[must_use]
103 pub fn with_relay_hint(mut self, relay: RelayUrl) -> Self {
104 self.relay_hint = Some(relay);
105 self
106 }
107}
108
109impl From<PublicKey> for Recipient {
110 fn from(public_key: PublicKey) -> Self {
111 Self::new(public_key)
112 }
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Hash)]
119pub struct ReplyTo {
120 pub event_id: crate::event::EventId,
122 pub relay_hint: Option<RelayUrl>,
124}
125
126#[must_use]
139pub fn build_chat_message_rumor(
140 sender: &Keys,
141 recipients: &[Recipient],
142 message: impl Into<String>,
143 created_at: Timestamp,
144 subject: Option<&str>,
145 reply_to: Option<&ReplyTo>,
146) -> UnsignedEvent {
147 let p_kind = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::P));
148 let e_kind = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::E));
149 let subject_kind = TagKind::from_wire(SUBJECT_TAG);
150
151 let mut tags: Vec<Tag> = Vec::with_capacity(
152 recipients.len() + usize::from(subject.is_some()) + usize::from(reply_to.is_some()),
153 );
154
155 for recipient in recipients {
156 let values = recipient.relay_hint.as_ref().map_or_else(
157 || vec![recipient.public_key.to_hex()],
158 |url| vec![recipient.public_key.to_hex(), url.as_str().to_owned()],
159 );
160 tags.push(Tag::with(&p_kind, values));
161 }
162
163 if let Some(reply) = reply_to {
164 let relay = reply
168 .relay_hint
169 .as_ref()
170 .map_or_else(String::new, |url| url.as_str().to_owned());
171 tags.push(Tag::with(
172 &e_kind,
173 [reply.event_id.to_hex(), relay, REPLY_MARKER.to_owned()],
174 ));
175 }
176
177 if let Some(title) = subject {
178 tags.push(Tag::with(&subject_kind, [title.to_owned()]));
179 }
180
181 UnsignedEvent::new(
182 *sender.public_key(),
183 created_at,
184 Kind::PRIVATE_DIRECT_MESSAGE,
185 Tags::from_vec(tags),
186 message,
187 )
188}
189
190pub fn wrap_for_many(
205 sender: &Keys,
206 recipients: &[Recipient],
207 rumor: &UnsignedEvent,
208 timestamps: nip59::Timestamps,
209) -> Result<Vec<Event>, Nip17Error> {
210 if recipients.is_empty() {
211 return Err(Nip17Error::NoRecipients);
212 }
213
214 let mut wraps = Vec::with_capacity(recipients.len() + 1);
215
216 let self_seal = nip59::create_seal(sender, sender.public_key(), rumor, timestamps.seal)?;
219 wraps.push(nip59::create_gift_wrap(
220 &self_seal,
221 sender.public_key(),
222 None,
223 timestamps.wrap,
224 )?);
225
226 for recipient in recipients {
228 let seal = nip59::create_seal(sender, &recipient.public_key, rumor, timestamps.seal)?;
229 wraps.push(nip59::create_gift_wrap(
230 &seal,
231 &recipient.public_key,
232 recipient.relay_hint.as_ref(),
233 timestamps.wrap,
234 )?);
235 }
236
237 Ok(wraps)
238}
239
240pub fn wrap_for(
250 sender: &Keys,
251 recipient: &Recipient,
252 rumor: &UnsignedEvent,
253 timestamps: nip59::Timestamps,
254) -> Result<Event, Nip17Error> {
255 let seal = nip59::create_seal(sender, &recipient.public_key, rumor, timestamps.seal)?;
256 Ok(nip59::create_gift_wrap(
257 &seal,
258 &recipient.public_key,
259 recipient.relay_hint.as_ref(),
260 timestamps.wrap,
261 )?)
262}
263
264pub fn unwrap_chat_message(
275 recipient: &Keys,
276 gift_wrap: &Event,
277) -> Result<UnsignedEvent, Nip17Error> {
278 let rumor = nip59::unwrap(recipient, gift_wrap).map_err(Nip17Error::Wrap)?;
279 if rumor.kind != Kind::PRIVATE_DIRECT_MESSAGE {
280 return Err(Nip17Error::UnexpectedKind(rumor.kind.as_u16()));
281 }
282 Ok(rumor)
283}
284
285pub fn unwrap_dm_payload(recipient: &Keys, gift_wrap: &Event) -> Result<UnsignedEvent, Nip17Error> {
298 nip59::unwrap(recipient, gift_wrap).map_err(Nip17Error::Wrap)
299}
300
301#[must_use]
311pub fn build_dm_relays_event(relays: &[RelayUrl]) -> EventBuilder {
312 let kind = TagKind::from_wire("relay");
313 let tags: Vec<Tag> = relays
314 .iter()
315 .map(|url| Tag::with(&kind, [url.as_str().to_owned()]))
316 .collect();
317 EventBuilder::new(Kind::DM_RELAYS, "").tags(tags)
318}
319
320pub fn parse_dm_relays_event(event: &Event) -> Result<Vec<RelayUrl>, Nip17Error> {
334 if event.kind != Kind::DM_RELAYS {
335 return Err(Nip17Error::UnexpectedKind(event.kind.as_u16()));
336 }
337 let relay_kind = TagKind::from_wire("relay");
338 let mut out = Vec::new();
339 for tag in &event.tags {
340 if tag.kind() != relay_kind {
341 continue;
343 }
344 let value = tag.values().get(1).ok_or(Nip17Error::MissingRelayUrl)?;
345 out.push(RelayUrl::parse(value)?);
346 }
347 Ok(out)
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::Keys;
354 use crate::event::EventId;
355
356 fn keys_alice() -> Keys {
357 Keys::parse("000000000000000000000000000000000000000000000000000000000000a1ce").unwrap()
358 }
359
360 fn keys_bob() -> Keys {
361 Keys::parse("00000000000000000000000000000000000000000000000000000000000000b0").unwrap()
362 }
363
364 fn keys_carol() -> Keys {
365 Keys::parse("00000000000000000000000000000000000000000000000000000000000ca800").unwrap()
366 }
367
368 #[test]
369 fn rumor_carries_p_tag_per_recipient() {
370 let alice = keys_alice();
371 let bob = keys_bob();
372 let carol = keys_carol();
373 let now = Timestamp::from_secs(1_700_000_000);
374
375 let rumor = build_chat_message_rumor(
376 &alice,
377 &[
378 Recipient::new(*bob.public_key()),
379 Recipient::new(*carol.public_key()),
380 ],
381 "hello",
382 now,
383 None,
384 None,
385 );
386
387 let p_kind = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::P));
388 let p_tags: Vec<&Tag> = rumor.tags.iter().filter(|t| t.kind() == p_kind).collect();
389 assert_eq!(p_tags.len(), 2);
390 assert_eq!(
391 p_tags[0].values().get(1).unwrap(),
392 &bob.public_key().to_hex()
393 );
394 assert_eq!(
395 p_tags[1].values().get(1).unwrap(),
396 &carol.public_key().to_hex()
397 );
398 assert_eq!(rumor.kind, Kind::PRIVATE_DIRECT_MESSAGE);
399 assert_eq!(rumor.content, "hello");
400 }
401
402 #[test]
403 fn rumor_carries_subject_and_reply_tags() {
404 let alice = keys_alice();
405 let bob = keys_bob();
406 let now = Timestamp::from_secs(1_700_000_000);
407 let parent = EventId::from_byte_array([0xab; 32]);
408 let relay = RelayUrl::parse("wss://relay.example/").unwrap();
409
410 let rumor = build_chat_message_rumor(
411 &alice,
412 &[Recipient::new(*bob.public_key()).with_relay_hint(relay.clone())],
413 "thread reply",
414 now,
415 Some("daily standup"),
416 Some(&ReplyTo {
417 event_id: parent,
418 relay_hint: Some(relay.clone()),
419 }),
420 );
421
422 let subject_tag = rumor
423 .tags
424 .find_first(&TagKind::from_wire(SUBJECT_TAG))
425 .unwrap();
426 assert_eq!(subject_tag.values().get(1).unwrap(), "daily standup");
427
428 let e_kind = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::E));
429 let e_tag = rumor.tags.find_first(&e_kind).unwrap();
430 let e_values = e_tag.values();
431 assert_eq!(e_values.get(1).unwrap(), &parent.to_hex());
432 assert_eq!(e_values.get(2).unwrap(), relay.as_str());
433 assert_eq!(e_values.get(3).unwrap(), REPLY_MARKER);
434 }
435
436 #[test]
437 fn wrap_for_many_produces_self_plus_recipient_copies() {
438 let alice = keys_alice();
439 let bob = keys_bob();
440 let carol = keys_carol();
441 let now = Timestamp::from_secs(1_700_000_000);
442 let rumor = build_chat_message_rumor(
443 &alice,
444 &[
445 Recipient::new(*bob.public_key()),
446 Recipient::new(*carol.public_key()),
447 ],
448 "hi all",
449 now,
450 None,
451 None,
452 );
453
454 let wraps = wrap_for_many(
455 &alice,
456 &[
457 Recipient::new(*bob.public_key()),
458 Recipient::new(*carol.public_key()),
459 ],
460 &rumor,
461 nip59::Timestamps::all_at(now),
462 )
463 .unwrap();
464
465 assert_eq!(wraps.len(), 3);
467
468 let recovered_self = unwrap_chat_message(&alice, &wraps[0]).unwrap();
470 assert_eq!(recovered_self.content, "hi all");
471
472 let recovered_bob = unwrap_chat_message(&bob, &wraps[1]).unwrap();
474 assert_eq!(recovered_bob.content, "hi all");
475
476 let recovered_carol = unwrap_chat_message(&carol, &wraps[2]).unwrap();
478 assert_eq!(recovered_carol.content, "hi all");
479 }
480
481 #[test]
482 fn wrap_for_many_rejects_empty_recipients() {
483 let alice = keys_alice();
484 let now = Timestamp::from_secs(1_700_000_000);
485 let rumor = build_chat_message_rumor(&alice, &[], "ghost", now, None, None);
486 let err = wrap_for_many(&alice, &[], &rumor, nip59::Timestamps::all_at(now)).unwrap_err();
487 assert!(matches!(err, Nip17Error::NoRecipients));
488 }
489
490 #[test]
491 fn unwrap_chat_message_rejects_wrong_inner_kind() {
492 let alice = keys_alice();
493 let bob = keys_bob();
494 let now = Timestamp::from_secs(1_700_000_000);
495
496 let rumor = UnsignedEvent::new(
498 *alice.public_key(),
499 now,
500 Kind::TEXT_NOTE,
501 Tags::new(),
502 "not a DM",
503 );
504 let seal = nip59::create_seal(&alice, bob.public_key(), &rumor, now).unwrap();
505 let wrap = nip59::create_gift_wrap(&seal, bob.public_key(), None, now).unwrap();
506
507 let err = unwrap_chat_message(&bob, &wrap).unwrap_err();
508 assert!(matches!(err, Nip17Error::UnexpectedKind(1)));
509
510 let recovered = unwrap_dm_payload(&bob, &wrap).unwrap();
512 assert_eq!(recovered.kind, Kind::TEXT_NOTE);
513 }
514
515 #[test]
516 fn dm_relays_round_trip() {
517 let alice = keys_alice();
518 let relays = vec![
519 RelayUrl::parse("wss://inbox.nostr.example/").unwrap(),
520 RelayUrl::parse("wss://dm.nostr.example/").unwrap(),
521 ];
522 let event = build_dm_relays_event(&relays)
523 .created_at(Timestamp::from_secs(1))
524 .sign_with_keys(&alice)
525 .unwrap();
526 assert_eq!(event.kind, Kind::DM_RELAYS);
527 let parsed = parse_dm_relays_event(&event).unwrap();
528 assert_eq!(parsed, relays);
529 }
530
531 #[test]
532 fn dm_relays_rejects_wrong_kind() {
533 let alice = keys_alice();
534 let event = EventBuilder::text_note("not a dm relays event")
535 .created_at(Timestamp::from_secs(1))
536 .sign_with_keys(&alice)
537 .unwrap();
538 let err = parse_dm_relays_event(&event).unwrap_err();
539 assert!(matches!(err, Nip17Error::UnexpectedKind(1)));
540 }
541
542 #[test]
543 fn dm_relays_rejects_missing_url() {
544 let alice = keys_alice();
545 let event = EventBuilder::new(Kind::DM_RELAYS, "")
546 .tag(Tag::new(["relay"]).unwrap())
547 .created_at(Timestamp::from_secs(1))
548 .sign_with_keys(&alice)
549 .unwrap();
550 let err = parse_dm_relays_event(&event).unwrap_err();
551 assert!(matches!(err, Nip17Error::MissingRelayUrl));
552 }
553}