1use std::collections::BTreeMap;
34
35use super::{Invite, MESSAGE_EVENT_KIND, Nip104Error, Session};
36use nostro2_traits::NostrKeypair;
37
38type Result<T> = std::result::Result<T, Nip104Error>;
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct ReceivedMessage {
43 pub peer: String,
45 pub device_id: String,
47 pub plaintext: Vec<u8>,
49}
50
51#[derive(Debug, Clone)]
53struct PeerRecord<K: NostrKeypair> {
54 devices: BTreeMap<String, Session<K>>,
55}
56
57impl<K: NostrKeypair> Default for PeerRecord<K> {
58 fn default() -> Self {
59 Self {
60 devices: BTreeMap::new(),
61 }
62 }
63}
64
65type SessionKey = (String, String);
67
68#[derive(Debug, Clone)]
85pub struct SessionManager<K: NostrKeypair> {
86 identity: K,
87 our_pubkey: String,
88 peers: BTreeMap<String, PeerRecord<K>>,
89 sender_index: BTreeMap<String, SessionKey>,
91}
92
93impl<K: NostrKeypair> SessionManager<K> {
94 #[must_use]
97 pub fn new(identity: K) -> Self {
98 let our_pubkey = identity.public_key();
99 Self {
100 identity,
101 our_pubkey,
102 peers: BTreeMap::new(),
103 sender_index: BTreeMap::new(),
104 }
105 }
106
107 #[must_use]
109 pub fn our_pubkey(&self) -> &str {
110 &self.our_pubkey
111 }
112
113 #[must_use]
115 pub fn has_session(&self, peer: &str) -> bool {
116 self.peers.get(peer).is_some_and(|p| !p.devices.is_empty())
117 }
118
119 pub fn peers(&self) -> impl Iterator<Item = &String> {
121 self.peers.keys()
122 }
123
124 #[must_use]
126 pub fn devices(&self, peer: &str) -> Vec<String> {
127 self.peers
128 .get(peer)
129 .map(|p| p.devices.keys().cloned().collect())
130 .unwrap_or_default()
131 }
132
133 #[must_use]
135 pub fn session_count(&self) -> usize {
136 self.peers.values().map(|p| p.devices.len()).sum()
137 }
138
139 pub fn install_session(&mut self, peer: &str, device_id: &str, session: Session<K>) {
142 let slot = (peer.to_owned(), device_id.to_owned());
143 self.forget_in_index(&slot);
146 for sender in session.accepted_senders() {
147 self.sender_index.insert(sender, slot.clone());
148 }
149 self.peers
150 .entry(peer.to_owned())
151 .or_default()
152 .devices
153 .insert(device_id.to_owned(), session);
154 }
155
156 pub fn sessions(&self) -> impl Iterator<Item = ((&str, &str), &super::SessionState)> {
163 self.peers.iter().flat_map(|(peer, record)| {
164 record
165 .devices
166 .iter()
167 .map(move |(device, session)| ((peer.as_str(), device.as_str()), &session.state))
168 })
169 }
170
171 fn forget_in_index(&mut self, slot: &SessionKey) {
173 self.sender_index.retain(|_, v| v != slot);
174 }
175
176 fn reindex(&mut self, slot: &SessionKey) {
180 self.forget_in_index(slot);
181 if let Some(session) = self.peers.get(&slot.0).and_then(|p| p.devices.get(&slot.1)) {
182 for sender in session.accepted_senders() {
183 self.sender_index.insert(sender, slot.clone());
184 }
185 }
186 }
187
188 pub fn accept_invite(
197 &mut self,
198 invite: &Invite,
199 owner_pubkey: Option<&str>,
200 created_at: i64,
201 ) -> Result<nostro2::NostrNote> {
202 let (session, response) = invite.accept::<K>(&self.identity, owner_pubkey, created_at)?;
203 let device_id = invite
204 .device_id
205 .clone()
206 .unwrap_or_else(|| invite.inviter.clone());
207 self.install_session(&invite.inviter, &device_id, session);
208 Ok(response)
209 }
210
211 pub fn receive_invite_response(
223 &mut self,
224 invite: &Invite,
225 event: &nostro2::NostrNote,
226 ) -> Result<String> {
227 let (session, recovered) = invite.receive::<K>(event, &self.identity)?;
228 let peer = recovered
229 .owner_public_key
230 .clone()
231 .unwrap_or_else(|| recovered.invitee_identity.clone());
232 self.install_session(&peer, &recovered.invitee_identity, session);
233 Ok(peer)
234 }
235
236 pub fn process_event(&mut self, event: &nostro2::NostrNote) -> Option<ReceivedMessage> {
247 if event.kind != MESSAGE_EVENT_KIND {
248 return None;
249 }
250 let slot = self.sender_index.get(&event.pubkey)?.clone();
252 let session = self.peers.get_mut(&slot.0)?.devices.get_mut(&slot.1)?;
253 let (next, plaintext) = session.plan_receive_event(event).ok()?;
254 session.apply(next);
255 self.reindex(&slot);
258 Some(ReceivedMessage {
259 peer: slot.0,
260 device_id: slot.1,
261 plaintext,
262 })
263 }
264
265 pub fn send(
276 &mut self,
277 peer: &str,
278 payload: &[u8],
279 created_at: i64,
280 ) -> Result<Vec<nostro2::NostrNote>> {
281 let record = self
282 .peers
283 .get_mut(peer)
284 .filter(|p| !p.devices.is_empty())
285 .ok_or_else(|| Nip104Error::UnknownPeer(peer.to_owned()))?;
286
287 let mut events = Vec::with_capacity(record.devices.len());
288 for session in record.devices.values_mut() {
289 if !session.can_send() {
290 continue;
291 }
292 let (next, event) = session.plan_send_event(payload, created_at)?;
293 session.apply(next);
294 events.push(event);
295 }
296 Ok(events)
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use nostro2_traits::NostrSigner as _;
306
307 type K = crate::tests::NipTester;
308
309 fn ident(seed: u8) -> K {
310 K::from_secret_bytes(&[seed; 32]).unwrap()
311 }
312
313 const NOW: i64 = 1_700_000_000;
314
315 #[test]
318 fn two_managers_handshake_and_chat() {
319 let mut alice = SessionManager::new(ident(0x01));
320 let mut bob = SessionManager::new(ident(0x02));
321
322 let invite = Invite::create_new::<K>(alice.our_pubkey(), None).unwrap();
324
325 let response = bob.accept_invite(&invite, None, NOW).unwrap();
328 assert!(bob.has_session(alice.our_pubkey()));
329
330 let peer = alice.receive_invite_response(&invite, &response).unwrap();
331 assert_eq!(peer, bob.our_pubkey());
332 assert!(alice.has_session(bob.our_pubkey()));
333
334 let outbound = bob.send(alice.our_pubkey(), b"hello alice", NOW).unwrap();
336 assert_eq!(outbound.len(), 1);
337 let got = alice.process_event(&outbound[0]).expect("alice decrypts");
338 assert_eq!(got.peer, bob.our_pubkey());
339 assert_eq!(got.plaintext, b"hello alice");
340
341 let reply = alice.send(bob.our_pubkey(), b"hi bob", NOW).unwrap();
343 assert_eq!(reply.len(), 1);
344 let got = bob.process_event(&reply[0]).expect("bob decrypts");
345 assert_eq!(got.peer, alice.our_pubkey());
346 assert_eq!(got.plaintext, b"hi bob");
347 }
348
349 #[test]
352 fn send_fans_out_to_every_device() {
353 let mut alice = SessionManager::new(ident(0x10));
354
355 let bob_owner = ident(0x20).public_key();
357 let mut bob_dev1 = SessionManager::new(ident(0x21));
358 let mut bob_dev2 = SessionManager::new(ident(0x22));
359
360 let invite = Invite::create_new::<K>(alice.our_pubkey(), None).unwrap();
362 let r1 = bob_dev1
363 .accept_invite(&invite, Some(&bob_owner), NOW)
364 .unwrap();
365 let r2 = bob_dev2
366 .accept_invite(&invite, Some(&bob_owner), NOW)
367 .unwrap();
368
369 let p1 = alice.receive_invite_response(&invite, &r1).unwrap();
371 let p2 = alice.receive_invite_response(&invite, &r2).unwrap();
372 assert_eq!(p1, bob_owner);
373 assert_eq!(p2, bob_owner);
374 assert_eq!(alice.devices(&bob_owner).len(), 2);
375
376 let m1 = bob_dev1.send(alice.our_pubkey(), b"d1 up", NOW).unwrap();
378 let m2 = bob_dev2.send(alice.our_pubkey(), b"d2 up", NOW).unwrap();
379 assert_eq!(alice.process_event(&m1[0]).unwrap().plaintext, b"d1 up");
380 assert_eq!(alice.process_event(&m2[0]).unwrap().plaintext, b"d2 up");
381
382 let fanned = alice.send(&bob_owner, b"broadcast", NOW).unwrap();
384 assert_eq!(fanned.len(), 2);
385
386 let to_dev1 = fanned
388 .iter()
389 .filter_map(|e| bob_dev1.process_event(e))
390 .collect::<Vec<_>>();
391 let to_dev2 = fanned
392 .iter()
393 .filter_map(|e| bob_dev2.process_event(e))
394 .collect::<Vec<_>>();
395 assert_eq!(to_dev1.len(), 1);
396 assert_eq!(to_dev2.len(), 1);
397 assert_eq!(to_dev1[0].plaintext, b"broadcast");
398 assert_eq!(to_dev2[0].plaintext, b"broadcast");
399 }
400
401 #[test]
405 fn sessions_snapshot_round_trip() {
406 let mut alice = SessionManager::new(ident(0x51));
407 let mut bob = SessionManager::new(ident(0x52));
408
409 let invite = Invite::create_new::<K>(alice.our_pubkey(), None).unwrap();
410 let response = bob.accept_invite(&invite, None, NOW).unwrap();
411 alice.receive_invite_response(&invite, &response).unwrap();
412
413 let up = bob.send(alice.our_pubkey(), b"hello", NOW).unwrap();
415 assert_eq!(alice.process_event(&up[0]).unwrap().plaintext, b"hello");
416
417 let snaps: Vec<((String, String), super::super::SessionState)> = alice
419 .sessions()
420 .map(|((p, d), st)| ((p.to_owned(), d.to_owned()), st.clone()))
421 .collect();
422 assert_eq!(snaps.len(), 1);
423 let mut alice2 = SessionManager::new(ident(0x51));
424 for ((peer, device), state) in snaps {
425 alice2.install_session(&peer, &device, Session::from_state(state));
426 }
427
428 let up2 = bob.send(alice.our_pubkey(), b"again", NOW).unwrap();
430 assert_eq!(alice2.process_event(&up2[0]).unwrap().plaintext, b"again");
431 let reply = alice2.send(bob.our_pubkey(), b"hi back", NOW).unwrap();
432 assert_eq!(bob.process_event(&reply[0]).unwrap().plaintext, b"hi back");
433 }
434
435 #[test]
436 fn send_to_unknown_peer_errors() {
437 let mut alice = SessionManager::new(ident(0x30));
438 let err = alice.send("deadbeef", b"hi", NOW).unwrap_err();
439 assert!(matches!(err, Nip104Error::UnknownPeer(_)));
440 }
441
442 #[test]
443 fn process_ignores_foreign_and_non_message_events() {
444 let mut alice = SessionManager::new(ident(0x40));
445 let mut bob = SessionManager::new(ident(0x41));
446
447 let invite = Invite::create_new::<K>(alice.our_pubkey(), None).unwrap();
448 let response = bob.accept_invite(&invite, None, NOW).unwrap();
449 alice.receive_invite_response(&invite, &response).unwrap();
450 let outbound = bob.send(alice.our_pubkey(), b"hi", NOW).unwrap();
451
452 let mut stranger = SessionManager::new(ident(0x42));
454 assert!(stranger.process_event(&outbound[0]).is_none());
455
456 assert!(alice.process_event(&response).is_none());
458 }
459
460 #[test]
466 fn fan_out_to_many_devices() {
467 const DEVICES: u8 = 24;
468 let mut alice = SessionManager::new(ident(0x01));
469 let owner = ident(0x02).public_key();
470
471 let invite = Invite::create_new::<K>(alice.our_pubkey(), None).unwrap();
473 let mut devices: Vec<SessionManager<K>> = Vec::new();
474 for d in 0..DEVICES {
475 let mut dev = SessionManager::new(ident(0x10 + d));
476 let resp = dev.accept_invite(&invite, Some(&owner), NOW).unwrap();
477 alice.receive_invite_response(&invite, &resp).unwrap();
478 let up = dev.send(alice.our_pubkey(), b"up", NOW).unwrap();
480 assert_eq!(alice.process_event(&up[0]).unwrap().plaintext, b"up");
481 devices.push(dev);
482 }
483 assert_eq!(alice.devices(&owner).len(), DEVICES as usize);
484
485 let fanned = alice.send(&owner, b"broadcast", NOW).unwrap();
487 assert_eq!(fanned.len(), DEVICES as usize);
488
489 for dev in &mut devices {
491 let hits: Vec<_> = fanned.iter().filter_map(|e| dev.process_event(e)).collect();
492 assert_eq!(hits.len(), 1, "each device takes exactly one copy");
493 assert_eq!(hits[0].plaintext, b"broadcast");
494 }
495 }
496
497 #[test]
500 fn sustained_bidirectional_conversation() {
501 let mut alice = SessionManager::new(ident(0x01));
502 let mut bob = SessionManager::new(ident(0x02));
503 let apk = alice.our_pubkey().to_owned();
504 let bpk = bob.our_pubkey().to_owned();
505
506 let invite = Invite::create_new::<K>(&apk, None).unwrap();
507 let resp = bob.accept_invite(&invite, None, NOW).unwrap();
508 alice.receive_invite_response(&invite, &resp).unwrap();
509
510 let first = bob.send(&apk, b"hi", NOW).unwrap();
512 assert_eq!(alice.process_event(&first[0]).unwrap().plaintext, b"hi");
513
514 for i in 0..100 {
516 let a_body = format!("a{i}");
517 let ev = alice.send(&bpk, a_body.as_bytes(), NOW).unwrap();
518 assert_eq!(
519 bob.process_event(&ev[0]).unwrap().plaintext,
520 a_body.as_bytes()
521 );
522
523 let b_body = format!("b{i}");
524 let ev = bob.send(&apk, b_body.as_bytes(), NOW).unwrap();
525 assert_eq!(
526 alice.process_event(&ev[0]).unwrap().plaintext,
527 b_body.as_bytes()
528 );
529 }
530 }
531
532 #[test]
535 fn replayed_message_event_ignored() {
536 let mut alice = SessionManager::new(ident(0x01));
537 let mut bob = SessionManager::new(ident(0x02));
538 let apk = alice.our_pubkey().to_owned();
539
540 let invite = Invite::create_new::<K>(&apk, None).unwrap();
541 let resp = bob.accept_invite(&invite, None, NOW).unwrap();
542 alice.receive_invite_response(&invite, &resp).unwrap();
543
544 let ev = bob.send(&apk, b"only once", NOW).unwrap();
545 assert_eq!(alice.process_event(&ev[0]).unwrap().plaintext, b"only once");
546 assert!(alice.process_event(&ev[0]).is_none());
548 }
549
550 #[test]
554 fn message_does_not_decrypt_under_foreign_session() {
555 let mut alice = SessionManager::new(ident(0x01));
556 let mut bob = SessionManager::new(ident(0x02));
557 let mut mallory = SessionManager::new(ident(0x03));
558 let apk = alice.our_pubkey().to_owned();
559 let mpk_owner = mallory.our_pubkey().to_owned();
560 let _ = mpk_owner;
561
562 let inv_b = Invite::create_new::<K>(&apk, None).unwrap();
564 let rb = bob.accept_invite(&inv_b, None, NOW).unwrap();
565 alice.receive_invite_response(&inv_b, &rb).unwrap();
566
567 let inv_m = Invite::create_new::<K>(&apk, None).unwrap();
569 let rm = mallory.accept_invite(&inv_m, None, NOW).unwrap();
570 alice.receive_invite_response(&inv_m, &rm).unwrap();
571
572 let ev = bob.send(&apk, b"for alice only", NOW).unwrap();
574 assert!(mallory.process_event(&ev[0]).is_none());
575 assert_eq!(
577 alice.process_event(&ev[0]).unwrap().plaintext,
578 b"for alice only"
579 );
580 }
581
582 #[test]
585 fn out_of_order_events_route_and_backfill() {
586 let mut alice = SessionManager::new(ident(0x01));
587 let mut bob = SessionManager::new(ident(0x02));
588 let apk = alice.our_pubkey().to_owned();
589 let bpk = bob.our_pubkey().to_owned();
590
591 let invite = Invite::create_new::<K>(&apk, None).unwrap();
592 let resp = bob.accept_invite(&invite, None, NOW).unwrap();
593 alice.receive_invite_response(&invite, &resp).unwrap();
594 let first = bob.send(&apk, b"open", NOW).unwrap();
595 alice.process_event(&first[0]).unwrap();
596
597 let e1 = alice.send(&bpk, b"m1", NOW).unwrap().pop().unwrap();
599 let e2 = alice.send(&bpk, b"m2", NOW).unwrap().pop().unwrap();
600 let e3 = alice.send(&bpk, b"m3", NOW).unwrap().pop().unwrap();
601 assert_eq!(bob.process_event(&e1).unwrap().plaintext, b"m1");
602 assert_eq!(bob.process_event(&e3).unwrap().plaintext, b"m3");
603 assert_eq!(bob.process_event(&e2).unwrap().plaintext, b"m2");
604 }
605
606 #[test]
611 fn routing_index_tracks_ratchet_turns() {
612 let mut alice = SessionManager::new(ident(0x01));
613 let mut bob = SessionManager::new(ident(0x02));
614 let apk = alice.our_pubkey().to_owned();
615 let bpk = bob.our_pubkey().to_owned();
616
617 let invite = Invite::create_new::<K>(&apk, None).unwrap();
618 let resp = bob.accept_invite(&invite, None, NOW).unwrap();
619 alice.receive_invite_response(&invite, &resp).unwrap();
620 let first = bob.send(&apk, b"open", NOW).unwrap();
621 alice.process_event(&first[0]).unwrap();
622
623 for i in 0..40 {
626 let a = format!("a{i}");
627 let ea = alice.send(&bpk, a.as_bytes(), NOW).unwrap();
628 assert_eq!(bob.process_event(&ea[0]).unwrap().plaintext, a.as_bytes());
629 let b = format!("b{i}");
630 let eb = bob.send(&apk, b.as_bytes(), NOW).unwrap();
631 assert_eq!(alice.process_event(&eb[0]).unwrap().plaintext, b.as_bytes());
632 }
633 }
634
635 #[test]
639 fn routing_index_keeps_old_chain_reachable() {
640 let mut alice = SessionManager::new(ident(0x01));
641 let mut bob = SessionManager::new(ident(0x02));
642 let apk = alice.our_pubkey().to_owned();
643 let bpk = bob.our_pubkey().to_owned();
644
645 let invite = Invite::create_new::<K>(&apk, None).unwrap();
646 let resp = bob.accept_invite(&invite, None, NOW).unwrap();
647 alice.receive_invite_response(&invite, &resp).unwrap();
648 let first = bob.send(&apk, b"open", NOW).unwrap();
649 alice.process_event(&first[0]).unwrap();
650
651 let e1 = alice.send(&bpk, b"old-1", NOW).unwrap().pop().unwrap();
653 let e2 = alice.send(&bpk, b"old-2", NOW).unwrap().pop().unwrap();
654
655 assert_eq!(bob.process_event(&e2).unwrap().plaintext, b"old-2");
658 let reply = bob.send(&apk, b"hi back", NOW).unwrap();
659 alice.process_event(&reply[0]).unwrap();
660
661 assert_eq!(bob.process_event(&e1).unwrap().plaintext, b"old-1");
663 }
664
665 #[test]
669 fn reinstalling_session_clears_stale_index_rows() {
670 let mut alice = SessionManager::new(ident(0x01));
671 let mut bob = SessionManager::new(ident(0x02));
672 let apk = alice.our_pubkey().to_owned();
673
674 let invite = Invite::create_new::<K>(&apk, None).unwrap();
675 let resp = bob.accept_invite(&invite, None, NOW).unwrap();
676 let peer = alice.receive_invite_response(&invite, &resp).unwrap();
677 let ev = bob.send(&apk, b"hello", NOW).unwrap();
678
679 let mut carol = SessionManager::new(ident(0x03));
682 let inv2 = Invite::create_new::<K>(&apk, None).unwrap();
683 let resp2 = carol.accept_invite(&inv2, None, NOW).unwrap();
684 let (replacement, _rec) = inv2.receive::<K>(&resp2, &ident(0x01)).unwrap();
685 let device = alice.devices(&peer).pop().unwrap();
686 alice.install_session(&peer, &device, replacement);
687
688 assert!(alice.process_event(&ev[0]).is_none());
690 }
691
692 #[test]
693 fn install_and_introspection() {
694 let mut alice = SessionManager::new(ident(0x50));
695 let mut bob = SessionManager::new(ident(0x51));
696
697 assert_eq!(alice.session_count(), 0);
698 assert!(!alice.has_session(bob.our_pubkey()));
699
700 let invite = Invite::create_new::<K>(alice.our_pubkey(), None).unwrap();
701 let response = bob.accept_invite(&invite, None, NOW).unwrap();
702 let peer = alice.receive_invite_response(&invite, &response).unwrap();
703
704 assert_eq!(alice.session_count(), 1);
705 assert!(alice.has_session(&peer));
706 assert_eq!(alice.peers().count(), 1);
707 assert_eq!(alice.devices(&peer), vec![bob.our_pubkey().to_owned()]);
708 }
709}