1use std::sync::{Arc, Mutex};
7
8use nostr_double_ratchet::{
9 AppKeys, CreateGroupOptions, DeviceEntry, FileStorageAdapter, GroupData, GroupDecryptedEvent,
10 GroupSendEvent, InMemoryStorage, Invite, NdrRuntime, Session, SessionManagerEvent,
11 SessionState, StorageAdapter,
12};
13
14mod error;
15pub use error::NdrError;
16
17#[uniffi::export]
19pub fn version() -> String {
20 env!("CARGO_PKG_VERSION").to_string()
21}
22
23#[derive(uniffi::Record)]
25pub struct FfiKeyPair {
26 pub public_key_hex: String,
27 pub private_key_hex: String,
28}
29
30#[derive(uniffi::Record)]
32pub struct InviteAcceptResult {
33 pub session: Arc<SessionHandle>,
34 pub response_event_json: String,
35}
36
37#[derive(uniffi::Record)]
39pub struct SendResult {
40 pub outer_event_json: String,
41 pub inner_event_json: String,
42}
43
44#[derive(uniffi::Record)]
46pub struct DecryptResult {
47 pub plaintext: String,
48 pub inner_event_json: String,
49}
50
51#[derive(uniffi::Record)]
53pub struct PubSubEvent {
54 pub kind: String,
55 pub subid: Option<String>,
56 pub filter_json: Option<String>,
57 pub event_json: Option<String>,
58 pub sender_pubkey_hex: Option<String>,
59 pub content: Option<String>,
60 pub event_id: Option<String>,
61}
62
63#[derive(uniffi::Record)]
65pub struct SessionManagerAcceptInviteResult {
66 pub owner_pubkey_hex: String,
67 pub inviter_device_pubkey_hex: String,
68 pub device_id: String,
69 pub created_new_session: bool,
70}
71
72#[derive(uniffi::Record)]
74pub struct FfiGroupData {
75 pub id: String,
76 pub name: String,
77 pub description: Option<String>,
78 pub picture: Option<String>,
79 pub members: Vec<String>,
80 pub admins: Vec<String>,
81 pub created_at_ms: u64,
82 pub secret: Option<String>,
83 pub accepted: Option<bool>,
84}
85
86#[derive(uniffi::Record)]
88pub struct GroupSendResult {
89 pub outer_event_json: String,
90 pub inner_event_json: String,
91 pub outer_event_id: String,
92 pub inner_event_id: String,
93}
94
95#[derive(uniffi::Record)]
97pub struct GroupCreateFanout {
98 pub enabled: bool,
99 pub attempted: u64,
100 pub succeeded: Vec<String>,
101 pub failed: Vec<String>,
102}
103
104#[derive(uniffi::Record)]
106pub struct GroupCreateResult {
107 pub group: FfiGroupData,
108 pub metadata_rumor_json: Option<String>,
109 pub fanout: GroupCreateFanout,
110}
111
112#[derive(uniffi::Record)]
114pub struct GroupDecryptedResult {
115 pub group_id: String,
116 pub sender_event_pubkey_hex: String,
117 pub sender_device_pubkey_hex: String,
118 pub sender_owner_pubkey_hex: Option<String>,
119 pub outer_event_id: String,
120 pub outer_created_at: u64,
121 pub key_id: u32,
122 pub message_number: u32,
123 pub inner_event_json: String,
124 pub inner_event_id: String,
125}
126
127#[derive(uniffi::Record)]
129pub struct GroupOuterSubscriptionPlanResult {
130 pub authors: Vec<String>,
131 pub added_authors: Vec<String>,
132}
133
134#[derive(uniffi::Record)]
136pub struct MessagePushSessionStateResult {
137 pub state_json: String,
138 pub tracked_sender_pubkeys: Vec<String>,
139 pub has_receiving_capability: bool,
140}
141
142#[uniffi::export]
144pub fn generate_keypair() -> FfiKeyPair {
145 let keys = nostr::Keys::generate();
146 FfiKeyPair {
147 public_key_hex: keys.public_key().to_hex(),
148 private_key_hex: keys.secret_key().to_secret_hex(),
149 }
150}
151
152#[uniffi::export]
154pub fn derive_public_key(privkey_hex: String) -> Result<String, NdrError> {
155 let privkey = parse_private_key(&privkey_hex)?;
156 let secret_key =
157 nostr::SecretKey::from_slice(&privkey).map_err(|e| NdrError::InvalidKey(e.to_string()))?;
158 Ok(nostr::Keys::new(secret_key).public_key().to_hex())
159}
160
161#[derive(uniffi::Record)]
163pub struct FfiDeviceEntry {
164 pub identity_pubkey_hex: String,
165 pub created_at: u64,
166 pub device_label: Option<String>,
167 pub client_label: Option<String>,
168}
169
170fn ffi_device_entry_from_app_keys(app_keys: &AppKeys, device: DeviceEntry) -> FfiDeviceEntry {
171 let labels = app_keys.get_device_labels(&device.identity_pubkey);
172 FfiDeviceEntry {
173 identity_pubkey_hex: hex::encode(device.identity_pubkey.to_bytes()),
174 created_at: device.created_at,
175 device_label: labels.and_then(|label| label.device_label.clone()),
176 client_label: labels.and_then(|label| label.client_label.clone()),
177 }
178}
179
180fn owner_keys_from_privkey_hex(owner_privkey_hex: &str) -> Result<nostr::Keys, NdrError> {
181 let owner_privkey = parse_private_key(owner_privkey_hex)?;
182 let owner_sk = nostr::SecretKey::from_slice(&owner_privkey)
183 .map_err(|e| NdrError::InvalidKey(e.to_string()))?;
184 Ok(nostr::Keys::new(owner_sk))
185}
186
187fn parse_app_keys_for_owner(
188 event: &nostr::Event,
189 owner_privkey_hex: Option<&str>,
190) -> Result<AppKeys, NdrError> {
191 match owner_privkey_hex {
192 Some(privkey_hex) => {
193 let owner_keys = owner_keys_from_privkey_hex(privkey_hex)?;
194 Ok(AppKeys::from_event_with_labels(event, &owner_keys)?)
195 }
196 None => Ok(AppKeys::from_event(event)?),
197 }
198}
199
200#[uniffi::export]
202pub fn create_signed_app_keys_event(
203 owner_pubkey_hex: String,
204 owner_privkey_hex: String,
205 devices: Vec<FfiDeviceEntry>,
206) -> Result<String, NdrError> {
207 let owner_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&owner_pubkey_hex)?;
208 let owner_keys = owner_keys_from_privkey_hex(&owner_privkey_hex)?;
209 if owner_keys.public_key() != owner_pubkey {
210 return Err(NdrError::InvalidKey(
211 "owner pubkey does not match owner private key".to_string(),
212 ));
213 }
214
215 let entries = devices
216 .iter()
217 .filter_map(|d| {
218 let pk = nostr_double_ratchet::utils::pubkey_from_hex(&d.identity_pubkey_hex).ok()?;
219 Some(DeviceEntry::new(pk, d.created_at))
220 })
221 .collect::<Vec<_>>();
222
223 let mut app_keys = AppKeys::new(entries);
224 for device in devices {
225 let Ok(identity_pubkey) =
226 nostr_double_ratchet::utils::pubkey_from_hex(&device.identity_pubkey_hex)
227 else {
228 continue;
229 };
230
231 if device.device_label.is_none() && device.client_label.is_none() {
232 continue;
233 }
234
235 app_keys.set_device_labels(
236 identity_pubkey,
237 device.device_label,
238 device.client_label,
239 None,
240 );
241 }
242 let unsigned = app_keys.get_encrypted_event(&owner_keys)?;
243 let signed = unsigned
244 .sign_with_keys(&owner_keys)
245 .map_err(|e| NdrError::Serialization(e.to_string()))?;
246 Ok(serde_json::to_string(&signed)?)
247}
248
249#[uniffi::export]
251pub fn parse_app_keys_event(
252 event_json: String,
253 owner_privkey_hex: Option<String>,
254) -> Result<Vec<FfiDeviceEntry>, NdrError> {
255 let event: nostr::Event = serde_json::from_str(&event_json)?;
256 let app_keys = parse_app_keys_for_owner(&event, owner_privkey_hex.as_deref())?;
257 Ok(app_keys
258 .get_all_devices()
259 .into_iter()
260 .map(|d| ffi_device_entry_from_app_keys(&app_keys, d))
261 .collect())
262}
263
264#[uniffi::export]
266pub fn resolve_latest_app_keys_devices(
267 event_jsons: Vec<String>,
268 owner_privkey_hex: Option<String>,
269) -> Result<Vec<FfiDeviceEntry>, NdrError> {
270 let events = event_jsons
271 .iter()
272 .filter_map(|event_json| serde_json::from_str::<nostr::Event>(event_json).ok())
273 .collect::<Vec<_>>();
274
275 let mut latest: Option<nostr_double_ratchet::AppKeysSnapshot> = None;
276
277 for event in events.iter() {
278 if !nostr_double_ratchet::is_app_keys_event(event) {
279 continue;
280 }
281
282 let Ok(app_keys) = parse_app_keys_for_owner(event, owner_privkey_hex.as_deref()) else {
283 continue;
284 };
285
286 latest = Some(match latest.as_ref() {
287 Some(current) => nostr_double_ratchet::apply_app_keys_snapshot(
288 Some(¤t.app_keys),
289 current.created_at,
290 &app_keys,
291 event.created_at.as_secs(),
292 ),
293 None => nostr_double_ratchet::AppKeysSnapshot {
294 decision: nostr_double_ratchet::AppKeysSnapshotDecision::Advanced,
295 app_keys,
296 created_at: event.created_at.as_secs(),
297 },
298 });
299 }
300
301 let Some(snapshot) = latest else {
302 return Ok(Vec::new());
303 };
304
305 Ok(snapshot
306 .app_keys
307 .get_all_devices()
308 .into_iter()
309 .map(|d| ffi_device_entry_from_app_keys(&snapshot.app_keys, d))
310 .collect())
311}
312
313#[uniffi::export]
315pub fn resolve_conversation_candidate_pubkeys(
316 owner_pubkey_hex: String,
317 rumor_pubkey_hex: String,
318 rumor_tags: Vec<Vec<String>>,
319 sender_pubkey_hex: String,
320) -> Vec<String> {
321 nostr_double_ratchet::resolve_conversation_candidate_pubkeys(
322 &owner_pubkey_hex,
323 &rumor_pubkey_hex,
324 &rumor_tags,
325 &sender_pubkey_hex,
326 )
327}
328
329#[derive(uniffi::Object)]
331pub struct InviteHandle {
332 inner: Mutex<Invite>,
333}
334
335#[uniffi::export]
336impl InviteHandle {
337 #[uniffi::constructor]
339 pub fn create_new(
340 inviter_pubkey_hex: String,
341 device_id: Option<String>,
342 max_uses: Option<u32>,
343 ) -> Result<Arc<Self>, NdrError> {
344 let inviter = nostr_double_ratchet::utils::pubkey_from_hex(&inviter_pubkey_hex)?;
345 let invite = Invite::create_new(inviter, device_id, max_uses.map(|n| n as usize))?;
346 Ok(Arc::new(Self {
347 inner: Mutex::new(invite),
348 }))
349 }
350
351 #[uniffi::constructor]
353 pub fn from_url(url: String) -> Result<Arc<Self>, NdrError> {
354 let invite = Invite::from_url(&url)?;
355 Ok(Arc::new(Self {
356 inner: Mutex::new(invite),
357 }))
358 }
359
360 #[uniffi::constructor]
362 pub fn from_event_json(event_json: String) -> Result<Arc<Self>, NdrError> {
363 let event: nostr::Event = serde_json::from_str(&event_json)?;
364 let invite = Invite::from_event(&event)?;
365 Ok(Arc::new(Self {
366 inner: Mutex::new(invite),
367 }))
368 }
369
370 #[uniffi::constructor]
372 pub fn deserialize(json: String) -> Result<Arc<Self>, NdrError> {
373 let invite = Invite::deserialize(&json)?;
374 Ok(Arc::new(Self {
375 inner: Mutex::new(invite),
376 }))
377 }
378
379 pub fn to_url(&self, root: String) -> Result<String, NdrError> {
381 let invite = self.inner.lock().unwrap();
382 Ok(invite.get_url(&root)?)
383 }
384
385 pub fn to_event_json(&self) -> Result<String, NdrError> {
387 let invite = self.inner.lock().unwrap();
388 let event = invite.get_event()?;
389 Ok(serde_json::to_string(&event)?)
390 }
391
392 pub fn serialize(&self) -> Result<String, NdrError> {
394 let invite = self.inner.lock().unwrap();
395 Ok(invite.serialize()?)
396 }
397
398 pub fn accept(
400 &self,
401 invitee_pubkey_hex: String,
402 invitee_privkey_hex: String,
403 device_id: Option<String>,
404 ) -> Result<InviteAcceptResult, NdrError> {
405 let invite = self.inner.lock().unwrap();
406 let invitee_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&invitee_pubkey_hex)?;
407 let invitee_privkey = parse_private_key(&invitee_privkey_hex)?;
408
409 let (session, response_event) =
410 invite.accept(invitee_pubkey, invitee_privkey, device_id)?;
411 let response_event_json = serde_json::to_string(&response_event)?;
412
413 Ok(InviteAcceptResult {
414 session: Arc::new(SessionHandle {
415 inner: Mutex::new(session),
416 }),
417 response_event_json,
418 })
419 }
420
421 pub fn accept_with_owner(
423 &self,
424 invitee_pubkey_hex: String,
425 invitee_privkey_hex: String,
426 device_id: Option<String>,
427 owner_pubkey_hex: Option<String>,
428 ) -> Result<InviteAcceptResult, NdrError> {
429 let invite = self.inner.lock().unwrap();
430 let invitee_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&invitee_pubkey_hex)?;
431 let invitee_privkey = parse_private_key(&invitee_privkey_hex)?;
432 let owner_pubkey = match owner_pubkey_hex {
433 Some(h) => Some(nostr_double_ratchet::utils::pubkey_from_hex(&h)?),
434 None => None,
435 };
436
437 let (session, response_event) =
438 invite.accept_with_owner(invitee_pubkey, invitee_privkey, device_id, owner_pubkey)?;
439 let response_event_json = serde_json::to_string(&response_event)?;
440
441 Ok(InviteAcceptResult {
442 session: Arc::new(SessionHandle {
443 inner: Mutex::new(session),
444 }),
445 response_event_json,
446 })
447 }
448
449 pub fn set_purpose(&self, purpose: Option<String>) {
451 let mut invite = self.inner.lock().unwrap();
452 invite.purpose = purpose;
453 }
454
455 pub fn set_owner_pubkey_hex(&self, owner_pubkey_hex: Option<String>) -> Result<(), NdrError> {
457 let mut invite = self.inner.lock().unwrap();
458 invite.owner_public_key = match owner_pubkey_hex {
459 Some(h) => Some(nostr_double_ratchet::utils::pubkey_from_hex(&h)?),
460 None => None,
461 };
462 Ok(())
463 }
464
465 pub fn process_response(
469 &self,
470 event_json: String,
471 inviter_privkey_hex: String,
472 ) -> Result<Option<InviteProcessResult>, NdrError> {
473 let invite = self.inner.lock().unwrap();
474 let event: nostr::Event = serde_json::from_str(&event_json)?;
475 let inviter_privkey = parse_private_key(&inviter_privkey_hex)?;
476
477 let response = invite.process_invite_response(&event, inviter_privkey)?;
478 let Some(response) = response else {
479 return Ok(None);
480 };
481
482 Ok(Some(InviteProcessResult {
483 session: Arc::new(SessionHandle {
484 inner: Mutex::new(response.session),
485 }),
486 invitee_pubkey_hex: response.invitee_identity.to_hex(),
487 device_id: response.device_id,
488 owner_pubkey_hex: response.owner_public_key.map(|pk| pk.to_hex()),
489 }))
490 }
491
492 pub fn get_inviter_pubkey_hex(&self) -> String {
494 let invite = self.inner.lock().unwrap();
495 invite.inviter.to_hex()
496 }
497
498 pub fn get_shared_secret_hex(&self) -> String {
500 let invite = self.inner.lock().unwrap();
501 hex::encode(invite.shared_secret)
502 }
503}
504
505#[derive(uniffi::Object)]
507pub struct SessionHandle {
508 inner: Mutex<Session>,
509}
510
511#[uniffi::export]
512impl SessionHandle {
513 #[uniffi::constructor]
515 pub fn init(
516 their_ephemeral_pubkey_hex: String,
517 our_ephemeral_privkey_hex: String,
518 is_initiator: bool,
519 shared_secret_hex: String,
520 name: Option<String>,
521 ) -> Result<Arc<Self>, NdrError> {
522 let their_pubkey =
523 nostr_double_ratchet::utils::pubkey_from_hex(&their_ephemeral_pubkey_hex)?;
524 let our_privkey = parse_private_key(&our_ephemeral_privkey_hex)?;
525 let shared_secret = parse_secret(&shared_secret_hex)?;
526
527 let session = Session::init(their_pubkey, our_privkey, is_initiator, shared_secret, name)?;
528
529 Ok(Arc::new(Self {
530 inner: Mutex::new(session),
531 }))
532 }
533
534 #[uniffi::constructor]
536 pub fn from_state_json(state_json: String) -> Result<Arc<Self>, NdrError> {
537 let state: SessionState =
538 nostr_double_ratchet::utils::deserialize_session_state(&state_json)?;
539 let session = Session::new(state, "restored".to_string());
540
541 Ok(Arc::new(Self {
542 inner: Mutex::new(session),
543 }))
544 }
545
546 pub fn state_json(&self) -> Result<String, NdrError> {
548 let session = self.inner.lock().unwrap();
549 Ok(nostr_double_ratchet::utils::serialize_session_state(
550 &session.state,
551 )?)
552 }
553
554 pub fn can_send(&self) -> bool {
556 let session = self.inner.lock().unwrap();
557 session.can_send()
558 }
559
560 pub fn send_text(&self, text: String) -> Result<SendResult, NdrError> {
562 let mut session = self.inner.lock().unwrap();
563 let inner_event = nostr_double_ratchet::build_text_rumor(
564 nostr::Keys::generate().public_key(),
565 text,
566 vec![],
567 )?;
568 let outer_event = session.send_event(inner_event.clone())?;
569 let inner_event_json = serde_json::to_string(&inner_event)?;
570
571 Ok(SendResult {
572 outer_event_json: serde_json::to_string(&outer_event)?,
573 inner_event_json,
574 })
575 }
576
577 pub fn decrypt_event(&self, outer_event_json: String) -> Result<DecryptResult, NdrError> {
579 let mut session = self.inner.lock().unwrap();
580 let event: nostr::Event = serde_json::from_str(&outer_event_json)?;
581
582 let plaintext = session.receive(&event)?.unwrap_or_default();
583
584 let inner_event_json = if plaintext.starts_with('{') {
586 plaintext.clone()
587 } else {
588 serde_json::json!({
590 "content": plaintext
591 })
592 .to_string()
593 };
594
595 Ok(DecryptResult {
596 plaintext,
597 inner_event_json,
598 })
599 }
600
601 pub fn is_dr_message(&self, event_json: String) -> bool {
603 if let Ok(event) = serde_json::from_str::<nostr::Event>(&event_json) {
604 event.kind == nostr::Kind::Custom(nostr_double_ratchet::MESSAGE_EVENT_KIND as u16)
605 } else {
606 false
607 }
608 }
609}
610
611#[derive(uniffi::Object)]
613pub struct SessionManagerHandle {
614 runtime: NdrRuntime,
615}
616
617#[uniffi::export]
618impl SessionManagerHandle {
619 #[uniffi::constructor]
621 pub fn new(
622 our_pubkey_hex: String,
623 our_identity_privkey_hex: String,
624 device_id: String,
625 owner_pubkey_hex: Option<String>,
626 ) -> Result<Arc<Self>, NdrError> {
627 let our_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&our_pubkey_hex)?;
628 let our_identity_key = parse_private_key(&our_identity_privkey_hex)?;
629 let owner_pubkey = match owner_pubkey_hex {
630 Some(h) => nostr_double_ratchet::utils::pubkey_from_hex(&h)?,
631 None => our_pubkey,
632 };
633
634 let storage: Arc<dyn StorageAdapter> = Arc::new(InMemoryStorage::new());
635 let runtime = NdrRuntime::new(
636 our_pubkey,
637 our_identity_key,
638 device_id,
639 owner_pubkey,
640 Some(storage),
641 None,
642 );
643
644 Ok(Arc::new(Self { runtime }))
645 }
646
647 #[uniffi::constructor]
649 pub fn new_with_storage_path(
650 our_pubkey_hex: String,
651 our_identity_privkey_hex: String,
652 device_id: String,
653 storage_path: String,
654 owner_pubkey_hex: Option<String>,
655 ) -> Result<Arc<Self>, NdrError> {
656 let our_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&our_pubkey_hex)?;
657 let our_identity_key = parse_private_key(&our_identity_privkey_hex)?;
658 let owner_pubkey = match owner_pubkey_hex {
659 Some(h) => nostr_double_ratchet::utils::pubkey_from_hex(&h)?,
660 None => our_pubkey,
661 };
662
663 let storage = FileStorageAdapter::new(std::path::PathBuf::from(storage_path))
664 .map_err(NdrError::from)?;
665 let storage: Arc<dyn StorageAdapter> = Arc::new(storage);
666
667 let runtime = NdrRuntime::new(
668 our_pubkey,
669 our_identity_key,
670 device_id,
671 owner_pubkey,
672 Some(storage),
673 None,
674 );
675
676 Ok(Arc::new(Self { runtime }))
677 }
678
679 pub fn init(&self) -> Result<(), NdrError> {
681 self.runtime.init()?;
682 Ok(())
683 }
684
685 pub fn setup_user(&self, user_pubkey_hex: String) -> Result<(), NdrError> {
687 let user_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&user_pubkey_hex)?;
688 self.runtime.setup_user(user_pubkey)?;
689 Ok(())
690 }
691
692 pub fn accept_invite_from_url(
697 &self,
698 invite_url: String,
699 owner_pubkey_hint_hex: Option<String>,
700 ) -> Result<SessionManagerAcceptInviteResult, NdrError> {
701 let invite = Invite::from_url(&invite_url)?;
702 let owner_pubkey_hint = owner_pubkey_hint_hex
703 .as_deref()
704 .map(nostr_double_ratchet::utils::pubkey_from_hex)
705 .transpose()?;
706
707 let accepted = self.runtime.accept_invite(&invite, owner_pubkey_hint)?;
708
709 Ok(SessionManagerAcceptInviteResult {
710 owner_pubkey_hex: accepted.owner_pubkey.to_hex(),
711 inviter_device_pubkey_hex: accepted.inviter_device_pubkey.to_hex(),
712 device_id: accepted.device_id,
713 created_new_session: accepted.created_new_session,
714 })
715 }
716
717 pub fn accept_invite_from_event_json(
719 &self,
720 event_json: String,
721 owner_pubkey_hint_hex: Option<String>,
722 ) -> Result<SessionManagerAcceptInviteResult, NdrError> {
723 let event: nostr::Event = serde_json::from_str(&event_json)?;
724 let invite = Invite::from_event(&event)?;
725 let owner_pubkey_hint = owner_pubkey_hint_hex
726 .as_deref()
727 .map(nostr_double_ratchet::utils::pubkey_from_hex)
728 .transpose()?;
729
730 let accepted = self.runtime.accept_invite(&invite, owner_pubkey_hint)?;
731
732 Ok(SessionManagerAcceptInviteResult {
733 owner_pubkey_hex: accepted.owner_pubkey.to_hex(),
734 inviter_device_pubkey_hex: accepted.inviter_device_pubkey.to_hex(),
735 device_id: accepted.device_id,
736 created_new_session: accepted.created_new_session,
737 })
738 }
739
740 pub fn send_text(
742 &self,
743 recipient_pubkey_hex: String,
744 text: String,
745 expires_at_seconds: Option<u64>,
746 ) -> Result<Vec<String>, NdrError> {
747 let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
748 let options = expires_at_seconds.map(|expires_at| nostr_double_ratchet::SendOptions {
749 expires_at: Some(expires_at),
750 ttl_seconds: None,
751 });
752 Ok(self.runtime.send_text(recipient, text, options)?)
753 }
754
755 pub fn send_text_with_inner_id(
758 &self,
759 recipient_pubkey_hex: String,
760 text: String,
761 expires_at_seconds: Option<u64>,
762 ) -> Result<SendTextResult, NdrError> {
763 let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
764 let options = expires_at_seconds.map(|expires_at| nostr_double_ratchet::SendOptions {
765 expires_at: Some(expires_at),
766 ttl_seconds: None,
767 });
768 let (inner_id, outer_event_ids) = self
769 .runtime
770 .send_text_with_inner_id(recipient, text, options)?;
771 Ok(SendTextResult {
772 inner_id,
773 outer_event_ids,
774 })
775 }
776
777 pub fn send_event_with_inner_id(
786 &self,
787 recipient_pubkey_hex: String,
788 kind: u32,
789 content: String,
790 tags_json: String,
791 created_at_seconds: Option<u64>,
792 ) -> Result<SendTextResult, NdrError> {
793 let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
794
795 let tags_vec: Vec<Vec<String>> = if tags_json.trim().is_empty() {
797 Vec::new()
798 } else {
799 serde_json::from_str(&tags_json)?
800 };
801
802 let mut ms_value: Option<u64> = None;
805 for t in tags_vec.iter() {
806 if t.first().map(|s| s.as_str()) != Some("ms") {
807 continue;
808 }
809 if let Some(v) = t.get(1) {
810 ms_value = v.parse::<u64>().ok();
811 break;
812 }
813 }
814
815 let mut tags: Vec<nostr::Tag> = Vec::with_capacity(tags_vec.len() + 1);
816 for t in tags_vec {
817 tags.push(nostr::Tag::parse(&t).map_err(|e| NdrError::InvalidEvent(e.to_string()))?);
818 }
819
820 let owner_pubkey = self.runtime.get_owner_pubkey();
821 let event = nostr_double_ratchet::build_inner_event(
822 owner_pubkey,
823 kind,
824 content,
825 tags,
826 nostr_double_ratchet::InnerEventBuildOptions {
827 created_at_seconds,
828 ms: ms_value,
829 ensure_ms_tag: true,
830 },
831 )?;
832 let inner_id = event
833 .id
834 .as_ref()
835 .map(|id| id.to_string())
836 .unwrap_or_default();
837
838 let outer_event_ids = self.runtime.send_event(recipient, event)?;
839
840 Ok(SendTextResult {
841 inner_id,
842 outer_event_ids,
843 })
844 }
845
846 pub fn send_rumor_json(
850 &self,
851 recipient_pubkey_hex: String,
852 rumor_json: String,
853 ) -> Result<SendTextResult, NdrError> {
854 let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
855 let event: nostr::UnsignedEvent = serde_json::from_str(&rumor_json)?;
856 let inner_id = unsigned_event_id_string(&event);
857 let outer_event_ids = self.runtime.send_event(recipient, event)?;
858
859 Ok(SendTextResult {
860 inner_id,
861 outer_event_ids,
862 })
863 }
864
865 pub fn group_upsert(&self, group: FfiGroupData) -> Result<(), NdrError> {
867 self.runtime.with_group_context(|_, group_manager, _| {
868 group_manager.upsert_group(ffi_group_data_to_group_data(group))
869 })?;
870 Ok(())
871 }
872
873 pub fn group_create(
875 &self,
876 name: String,
877 member_owner_pubkeys: Vec<String>,
878 fanout_metadata: Option<bool>,
879 now_ms: Option<u64>,
880 ) -> Result<GroupCreateResult, NdrError> {
881 let member_refs: Vec<&str> = member_owner_pubkeys.iter().map(String::as_str).collect();
882 let should_fanout = fanout_metadata.unwrap_or(true);
883
884 self.runtime
885 .with_group_context(|session_manager, group_manager, _| {
886 let mut send_pairwise = |recipient_owner: nostr::PublicKey,
887 rumor: &nostr::UnsignedEvent|
888 -> nostr_double_ratchet::Result<()> {
889 session_manager.send_event(recipient_owner, rumor.clone())?;
890 Ok(())
891 };
892
893 let mut opts = CreateGroupOptions {
894 send_pairwise: None,
895 fanout_metadata: should_fanout,
896 now_ms,
897 };
898 if should_fanout {
899 opts.send_pairwise = Some(&mut send_pairwise);
900 }
901
902 let created = group_manager.create_group(&name, &member_refs, opts)?;
903 let metadata_rumor_json = created
904 .metadata_rumor
905 .as_ref()
906 .map(serde_json::to_string)
907 .transpose()?;
908
909 Ok(GroupCreateResult {
910 group: group_data_to_ffi_group_data(created.group),
911 metadata_rumor_json,
912 fanout: GroupCreateFanout {
913 enabled: created.fanout.enabled,
914 attempted: created.fanout.attempted as u64,
915 succeeded: created.fanout.succeeded,
916 failed: created.fanout.failed,
917 },
918 })
919 })
920 }
921
922 pub fn group_remove(&self, group_id: String) {
924 self.runtime
925 .with_group_context(|_, group_manager, _| group_manager.remove_group(&group_id));
926 }
927
928 pub fn group_known_sender_event_pubkeys(&self) -> Vec<String> {
930 self.runtime
931 .group_known_sender_event_pubkeys()
932 .into_iter()
933 .map(|pk| pk.to_hex())
934 .collect()
935 }
936
937 pub fn group_outer_subscription_plan(&self) -> GroupOuterSubscriptionPlanResult {
940 let plan = self.runtime.group_outer_subscription_plan();
941 GroupOuterSubscriptionPlanResult {
942 authors: plan.authors.into_iter().map(|pk| pk.to_hex()).collect(),
943 added_authors: plan
944 .added_authors
945 .into_iter()
946 .map(|pk| pk.to_hex())
947 .collect(),
948 }
949 }
950
951 pub fn group_send_event(
956 &self,
957 group_id: String,
958 kind: u32,
959 content: String,
960 tags_json: String,
961 now_ms: Option<u64>,
962 ) -> Result<GroupSendResult, NdrError> {
963 let tags: Vec<Vec<String>> = if tags_json.trim().is_empty() {
964 Vec::new()
965 } else {
966 serde_json::from_str(&tags_json)?
967 };
968
969 self.runtime
970 .with_group_context(|session_manager, group_manager, event_tx| {
971 let mut send_pairwise = |recipient_owner: nostr::PublicKey,
972 rumor: &nostr::UnsignedEvent|
973 -> nostr_double_ratchet::Result<()> {
974 session_manager.send_event(recipient_owner, rumor.clone())?;
975 Ok(())
976 };
977
978 let mut publish_outer = |outer: &nostr::Event| -> nostr_double_ratchet::Result<()> {
979 event_tx
980 .send(SessionManagerEvent::PublishSigned(outer.clone()))
981 .map_err(|e| nostr_double_ratchet::Error::Storage(e.to_string()))?;
982 Ok(())
983 };
984
985 let result = group_manager.send_event(
986 &group_id,
987 GroupSendEvent {
988 kind,
989 content,
990 tags,
991 },
992 &mut send_pairwise,
993 &mut publish_outer,
994 now_ms,
995 )?;
996
997 Ok(GroupSendResult {
998 outer_event_json: serde_json::to_string(&result.outer)?,
999 inner_event_json: serde_json::to_string(&result.inner)?,
1000 outer_event_id: result.outer.id.to_string(),
1001 inner_event_id: unsigned_event_id_string(&result.inner),
1002 })
1003 })
1004 }
1005
1006 pub fn group_handle_incoming_session_event(
1008 &self,
1009 event_json: String,
1010 from_owner_pubkey_hex: String,
1011 from_sender_device_pubkey_hex: Option<String>,
1012 ) -> Result<Vec<GroupDecryptedResult>, NdrError> {
1013 let event: nostr::UnsignedEvent = serde_json::from_str(&event_json)?;
1014 let from_owner_pubkey =
1015 nostr_double_ratchet::utils::pubkey_from_hex(&from_owner_pubkey_hex)?;
1016
1017 let from_sender_device_pubkey = match from_sender_device_pubkey_hex {
1018 Some(hex) => Some(nostr_double_ratchet::utils::pubkey_from_hex(&hex)?),
1019 None => Some(event.pubkey),
1020 };
1021
1022 let decrypted = self.runtime.group_handle_incoming_session_event(
1023 &event,
1024 from_owner_pubkey,
1025 from_sender_device_pubkey,
1026 );
1027 Ok(decrypted
1028 .into_iter()
1029 .map(group_decrypted_to_result)
1030 .collect())
1031 }
1032
1033 pub fn group_handle_outer_event(
1035 &self,
1036 event_json: String,
1037 ) -> Result<Option<GroupDecryptedResult>, NdrError> {
1038 let event: nostr::Event = serde_json::from_str(&event_json)?;
1039 Ok(self
1040 .runtime
1041 .group_handle_outer_event(&event)
1042 .map(group_decrypted_to_result))
1043 }
1044
1045 pub fn send_receipt(
1047 &self,
1048 recipient_pubkey_hex: String,
1049 receipt_type: String,
1050 message_ids: Vec<String>,
1051 expires_at_seconds: Option<u64>,
1052 ) -> Result<Vec<String>, NdrError> {
1053 let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
1054 let options = expires_at_seconds.map(|expires_at| nostr_double_ratchet::SendOptions {
1055 expires_at: Some(expires_at),
1056 ttl_seconds: None,
1057 });
1058 Ok(self
1059 .runtime
1060 .send_receipt(recipient, &receipt_type, message_ids, options)?)
1061 }
1062
1063 pub fn send_typing(
1065 &self,
1066 recipient_pubkey_hex: String,
1067 expires_at_seconds: Option<u64>,
1068 ) -> Result<Vec<String>, NdrError> {
1069 let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
1070 let options = expires_at_seconds.map(|expires_at| nostr_double_ratchet::SendOptions {
1071 expires_at: Some(expires_at),
1072 ttl_seconds: None,
1073 });
1074 Ok(self.runtime.send_typing(recipient, options)?)
1075 }
1076
1077 pub fn send_reaction(
1079 &self,
1080 recipient_pubkey_hex: String,
1081 message_id: String,
1082 emoji: String,
1083 expires_at_seconds: Option<u64>,
1084 ) -> Result<Vec<String>, NdrError> {
1085 let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
1086 let options = expires_at_seconds.map(|expires_at| nostr_double_ratchet::SendOptions {
1087 expires_at: Some(expires_at),
1088 ttl_seconds: None,
1089 });
1090 Ok(self
1091 .runtime
1092 .send_reaction(recipient, message_id, emoji, options)?)
1093 }
1094
1095 pub fn import_session_state(
1097 &self,
1098 peer_pubkey_hex: String,
1099 state_json: String,
1100 device_id: Option<String>,
1101 ) -> Result<(), NdrError> {
1102 let peer_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&peer_pubkey_hex)?;
1103 let state: SessionState =
1104 nostr_double_ratchet::utils::deserialize_session_state(&state_json)?;
1105 self.runtime
1106 .import_session_state(peer_pubkey, device_id, state)?;
1107 Ok(())
1108 }
1109
1110 pub fn get_active_session_state(
1112 &self,
1113 peer_pubkey_hex: String,
1114 ) -> Result<Option<String>, NdrError> {
1115 let peer_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&peer_pubkey_hex)?;
1116 if let Some(state) = self.runtime.export_active_session_state(peer_pubkey)? {
1117 Ok(Some(nostr_double_ratchet::utils::serialize_session_state(
1118 &state,
1119 )?))
1120 } else {
1121 Ok(None)
1122 }
1123 }
1124
1125 pub fn known_peer_owner_pubkeys(&self) -> Vec<String> {
1127 self.runtime
1128 .known_peer_owner_pubkeys()
1129 .into_iter()
1130 .map(|pubkey| pubkey.to_hex())
1131 .collect()
1132 }
1133
1134 pub fn get_stored_user_record_json(
1136 &self,
1137 peer_owner_pubkey_hex: String,
1138 ) -> Result<Option<String>, NdrError> {
1139 let peer_owner_pubkey =
1140 nostr_double_ratchet::utils::pubkey_from_hex(&peer_owner_pubkey_hex)?;
1141 Ok(self
1142 .runtime
1143 .get_stored_user_record_json(peer_owner_pubkey)?)
1144 }
1145
1146 pub fn get_message_push_author_pubkeys(
1148 &self,
1149 peer_owner_pubkey_hex: String,
1150 ) -> Result<Vec<String>, NdrError> {
1151 let peer_owner_pubkey =
1152 nostr_double_ratchet::utils::pubkey_from_hex(&peer_owner_pubkey_hex)?;
1153 Ok(self
1154 .runtime
1155 .get_message_push_author_pubkeys(peer_owner_pubkey)
1156 .into_iter()
1157 .map(|pubkey| pubkey.to_hex())
1158 .collect())
1159 }
1160
1161 pub fn get_message_push_session_states(
1163 &self,
1164 peer_owner_pubkey_hex: String,
1165 ) -> Result<Vec<MessagePushSessionStateResult>, NdrError> {
1166 let peer_owner_pubkey =
1167 nostr_double_ratchet::utils::pubkey_from_hex(&peer_owner_pubkey_hex)?;
1168 self.runtime
1169 .get_message_push_session_states(peer_owner_pubkey)
1170 .into_iter()
1171 .map(|snapshot| {
1172 Ok(MessagePushSessionStateResult {
1173 state_json: nostr_double_ratchet::utils::serialize_session_state(
1174 &snapshot.state,
1175 )?,
1176 tracked_sender_pubkeys: snapshot
1177 .tracked_sender_pubkeys
1178 .into_iter()
1179 .map(|pubkey| pubkey.to_hex())
1180 .collect(),
1181 has_receiving_capability: snapshot.has_receiving_capability,
1182 })
1183 })
1184 .collect()
1185 }
1186
1187 pub fn process_event(&self, event_json: String) -> Result<(), NdrError> {
1189 let event: nostr::Event = serde_json::from_str(&event_json)?;
1190 self.runtime.process_received_event(event);
1191 Ok(())
1192 }
1193
1194 pub fn drain_events(&self) -> Result<Vec<PubSubEvent>, NdrError> {
1196 let mut events = Vec::new();
1197
1198 for event in self.runtime.drain_events() {
1199 let pubsub_event = match event {
1200 SessionManagerEvent::Publish(unsigned) => PubSubEvent {
1201 kind: "publish".to_string(),
1202 subid: None,
1203 filter_json: None,
1204 event_json: Some(serde_json::to_string(&unsigned)?),
1205 sender_pubkey_hex: None,
1206 content: None,
1207 event_id: None,
1208 },
1209 SessionManagerEvent::PublishSigned(signed) => PubSubEvent {
1210 kind: "publish_signed".to_string(),
1211 subid: None,
1212 filter_json: None,
1213 event_json: Some(serde_json::to_string(&signed)?),
1214 sender_pubkey_hex: None,
1215 content: None,
1216 event_id: None,
1217 },
1218 SessionManagerEvent::PublishSignedForInnerEvent {
1219 event,
1220 inner_event_id,
1221 ..
1222 } => PubSubEvent {
1223 kind: "publish_signed".to_string(),
1224 subid: None,
1225 filter_json: None,
1226 event_json: Some(serde_json::to_string(&event)?),
1227 sender_pubkey_hex: None,
1228 content: None,
1229 event_id: inner_event_id,
1230 },
1231 SessionManagerEvent::Subscribe { subid, filter_json } => PubSubEvent {
1232 kind: "subscribe".to_string(),
1233 subid: Some(subid),
1234 filter_json: Some(filter_json),
1235 event_json: None,
1236 sender_pubkey_hex: None,
1237 content: None,
1238 event_id: None,
1239 },
1240 SessionManagerEvent::Unsubscribe(subid) => PubSubEvent {
1241 kind: "unsubscribe".to_string(),
1242 subid: Some(subid),
1243 filter_json: None,
1244 event_json: None,
1245 sender_pubkey_hex: None,
1246 content: None,
1247 event_id: None,
1248 },
1249 SessionManagerEvent::DecryptedMessage {
1250 sender,
1251 content,
1252 event_id,
1253 ..
1254 } => PubSubEvent {
1255 kind: "decrypted_message".to_string(),
1256 subid: None,
1257 filter_json: None,
1258 event_json: None,
1259 sender_pubkey_hex: Some(sender.to_hex()),
1260 content: Some(content),
1261 event_id,
1262 },
1263 SessionManagerEvent::ReceivedEvent(event) => PubSubEvent {
1264 kind: "received_event".to_string(),
1265 subid: None,
1266 filter_json: None,
1267 event_json: Some(serde_json::to_string(&event)?),
1268 sender_pubkey_hex: None,
1269 content: None,
1270 event_id: None,
1271 },
1272 };
1273 events.push(pubsub_event);
1274 }
1275
1276 Ok(events)
1277 }
1278
1279 pub fn get_device_id(&self) -> String {
1281 self.runtime.get_device_id().to_string()
1282 }
1283
1284 pub fn get_our_pubkey_hex(&self) -> String {
1286 self.runtime.get_our_pubkey().to_hex()
1287 }
1288
1289 pub fn get_owner_pubkey_hex(&self) -> String {
1291 self.runtime.get_owner_pubkey().to_hex()
1292 }
1293
1294 pub fn get_total_sessions(&self) -> u64 {
1296 self.runtime.get_total_sessions() as u64
1297 }
1298}
1299
1300#[cfg(test)]
1301mod architecture_tests {
1302 #[test]
1303 fn ffi_handle_does_not_reach_into_session_manager() {
1304 let source = include_str!("lib.rs");
1305 let banned = concat!(".session_", "manager()");
1306 assert!(
1307 !source.contains(banned),
1308 "FFI should use NdrRuntime APIs instead of direct SessionManager access"
1309 );
1310 }
1311}
1312
1313fn parse_private_key(hex_str: &str) -> Result<[u8; 32], NdrError> {
1315 let bytes = hex::decode(hex_str).map_err(|_| NdrError::InvalidKey("Invalid hex".into()))?;
1316 if bytes.len() != 32 {
1317 return Err(NdrError::InvalidKey("Private key must be 32 bytes".into()));
1318 }
1319 let mut arr = [0u8; 32];
1320 arr.copy_from_slice(&bytes);
1321 Ok(arr)
1322}
1323
1324fn parse_secret(hex_str: &str) -> Result<[u8; 32], NdrError> {
1326 let bytes = hex::decode(hex_str).map_err(|_| NdrError::Serialization("Invalid hex".into()))?;
1327 if bytes.len() != 32 {
1328 return Err(NdrError::Serialization("Secret must be 32 bytes".into()));
1329 }
1330 let mut arr = [0u8; 32];
1331 arr.copy_from_slice(&bytes);
1332 Ok(arr)
1333}
1334
1335fn ffi_group_data_to_group_data(group: FfiGroupData) -> GroupData {
1336 GroupData {
1337 id: group.id,
1338 name: group.name,
1339 description: group.description,
1340 picture: group.picture,
1341 members: group.members,
1342 admins: group.admins,
1343 created_at: group.created_at_ms,
1344 secret: group.secret,
1345 accepted: group.accepted,
1346 }
1347}
1348
1349fn group_data_to_ffi_group_data(group: GroupData) -> FfiGroupData {
1350 FfiGroupData {
1351 id: group.id,
1352 name: group.name,
1353 description: group.description,
1354 picture: group.picture,
1355 members: group.members,
1356 admins: group.admins,
1357 created_at_ms: group.created_at,
1358 secret: group.secret,
1359 accepted: group.accepted,
1360 }
1361}
1362
1363fn unsigned_event_id_string(event: &nostr::UnsignedEvent) -> String {
1364 event
1365 .id
1366 .as_ref()
1367 .map(std::string::ToString::to_string)
1368 .unwrap_or_default()
1369}
1370
1371fn group_decrypted_to_result(event: GroupDecryptedEvent) -> GroupDecryptedResult {
1372 GroupDecryptedResult {
1373 group_id: event.group_id,
1374 sender_event_pubkey_hex: event.sender_event_pubkey.to_hex(),
1375 sender_device_pubkey_hex: event.sender_device_pubkey.to_hex(),
1376 sender_owner_pubkey_hex: event.sender_owner_pubkey.map(|pk| pk.to_hex()),
1377 outer_event_id: event.outer_event_id,
1378 outer_created_at: event.outer_created_at,
1379 key_id: event.key_id,
1380 message_number: event.message_number,
1381 inner_event_json: serde_json::to_string(&event.inner).unwrap_or_default(),
1382 inner_event_id: unsigned_event_id_string(&event.inner),
1383 }
1384}
1385
1386uniffi::setup_scaffolding!();
1387
1388#[derive(uniffi::Record)]
1390pub struct InviteProcessResult {
1391 pub session: Arc<SessionHandle>,
1392 pub invitee_pubkey_hex: String,
1393 pub device_id: Option<String>,
1394 pub owner_pubkey_hex: Option<String>,
1395}
1396
1397#[derive(uniffi::Record)]
1399pub struct SendTextResult {
1400 pub inner_id: String,
1401 pub outer_event_ids: Vec<String>,
1402}
1403
1404#[cfg(test)]
1405mod tests {
1406 use super::*;
1407
1408 #[test]
1409 fn test_version() {
1410 let v = version();
1411 assert!(!v.is_empty());
1412 assert!(v.contains('.'));
1413 }
1414
1415 #[test]
1416 fn test_keypair_generate_formats_hex() {
1417 let kp = generate_keypair();
1418 assert_eq!(kp.public_key_hex.len(), 64);
1419 assert_eq!(kp.private_key_hex.len(), 64);
1420 assert!(hex::decode(&kp.public_key_hex).is_ok());
1422 assert!(hex::decode(&kp.private_key_hex).is_ok());
1423 }
1424
1425 #[test]
1426 fn test_derive_public_key_matches_generate() {
1427 let kp = generate_keypair();
1428 let pubkey = derive_public_key(kp.private_key_hex.clone()).unwrap();
1429 assert_eq!(pubkey, kp.public_key_hex);
1430 }
1431
1432 #[test]
1433 fn test_invite_url_roundtrip() {
1434 let kp = generate_keypair();
1435 let invite = InviteHandle::create_new(kp.public_key_hex.clone(), None, None).unwrap();
1436
1437 let url = invite.to_url("https://example.com".to_string()).unwrap();
1438 assert!(url.starts_with("https://example.com"));
1439
1440 let restored = InviteHandle::from_url(url).unwrap();
1441 assert_eq!(
1442 invite.get_inviter_pubkey_hex(),
1443 restored.get_inviter_pubkey_hex()
1444 );
1445 assert_eq!(
1446 invite.get_shared_secret_hex(),
1447 restored.get_shared_secret_hex()
1448 );
1449 }
1450
1451 #[test]
1452 fn test_invite_serialize_roundtrip() {
1453 let kp = generate_keypair();
1454 let invite =
1455 InviteHandle::create_new(kp.public_key_hex.clone(), Some("device1".into()), Some(5))
1456 .unwrap();
1457
1458 let json = invite.serialize().unwrap();
1459 let restored = InviteHandle::deserialize(json).unwrap();
1460
1461 assert_eq!(
1462 invite.get_inviter_pubkey_hex(),
1463 restored.get_inviter_pubkey_hex()
1464 );
1465 assert_eq!(
1466 invite.get_shared_secret_hex(),
1467 restored.get_shared_secret_hex()
1468 );
1469 }
1470
1471 #[test]
1472 fn test_invite_accept_returns_session_and_event() {
1473 let inviter_kp = generate_keypair();
1474 let invitee_kp = generate_keypair();
1475
1476 let invite =
1477 InviteHandle::create_new(inviter_kp.public_key_hex.clone(), None, None).unwrap();
1478 let url = invite.to_url("https://example.com".to_string()).unwrap();
1479
1480 let invite_copy = InviteHandle::from_url(url).unwrap();
1481 let result = invite_copy
1482 .accept(
1483 invitee_kp.public_key_hex.clone(),
1484 invitee_kp.private_key_hex.clone(),
1485 None,
1486 )
1487 .unwrap();
1488
1489 assert!(!result.response_event_json.is_empty());
1490 assert!(result.session.can_send());
1491 }
1492
1493 #[test]
1494 fn test_invite_process_response_yields_working_session_pair() {
1495 let alice_kp = generate_keypair();
1496 let bob_kp = generate_keypair();
1497
1498 let invite = InviteHandle::create_new(alice_kp.public_key_hex.clone(), None, None).unwrap();
1499 let accept = invite
1500 .accept(
1501 bob_kp.public_key_hex.clone(),
1502 bob_kp.private_key_hex.clone(),
1503 None,
1504 )
1505 .unwrap();
1506
1507 let processed = invite
1508 .process_response(
1509 accept.response_event_json.clone(),
1510 alice_kp.private_key_hex.clone(),
1511 )
1512 .unwrap()
1513 .unwrap();
1514
1515 let bob_send = accept.session.send_text("hi".to_string()).unwrap();
1517 let alice_decrypt = processed
1518 .session
1519 .decrypt_event(bob_send.outer_event_json.clone())
1520 .unwrap();
1521 assert!(alice_decrypt.plaintext.contains("hi"));
1522
1523 assert!(processed.session.can_send());
1525
1526 let alice_reply = processed.session.send_text("ok".to_string()).unwrap();
1527 let bob_decrypt = accept
1528 .session
1529 .decrypt_event(alice_reply.outer_event_json)
1530 .unwrap();
1531 assert!(bob_decrypt.plaintext.contains("ok"));
1532 }
1533
1534 #[test]
1535 fn test_session_send_receive() {
1536 let alice_kp = generate_keypair();
1538 let bob_kp = generate_keypair();
1539
1540 let invite = InviteHandle::create_new(alice_kp.public_key_hex.clone(), None, None).unwrap();
1542 let invite_json = invite.serialize().unwrap();
1543
1544 let bob_invite = InviteHandle::deserialize(invite_json).unwrap();
1546 let accept_result = bob_invite
1547 .accept(
1548 bob_kp.public_key_hex.clone(),
1549 bob_kp.private_key_hex.clone(),
1550 None,
1551 )
1552 .unwrap();
1553
1554 let bob_session = accept_result.session;
1555
1556 assert!(bob_session.can_send());
1559
1560 let send_result = bob_session.send_text("Hello Alice!".to_string()).unwrap();
1562 assert!(!send_result.outer_event_json.is_empty());
1563 }
1564
1565 #[test]
1566 fn test_session_state_roundtrip() {
1567 let alice_kp = generate_keypair();
1568 let bob_kp = generate_keypair();
1569
1570 let invite = InviteHandle::create_new(alice_kp.public_key_hex.clone(), None, None).unwrap();
1571 let invite_json = invite.serialize().unwrap();
1572
1573 let bob_invite = InviteHandle::deserialize(invite_json).unwrap();
1574 let accept_result = bob_invite
1575 .accept(
1576 bob_kp.public_key_hex.clone(),
1577 bob_kp.private_key_hex.clone(),
1578 None,
1579 )
1580 .unwrap();
1581
1582 let session = accept_result.session;
1583 let state_json = session.state_json().unwrap();
1584
1585 let restored = SessionHandle::from_state_json(state_json).unwrap();
1587 assert_eq!(session.can_send(), restored.can_send());
1588 }
1589
1590 #[test]
1591 fn test_group_send_event_tracks_sender_event_pubkey() {
1592 let kp = generate_keypair();
1593 let manager = SessionManagerHandle::new(
1594 kp.public_key_hex.clone(),
1595 kp.private_key_hex.clone(),
1596 kp.public_key_hex.clone(),
1597 None,
1598 )
1599 .unwrap();
1600 manager.init().unwrap();
1601
1602 manager
1603 .group_upsert(FfiGroupData {
1604 id: "group-ffi-test".to_string(),
1605 name: "ffi".to_string(),
1606 description: None,
1607 picture: None,
1608 members: vec![kp.public_key_hex.clone()],
1609 admins: vec![kp.public_key_hex.clone()],
1610 created_at_ms: 1_700_000_000_000,
1611 secret: None,
1612 accepted: Some(true),
1613 })
1614 .unwrap();
1615
1616 assert!(manager.group_known_sender_event_pubkeys().is_empty());
1617
1618 let send = manager
1619 .group_send_event(
1620 "group-ffi-test".to_string(),
1621 14,
1622 "hello".to_string(),
1623 "[]".to_string(),
1624 Some(1_700_000_000_000),
1625 )
1626 .unwrap();
1627 assert!(!send.outer_event_json.is_empty());
1628 assert!(!send.inner_event_json.is_empty());
1629 assert!(!send.outer_event_id.is_empty());
1630 assert!(!send.inner_event_id.is_empty());
1631
1632 let sender_event_pubkeys = manager.group_known_sender_event_pubkeys();
1633 assert_eq!(
1634 sender_event_pubkeys.len(),
1635 0,
1636 "local sender-event pubkeys should be filtered from subscription lists"
1637 );
1638 }
1639
1640 #[test]
1641 fn test_group_create_returns_group_and_metadata_rumor() {
1642 let kp = generate_keypair();
1643 let manager = SessionManagerHandle::new(
1644 kp.public_key_hex.clone(),
1645 kp.private_key_hex.clone(),
1646 kp.public_key_hex.clone(),
1647 None,
1648 )
1649 .unwrap();
1650 manager.init().unwrap();
1651
1652 let created = manager
1653 .group_create(
1654 "ffi-created".to_string(),
1655 vec![kp.public_key_hex.clone()],
1656 Some(true),
1657 Some(1_700_000_123_000),
1658 )
1659 .unwrap();
1660
1661 assert_eq!(created.group.name, "ffi-created");
1662 assert!(created.group.members.contains(&kp.public_key_hex));
1663 assert!(created.metadata_rumor_json.is_some());
1664 assert!(created.fanout.enabled);
1665 }
1666
1667 #[test]
1668 fn test_send_rumor_json_preserves_inner_id() {
1669 let alice = generate_keypair();
1670 let bob = generate_keypair();
1671 let sender_device = generate_keypair();
1672
1673 let manager = SessionManagerHandle::new(
1674 alice.public_key_hex.clone(),
1675 alice.private_key_hex.clone(),
1676 alice.public_key_hex.clone(),
1677 None,
1678 )
1679 .unwrap();
1680 manager.init().unwrap();
1681
1682 let our_next = nostr::Keys::generate();
1683 let state = SessionState {
1684 root_key: [7u8; 32],
1685 their_current_nostr_public_key: Some(
1686 nostr_double_ratchet::utils::pubkey_from_hex(&bob.public_key_hex).unwrap(),
1687 ),
1688 their_next_nostr_public_key: None,
1689 our_current_nostr_key: None,
1690 our_next_nostr_key: nostr_double_ratchet::SerializableKeyPair {
1691 public_key: our_next.public_key(),
1692 private_key: our_next.secret_key().secret_bytes(),
1693 },
1694 receiving_chain_key: None,
1695 sending_chain_key: Some([9u8; 32]),
1696 sending_chain_message_number: 0,
1697 receiving_chain_message_number: 0,
1698 previous_sending_chain_message_count: 0,
1699 skipped_keys: std::collections::HashMap::new(),
1700 };
1701
1702 manager
1703 .import_session_state(
1704 bob.public_key_hex.clone(),
1705 nostr_double_ratchet::utils::serialize_session_state(&state).unwrap(),
1706 Some("bob-device".to_string()),
1707 )
1708 .unwrap();
1709
1710 let rumor = nostr::EventBuilder::new(nostr::Kind::Custom(14), "raw rumor")
1711 .tags(vec![
1712 nostr::Tag::parse(&["l".to_string(), "group-ffi-test".to_string()]).unwrap(),
1713 nostr::Tag::parse(&["ms".to_string(), "1700000000000".to_string()]).unwrap(),
1714 ])
1715 .custom_created_at(nostr::Timestamp::from(1_700_000_000))
1716 .build(
1717 nostr_double_ratchet::utils::pubkey_from_hex(&sender_device.public_key_hex)
1718 .unwrap(),
1719 );
1720 let rumor_json = serde_json::to_string(&rumor).unwrap();
1721
1722 let send = manager
1723 .send_rumor_json(bob.public_key_hex.clone(), rumor_json)
1724 .unwrap();
1725
1726 assert_eq!(
1727 send.inner_id,
1728 rumor.id.as_ref().map(ToString::to_string).unwrap()
1729 );
1730 }
1731
1732 #[test]
1733 fn test_is_dr_message() {
1734 let kp = generate_keypair();
1735 let invite = InviteHandle::create_new(kp.public_key_hex.clone(), None, None).unwrap();
1736
1737 let bob_kp = generate_keypair();
1738 let accept_result = invite
1739 .accept(
1740 bob_kp.public_key_hex.clone(),
1741 bob_kp.private_key_hex.clone(),
1742 None,
1743 )
1744 .unwrap();
1745
1746 let session = accept_result.session;
1747 let send_result = session.send_text("test".to_string()).unwrap();
1748
1749 assert!(session.is_dr_message(send_result.outer_event_json));
1751
1752 let non_dr_event = serde_json::json!({
1754 "id": "0000000000000000000000000000000000000000000000000000000000000000",
1755 "pubkey": "0000000000000000000000000000000000000000000000000000000000000000",
1756 "created_at": 0,
1757 "kind": 1,
1758 "tags": [],
1759 "content": "test",
1760 "sig": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
1761 });
1762 assert!(!session.is_dr_message(non_dr_event.to_string()));
1763 }
1764
1765 #[test]
1766 fn test_app_keys_labels_roundtrip() {
1767 let owner = generate_keypair();
1768 let laptop = generate_keypair();
1769 let phone = generate_keypair();
1770
1771 let event_json = create_signed_app_keys_event(
1772 owner.public_key_hex.clone(),
1773 owner.private_key_hex.clone(),
1774 vec![
1775 FfiDeviceEntry {
1776 identity_pubkey_hex: laptop.public_key_hex.clone(),
1777 created_at: 1_700_000_000,
1778 device_label: Some("Sirius MacBook".to_string()),
1779 client_label: Some("Iris Chat Desktop".to_string()),
1780 },
1781 FfiDeviceEntry {
1782 identity_pubkey_hex: phone.public_key_hex.clone(),
1783 created_at: 1_700_000_100,
1784 device_label: Some("Linked device".to_string()),
1785 client_label: Some("Iris Chat Mobile".to_string()),
1786 },
1787 ],
1788 )
1789 .unwrap();
1790
1791 let parsed =
1792 parse_app_keys_event(event_json.clone(), Some(owner.private_key_hex.clone())).unwrap();
1793 let resolved =
1794 resolve_latest_app_keys_devices(vec![event_json], Some(owner.private_key_hex)).unwrap();
1795
1796 for devices in [parsed, resolved] {
1797 assert_eq!(devices.len(), 2);
1798
1799 let laptop_entry = devices
1800 .iter()
1801 .find(|entry| entry.identity_pubkey_hex == laptop.public_key_hex)
1802 .unwrap();
1803 assert_eq!(laptop_entry.device_label.as_deref(), Some("Sirius MacBook"));
1804 assert_eq!(
1805 laptop_entry.client_label.as_deref(),
1806 Some("Iris Chat Desktop")
1807 );
1808
1809 let phone_entry = devices
1810 .iter()
1811 .find(|entry| entry.identity_pubkey_hex == phone.public_key_hex)
1812 .unwrap();
1813 assert_eq!(phone_entry.device_label.as_deref(), Some("Linked device"));
1814 assert_eq!(
1815 phone_entry.client_label.as_deref(),
1816 Some("Iris Chat Mobile")
1817 );
1818 }
1819 }
1820}