Skip to main content

iris_chat_protocol/protocol_engine/
engine_sends.rs

1use super::*;
2
3impl ProtocolEngine {
4    pub fn observe_invite_event(&mut self, event: &Event) -> anyhow::Result<ProtocolRetryBatch> {
5        Ok(self.observe_invite_event_with_outcome(event)?.retry_batch)
6    }
7
8    pub fn observe_invite_event_with_outcome(
9        &mut self,
10        event: &Event,
11    ) -> anyhow::Result<ProtocolMetadataIngestionResult> {
12        let mut invite = parse_invite_event(event)?;
13        let invite_owner = invite
14            .inviter_owner_pubkey
15            .or_else(|| self.verified_roster_owner_for_device(invite.inviter_device_pubkey))
16            .unwrap_or_else(|| NdrOwnerPubkey::from_bytes(invite.inviter_device_pubkey.to_bytes()));
17        if invite.inviter_owner_pubkey.is_none() {
18            invite.inviter_owner_pubkey = Some(invite_owner);
19        }
20        if invite.inviter_device_pubkey == self.local_device {
21            return Ok(ProtocolMetadataIngestionResult::default());
22        }
23        let session_checkpoint = self.session_manager.clone();
24        self.session_manager
25            .observe_device_invite(invite_owner, invite)?;
26        // The manager admits stale/equal-time invites too. Compare its complete
27        // state so replay cannot rewrite every pending payload or reset retry
28        // deadlines, while a real invite replacement still wakes delivery.
29        if self.session_manager.snapshot() == session_checkpoint.snapshot() {
30            return Ok(ProtocolMetadataIngestionResult::default());
31        }
32        let pending_inbound_checkpoint = self.pending_inbound.clone();
33        let pending_group_fanouts_checkpoint = self.pending_group_fanouts.clone();
34        let pending_group_pairwise_checkpoint = self.pending_group_pairwise_payloads.clone();
35        self.invalidate_known_message_author_cache();
36        self.wake_pending_protocol_for_owner(invite_owner);
37        if let Err(error) = self.persist() {
38            self.session_manager = session_checkpoint;
39            self.invalidate_pending_inbound_retry_deadline();
40            self.pending_inbound = pending_inbound_checkpoint;
41            self.invalidate_pending_group_fanout_retry_deadline();
42            self.pending_group_fanouts = pending_group_fanouts_checkpoint;
43            self.pending_group_pairwise_payloads = pending_group_pairwise_checkpoint;
44            self.invalidate_known_message_author_cache();
45            return Err(error);
46        }
47        Ok(ProtocolMetadataIngestionResult {
48            changed: true,
49            retry_batch: self.retry_pending_protocol(NdrUnixSeconds(event.created_at.as_secs()))?,
50        })
51    }
52
53    pub fn observe_invite_response_event(
54        &mut self,
55        event: &Event,
56    ) -> anyhow::Result<ProtocolRetryBatch> {
57        let Some(local_invite_recipient) = self
58            .session_manager
59            .snapshot()
60            .local_invite
61            .as_ref()
62            .map(|invite| invite.inviter_ephemeral_public_key)
63        else {
64            return Ok(ProtocolRetryBatch::default());
65        };
66        let envelope = parse_invite_response_event(event)?;
67        if envelope.recipient != local_invite_recipient {
68            return Ok(ProtocolRetryBatch::default());
69        }
70        let checkpoint = self.state_checkpoint();
71        let mut rng = OsRng;
72        let mut ctx = ProtocolContext::new(NdrUnixSeconds(event.created_at.as_secs()), &mut rng);
73        let processed = match self
74            .session_manager
75            .observe_invite_response(&mut ctx, &envelope)
76        {
77            Ok(processed) => processed,
78            Err(NdrError::Domain(DomainError::InviteAlreadyUsed)) => {
79                return Ok(ProtocolRetryBatch::default());
80            }
81            Err(error) => return Err(error.into()),
82        };
83        self.invalidate_known_message_author_cache();
84        if let Some(processed) = processed.as_ref() {
85            self.wake_pending_protocol_for_owner(
86                processed
87                    .claimed_owner_pubkey
88                    .unwrap_or(processed.owner_pubkey),
89            );
90        }
91        if let Some(processed) = processed.as_ref() {
92            if let Some(invite) = self.local_invite() {
93                let proof_batch = match self.ingest_invite_response_owner_proof(
94                    &invite,
95                    event,
96                    public_owner(
97                        processed
98                            .claimed_owner_pubkey
99                            .unwrap_or(processed.owner_pubkey),
100                    )?,
101                    public_device(processed.device_pubkey)?,
102                ) {
103                    Ok(batch) => batch,
104                    Err(error) => {
105                        self.restore_checkpoint(checkpoint);
106                        self.invalidate_known_message_author_cache();
107                        return Err(error);
108                    }
109                };
110                if !proof_batch.is_empty() {
111                    return Ok(proof_batch);
112                }
113            }
114        }
115        if let Err(error) = self.persist() {
116            self.restore_checkpoint(checkpoint);
117            self.invalidate_known_message_author_cache();
118            return Err(error);
119        }
120        self.retry_pending_protocol(ctx.now)
121    }
122
123    pub fn accept_invite(
124        &mut self,
125        invite: &Invite,
126        owner_pubkey_hint: Option<PublicKey>,
127    ) -> anyhow::Result<ProtocolAcceptInviteOutcome> {
128        let invite_owner = resolve_invite_owner(invite, owner_pubkey_hint)?;
129        let inviter_device = public_device(invite.inviter_device_pubkey)?;
130        let owner = ndr_owner(invite_owner);
131
132        if invite_owner != inviter_device {
133            let Some(authorized) = self.invite_owner_app_keys_membership(owner, inviter_device)
134            else {
135                return Ok(ProtocolAcceptInviteOutcome::Blocked(
136                    ProtocolAcceptInviteBlock::MissingOwnerRoster {
137                        owner_pubkey: invite_owner,
138                        device_pubkey: inviter_device,
139                    },
140                ));
141            };
142            if !authorized {
143                return Ok(ProtocolAcceptInviteOutcome::Blocked(
144                    ProtocolAcceptInviteBlock::UnauthorizedDevice {
145                        owner_pubkey: invite_owner,
146                        device_pubkey: inviter_device,
147                    },
148                ));
149            }
150        }
151
152        let mut invite = invite.clone();
153        invite.inviter_owner_pubkey = Some(owner);
154        let checkpoint = self.state_checkpoint();
155        self.session_manager
156            .observe_device_invite(owner, invite.clone())?;
157        if invite_owner == inviter_device {
158            let has_roster = self
159                .session_manager
160                .snapshot()
161                .users
162                .into_iter()
163                .find(|user| user.owner_pubkey == owner)
164                .and_then(|user| user.roster)
165                .is_some();
166            if !has_roster {
167                self.session_manager.observe_peer_roster(
168                    owner,
169                    DeviceRoster::new(
170                        NdrUnixSeconds(unix_now().get()),
171                        vec![AuthorizedDevice::new(
172                            invite.inviter_device_pubkey,
173                            invite.created_at,
174                        )],
175                    ),
176                );
177            }
178        }
179        self.invalidate_known_message_author_cache();
180        let now = unix_now();
181        let result = (|| -> anyhow::Result<ProtocolAcceptInviteResult> {
182            let mut rng = OsRng;
183            let mut ctx = ProtocolContext::new(NdrUnixSeconds(now.get()), &mut rng);
184            let (mut session, response) = invite.accept_with_owner_context(
185                &mut ctx,
186                self.local_device,
187                self.local_device_secret,
188                Some(self.local_owner),
189            )?;
190
191            // Publish the actual invite response, then an expired typing rumor
192            // through the newly-created session. The second event bootstraps
193            // the inviter's receiving state without showing a typing indicator.
194            let response_event = self.cached_invite_response_with_owner_proof(&response)?;
195            let mut typing = pairwise_codec::typing_event(
196                self.owner_pubkey,
197                pairwise_codec::EncodeOptions::new(now.get(), current_unix_millis())
198                    .with_expiration(1),
199            )?;
200            typing.ensure_id();
201            let typing_payload = serde_json::to_vec(&typing)?;
202            let plan = session.plan_send(&typing_payload, ctx.now)?;
203            let envelope = session.apply_send(plan).envelope;
204            let typing_delivery = Delivery {
205                owner_pubkey: owner,
206                device_pubkey: invite.inviter_device_pubkey,
207                envelope,
208            };
209            let typing_event = message_event_for_delivery(&typing_delivery)?;
210            let typing_carrier = pairwise_carrier(&typing_delivery)?;
211
212            self.session_manager.import_session_state(
213                owner,
214                invite.inviter_device_pubkey,
215                session.state,
216                ctx.now,
217            );
218            self.invalidate_known_message_author_cache();
219            self.persist()?;
220
221            let chat_id = invite_owner.to_hex();
222            Ok(ProtocolAcceptInviteResult {
223                owner_pubkey: invite_owner,
224                inviter_device_pubkey: inviter_device,
225                device_id: inviter_device.to_hex(),
226                effects: vec![
227                    ProtocolEffect::Publish(ProtocolPublish {
228                        authored_at_secs: Some(now.get()),
229                        event: response_event,
230                        chat_id: chat_id.clone(),
231                        inner_event_id: None,
232                        carrier: ProtocolCarrier::Unmarked,
233                    }),
234                    ProtocolEffect::Publish(ProtocolPublish {
235                        authored_at_secs: Some(now.get()),
236                        event: typing_event,
237                        chat_id,
238                        inner_event_id: None,
239                        carrier: typing_carrier,
240                    }),
241                ],
242            })
243        })();
244
245        match result {
246            Ok(result) => Ok(ProtocolAcceptInviteOutcome::Accepted(result)),
247            Err(error) => {
248                self.restore_checkpoint(checkpoint);
249                Err(error)
250            }
251        }
252    }
253
254    pub fn import_session_state(
255        &mut self,
256        peer_pubkey: PublicKey,
257        device_id: Option<String>,
258        state: SessionState,
259        now: UnixSeconds,
260    ) -> anyhow::Result<ProtocolRetryBatch> {
261        let device_pubkey = device_id
262            .as_deref()
263            .and_then(|value| PublicKey::parse(value).ok())
264            .map(ndr_device)
265            .unwrap_or_else(|| ndr_device(peer_pubkey));
266        self.session_manager.import_session_state(
267            ndr_owner(peer_pubkey),
268            device_pubkey,
269            state,
270            NdrUnixSeconds(now.get()),
271        );
272        self.invalidate_known_message_author_cache();
273        self.wake_pending_protocol_for_owner(ndr_owner(peer_pubkey));
274        self.persist()?;
275        self.retry_pending_protocol(NdrUnixSeconds(now.get()))
276    }
277
278    pub fn create_group(
279        &mut self,
280        name: String,
281        member_owners: Vec<PublicKey>,
282        now: UnixSeconds,
283    ) -> anyhow::Result<ProtocolGroupSendResult> {
284        self.with_state_checkpoint(|engine| {
285            let mut rng = OsRng;
286            let mut ctx = ProtocolContext::new(NdrUnixSeconds(now.get()), &mut rng);
287            let result = engine.group_manager.create_group_with_protocol(
288                &mut engine.session_manager,
289                &mut ctx,
290                name,
291                member_owners.into_iter().map(ndr_owner).collect(),
292                GroupProtocol::sender_key_v1(),
293            )?;
294            let mut output = engine.protocol_group_send_from_prepared(&result.prepared, None)?;
295            output.snapshot = Some(result.group);
296            engine.persist()?;
297            Ok(output)
298        })
299    }
300
301    fn ensure_supported_group_protocol(&self, group_id: &str) -> anyhow::Result<()> {
302        if self
303            .group_manager
304            .group(group_id)
305            .is_some_and(|group| !group.protocol.is_sender_key_v1())
306        {
307            anyhow::bail!("group `{group_id}` uses an unsupported legacy group protocol");
308        }
309        Ok(())
310    }
311
312    pub fn update_group_name(
313        &mut self,
314        group_id: &str,
315        name: String,
316    ) -> anyhow::Result<ProtocolGroupSendResult> {
317        self.with_state_checkpoint(|engine| {
318            engine.ensure_supported_group_protocol(group_id)?;
319            let mut rng = OsRng;
320            let mut ctx = ProtocolContext::new(NdrUnixSeconds(unix_now().get()), &mut rng);
321            let prepared = engine.group_manager.update_name(
322                &mut engine.session_manager,
323                &mut ctx,
324                group_id,
325                name,
326            )?;
327            let mut output = engine.protocol_group_send_from_prepared(&prepared, None)?;
328            output.snapshot = engine.group_manager.group(group_id);
329            engine.persist()?;
330            Ok(output)
331        })
332    }
333
334    pub fn update_group_picture(
335        &mut self,
336        group_id: &str,
337        picture: Option<String>,
338    ) -> anyhow::Result<ProtocolGroupSendResult> {
339        self.with_state_checkpoint(|engine| {
340            engine.ensure_supported_group_protocol(group_id)?;
341            let mut rng = OsRng;
342            let mut ctx = ProtocolContext::new(NdrUnixSeconds(unix_now().get()), &mut rng);
343            let prepared = engine.group_manager.update_picture(
344                &mut engine.session_manager,
345                &mut ctx,
346                group_id,
347                picture,
348            )?;
349            let mut output = engine.protocol_group_send_from_prepared(&prepared, None)?;
350            output.snapshot = engine.group_manager.group(group_id);
351            engine.persist()?;
352            Ok(output)
353        })
354    }
355
356    pub fn update_group_about(
357        &mut self,
358        group_id: &str,
359        about: Option<String>,
360    ) -> anyhow::Result<ProtocolGroupSendResult> {
361        self.with_state_checkpoint(|engine| {
362            engine.ensure_supported_group_protocol(group_id)?;
363            let mut rng = OsRng;
364            let mut ctx = ProtocolContext::new(NdrUnixSeconds(unix_now().get()), &mut rng);
365            let prepared = engine.group_manager.update_about(
366                &mut engine.session_manager,
367                &mut ctx,
368                group_id,
369                about,
370            )?;
371            let mut output = engine.protocol_group_send_from_prepared(&prepared, None)?;
372            output.snapshot = engine.group_manager.group(group_id);
373            engine.persist()?;
374            Ok(output)
375        })
376    }
377
378    pub fn add_group_members(
379        &mut self,
380        group_id: &str,
381        members: Vec<PublicKey>,
382    ) -> anyhow::Result<ProtocolGroupSendResult> {
383        self.with_state_checkpoint(|engine| {
384            engine.ensure_supported_group_protocol(group_id)?;
385            let mut rng = OsRng;
386            let mut ctx = ProtocolContext::new(NdrUnixSeconds(unix_now().get()), &mut rng);
387            let prepared = engine.group_manager.add_members(
388                &mut engine.session_manager,
389                &mut ctx,
390                group_id,
391                members.into_iter().map(ndr_owner).collect(),
392            )?;
393            let mut output = engine.protocol_group_send_from_prepared(&prepared, None)?;
394            output.snapshot = engine.group_manager.group(group_id);
395            engine.persist()?;
396            Ok(output)
397        })
398    }
399
400    /// Admission and its distinct-owner receipt share one durable checkpoint.
401    /// Force persistence even in a receive batch: never publish membership before
402    /// the use limit is durable. Retry after a crash cannot admit an extra owner.
403    pub fn admit_group_invite_member(
404        &mut self,
405        group_id: &str,
406        token: &str,
407        member: PublicKey,
408        max_uses: Option<u32>,
409    ) -> anyhow::Result<ProtocolGroupSendResult> {
410        anyhow::ensure!(
411            token.len() == 64 && token.bytes().all(|c| c.is_ascii_hexdigit()),
412            "Invalid invite token"
413        );
414        anyhow::ensure!(
415            max_uses.is_none_or(|n| (1..=10000).contains(&n)),
416            "Invalid invite limit"
417        );
418        self.with_state_checkpoint(|engine| {
419            engine.ensure_supported_group_protocol(group_id)?;
420            let group = engine
421                .group_manager
422                .group(group_id)
423                .ok_or_else(|| anyhow::anyhow!("Group unavailable"))?;
424            anyhow::ensure!(
425                group.admins.contains(&engine.local_owner)
426                    && group.members.contains(&engine.local_owner),
427                "Admin required"
428            );
429            let member_owner = ndr_owner(member);
430            let key = format!("{group_id}:{token}");
431            let used = engine.group_invite_uses.get(&key);
432            anyhow::ensure!(
433                !used.is_some_and(|owners| owners.contains(&member_owner)),
434                "Invite already used"
435            );
436            anyhow::ensure!(
437                max_uses.is_none_or(|limit| used.map_or(0, BTreeSet::len) < limit as usize),
438                "Invite is full"
439            );
440            anyhow::ensure!(!group.members.contains(&member_owner), "Already a member");
441            let mut rng = OsRng;
442            let mut ctx = ProtocolContext::new(NdrUnixSeconds(unix_now().get()), &mut rng);
443            let prepared = engine.group_manager.add_members(
444                &mut engine.session_manager,
445                &mut ctx,
446                group_id,
447                vec![member_owner],
448            )?;
449            let mut output = engine.protocol_group_send_from_prepared(&prepared, None)?;
450            output.snapshot = engine.group_manager.group(group_id);
451            engine
452                .group_invite_uses
453                .entry(key)
454                .or_default()
455                .insert(member_owner);
456            engine.persist_now()?;
457            Ok(output)
458        })
459    }
460
461    pub fn group_invite_use_count(&self, group_id: &str, token: &str) -> usize {
462        self.group_invite_uses
463            .get(&format!("{group_id}:{token}"))
464            .map_or(0, BTreeSet::len)
465    }
466
467    pub fn group_snapshot(&self, group_id: &str) -> Option<GroupSnapshot> {
468        self.group_manager.group(group_id)
469    }
470
471    pub fn group_invite_used_by(&self, group_id: &str, token: &str, member: PublicKey) -> bool {
472        self.group_invite_uses
473            .get(&format!("{group_id}:{token}"))
474            .is_some_and(|owners| owners.contains(&ndr_owner(member)))
475    }
476
477    pub fn remove_group_member(
478        &mut self,
479        group_id: &str,
480        member: PublicKey,
481    ) -> anyhow::Result<ProtocolGroupSendResult> {
482        self.with_state_checkpoint(|engine| {
483            engine.ensure_supported_group_protocol(group_id)?;
484            let mut rng = OsRng;
485            let mut ctx = ProtocolContext::new(NdrUnixSeconds(unix_now().get()), &mut rng);
486            let prepared = engine.group_manager.remove_members(
487                &mut engine.session_manager,
488                &mut ctx,
489                group_id,
490                vec![ndr_owner(member)],
491            )?;
492            let mut output = engine.protocol_group_send_from_prepared(&prepared, None)?;
493            output.snapshot = engine.group_manager.group(group_id);
494            engine.persist()?;
495            Ok(output)
496        })
497    }
498
499    pub fn set_group_admin(
500        &mut self,
501        group_id: &str,
502        member: PublicKey,
503        is_admin: bool,
504    ) -> anyhow::Result<ProtocolGroupSendResult> {
505        self.with_state_checkpoint(|engine| {
506            engine.ensure_supported_group_protocol(group_id)?;
507            let mut rng = OsRng;
508            let mut ctx = ProtocolContext::new(NdrUnixSeconds(unix_now().get()), &mut rng);
509            let prepared = if is_admin {
510                engine.group_manager.add_admins(
511                    &mut engine.session_manager,
512                    &mut ctx,
513                    group_id,
514                    vec![ndr_owner(member)],
515                )?
516            } else {
517                engine.group_manager.remove_admins(
518                    &mut engine.session_manager,
519                    &mut ctx,
520                    group_id,
521                    vec![ndr_owner(member)],
522                )?
523            };
524            let mut output = engine.protocol_group_send_from_prepared(&prepared, None)?;
525            output.snapshot = engine.group_manager.group(group_id);
526            engine.persist()?;
527            Ok(output)
528        })
529    }
530
531    pub fn send_group_payload(
532        &mut self,
533        group_id: &str,
534        payload: Vec<u8>,
535        inner_event_id: Option<String>,
536    ) -> anyhow::Result<ProtocolGroupSendResult> {
537        self.with_state_checkpoint(|engine| {
538            engine.ensure_supported_group_protocol(group_id)?;
539            let mut rng = OsRng;
540            let mut ctx = ProtocolContext::new(NdrUnixSeconds(unix_now().get()), &mut rng);
541            let prepared = engine.group_manager.send_message(
542                &mut engine.session_manager,
543                &mut ctx,
544                group_id,
545                payload,
546            )?;
547            let message_id = inner_event_id.clone();
548            let mut output = engine.protocol_group_send_from_prepared(&prepared, inner_event_id)?;
549            output.snapshot = engine.group_manager.group(group_id);
550            engine.prepare_private_group_wakes(group_id, &mut output, message_id.clone())?;
551            // The shared ciphertext is only a payload inside recipient-specific
552            // ratchets. It must never create a public channel or publication.
553            // Both shapes look alike on the wire, so only provenance decides.
554            let private_ids = output
555                .effects
556                .iter()
557                .filter_map(|effect| {
558                    let ProtocolEffect::Publish(publish) = effect;
559                    (publish.carrier != ProtocolCarrier::SharedGroup)
560                        .then_some(publish.event.id.to_hex())
561                })
562                .collect::<HashSet<_>>();
563            output.effects.retain(|effect| {
564                let ProtocolEffect::Publish(publish) = effect;
565                private_ids.contains(&publish.event.id.to_hex())
566            });
567            output.event_ids.retain(|id| private_ids.contains(id));
568            output.has_pending_deliveries = engine.pending_group_fanouts.iter().any(|pending| {
569                pending.group_id == group_id && pending.inner_event_id == message_id
570            });
571            engine.retain_group_publications(&output.effects, None)?;
572            output.message_id = message_id;
573            // The ratchets and every ready publication must survive before the
574            // caller may hand this batch to any network carrier.
575            engine.persist_now()?;
576            Ok(output)
577        })
578    }
579
580    pub fn send_direct_text(
581        &mut self,
582        peer_pubkey: PublicKey,
583        chat_id: &str,
584        text: &str,
585        expires_at_secs: Option<u64>,
586        now: UnixSeconds,
587    ) -> anyhow::Result<ProtocolDirectSendResult> {
588        self.send_direct_text_created_at(peer_pubkey, chat_id, text, expires_at_secs, now, now)
589    }
590
591    /// Keep the authored time separate from the transport/retry clock.
592    #[allow(clippy::too_many_arguments)]
593    pub fn send_direct_text_created_at(
594        &mut self,
595        peer_pubkey: PublicKey,
596        chat_id: &str,
597        text: &str,
598        expires_at_secs: Option<u64>,
599        created_at: UnixSeconds,
600        now: UnixSeconds,
601    ) -> anyhow::Result<ProtocolDirectSendResult> {
602        let now_ms = current_unix_millis();
603        let mut options = pairwise_codec::EncodeOptions::new(created_at.get(), now_ms);
604        if let Some(expires_at_secs) = expires_at_secs {
605            options = options.with_expiration(expires_at_secs);
606        }
607        let rumor = pairwise_codec::message_event(self.owner_pubkey, text.to_string(), options)?;
608        self.send_direct_unsigned_event(peer_pubkey, chat_id, rumor, now)
609    }
610
611    pub fn send_direct_unsigned_event(
612        &mut self,
613        peer_pubkey: PublicKey,
614        chat_id: &str,
615        mut rumor: UnsignedEvent,
616        now: UnixSeconds,
617    ) -> anyhow::Result<ProtocolDirectSendResult> {
618        rumor.ensure_id();
619        let message_id = rumor
620            .id
621            .as_ref()
622            .map(ToString::to_string)
623            .unwrap_or_default();
624        let remote_payload = serde_json::to_vec(&rumor)?;
625        let sibling_payload = local_sibling_payload(peer_pubkey, &remote_payload)?;
626        self.with_state_checkpoint(|engine| {
627            let self_chat = peer_pubkey == engine.owner_pubkey;
628            let (mut event_ids, mut effects) = if self_chat {
629                (Vec::new(), Vec::new())
630            } else {
631                engine.queue_remote_payload(
632                    peer_pubkey,
633                    chat_id,
634                    remote_payload,
635                    Some(message_id.clone()),
636                    &message_id,
637                    now,
638                )?
639            };
640            let siblings: BTreeSet<_> = engine
641                .session_manager
642                .roster(engine.local_owner)
643                .into_iter()
644                .flat_map(|roster| roster.devices())
645                .map(|entry| entry.device_pubkey)
646                .filter(|device| *device != engine.local_device)
647                .collect();
648            let needs_sibling_delivery = self_chat && !siblings.is_empty();
649            let sibling_effects = engine.queue_local_sibling_payload(
650                chat_id,
651                sibling_payload,
652                &message_id,
653                now,
654                Some(siblings),
655            );
656            // A self-chat delivery tracks message envelopes, excluding handshakes.
657            event_ids.extend(
658                sibling_effects
659                    .iter()
660                    .filter(|ProtocolEffect::Publish(publish)| {
661                        !self_chat || publish.inner_event_id.is_some()
662                    })
663                    .map(|ProtocolEffect::Publish(publish)| publish.event.id.to_string()),
664            );
665            anyhow::ensure!(
666                !needs_sibling_delivery || !event_ids.is_empty(),
667                "no linked device delivery prepared"
668            );
669            effects.extend(sibling_effects);
670            engine.persist()?;
671            Ok(ProtocolDirectSendResult {
672                message_id,
673                event_ids,
674                effects,
675            })
676        })
677    }
678
679    pub fn send_direct_unsigned_event_to_peer_only(
680        &mut self,
681        peer_pubkey: PublicKey,
682        chat_id: &str,
683        mut rumor: UnsignedEvent,
684        now: UnixSeconds,
685    ) -> anyhow::Result<ProtocolDirectSendResult> {
686        rumor.ensure_id();
687        let message_id = rumor
688            .id
689            .as_ref()
690            .map(ToString::to_string)
691            .unwrap_or_default();
692        let remote_payload = serde_json::to_vec(&rumor)?;
693        self.with_state_checkpoint(|engine| {
694            engine.send_direct_remote_payload_inner(
695                peer_pubkey,
696                chat_id,
697                remote_payload,
698                Some(message_id.clone()),
699                message_id,
700                now,
701            )
702        })
703    }
704
705    pub fn send_local_sibling_unsigned_event(
706        &mut self,
707        conversation_owner: PublicKey,
708        chat_id: &str,
709        mut rumor: UnsignedEvent,
710        now: UnixSeconds,
711    ) -> anyhow::Result<ProtocolDirectSendResult> {
712        rumor.ensure_id();
713        let message_id = rumor
714            .id
715            .as_ref()
716            .map(ToString::to_string)
717            .unwrap_or_default();
718        let payload = serde_json::to_vec(&rumor)?;
719        let sibling_payload = local_sibling_payload(conversation_owner, &payload)?;
720        self.with_state_checkpoint(|engine| {
721            let effects = engine.queue_local_sibling_payload(
722                chat_id,
723                sibling_payload,
724                &message_id,
725                now,
726                None,
727            );
728            engine.persist()?;
729            Ok(ProtocolDirectSendResult {
730                message_id,
731                event_ids: effects
732                    .iter()
733                    .map(|effect| match effect {
734                        ProtocolEffect::Publish(publish) => publish.event.id.to_string(),
735                    })
736                    .collect(),
737                effects,
738            })
739        })
740    }
741
742    fn queue_local_sibling_payload(
743        &mut self,
744        chat_id: &str,
745        sibling_payload: Vec<u8>,
746        message_id: &str,
747        now: UnixSeconds,
748        eligible_devices: Option<BTreeSet<NdrDevicePubkey>>,
749    ) -> Vec<ProtocolEffect> {
750        let existing = self
751            .pending_local_sibling_sends
752            .iter()
753            .position(|pending| pending.message_id == message_id && pending.chat_id == chat_id)
754            .map(|index| self.pending_local_sibling_sends.remove(index));
755        let mut pending = existing.unwrap_or_else(|| ProtocolPendingLocalSiblingSend {
756            eligible_devices,
757            chat_id: chat_id.to_string(),
758            payload: sibling_payload,
759            message_id: message_id.to_string(),
760            completed_devices: BTreeSet::new(),
761            created_at_secs: now.get(),
762            next_retry_at_secs: now.get(),
763        });
764        let sessions = self.session_manager.clone();
765        let (effects, complete) = match self
766            .prepare_pending_local_sibling_send(&mut pending, NdrUnixSeconds(now.get()))
767        {
768            Ok(result) => result,
769            Err(_) => {
770                self.session_manager = sessions;
771                (Vec::new(), false)
772            }
773        };
774        if !complete {
775            self.pending_local_sibling_sends.push(pending);
776        }
777        effects
778    }
779
780    fn send_direct_remote_payload_inner(
781        &mut self,
782        peer_pubkey: PublicKey,
783        chat_id: &str,
784        remote_payload: Vec<u8>,
785        inner_event_id: Option<String>,
786        message_id: String,
787        now: UnixSeconds,
788    ) -> anyhow::Result<ProtocolDirectSendResult> {
789        let (event_ids, effects) = self.queue_remote_payload(
790            peer_pubkey,
791            chat_id,
792            remote_payload,
793            inner_event_id,
794            &message_id,
795            now,
796        )?;
797        self.persist()?;
798        Ok(ProtocolDirectSendResult {
799            message_id,
800            event_ids,
801            effects,
802        })
803    }
804}