Skip to main content

mail4agent_messenger/
core.rs

1//! [`MessengerCore`] — the single-writer kernel tying every earlier piece
2//! together (plan §3's `lib.rs`/`sync_engine.rs`, work pieces M13a+M13b):
3//! the `/sync` loop, to-device/device-list/room ingestion, Megolm decrypt,
4//! the receive-side [`MessengerCommand`] variants (M13a), and the per-room
5//! ordered send pipeline plus every room-membership/tag/typing command
6//! (M13b, this module's own "M13b send pipeline" section below).
7//!
8//! # Kernel API shape
9//!
10//! [`MessengerCore::open`] rebuilds a core from whatever a shell already
11//! has durably stored (sealed records) plus fresh, caller-supplied config.
12//! From there, a shell drives one tick at a time:
13//!
14//! 1. [`MessengerCore::releasable_requests`] — everything safe to send
15//!    right now (the flush-before-send barrier already applied).
16//! 2. Execute each, feed the result back via [`MessengerCore::on_response`]
17//!    or [`MessengerCore::on_transport_error`].
18//! 3. [`MessengerCore::take_flush_batch`]/[`MessengerCore::ack_flush`] —
19//!    persist whatever mutated, ack once durable.
20//! 4. [`MessengerCore::events`] — drain whatever became visible, to diff
21//!    against the adapter's own last-rendered snapshot.
22//!
23//! No function here calls a clock or an RNG itself (crate doc, `ids`/
24//! `outgoing_queue`'s own module docs): every entry point that needs time
25//! takes `now_ms` explicitly, and retry jitter comes from the
26//! [`crate::outgoing_queue::Jitter`] supplied to [`MessengerCore::open`].
27//!
28//! # Counters that must never repeat across a restart
29//!
30//! [`crate::ids::RequestId`]/[`crate::ids::TxnId`] are minted from one
31//! monotonic seed per device session (`ids`'s own module doc) — this
32//! module is that seed's one owner, persisted as [`Counters`]. A bump is
33//! written through [`crate::store::StateStore::save_counters`] in the same
34//! synchronous call that mints the id ([`MessengerCore::next_request_id`]),
35//! which is what lets the ordinary flush-before-send barrier
36//! (`crate::persist`'s module doc) cover it for free: the very next
37//! [`crate::outgoing_queue::OutgoingQueue::enqueue`] call this id feeds
38//! into calls [`crate::persist::FlushEpoch::seal_for_request`] itself,
39//! which folds the counter's own not-yet-batched dirty write into the same
40//! batch the new request's `required_seq` waits on. A reused id after a
41//! crash would be silently deduplicated by the server into the OLD
42//! event/request — the exact "resend a `/keys/claim`, waste one OTK" cost
43//! [`crate::outgoing_queue`]'s own module doc accepts is very different
44//! from resending a `TxnId` a room member already has under a different
45//! plaintext, which is why this restore rule is load-bearing, not
46//! defensive polish.
47//!
48//! [`MessengerCore::open`] restores each sequence to
49//! `max(persisted, 1 + highest seed still embedded in a pending request)`
50//! — see [`restore_counter`]'s own doc for why the `pending` half of that
51//! max matters at all given the write-before-mint discipline above (belt
52//! and suspenders, not the primary guarantee). Only [`crate::ids::RequestId`]
53//! has a structural "still pending" source to scan
54//! ([`crate::store::StateStore::pending_requests`] — every pending request
55//! carries its own id); [`crate::ids::TxnId`] does not (a `TxnId` is baked
56//! into a request's `path`/`body`, never a distinct field), so its restore
57//! relies solely on the persisted value. [`MessengerCore::next_txn_id`] is
58//! the send pipeline's own counterpart to
59//! [`MessengerCore::next_request_id`] — symmetric restore, symmetric
60//! write-before-mint discipline.
61//!
62//! # Sync ingestion order
63//!
64//! One `/sync` request is ever outstanding at a time
65//! ([`crate::outgoing_queue::Lane::Sync`]'s own cap), 30s long-poll,
66//! `since` = the persisted [`crate::store::StateStore::sync_token`] (initial
67//! sync when `None`). On a successful response, in order: (1) to-device
68//! events — Olm-decrypt, route `m.room_key` into
69//! [`crate::crypto::group_sessions::GroupSessionManager`], drop a
70//! replayed/otherwise-unrecoverable decrypt silently, keep an
71//! [`crate::crypto::olm_sessions::OlmDecryptError::UnknownSenderDevice`] in
72//! a small bounded retry list re-tried after the next `/keys/query`.
73//! [`crate::crypto::olm_sessions::OlmSessionManager::decrypt_to_device`]
74//! checks the sender's device BEFORE it creates or advances any session, so
75//! a refused event has consumed nothing (no one-time key, no ratchet step) and
76//! the retry decrypts it from scratch -- the room key inside the very first
77//! message from a peer whose device this account has not queried yet (the
78//! usual case: the to-device share arrives before any `/keys/query` for
79//! them) is not lost.
80//! A missing inbound Megolm session sends `m.room_key_request` to the
81//! event sender and to this account's other devices. A request from a
82//! joined or invited room member is answered with `m.forwarded_room_key`
83//! (the inbound session exported at its first known index) or, if this
84//! device only still has the outbound session, with `m.room_key`.
85//! `m.forwarded_room_key` is imported and bound to the original sender.
86//! `m.room_key.withheld` records the session so the request is not
87//! repeated. (2) `device_lists.changed`/
88//! `left` → [`crate::crypto::device_tracker::DeviceTracker`]; (3) OTK
89//! counts / unused fallback types →
90//! [`crate::crypto::account::OlmAccountState::on_sync_counts`], may enqueue
91//! `/keys/upload`; (4) any outdated tracked user → one `/keys/query`; (5)
92//! rooms join/invite/leave (a member seen joining an encrypted room is marked
93//! outdated and queried right after, step 5b) — state → [`crate::room::state::RoomState`],
94//! timeline → [`crate::room::timeline::Timeline::apply_timeline_batch`],
95//! Megolm events decrypted via
96//! [`crate::crypto::group_sessions::GroupSessionManager::decrypt_event`] →
97//! [`crate::room::timeline::Timeline::set_decrypted`] or
98//! `Undecryptable{reason}`; ephemeral typing/receipts; room account data;
99//! summary + unread; (6) global account data (`m.direct`) → later read by
100//! [`MessengerCore::room_kind`]; (7) the new sync token is persisted LAST,
101//! after everything it covers. When a room key newly arrives (step 1),
102//! every `Undecryptable` item across every loaded timeline whose session id
103//! now matches is retried automatically, in addition to the caller's own
104//! [`MessengerCommand::RetryDecryption`].
105
106use crate::crypto::account::OlmAccountState;
107use crate::crypto::cross_signing::CrossSigningState;
108use crate::crypto::device_tracker::{DeviceTracker, StoredDevice};
109use crate::crypto::group_sessions::{GroupDecryptError, GroupSessionManager, RoomEventPlaintext};
110use crate::crypto::olm_sessions::{DecryptedToDevice, OlmDecryptError, OlmSessionManager};
111use crate::error::MessengerError;
112use crate::ids::{DeviceId, EventId, RequestId, RoomId, TxnId, UserId};
113use crate::outgoing_queue::{Jitter, Lane, OutgoingQueue, PendingRequest, ResponseOutcome};
114use crate::persist::{FlushBatch, SealedRecord};
115use crate::room::state::RoomState;
116use crate::room::timeline::{
117    interpret_content, ForwardRefusal, ItemContent, SendState, Timeline, KEY_FORWARDED, KEY_FORWARDED_FROM,
118};
119use crate::room::RoomKind;
120use crate::store::sealed::SealedRecordCodec;
121use crate::store::{CryptoStore, RecordCodec, StateStore, Store};
122use crate::wire::events::{
123    DirectContent, ForwardedRoomKeyContent, InReplyTo, MegolmEncryptedContent, Membership, RawEvent, ReceiptContent,
124    RelatesTo, RoomEncryptedContent, RoomKeyContent, RoomKeyRequestAction, RoomKeyRequestBody, RoomKeyRequestContent,
125    RoomKeyWithheldContent, StrippedStateEvent, TagContent, TagInfo, TextLikeMessageContent, ToDeviceEvent,
126    TypingContent, Unsigned,
127};
128use crate::wire::sync::{parse_sync_response, InvitedRoom, JoinedRoom, LeftRoom};
129use crate::wire::{percent_decode_segment, HttpResponseDescriptor, OutgoingRequest, OutgoingRequestKind};
130use serde::{Deserialize, Serialize};
131use std::collections::{BTreeMap, BTreeSet, VecDeque};
132use zeroize::Zeroizing;
133
134/// `/sync`'s own long-poll timeout (research doc §1.4's convention).
135const SYNC_TIMEOUT_MS: u64 = 30_000;
136/// `GET /rooms/{roomId}/messages`'s page size for
137/// [`MessengerCommand::LoadOlder`].
138const LOAD_OLDER_PAGE_SIZE: u32 = 50;
139/// How long a `typing: true` indicator asks the server to hold before
140/// auto-clearing it (client-server API's own `timeout` field).
141const TYPING_TIMEOUT_MS: u64 = 30_000;
142/// How many to-device events with an as-yet-[`OlmDecryptError::UnknownSenderDevice`]
143/// sender this core holds for a retry after the next `/keys/query`, oldest
144/// evicted first — same bounded-FIFO doctrine as
145/// [`crate::room::timeline`]'s own pending-relation queue.
146const UNKNOWN_SENDER_RETRY_CAPACITY: usize = 256;
147/// How often [`MessengerCommand::SetTyping`] actually sends a `typing:
148/// true` PUT while the caller keeps re-asserting it — a `false` (stop) is
149/// never debounced (M13b's own send-pipeline doc).
150const TYPING_DEBOUNCE_MS: i64 = 4_000;
151/// [`GroupSessionManager::chunk_recipients_for_send_to_device`]'s own cap
152/// already bounds one `sendToDevice` request's device count; this is the
153/// wire event type every such request carries when it is sharing a Megolm
154/// room key -- the OUTER to-device type is always `m.room.encrypted` (the
155/// body is already Olm ciphertext; the inner, only-visible-after-decrypt
156/// type is `m.room_key`), matching every other Olm-wrapped to-device send
157/// this crate builds (`crypto::olm_sessions`'s own module doc).
158const ROOM_KEY_SHARE_EVENT_TYPE: &str = "m.room.encrypted";
159
160/// This device's own [`crate::ids::RequestId`]/[`crate::ids::TxnId`]
161/// minting sequences — see this module's own doc, "Counters that must
162/// never repeat across a restart".
163#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
164pub struct Counters {
165    /// The next, never-yet-handed-out [`crate::ids::RequestId`] seed.
166    pub next_request_id: u64,
167    /// The next, never-yet-handed-out [`crate::ids::TxnId`] seed. Not
168    /// minted from anywhere in this piece yet (see this module's own doc)
169    /// — restored and persisted symmetrically with `next_request_id` so
170    /// M13b's send pipeline needs no new persistence of its own.
171    pub next_txn_id: u64,
172}
173
174/// Computes the value a counter must resume from after a restart: never
175/// below what was durably persisted, and never low enough to reissue an id
176/// a still-pending request already carries. `max_used_seed` is the highest
177/// seed [`MessengerCore::open`] found embedded in a request
178/// [`crate::store::StateStore::pending_requests`] is still tracking (`None`
179/// when nothing is pending, or when this counter has no such structural
180/// source to scan at all — see this module's own doc for
181/// [`crate::ids::TxnId`]'s case). This is a defensive second check, not the
182/// primary guarantee: under the write-before-mint discipline
183/// [`MessengerCore::next_request_id`] follows, the persisted value alone
184/// should already dominate every pending id.
185/// Txn seed for a fresh store: microseconds since the Unix epoch, so it is
186/// above every id this device could have minted from an earlier store.
187fn fresh_txn_seed() -> u64 {
188    std::time::SystemTime::now()
189        .duration_since(std::time::UNIX_EPOCH)
190        .map(|d| d.as_micros() as u64)
191        .unwrap_or(0)
192}
193
194fn restore_counter(persisted_next: u64, max_used_seed: Option<u64>) -> u64 {
195    match max_used_seed {
196        Some(seed) => persisted_next.max(seed.saturating_add(1)),
197        None => persisted_next,
198    }
199}
200
201/// Static identity a shell supplies once at [`MessengerCore::open`] time —
202/// never re-derived, never changed for the lifetime of a device session
203/// (plan §4.1).
204#[derive(Clone, Debug, PartialEq, Eq)]
205pub struct CoreConfig {
206    /// This account's own user id.
207    pub user_id: UserId,
208    /// This device's own, server-assigned device id.
209    pub device_id: DeviceId,
210    /// The homeserver's own server name — not consumed by this crate's own
211    /// logic yet (every id this core builds, including
212    /// [`MessengerCommand::CreateRoom`]'s, is either read whole off the wire
213    /// or supplied by a caller who already has one; `CreateRoomKind` has no
214    /// alias field), kept on [`CoreConfig`] because a shell already has to
215    /// know it to reach the homeserver at all, and a still-later piece
216    /// (minting a room alias's local part into a full alias) will need it.
217    pub server_name: String,
218}
219
220/// Secret material a shell supplies once at [`MessengerCore::open`] time,
221/// alongside [`CoreConfig`]'s static identity fields. The session private
222/// key is not here: it is the Olm account
223/// [`crate::crypto::account::OlmAccountState::load_or_create`] generates
224/// (`mail4agent_vodozemac::olm::Account::new`) and pickles into the store.
225/// These two fields are optional 32-byte keys the caller already holds.
226/// `Default` leaves both absent. Nothing in this crate reads `backup_key`
227/// yet.
228#[derive(Default)]
229pub struct CoreSecrets {
230    /// This device's record-sealing key. `None` when this core is opened
231    /// with a non-sealing codec (every test in this crate uses
232    /// [`crate::store::InsecurePlainCodecForTests`] instead).
233    /// [`MessengerCore::open_sealed`] is the only consumer: it requires
234    /// this field to build its own [`SealedRecordCodec`] and fails loudly
235    /// if it is absent rather than silently falling back to an unsealed
236    /// store.
237    pub store_seal_key: Option<Zeroizing<[u8; 32]>>,
238    /// This account's server-side key-backup key, supplied by the caller.
239    /// Carried for a later Megolm session backup/restore; nothing in this
240    /// crate reads it yet.
241    pub backup_key: Option<Zeroizing<[u8; 32]>>,
242}
243
244/// Which `m.room.message` shape [`OutgoingMessage`] renders as (M13b send
245/// pipeline).
246#[derive(Clone, Copy, Debug, PartialEq, Eq)]
247pub enum MessageKind {
248    /// `msgtype: "m.text"`.
249    Text,
250    /// `msgtype: "m.notice"`.
251    Notice,
252    /// `msgtype: "m.emote"`.
253    Emote,
254}
255
256impl MessageKind {
257    fn msgtype(self) -> &'static str {
258        match self {
259            MessageKind::Text => "m.text",
260            MessageKind::Notice => "m.notice",
261            MessageKind::Emote => "m.emote",
262        }
263    }
264}
265
266/// One message a caller wants sent via [`MessengerCommand::SendMessage`] —
267/// M13b's binding content shape: text/notice/emote, an optional reply
268/// pointer, and an optional edit target (which must name one of the
269/// caller's own already-sent events — [`MessengerCore::dispatch`] rejects
270/// an edit of someone else's event rather than silently sending a
271/// replacement the room's other members will refuse to accept, per
272/// `m.replace`'s own "must come from the original sender" rule,
273/// `crate::room::relations`'s own module doc). `reply_to` and `edit_of`
274/// are mutually exclusive at the wire level — if both are set, `edit_of`
275/// wins (an edit's own `m.relates_to` is `m.replace`, not a reply).
276#[derive(Clone, Debug, PartialEq, Eq)]
277pub struct OutgoingMessage {
278    /// Which `msgtype` this renders as.
279    pub kind: MessageKind,
280    /// The message's own plain-text body (the edited/new body, if this is
281    /// an edit).
282    pub body: String,
283    /// A rich-reply pointer (`m.in_reply_to`), if any.
284    pub reply_to: Option<EventId>,
285    /// The event this message replaces, if this is an edit — must be one
286    /// of the caller's own already-sent events (this module's own doc).
287    pub edit_of: Option<EventId>,
288}
289
290/// Which shape [`MessengerCommand::CreateRoom`] builds, mapped onto the
291/// server's own `POST /createRoom` field set (`visibility`/`is_direct`/
292/// `invite`/`name`/`topic` — the server derives `kind` itself from
293/// `visibility`+`is_direct`, per the server plan's §5; a client never
294/// sends a `preset`/`kind` field directly).
295#[derive(Clone, Debug, PartialEq, Eq)]
296pub enum CreateRoomKind {
297    /// A 1:1 conversation: `is_direct: true`, exactly one invitee. Also
298    /// merges the new room into this account's own `m.direct` account data
299    /// once the room is created (this module's own doc).
300    Dm {
301        /// The other party.
302        peer: UserId,
303    },
304    /// A private multi-person room.
305    Group {
306        /// The room's display name.
307        name: String,
308        /// Users to invite at creation time.
309        invite: Vec<UserId>,
310        /// `true` gives every member the `invite` power level (`0`);
311        /// `false` restricts inviting to a moderator/owner (`50`) — the
312        /// server plan's §5 `group` column.
313        members_can_invite: bool,
314    },
315    /// A public, announcement-style room: join_rule public (anyone can
316    /// join) and plaintext (the server keeps channel posts unencrypted).
317    /// Only moderators write until `events_default` is lowered (an open room).
318    Channel {
319        /// The room's display name.
320        name: String,
321        /// The room's topic, if any.
322        topic: Option<String>,
323    },
324}
325
326/// One command a shell/adapter dispatches into the core — both the M13a
327/// receive-side variants (`MarkRead`, `SetTyping`, `RetryDecryption`,
328/// `LoadOlder`) and the M13b send/room-membership ones. Only `PartialEq`
329/// (not `Eq`): [`MessengerCommand::SetTag`]'s `order: Option<f64>` cannot
330/// implement `Eq`.
331#[derive(Clone, Debug, PartialEq)]
332pub enum MessengerCommand {
333    /// Sends one message into a room's per-room ordered send pipeline
334    /// (this module's own doc) — encrypts first if the room is encrypted,
335    /// sharing the room key to any not-yet-shared device before the
336    /// message itself is sent. A [`crate::room::timeline::TimelineItem`]
337    /// local echo appears immediately, in [`crate::room::timeline::SendState::Sending`].
338    SendMessage {
339        /// The room to send into.
340        room_id: RoomId,
341        /// The message content.
342        message: OutgoingMessage,
343        /// A caller-supplied transaction id (deterministic tests); `None`
344        /// mints a fresh one from this device's own counter.
345        txn_id: Option<TxnId>,
346    },
347    /// Forwards one event of `from_room` into `to_room`, through the same
348    /// per-room send pipeline as [`MessengerCommand::SendMessage`] (so it is
349    /// Megolm-encrypted when `to_room` is encrypted).
350    ///
351    /// The forward rules live HERE, in the engine. The content is read from
352    /// the source timeline item's DECRYPTED content -- for an edited item the
353    /// latest edit's `m.new_content` -- with `m.relates_to`, `m.mentions`
354    /// and `m.new_content` removed and any earlier forward marker replaced
355    /// by this hop's own:
356    ///
357    /// - source is a private/encrypted room (DM/group): `"forwarded": true`
358    ///   and nothing else -- no original sender, no room id, no room name;
359    /// - source is a public channel (`RoomKind::Channel`):
360    ///   `"forwarded_from": {"room_id", "room_name"}` --
361    ///   no sender (public join does not make content plaintext).
362    ///
363    /// The event type `m.room.message` is kept. An unrecognized `msgtype`
364    /// passes through as the raw content object plus the marker.
365    ///
366    /// Refused with [`MessengerError::IntentRefused`], naming the event,
367    /// when the source item is missing, redacted, still sealed
368    /// (undecryptable or not decrypted yet), or not an `m.room.message`;
369    /// and when `to_room` is unknown to this core (its encryption state
370    /// cannot be known, and a plaintext send into it could leak an
371    /// encrypted room's content).
372    Forward {
373        /// The room the event is taken from.
374        from_room: RoomId,
375        /// The event being forwarded.
376        event_id: EventId,
377        /// The room it is sent into.
378        to_room: RoomId,
379        /// A caller-supplied transaction id; `None` mints a fresh one.
380        txn_id: Option<TxnId>,
381    },
382    /// Sends a plaintext `m.reaction` — never Megolm-encrypted, even in an
383    /// encrypted room (allowed there as metadata, per the server's own
384    /// rule this crate mirrors).
385    React {
386        /// The room the reaction is sent in.
387        room_id: RoomId,
388        /// The event being reacted to.
389        target: EventId,
390        /// The reaction's own key (typically an emoji).
391        key: String,
392    },
393    /// Redacts an event (`PUT /rooms/{roomId}/redact/{eventId}/{txnId}`).
394    Redact {
395        /// The room the target event lives in.
396        room_id: RoomId,
397        /// The event being redacted.
398        target: EventId,
399        /// Why, if given.
400        reason: Option<String>,
401    },
402    /// Re-queues a [`crate::room::timeline::SendState::Failed`] send at the
403    /// back of its room's own send queue, WITHOUT re-encrypting it if it
404    /// had already been encrypted (the exact same ciphertext, the exact
405    /// same `txn_id` — this module's own doc). A no-op if `txn_id` names
406    /// no currently-failed send in `room_id`.
407    RetrySend {
408        /// The room the failed send belongs to.
409        room_id: RoomId,
410        /// The failed send's own transaction id.
411        txn_id: TxnId,
412    },
413    /// Creates a room (`POST /createRoom`).
414    CreateRoom {
415        /// Which shape to create.
416        kind: CreateRoomKind,
417    },
418    /// Joins a room this account has been invited to (or a public
419    /// channel).
420    JoinRoom {
421        /// The room to join.
422        room_id: RoomId,
423    },
424    /// Leaves a room.
425    LeaveRoom {
426        /// The room to leave.
427        room_id: RoomId,
428    },
429    /// Invites a user to a room.
430    Invite {
431        /// The room to invite into.
432        room_id: RoomId,
433        /// The user being invited.
434        user_id: UserId,
435    },
436    /// Removes a member from a room (`POST /rooms/{roomId}/kick`) — the
437    /// next encrypted send in this room rotates its Megolm session
438    /// automatically, excluding them (`crypto::group_sessions`'s own
439    /// rotation-trigger doc).
440    Kick {
441        /// The room to remove them from.
442        room_id: RoomId,
443        /// The member being removed.
444        user_id: UserId,
445        /// Why, if given.
446        reason: Option<String>,
447    },
448    /// Sets one user's power level in a room (`m.room.power_levels`, the
449    /// rest of the current content kept). The server enforces who may.
450    SetPowerLevel {
451        /// The room.
452        room_id: RoomId,
453        /// The user whose level changes.
454        user_id: UserId,
455        /// The new level (100 = room admin).
456        level: i64,
457    },
458    /// Sets the level needed to send an ordinary event in a room
459    /// (`events_default` of `m.room.power_levels`, the rest kept). `0` lets
460    /// every member write; a channel starts at `50` (announcement-only).
461    SetEventsDefault {
462        /// The room.
463        room_id: RoomId,
464        /// The new `events_default`.
465        level: i64,
466    },
467    /// Adds (or updates) an `m.tag` room-account-data tag.
468    SetTag {
469        /// The room being tagged.
470        room_id: RoomId,
471        /// The tag's own name.
472        tag: String,
473        /// The tag's sort order among sibling tags, if any.
474        order: Option<f64>,
475    },
476    /// Removes a tag from a room.
477    RemoveTag {
478        /// The room the tag is removed from.
479        room_id: RoomId,
480        /// The tag's own name.
481        tag: String,
482    },
483    /// Sets this room's read-up-to marker to `event_id` (`POST
484    /// /rooms/{roomId}/read_markers`).
485    MarkRead {
486        /// The room being marked read.
487        room_id: RoomId,
488        /// The last event the user has seen.
489        event_id: EventId,
490    },
491    /// Starts or stops this device's own typing indicator in a room (`PUT
492    /// /rooms/{roomId}/typing/{userId}`).
493    SetTyping {
494        /// The room to set the indicator in.
495        room_id: RoomId,
496        /// `true` to start, `false` to clear.
497        typing: bool,
498    },
499    /// Re-attempts decrypting one currently-undecryptable timeline item
500    /// (e.g. after the user manually re-verifies a device) — the same
501    /// retry this core already runs automatically the moment a matching
502    /// room key arrives (this module's own doc).
503    RetryDecryption {
504        /// The room the item lives in.
505        room_id: RoomId,
506        /// The item's event id.
507        event_id: EventId,
508    },
509    /// Fetches one older page of a room's timeline (`GET
510    /// /rooms/{roomId}/messages?dir=b`), prepending it once the response
511    /// arrives. A no-op if this room has no further history to page into
512    /// (no gap, no back-pagination token).
513    LoadOlder {
514        /// The room to page further back into.
515        room_id: RoomId,
516    },
517    /// Sets one piece of this account's own global account data (`PUT
518    /// /user/{userId}/account_data/{type}`) -- optimistically applied to
519    /// [`MessengerCore::global_account_data`] in the same call (this
520    /// module's own doc: the account-data endpoint replaces the whole
521    /// value, so there is nothing to merge).
522    SetAccountData {
523        /// The account-data type being set.
524        event_type: String,
525        /// The full new value.
526        content: serde_json::Value,
527    },
528    /// Sets one piece of `room_id`'s own room-scoped account data (`PUT
529    /// /user/{userId}/rooms/{roomId}/account_data/{type}`) -- optimistically
530    /// applied to [`MessengerCore::room_account_data`] in the same call.
531    /// [`MessengerCommand::SetTag`]/`RemoveTag` are the `"m.tag"`-specific
532    /// counterpart to this general command and behave the same way. Every
533    /// account-data write travels on [`crate::outgoing_queue::Lane::
534    /// AccountData`]: strictly FIFO, one in flight, so two writes to the
535    /// same key can neither race nor land out of order.
536    SetRoomAccountData {
537        /// The room the account data is scoped to.
538        room_id: RoomId,
539        /// The account-data type being set.
540        event_type: String,
541        /// The full new value.
542        content: serde_json::Value,
543    },
544    /// Searches the public-room directory (`POST /publicRooms`). The
545    /// latest result set is read back through
546    /// [`MessengerCore::public_rooms_result`]; a response to a search
547    /// superseded by a later one is dropped (this module's own doc).
548    SearchPublicRooms {
549        /// The search term.
550        term: String,
551    },
552    /// Searches the user directory (`POST /user_directory/search`). The
553    /// latest result set is read back through
554    /// [`MessengerCore::user_search_result`]; same supersession rule as
555    /// [`MessengerCommand::SearchPublicRooms`].
556    SearchUsers {
557        /// The search term.
558        term: String,
559    },
560}
561
562/// One user-visible change a shell/adapter should re-read from a snapshot
563/// getter. See [`MessengerCore::events`] for delivery semantics and
564/// [`MessengerCore::change_counter`] for the cheap "did anything change at
565/// all" signal.
566#[derive(Clone, Debug, PartialEq, Eq)]
567pub enum MessengerEvent {
568    /// The room list itself changed shape this tick (a room appeared via
569    /// join/invite/leave, or an already-known room's own state changed).
570    RoomsChanged,
571    /// `room_id`'s timeline gained or updated an item.
572    TimelineChanged {
573        /// The affected room.
574        room_id: RoomId,
575    },
576    /// `room_id`'s typing-user set changed.
577    TypingChanged {
578        /// The affected room.
579        room_id: RoomId,
580    },
581    /// `room_id`'s read-receipt set changed.
582    ReceiptsChanged {
583        /// The affected room.
584        room_id: RoomId,
585    },
586    /// `room_id`'s server-reported unread count changed.
587    UnreadChanged {
588        /// The affected room.
589        room_id: RoomId,
590    },
591    /// A previously-known device's Curve25519/Ed25519 keys changed — the
592    /// new keys were applied (`verified` cleared). Outbound Megolm shares
593    /// to that device are forgotten so the next send re-shares the room key.
594    DeviceKeyChanged {
595        /// The device's owner.
596        user_id: UserId,
597        /// The device whose keys changed.
598        device_id: DeviceId,
599    },
600    /// [`MessengerCore::public_rooms_result`] changed (a
601    /// [`MessengerCommand::SearchPublicRooms`] response landed and was not
602    /// superseded).
603    PublicRoomsChanged,
604    /// [`MessengerCore::user_search_result`] changed (a
605    /// [`MessengerCommand::SearchUsers`] response landed and was not
606    /// superseded).
607    UserSearchChanged,
608}
609
610/// One Megolm room event this core could not decrypt yet, kept so a later
611/// room-key arrival or an explicit [`MessengerCommand::RetryDecryption`]
612/// can re-attempt it without re-fetching anything — see this module's own
613/// "Sync ingestion order" doc.
614#[derive(Clone)]
615struct PendingDecryptItem {
616    session_id: String,
617    sender: UserId,
618    origin_server_ts: i64,
619    content: MegolmEncryptedContent,
620}
621
622/// `GET /rooms/{roomId}/messages`'s response shape, the parts this module
623/// reads (research doc §1.5) — `chunk` arrives newest-first for a `dir=b`
624/// page, reversed by [`MessengerCore::ingest_room_messages_response`]
625/// before [`crate::room::timeline::Timeline::prepend_back_page`] (which
626/// wants oldest-first, matching its own doc).
627#[derive(Deserialize)]
628struct RoomMessagesResponseBody {
629    #[serde(default)]
630    chunk: Vec<RawEvent>,
631    #[serde(default)]
632    end: Option<String>,
633}
634
635/// `PUT .../send/...`'s own response shape — the parts this module reads.
636#[derive(Deserialize)]
637struct SendResponseBody {
638    event_id: EventId,
639}
640
641/// `POST /createRoom`'s own response shape — the parts this module reads.
642#[derive(Deserialize)]
643struct CreateRoomResponseBody {
644    room_id: RoomId,
645}
646
647/// One row of a `POST /publicRooms` result -- read back through
648/// [`MessengerCore::public_rooms_result`].
649#[derive(Clone, Debug, PartialEq)]
650pub struct PublicRoomsResultEntry {
651    /// The room's own id.
652    pub room_id: RoomId,
653    /// The room's display name, if it set one.
654    pub name: Option<String>,
655    /// The room's topic, if it set one.
656    pub topic: Option<String>,
657    /// The room's own joined-member count.
658    pub num_joined_members: u64,
659}
660
661/// `POST /publicRooms`'s own response shape — the parts this module reads.
662#[derive(Deserialize)]
663struct PublicRoomsResponseBody {
664    #[serde(default)]
665    chunk: Vec<PublicRoomsChunkEntry>,
666}
667
668#[derive(Deserialize)]
669struct PublicRoomsChunkEntry {
670    room_id: RoomId,
671    #[serde(default)]
672    name: Option<String>,
673    #[serde(default)]
674    topic: Option<String>,
675    #[serde(default)]
676    num_joined_members: u64,
677}
678
679/// One row of a `POST /user_directory/search` result -- read back through
680/// [`MessengerCore::user_search_result`].
681#[derive(Clone, Debug, PartialEq)]
682pub struct UserDirectoryResultEntry {
683    /// The user's own id.
684    pub user_id: UserId,
685    /// The user's display name, if they set one.
686    pub display_name: Option<String>,
687}
688
689/// `POST /user_directory/search`'s own response shape — the parts this
690/// module reads.
691#[derive(Deserialize)]
692struct UserDirectorySearchResponseBody {
693    #[serde(default)]
694    results: Vec<UserDirectorySearchResultEntry>,
695}
696
697#[derive(Deserialize)]
698struct UserDirectorySearchResultEntry {
699    user_id: UserId,
700    #[serde(default)]
701    display_name: Option<String>,
702}
703
704/// One room-scoped send's own content, before it is turned into wire bytes
705/// (M13b send pipeline). A reaction/redaction never enters the key-sharing
706/// half of the pipeline at all — only a room event ([`SendPayload::Message`],
707/// [`SendPayload::Event`]) can be Megolm-encrypted.
708#[derive(Clone)]
709enum SendPayload {
710    /// A composed message — goes through the key-query/claim/share phases
711    /// when its room is encrypted.
712    Message(OutgoingMessage),
713    /// Any other room event whose wire type and plaintext content are
714    /// already final (a forward) — walks the same phases as
715    /// [`SendPayload::Message`].
716    Event {
717        /// The wire event type (`m.room.message`).
718        event_type: String,
719        /// The event's plaintext content.
720        content: serde_json::Value,
721    },
722    /// A plaintext `m.reaction` — sent as-is, in any room.
723    Reaction {
724        /// The event being reacted to.
725        target: EventId,
726        /// The reaction's own key.
727        key: String,
728    },
729    /// A redaction (`PUT .../redact/...`) — sent as-is, in any room.
730    Redaction {
731        /// The event being redacted.
732        target: EventId,
733        /// Why, if given.
734        reason: Option<String>,
735    },
736}
737
738/// A room event as it goes on the wire before any encryption — what
739/// [`SendPayload::room_event`] derives from a [`SendPayload::Message`] or a
740/// [`SendPayload::Event`].
741struct RoomEventWire {
742    event_type: String,
743    content: serde_json::Value,
744    /// The cleartext `m.relates_to` an encrypted envelope must copy
745    /// ([`message_relates_to`]).
746    relates_to: Option<RelatesTo>,
747}
748
749impl SendPayload {
750    /// The room-event shape of this payload; `None` for a reaction or a
751    /// redaction (never encrypted, never echoed into the timeline).
752    fn room_event(&self) -> Option<RoomEventWire> {
753        match self {
754            SendPayload::Message(message) => Some(RoomEventWire {
755                event_type: "m.room.message".to_string(),
756                content: message_wire_content(message),
757                relates_to: message_relates_to(message),
758            }),
759            SendPayload::Event { event_type, content } => {
760                Some(RoomEventWire { event_type: event_type.clone(), content: content.clone(), relates_to: None })
761            }
762            SendPayload::Reaction { .. } | SendPayload::Redaction { .. } => None,
763        }
764    }
765
766    fn is_room_event(&self) -> bool {
767        matches!(self, SendPayload::Message(_) | SendPayload::Event { .. })
768    }
769}
770
771/// The exact wire request a [`PendingSend`] will make once it reaches
772/// [`SendPhase::AwaitingSend`] — computed once (either directly, for a
773/// plaintext send, or after Megolm-encrypting a [`SendPayload::Message`])
774/// and cached from then on, so a later [`MessengerCommand::RetrySend`]
775/// never re-derives it (never re-encrypts — this module's own doc).
776#[derive(Clone)]
777enum WireSend {
778    /// `PUT /rooms/{roomId}/send/{eventType}/{txnId}`.
779    Event {
780        /// The wire event type (`m.room.message`, `m.room.encrypted`, or
781        /// `m.reaction`).
782        event_type: String,
783        /// The event's own content.
784        content: serde_json::Value,
785    },
786    /// `PUT /rooms/{roomId}/redact/{eventId}/{txnId}`.
787    Redact {
788        /// The event being redacted.
789        target: EventId,
790        /// Why, if given.
791        reason: Option<String>,
792    },
793}
794
795/// Where one room-scoped send currently sits in its own pipeline (M13b's
796/// module doc: key-query -> key-claim -> key-share -> the room event
797/// itself, in that order, for an encrypted [`SendPayload::Message`];
798/// straight to [`SendPhase::AwaitingSend`] for everything else).
799#[derive(Clone)]
800enum SendPhase {
801    /// Waiting for an earlier send in the same room to finish first —
802    /// [`MessengerCore::start_send_pipeline`] has not run for this send
803    /// yet.
804    Queued,
805    /// Waiting on one `/keys/query` this pipeline itself minted, for a
806    /// member whose device list is outdated or was never tracked at all —
807    /// [`MessengerCore::send_request_owner`] is what routes the eventual
808    /// response back to this send, so this phase itself carries no request
809    /// id of its own.
810    AwaitingKeysQuery,
811    /// Waiting on one `/keys/claim`, for recipient devices this account
812    /// has no Olm session with yet.
813    AwaitingKeysClaim {
814        /// Exactly the devices that request claimed a key for — needed to
815        /// apply the response ([`OlmSessionManager::on_keys_claim_response`]
816        /// takes the original request list, not just the response body).
817        devices: Vec<StoredDevice>,
818    },
819    /// Waiting on every `sendToDevice` request sharing the room key with
820    /// its not-yet-shared recipients — the room event itself is withheld
821    /// until every one of these succeeds (this module's own doc: "WAIT for
822    /// those requests to succeed before sending the room event").
823    AwaitingKeyShare {
824        /// Every still-outstanding share request's own id.
825        pending: BTreeSet<RequestId>,
826    },
827    /// Waiting on the room event/redaction itself.
828    AwaitingSend,
829    /// Terminal: the send (or one of its pipeline steps) failed outright.
830    /// Kept (rather than removed) so [`MessengerCommand::RetrySend`] can
831    /// find it again, and so [`MessengerCore::send_failure_reason`] can
832    /// surface why.
833    Failed {
834        /// The Matrix `errcode` the failing response carried.
835        errcode: String,
836    },
837}
838
839/// One room-scoped send this core is tracking end to end — from
840/// [`MessengerCommand::SendMessage`]/`React`/`Redact` through to a
841/// terminal [`SendPhase::AwaitingSend`] response. See this module's own
842/// doc for the pipeline every [`SendPayload::Message`] in an encrypted
843/// room walks through.
844#[derive(Clone)]
845struct PendingSend {
846    /// The room this send belongs to — also this send's own place in
847    /// [`MessengerCore::pending_sends`]'s per-room FIFO.
848    room_id: RoomId,
849    /// What is being sent.
850    payload: SendPayload,
851    /// Where this send currently sits in its own pipeline.
852    phase: SendPhase,
853    /// The exact wire request, once computed — see [`WireSend`]'s own doc.
854    wire: Option<WireSend>,
855}
856
857/// One account-data slot: `(room, event type)`; `None` is this account's own
858/// global account data.
859type AccountDataKey = (Option<RoomId>, String);
860
861/// Bookkeeping for one [`AccountDataKey`] that has account-data writes
862/// queued or in flight (an entry exists only while `pending > 0`).
863///
864/// A write updates the cached value optimistically. Without this guard a
865/// `/sync` that echoes an OLDER write of the same key would overwrite that
866/// optimistic value, and the next edit would build on the stale copy and
867/// silently drop a change. While writes are outstanding, `/sync` only
868/// records what the server reports (`server_value`); the optimistic cache
869/// stays authoritative. When the last write settles: a success keeps the
870/// optimistic value (it is what the server now holds; the next sync
871/// confirms it), a terminal failure restores `server_value`.
872struct AccountDataGuard {
873    /// Writes for this key queued or in flight.
874    pending: u32,
875    /// The last value the server is known to hold for the key: whatever
876    /// the cache held when the first outstanding write was made, then
877    /// whatever `/sync` reported since. `None`: nothing known (never seen).
878    server_value: Option<serde_json::Value>,
879}
880
881/// The `(room, event type)` an account-data PUT `path` writes, or `None` if
882/// `path` is not an account-data path.
883fn account_data_key_from_path(path: &str) -> Option<AccountDataKey> {
884    let segments: Vec<&str> = path.split('/').collect();
885    let n = segments.len();
886    if n < 4 || segments[n - 2] != "account_data" {
887        return None;
888    }
889    let event_type = percent_decode_segment(segments[n - 1])?;
890    if segments[n - 4] == "user" {
891        return Some((None, event_type));
892    }
893    if n >= 6 && segments[n - 4] == "rooms" && segments[n - 6] == "user" {
894        let room_id = RoomId::parse(percent_decode_segment(segments[n - 3])?).ok()?;
895        return Some((Some(room_id), event_type));
896    }
897    None
898}
899
900/// The single-writer kernel — see this module's own doc.
901pub struct MessengerCore<C: RecordCodec> {
902    config: CoreConfig,
903    store: Store<C>,
904    account: OlmAccountState,
905    /// Automatic cross-signing state (M3), loaded lazily on the first sync.
906    xsign: Option<CrossSigningState>,
907    xsign_inflight: bool,
908    xsign_attempts: u8,
909    outgoing: OutgoingQueue,
910    counters: Counters,
911    jitter: Box<dyn Jitter>,
912
913    rooms: BTreeMap<RoomId, RoomState>,
914    timelines: BTreeMap<RoomId, Timeline>,
915    direct_account_data: Option<DirectContent>,
916    global_account_data: BTreeMap<String, serde_json::Value>,
917    room_account_data: BTreeMap<RoomId, BTreeMap<String, serde_json::Value>>,
918    typing: BTreeMap<RoomId, Vec<UserId>>,
919    receipts: BTreeMap<RoomId, ReceiptContent>,
920
921    /// The latest [`MessengerCommand::SearchPublicRooms`] result set, read
922    /// back through [`MessengerCore::public_rooms_result`].
923    public_rooms_result: Vec<PublicRoomsResultEntry>,
924    /// The [`RequestId`] of the most recently dispatched
925    /// [`MessengerCommand::SearchPublicRooms`] -- a response naming any
926    /// other id is a superseded search and is dropped (this module's own
927    /// doc).
928    latest_public_rooms_request: Option<RequestId>,
929    /// The latest [`MessengerCommand::SearchUsers`] result set, read back
930    /// through [`MessengerCore::user_search_result`].
931    user_search_result: Vec<UserDirectoryResultEntry>,
932    /// Same supersession tracking as `latest_public_rooms_request`, for
933    /// [`MessengerCommand::SearchUsers`].
934    latest_user_search_request: Option<RequestId>,
935
936    /// Keys with account-data writes outstanding -- see
937    /// [`AccountDataGuard`]. In-memory only: rebuilt at
938    /// [`MessengerCore::open`] from the persisted request queue.
939    account_data_guard: BTreeMap<AccountDataKey, AccountDataGuard>,
940    /// Which key each outstanding account-data write belongs to.
941    account_data_write_keys: BTreeMap<RequestId, AccountDataKey>,
942
943    pending_decrypt: BTreeMap<RoomId, BTreeMap<EventId, PendingDecryptItem>>,
944    /// `(room, session)` pairs this process already asked for. Memory-only:
945    /// a restart asks again, which is what recovers a share the previous
946    /// process acked and then lost.
947    key_requests_sent: BTreeSet<(RoomId, String)>,
948    /// M4: standalone `/keys/claim` calls made so a joiner can reach old
949    /// members (id -> devices claimed).
950    history_claims: BTreeMap<RequestId, Vec<StoredDevice>>,
951    /// M4: devices a history claim was already attempted for this process.
952    history_claim_tried: BTreeSet<(UserId, DeviceId)>,
953    /// Sessions a peer refused with `m.room_key.withheld`. Stops the retry.
954    withheld_sessions: BTreeSet<(RoomId, String)>,
955    /// `(requester, device, request_id)` already answered with a share.
956    key_requests_answered: BTreeSet<(UserId, DeviceId, String)>,
957    unknown_sender_retry: VecDeque<ToDeviceEvent>,
958
959    /// The last `now_ms` a [`MessengerCommand::SetTyping { typing: true,
960    /// .. }`] actually sent a PUT at, per room — the debounce window
961    /// [`TYPING_DEBOUNCE_MS`] is measured from (this module's own doc). No
962    /// entry means either never sent, or explicitly stopped.
963    typing_debounce: BTreeMap<RoomId, i64>,
964    /// Every room-scoped send's own transaction id, in FIFO send order —
965    /// only the front of each room's queue is active (this module's own
966    /// "one message's pipeline completes before the next ... is
967    /// encrypted" doc); [`MessengerCore::finish_active_send`] pops it and
968    /// starts the next one.
969    pending_sends: BTreeMap<RoomId, VecDeque<TxnId>>,
970    /// Every [`PendingSend`] this core is tracking, keyed by its own
971    /// `txn_id` — kept even after it leaves `pending_sends` (a terminal
972    /// [`SendPhase::Failed`] stays here so [`MessengerCommand::RetrySend`]
973    /// can find it again).
974    send_state: BTreeMap<TxnId, PendingSend>,
975    /// Which [`PendingSend`] a still-pending request (a key-query,
976    /// key-claim, key-share, or the send/redact itself) this pipeline
977    /// minted belongs to — same reasoning as `pending_kinds`.
978    send_request_owner: BTreeMap<RequestId, TxnId>,
979    /// Which [`CreateRoomKind`] a still-pending `POST /createRoom` request
980    /// was building, so its response can merge `m.direct` for a
981    /// [`CreateRoomKind::Dm`] (this module's own doc).
982    pending_create_room: BTreeMap<RequestId, CreateRoomKind>,
983
984    /// Every [`OutgoingRequestKind`] this core itself minted and is still
985    /// waiting on, keyed by the [`RequestId`] it enqueued it under —
986    /// [`crate::outgoing_queue::OutgoingQueue`] does not expose a way to
987    /// look this back up given only an id, so this core tracks it
988    /// alongside instead (see [`MessengerCore::enqueue_request`]).
989    pending_kinds: BTreeMap<RequestId, OutgoingRequestKind>,
990    /// Which room a still-pending `RoomMessages` request was paginating —
991    /// same reasoning as `pending_kinds`.
992    pending_room_messages: BTreeMap<RequestId, RoomId>,
993    /// The currently outstanding `/sync` request's id, if any — this
994    /// core's own single-flight tracking (this module's own doc).
995    sync_request_id: Option<RequestId>,
996    /// Whether a `/sync` request has completed successfully in THIS core's
997    /// lifetime. Until then the next `/sync` is minted with `timeout=0` (an
998    /// immediate catch-up answer, the matrix-js-sdk convention); only after
999    /// it does the core long-poll.
1000    sync_caught_up: bool,
1001    /// [`FlushBatch`]es [`crate::outgoing_queue::OutgoingQueue::enqueue`]
1002    /// already drained from the store's own pending set (to seal a new
1003    /// request's [`crate::persist::RequiredSeq`]) but that a shell has not
1004    /// yet drained via [`MessengerCore::take_flush_batch`] — see
1005    /// [`MessengerCore::enqueue_request`]'s own doc for why this can't
1006    /// simply be re-derived from `store.take_flush_batch()` later.
1007    pending_flush_batches: VecDeque<FlushBatch>,
1008
1009    events: Vec<MessengerEvent>,
1010    change_counter: u64,
1011    /// The last response this core could not ingest (a wire shape it does
1012    /// not decode, say), for [`MessengerCore::take_ingest_error`]. The
1013    /// response is otherwise dropped, so without this a shell would see
1014    /// nothing but a `/sync` loop that never advances.
1015    ingest_error: Option<String>,
1016}
1017
1018impl<C: RecordCodec> MessengerCore<C> {
1019    /// Signs an allow-listed scoped payload with this device's Ed25519 key; see
1020    /// [`crate::crypto::account::OlmAccountState::sign_scoped`]. (public key, signature), base64.
1021    pub fn sign_scoped(&self, domain: &str, payload: &[u8]) -> Result<(String, String), MessengerError> {
1022        self.account.sign_scoped(domain, payload)
1023    }
1024
1025    /// Rebuilds a core from whatever a shell already read back from
1026    /// durable storage (`records`, possibly empty for a brand-new device),
1027    /// `config`, `secrets` (caller-supplied [`CoreSecrets`]: `store_seal_key`
1028    /// is consumed by [`MessengerCore::open_sealed`] before it reaches here,
1029    /// and `backup_key` is not read by this kernel), and a `jitter` source for
1030    /// [`crate::outgoing_queue::OutgoingQueue`]'s own retry backoff. `now_ms`
1031    /// is accepted for API symmetry with every other entry point (this
1032    /// crate's own "no clock inside" rule) but not read by anything in this
1033    /// piece's own restore logic yet — timeline content itself is never
1034    /// persisted (re-fetched off the next `/sync`/back-page instead, this
1035    /// module's own doc), so there is nothing time-sensitive to restore
1036    /// here before a later piece needs one.
1037    pub fn open(
1038        records: impl IntoIterator<Item = SealedRecord>,
1039        codec: C,
1040        config: CoreConfig,
1041        secrets: CoreSecrets,
1042        _now_ms: i64,
1043        jitter: Box<dyn Jitter>,
1044    ) -> Result<Self, MessengerError> {
1045        // The seal key lives in `codec` (`open_sealed` builds a
1046        // `SealedRecordCodec` from `store_seal_key`). `backup_key` stays on
1047        // `CoreSecrets` for the caller; this kernel does not read it.
1048        let _ = secrets;
1049        let mut store = Store::load(records, codec, config.device_id.clone())?;
1050        let mut outgoing = OutgoingQueue::load(&store)?;
1051        let account = OlmAccountState::load_or_create(&mut store)?;
1052
1053        let mut rooms = BTreeMap::new();
1054        let mut timelines = BTreeMap::new();
1055        for room_id in store.room_ids()? {
1056            if let Some(bytes) = store.room_state(&room_id)? {
1057                let state: RoomState = serde_json::from_slice(bytes)
1058                    .map_err(|source| MessengerError::Crypto(format!("decode room state for {room_id}: {source}")))?;
1059                rooms.insert(room_id.clone(), state);
1060                timelines.insert(room_id, Timeline::new());
1061            }
1062        }
1063
1064        let pending_requests = store.pending_requests()?;
1065        let max_pending_request_seed = pending_requests.iter().filter_map(|req| req.request.id.as_seed()).max();
1066        // A `/sync` restored from the store carries the timeout it was minted
1067        // with (a 30 s long-poll): replaying it would stall a resumed session
1068        // for that long before anything is known. It has no side effect to
1069        // lose, so drop it (its id already counted toward the seed above);
1070        // `ensure_sync_enqueued` mints a fresh catch-up sync from the
1071        // persisted token.
1072        outgoing.discard_kind(&mut store, OutgoingRequestKind::Sync)?;
1073        let persisted_counters = store.counters()?.unwrap_or_default();
1074        let mut counters = Counters {
1075            next_request_id: restore_counter(persisted_counters.next_request_id, max_pending_request_seed),
1076            next_txn_id: restore_counter(persisted_counters.next_txn_id, None),
1077        };
1078        if counters != persisted_counters {
1079            // Restoration actually advanced a value beyond what was on
1080            // disk (the defensive "pending" half of `restore_counter`
1081            // fired) -- persist the advanced value immediately so it, in
1082            // turn, can never be handed out a second time by a future
1083            // restart.
1084            store.save_counters(counters)?;
1085        }
1086        // A store lost and recreated for a device that already exists
1087        // server-side (the bearer survived in the keychain) must not restart
1088        // txn ids low: the server would dedup each send into an OLD event
1089        // and report it as sent. Floor the in-memory counter at the wall
1090        // clock; `next_txn_id` persists it before the first id is used.
1091        counters.next_txn_id = counters.next_txn_id.max(fresh_txn_seed());
1092
1093        let mut core = Self {
1094            config,
1095            store,
1096            account,
1097            outgoing,
1098            counters,
1099            jitter,
1100            rooms,
1101            timelines,
1102            direct_account_data: None,
1103            global_account_data: BTreeMap::new(),
1104            room_account_data: BTreeMap::new(),
1105            typing: BTreeMap::new(),
1106            receipts: BTreeMap::new(),
1107            public_rooms_result: Vec::new(),
1108            latest_public_rooms_request: None,
1109            user_search_result: Vec::new(),
1110            latest_user_search_request: None,
1111            account_data_guard: BTreeMap::new(),
1112            account_data_write_keys: BTreeMap::new(),
1113            pending_decrypt: BTreeMap::new(),
1114            key_requests_sent: BTreeSet::new(),
1115            history_claims: BTreeMap::new(),
1116            history_claim_tried: BTreeSet::new(),
1117            withheld_sessions: BTreeSet::new(),
1118            key_requests_answered: BTreeSet::new(),
1119            unknown_sender_retry: VecDeque::new(),
1120            typing_debounce: BTreeMap::new(),
1121            pending_sends: BTreeMap::new(),
1122            send_state: BTreeMap::new(),
1123            send_request_owner: BTreeMap::new(),
1124            pending_create_room: BTreeMap::new(),
1125            pending_kinds: BTreeMap::new(),
1126            pending_room_messages: BTreeMap::new(),
1127            sync_request_id: None,
1128            sync_caught_up: false,
1129            pending_flush_batches: VecDeque::new(),
1130            events: Vec::new(),
1131            change_counter: 0,
1132            ingest_error: None,
1133            xsign: None,
1134            xsign_inflight: false,
1135            xsign_attempts: 0,
1136        };
1137        core.rebuild_account_data_guard(&pending_requests);
1138        Ok(core)
1139    }
1140
1141    /// Rebuilds [`MessengerCore::account_data_guard`] (and the optimistic
1142    /// cache it protects) from account-data writes that were still pending
1143    /// when the core last stopped: each persisted request counts as one
1144    /// outstanding write for its key, and the newest request's body -- the
1145    /// value the server will hold once the queue drains -- is the cached
1146    /// value again. The server's own current value is unknown until
1147    /// `/sync` reports it.
1148    fn rebuild_account_data_guard(&mut self, pending_requests: &[PendingRequest]) {
1149        let mut writes: Vec<&PendingRequest> = pending_requests
1150            .iter()
1151            .filter(|record| matches!(record.request.kind, OutgoingRequestKind::AccountData | OutgoingRequestKind::RoomAccountData))
1152            .collect();
1153        writes.sort_by(|a, b| a.request.id.cmp(&b.request.id));
1154        for record in writes {
1155            let Some(key) = account_data_key_from_path(&record.request.path) else { continue };
1156            let Some(body) = record.request.body.clone() else { continue };
1157            let guard = self.account_data_guard.entry(key.clone()).or_insert(AccountDataGuard { pending: 0, server_value: None });
1158            guard.pending += 1;
1159            self.store_account_data_cache(&key, Some(body));
1160            self.account_data_write_keys.insert(record.request.id.clone(), key);
1161        }
1162    }
1163
1164    /// Mints the next [`RequestId`], persisting the advanced [`Counters`]
1165    /// in the same call — see this module's own doc for why this ordering
1166    /// is what makes the flush-before-send barrier cover it for free.
1167    fn next_request_id(&mut self) -> Result<RequestId, MessengerError> {
1168        let seed = self.counters.next_request_id;
1169        self.counters.next_request_id = seed.saturating_add(1);
1170        self.store.save_counters(self.counters)?;
1171        Ok(RequestId::next(seed))
1172    }
1173
1174    /// Mints the next [`TxnId`], persisting the advanced [`Counters`] in
1175    /// the same call — symmetric with [`MessengerCore::next_request_id`]
1176    /// (this module's own doc's "Counters that must never repeat across a
1177    /// restart" section).
1178    fn next_txn_id(&mut self) -> Result<TxnId, MessengerError> {
1179        let seed = self.counters.next_txn_id;
1180        self.counters.next_txn_id = seed.saturating_add(1);
1181        self.store.save_counters(self.counters)?;
1182        Ok(TxnId::new(seed))
1183    }
1184
1185    /// Enqueues `request` on `lane`, remembering its kind so a later
1186    /// [`MessengerCore::on_response`] can route the response without
1187    /// [`crate::outgoing_queue::OutgoingQueue`] needing a by-id lookup of
1188    /// its own.
1189    ///
1190    /// [`crate::outgoing_queue::OutgoingQueue::enqueue`] already drains
1191    /// whatever was pending into a [`FlushBatch`] the instant it needs a
1192    /// [`crate::persist::RequiredSeq`] to seal the new request against
1193    /// (`crate::persist`'s own doc) — that batch is gone from the store's
1194    /// own pending set the moment it is produced, so if this method
1195    /// discarded it here, [`MessengerCore::take_flush_batch`] would never
1196    /// see it again and the request (and everything after it in the same
1197    /// lane) would stay unreleasable forever. [`MessengerCore::pending_flush_batches`]
1198    /// is where it waits instead, until a shell actually drains it.
1199    fn enqueue_request(&mut self, request: OutgoingRequest, lane: Lane) -> Result<(), MessengerError> {
1200        let id = request.id.clone();
1201        let kind = request.kind;
1202        if let Some(batch) = self.outgoing.enqueue(&mut self.store, request, lane)? {
1203            self.pending_flush_batches.push_back(batch);
1204        }
1205        self.pending_kinds.insert(id, kind);
1206        Ok(())
1207    }
1208
1209    fn emit(&mut self, event: MessengerEvent) {
1210        self.change_counter = self.change_counter.wrapping_add(1);
1211        self.events.push(event);
1212    }
1213
1214    fn drain_events(&mut self) -> Vec<MessengerEvent> {
1215        std::mem::take(&mut self.events)
1216    }
1217
1218    // ---------------------------------------------------------------
1219    // M13b send pipeline
1220    // ---------------------------------------------------------------
1221
1222    /// Starts (or queues) one room-scoped send — the shared entry point
1223    /// for [`MessengerCommand::SendMessage`]/`Forward`/`React`/
1224    /// `Redact`. Only a room event ([`SendPayload::Message`],
1225    /// [`SendPayload::Event`]) gets an immediate timeline local echo (this
1226    /// module's own doc): appended as [`SendState::LocalEcho`] then
1227    /// immediately moved to [`SendState::Sending`], matching the plan's
1228    /// own wording ("local echo in the timeline with `SendState::Sending`
1229    /// -> `MessengerEvent::TimelineChanged`") — both happen inside this one
1230    /// call, so a caller never observes the intermediate `LocalEcho` value.
1231    /// Sending a message also stops this device's own typing indicator in
1232    /// the room, unconditionally (module doc: "false on send").
1233    fn start_new_send(
1234        &mut self,
1235        room_id: RoomId,
1236        payload: SendPayload,
1237        caller_txn_id: Option<TxnId>,
1238        now_ms: i64,
1239    ) -> Result<(), MessengerError> {
1240        let txn_id = match caller_txn_id {
1241            Some(txn_id) => txn_id,
1242            None => self.next_txn_id()?,
1243        };
1244
1245        if let Some(wire) = payload.room_event() {
1246            if self.typing_debounce.remove(&room_id).is_some() {
1247                if let Ok(request_id) = self.next_request_id() {
1248                    let request = OutgoingRequest::typing(request_id, &room_id, &self.config.user_id, false, None);
1249                    let _ = self.enqueue_request(request, Lane::Other);
1250                }
1251            }
1252            let content = match &payload {
1253                SendPayload::Message(message) => message_local_echo_content(message),
1254                _ => interpret_content(&wire.event_type, None, &wire.content),
1255            };
1256            let timeline = self.timelines.entry(room_id.clone()).or_default();
1257            timeline.push_local_echo(
1258                txn_id.clone(),
1259                self.config.user_id.clone(),
1260                now_ms,
1261                &wire.event_type,
1262                content,
1263                wire.content,
1264            );
1265            timeline.mark_send_state(&txn_id, SendState::Sending);
1266            self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
1267        }
1268
1269        self.send_state.insert(txn_id.clone(), PendingSend { room_id: room_id.clone(), payload, phase: SendPhase::Queued, wire: None });
1270        let queue = self.pending_sends.entry(room_id).or_default();
1271        let was_empty = queue.is_empty();
1272        queue.push_back(txn_id.clone());
1273        if was_empty {
1274            self.start_send_pipeline(&txn_id, now_ms)?;
1275        }
1276        Ok(())
1277    }
1278
1279    /// Re-queues a [`SendPhase::Failed`] send at the back of its room's own
1280    /// FIFO, reusing its already-computed [`WireSend`] (never re-encrypting
1281    /// — [`MessengerCommand::RetrySend`]'s own doc). A no-op if `txn_id`
1282    /// names no currently-failed send in `room_id`.
1283    fn retry_send(&mut self, room_id: &RoomId, txn_id: &TxnId, now_ms: i64) -> Result<(), MessengerError> {
1284        let Some(pending) = self.send_state.get(txn_id) else { return Ok(()) };
1285        if pending.room_id != *room_id || !matches!(pending.phase, SendPhase::Failed { .. }) {
1286            return Ok(());
1287        }
1288        self.set_send_phase(txn_id, SendPhase::Queued);
1289        let queue = self.pending_sends.entry(room_id.clone()).or_default();
1290        let was_empty = queue.is_empty();
1291        queue.push_back(txn_id.clone());
1292        if was_empty {
1293            self.start_send_pipeline(txn_id, now_ms)?;
1294        }
1295        Ok(())
1296    }
1297
1298    fn set_send_phase(&mut self, txn_id: &TxnId, phase: SendPhase) {
1299        if let Some(entry) = self.send_state.get_mut(txn_id) {
1300            entry.phase = phase;
1301        }
1302    }
1303
1304    /// Advances `txn_id` (which must currently be the front of its room's
1305    /// own FIFO) to its next pipeline step. If [`PendingSend::wire`] is
1306    /// already computed (a retry, or any send re-entering this function
1307    /// after an earlier step completed), it goes straight to
1308    /// [`MessengerCore::begin_awaiting_send`] — never re-derived. Otherwise:
1309    /// a room event ([`SendPayload::Message`]/[`SendPayload::Event`]) in a
1310    /// currently-encrypted room enters the key pipeline; everything else
1311    /// builds its plaintext wire body directly.
1312    fn start_send_pipeline(&mut self, txn_id: &TxnId, now_ms: i64) -> Result<(), MessengerError> {
1313        let Some(pending) = self.send_state.get(txn_id) else { return Ok(()) };
1314        let room_id = pending.room_id.clone();
1315
1316        if pending.wire.is_some() {
1317            return self.begin_awaiting_send(txn_id, &room_id);
1318        }
1319
1320        let is_room_event = pending.payload.is_room_event();
1321        let encrypted_room = self.rooms.get(&room_id).is_some_and(|room| room.encryption.is_some());
1322
1323        if is_room_event && encrypted_room {
1324            self.begin_key_pipeline(txn_id, &room_id, now_ms)
1325        } else {
1326            self.build_plaintext_wire_body(txn_id);
1327            self.begin_awaiting_send(txn_id, &room_id)
1328        }
1329    }
1330
1331    /// Every user this account must consider a Megolm recipient for
1332    /// `room_id`: every currently join-or-invite member, plus this
1333    /// account's own user id (its OTHER devices also need the room key —
1334    /// `crypto::group_sessions`'s own module doc).
1335    fn encrypted_room_member_ids(&self, room_id: &RoomId) -> BTreeSet<UserId> {
1336        let mut ids: BTreeSet<UserId> = self
1337            .rooms
1338            .get(room_id)
1339            .map(|state| {
1340                state
1341                    .members
1342                    .iter()
1343                    .filter(|(_, member)| matches!(member.membership, Membership::Join | Membership::Invite))
1344                    .map(|(user_id, _)| user_id.clone())
1345                    .collect()
1346            })
1347            .unwrap_or_default();
1348        ids.insert(self.config.user_id.clone());
1349        ids
1350    }
1351
1352    /// Marks any of `member_ids` this account has never tracked at all as
1353    /// newly tracked-and-outdated ([`DeviceTracker::on_device_lists`]),
1354    /// then returns every one of `member_ids` that is currently outdated
1355    /// (whether it already was, or just became so) — the send pipeline's
1356    /// own "any member's device list is outdated or unknown" check.
1357    fn mark_outdated_or_untracked(&mut self, member_ids: &BTreeSet<UserId>) -> Result<Vec<UserId>, MessengerError> {
1358        let tracked: BTreeMap<UserId, bool> = self.store.tracked_users()?.into_iter().collect();
1359        let mut outdated = Vec::new();
1360        let mut newly_tracked = Vec::new();
1361        for user_id in member_ids {
1362            match tracked.get(user_id) {
1363                Some(true) => outdated.push(user_id.clone()),
1364                Some(false) => {}
1365                None => newly_tracked.push(user_id.clone()),
1366            }
1367        }
1368        if !newly_tracked.is_empty() {
1369            DeviceTracker::on_device_lists(&mut self.store, &newly_tracked, &[])?;
1370            outdated.extend(newly_tracked);
1371        }
1372        Ok(outdated)
1373    }
1374
1375    /// Every device this store currently has on file for any of
1376    /// `member_ids`.
1377    fn known_devices_for(&self, member_ids: &BTreeSet<UserId>) -> Result<Vec<StoredDevice>, MessengerError> {
1378        let mut all = Vec::new();
1379        for user_id in member_ids {
1380            all.extend(DeviceTracker::devices_for_user(&self.store, user_id)?);
1381        }
1382        Ok(all)
1383    }
1384
1385    /// `known_devices`, filtered down to the devices this pipeline must
1386    /// actually reach: a current member's, not blocked, not this device's
1387    /// own.
1388    fn candidate_recipient_devices(&self, member_ids: &BTreeSet<UserId>, known_devices: &[StoredDevice]) -> Vec<StoredDevice> {
1389        known_devices
1390            .iter()
1391            .filter(|device| member_ids.contains(&device.user_id) && !device.blocked)
1392            .filter(|device| !(device.user_id == self.config.user_id && device.device_id == self.config.device_id))
1393            .cloned()
1394            .collect()
1395    }
1396
1397    /// Step 1 of the encrypted-message pipeline (this module's own doc):
1398    /// if any recipient's device list is outdated or was never tracked at
1399    /// all, mint one `/keys/query` and wait for it; otherwise fall through
1400    /// to step 2.
1401    fn begin_key_pipeline(&mut self, txn_id: &TxnId, room_id: &RoomId, now_ms: i64) -> Result<(), MessengerError> {
1402        let members = self.encrypted_room_member_ids(room_id);
1403        let outdated = self.mark_outdated_or_untracked(&members)?;
1404        if !outdated.is_empty() {
1405            let request_id = self.next_request_id()?;
1406            if let Some(request) = DeviceTracker::keys_query_request(&self.store, request_id.clone())? {
1407                self.send_request_owner.insert(request_id.clone(), txn_id.clone());
1408                self.enqueue_request(request, Lane::Other)?;
1409                self.set_send_phase(txn_id, SendPhase::AwaitingKeysQuery);
1410                return Ok(());
1411            }
1412        }
1413        self.begin_keys_claim_or_share(txn_id, room_id, now_ms)
1414    }
1415
1416    /// Step 2: if any recipient candidate has no established Olm session
1417    /// yet, mint one `/keys/claim` and wait for it; otherwise fall through
1418    /// to step 3.
1419    fn begin_keys_claim_or_share(&mut self, txn_id: &TxnId, room_id: &RoomId, now_ms: i64) -> Result<(), MessengerError> {
1420        let members = self.encrypted_room_member_ids(room_id);
1421        let known_devices = self.known_devices_for(&members)?;
1422        let candidates = self.candidate_recipient_devices(&members, &known_devices);
1423        let missing: Vec<StoredDevice> =
1424            OlmSessionManager::sessions_missing_for(&self.store, &candidates)?.into_iter().cloned().collect();
1425        if !missing.is_empty() {
1426            let request_id = self.next_request_id()?;
1427            let refs: Vec<&StoredDevice> = missing.iter().collect();
1428            if let Some(request) = OlmSessionManager::keys_claim_request(request_id.clone(), &refs) {
1429                self.send_request_owner.insert(request_id.clone(), txn_id.clone());
1430                self.enqueue_request(request, Lane::Other)?;
1431                self.set_send_phase(txn_id, SendPhase::AwaitingKeysClaim { devices: missing });
1432                return Ok(());
1433            }
1434        }
1435        self.begin_share_or_encrypt(txn_id, room_id, now_ms)
1436    }
1437
1438    /// Step 3: ensures a current outbound Megolm session, fans out the
1439    /// room key (Olm to-device) to every not-yet-shared, currently-reachable
1440    /// device, and waits for every one of those `sendToDevice` calls to
1441    /// succeed before the room event itself is ever encrypted or sent
1442    /// (module doc: a share failure fails the whole message, not just the
1443    /// unreachable device). A device this account still has no Olm session
1444    /// with after step 2 (a refused/unanswered `/keys/claim`) is left out
1445    /// of the fan-out rather than failing the send outright — the same
1446    /// "can't reach everyone, degrade rather than block" doctrine this
1447    /// crate already applies to an unresolved UTD. If nothing needs
1448    /// sharing at all (already fully shared, or nobody reachable), falls
1449    /// straight through to encrypting and sending.
1450    fn begin_share_or_encrypt(&mut self, txn_id: &TxnId, room_id: &RoomId, now_ms: i64) -> Result<(), MessengerError> {
1451        let Some(encryption) = self.rooms.get(room_id).and_then(|room| room.encryption.clone()) else {
1452            self.build_plaintext_wire_body(txn_id);
1453            return self.begin_awaiting_send(txn_id, room_id);
1454        };
1455        let members = self.encrypted_room_member_ids(room_id);
1456        let known_devices = self.known_devices_for(&members)?;
1457        let update = GroupSessionManager::ensure_outbound_session(
1458            &mut self.store,
1459            room_id,
1460            &self.config.user_id,
1461            &self.config.device_id,
1462            &encryption,
1463            &members,
1464            &known_devices,
1465            now_ms,
1466        )?;
1467        let own_keys = self.account.identity_keys();
1468        GroupSessionManager::adopt_own_outbound_session(
1469            &mut self.store,
1470            room_id,
1471            &self.config.user_id,
1472            own_keys.curve25519,
1473            own_keys.ed25519,
1474            &update,
1475        )?;
1476
1477        if let Some(room_key_content) = update.room_key_content.clone() {
1478            let missing_ids: BTreeSet<(UserId, DeviceId)> = OlmSessionManager::sessions_missing_for(&self.store, &update.new_recipients)?
1479                .into_iter()
1480                .map(|device| (device.user_id.clone(), device.device_id.clone()))
1481                .collect();
1482            let sessioned: Vec<StoredDevice> = update
1483                .new_recipients
1484                .into_iter()
1485                .filter(|device| !missing_ids.contains(&(device.user_id.clone(), device.device_id.clone())))
1486                .collect();
1487
1488            let mut pending_ids = BTreeSet::new();
1489            for chunk in GroupSessionManager::chunk_recipients_for_send_to_device(&sessioned) {
1490                let body = GroupSessionManager::build_room_key_send_to_device_body(
1491                    &mut self.store,
1492                    &self.account,
1493                    &self.config.user_id,
1494                    &self.config.device_id,
1495                    chunk,
1496                    &room_key_content,
1497                )?;
1498                let request_id = self.next_request_id()?;
1499                let share_txn = self.next_txn_id()?;
1500                let request = OutgoingRequest::send_to_device(request_id.clone(), ROOM_KEY_SHARE_EVENT_TYPE, &share_txn, body);
1501                self.send_request_owner.insert(request_id.clone(), txn_id.clone());
1502                self.enqueue_request(request, Lane::ToDevice)?;
1503                pending_ids.insert(request_id);
1504            }
1505            if !pending_ids.is_empty() {
1506                self.set_send_phase(txn_id, SendPhase::AwaitingKeyShare { pending: pending_ids });
1507                return Ok(());
1508            }
1509        }
1510
1511        self.finish_encrypt_and_send(txn_id, room_id)?;
1512        self.begin_awaiting_send(txn_id, room_id)
1513    }
1514
1515    /// Step 4: Megolm-encrypts a room event under `room_id`'s (now current)
1516    /// outbound session — the inner type is the event's own (`m.room.message`,
1517    /// `m.sticker`), the outer is always `m.room.encrypted` — and caches the
1518    /// resulting [`WireSend::Event`]. A no-op for any other payload kind
1519    /// (never reached for one, since only a room event in an encrypted room
1520    /// enters the key pipeline at all).
1521    fn finish_encrypt_and_send(&mut self, txn_id: &TxnId, room_id: &RoomId) -> Result<(), MessengerError> {
1522        let Some(wire) = self.send_state.get(txn_id).and_then(|pending| pending.payload.room_event()) else {
1523            return Ok(());
1524        };
1525        let sender_curve = self.account.identity_keys().curve25519.to_base64();
1526        let encrypted = GroupSessionManager::encrypt_event(
1527            &mut self.store,
1528            room_id,
1529            &sender_curve,
1530            &self.config.device_id,
1531            &wire.event_type,
1532            wire.content,
1533            wire.relates_to,
1534        )?;
1535        let wire_content = serde_json::to_value(RoomEncryptedContent::Megolm(encrypted))?;
1536        if let Some(entry) = self.send_state.get_mut(txn_id) {
1537            entry.wire = Some(WireSend::Event { event_type: "m.room.encrypted".to_string(), content: wire_content });
1538        }
1539        Ok(())
1540    }
1541
1542    /// Builds the plaintext [`WireSend`] for a room event
1543    /// ([`SendPayload::Message`]/[`SendPayload::Event`]) in an unencrypted
1544    /// room, a [`SendPayload::Reaction`] (any room), or a
1545    /// [`SendPayload::Redaction`] (any room) — the cases that never touch the
1546    /// key pipeline at all.
1547    fn build_plaintext_wire_body(&mut self, txn_id: &TxnId) {
1548        let Some(pending) = self.send_state.get(txn_id) else { return };
1549        let wire = match &pending.payload {
1550            SendPayload::Message(_) | SendPayload::Event { .. } => match pending.payload.room_event() {
1551                Some(room_event) => WireSend::Event { event_type: room_event.event_type, content: room_event.content },
1552                None => return,
1553            },
1554            SendPayload::Reaction { target, key } => WireSend::Event {
1555                event_type: "m.reaction".to_string(),
1556                content: serde_json::json!({
1557                    "m.relates_to": { "rel_type": "m.annotation", "event_id": target.as_str(), "key": key }
1558                }),
1559            },
1560            SendPayload::Redaction { target, reason } => WireSend::Redact { target: target.clone(), reason: reason.clone() },
1561        };
1562        if let Some(entry) = self.send_state.get_mut(txn_id) {
1563            entry.wire = Some(wire);
1564        }
1565    }
1566
1567    /// Mints (or re-mints, for a retry) the actual `PUT .../send/...` or
1568    /// `PUT .../redact/...` request from [`PendingSend::wire`], on this
1569    /// room's own [`Lane::Room`] (preserves the room's own send order —
1570    /// this crate's `outgoing_queue`'s own doc).
1571    fn begin_awaiting_send(&mut self, txn_id: &TxnId, room_id: &RoomId) -> Result<(), MessengerError> {
1572        let Some(wire) = self.send_state.get(txn_id).and_then(|pending| pending.wire.clone()) else { return Ok(()) };
1573        let request_id = self.next_request_id()?;
1574        let request = match &wire {
1575            WireSend::Event { event_type, content } => {
1576                OutgoingRequest::room_send(request_id.clone(), room_id, event_type, txn_id, content.clone())
1577            }
1578            WireSend::Redact { target, reason } => {
1579                let body = match reason {
1580                    Some(reason) => serde_json::json!({ "reason": reason }),
1581                    None => serde_json::json!({}),
1582                };
1583                OutgoingRequest::room_redact(request_id.clone(), room_id, target, txn_id, body)
1584            }
1585        };
1586        self.send_request_owner.insert(request_id, txn_id.clone());
1587        self.enqueue_request(request, Lane::Room(room_id.clone()))?;
1588        self.set_send_phase(txn_id, SendPhase::AwaitingSend);
1589        Ok(())
1590    }
1591
1592    /// Routes one successful response for a request this send pipeline
1593    /// itself minted onward to whatever step comes next. A no-op for any
1594    /// `kind` this pipeline never mints on its own behalf (defensive —
1595    /// every `send_request_owner` entry is inserted alongside a specific
1596    /// kind, this match is exhaustive over that set).
1597    fn advance_send_on_success(
1598        &mut self,
1599        txn_id: &TxnId,
1600        request_id: &RequestId,
1601        kind: Option<OutgoingRequestKind>,
1602        response: &HttpResponseDescriptor,
1603        now_ms: i64,
1604    ) {
1605        match kind {
1606            Some(OutgoingRequestKind::KeysQuery) => {
1607                let Some(room_id) = self.send_state.get(txn_id).map(|pending| pending.room_id.clone()) else { return };
1608                let _ = self.begin_keys_claim_or_share(txn_id, &room_id, now_ms);
1609            }
1610            Some(OutgoingRequestKind::KeysClaim) => {
1611                let Some(pending) = self.send_state.get(txn_id) else { return };
1612                let room_id = pending.room_id.clone();
1613                if let SendPhase::AwaitingKeysClaim { devices, .. } = &pending.phase {
1614                    let devices = devices.clone();
1615                    let refs: Vec<&StoredDevice> = devices.iter().collect();
1616                    let _ = OlmSessionManager::on_keys_claim_response(&mut self.store, &self.account, &refs, &response.body);
1617                }
1618                let _ = self.begin_share_or_encrypt(txn_id, &room_id, now_ms);
1619            }
1620            Some(OutgoingRequestKind::SendToDevice) => {
1621                let mut remaining = None;
1622                if let Some(entry) = self.send_state.get_mut(txn_id) {
1623                    if let SendPhase::AwaitingKeyShare { pending } = &mut entry.phase {
1624                        pending.remove(request_id);
1625                        remaining = Some(pending.len());
1626                    }
1627                }
1628                if remaining == Some(0) {
1629                    let Some(room_id) = self.send_state.get(txn_id).map(|pending| pending.room_id.clone()) else { return };
1630                    let _ = self.finish_encrypt_and_send(txn_id, &room_id);
1631                    let _ = self.begin_awaiting_send(txn_id, &room_id);
1632                }
1633            }
1634            Some(OutgoingRequestKind::RoomSend) | Some(OutgoingRequestKind::RoomRedact) => {
1635                self.complete_send_success(txn_id, response, now_ms);
1636            }
1637            _ => {}
1638        }
1639    }
1640
1641    /// The room event/redaction itself succeeded: for a room event
1642    /// ([`SendPayload::Message`]/[`SendPayload::Event`]), attaches the
1643    /// server-confirmed event id to its echo (module doc: "200 -> store `event_id` on the echo
1644    /// (`SendState::Sent`)"); either way, removes this send's own
1645    /// bookkeeping and starts the room's next queued send, if any.
1646    fn complete_send_success(&mut self, txn_id: &TxnId, response: &HttpResponseDescriptor, now_ms: i64) {
1647        let Some(pending) = self.send_state.get(txn_id) else { return };
1648        let room_id = pending.room_id.clone();
1649        if pending.payload.is_room_event() {
1650            if let Ok(body) = serde_json::from_slice::<SendResponseBody>(&response.body) {
1651                if let Some(timeline) = self.timelines.get_mut(&room_id) {
1652                    timeline.set_echo_event_id(txn_id, body.event_id);
1653                }
1654                self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
1655            }
1656        }
1657        self.send_state.remove(txn_id);
1658        self.finish_active_send(&room_id, txn_id, now_ms);
1659    }
1660
1661    /// A step in `txn_id`'s own pipeline failed outright (a non-retryable
1662    /// 4xx anywhere along it): marks a room event's echo
1663    /// [`SendState::Failed`], moves this send to the terminal
1664    /// [`SendPhase::Failed`] (kept, not removed — [`MessengerCommand::RetrySend`]
1665    /// needs it), and starts the room's next queued send, if any.
1666    fn fail_send(&mut self, txn_id: &TxnId, errcode: String, now_ms: i64) {
1667        let Some(pending) = self.send_state.get(txn_id) else { return };
1668        let room_id = pending.room_id.clone();
1669        if pending.payload.is_room_event() {
1670            if let Some(timeline) = self.timelines.get_mut(&room_id) {
1671                timeline.mark_send_state(txn_id, SendState::Failed { reason: errcode.clone() });
1672            }
1673            self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
1674        }
1675        self.set_send_phase(txn_id, SendPhase::Failed { errcode });
1676        self.finish_active_send(&room_id, txn_id, now_ms);
1677    }
1678
1679    /// Pops `txn_id` off the front of `room_id`'s own send FIFO (if that is
1680    /// where it still is) and starts the next queued send's pipeline, if
1681    /// any — the "one message's pipeline completes before the next ... is
1682    /// encrypted" ordering rule, enforced structurally by never starting
1683    /// entry N+1 before entry N reaches a terminal state.
1684    fn finish_active_send(&mut self, room_id: &RoomId, txn_id: &TxnId, now_ms: i64) {
1685        if let Some(queue) = self.pending_sends.get_mut(room_id) {
1686            if queue.front() == Some(txn_id) {
1687                queue.pop_front();
1688            } else {
1689                queue.retain(|id| id != txn_id);
1690            }
1691        }
1692        let next = self.pending_sends.get(room_id).and_then(|queue| queue.front().cloned());
1693        if let Some(next_txn) = next {
1694            let _ = self.start_send_pipeline(&next_txn, now_ms);
1695        }
1696    }
1697
1698    /// A shell calls this once per tick, executes each, and feeds the
1699    /// result back via [`MessengerCore::on_response`]/
1700    /// [`MessengerCore::on_transport_error`]. Also where the next `/sync`
1701    /// request is minted, if none is currently outstanding (this module's
1702    /// own doc).
1703    ///
1704    /// **One-shot per request: never discard a non-empty result.** Every
1705    /// [`OutgoingRequest`] this call returns is marked in flight by
1706    /// [`crate::outgoing_queue::OutgoingQueue::releasable`] as a side
1707    /// effect of being returned — it will not be offered again until a
1708    /// matching [`MessengerCore::on_response`]/
1709    /// [`MessengerCore::on_transport_error`] call clears that flag. A
1710    /// caller that calls this method purely to trigger the mint side
1711    /// effect (e.g. to force a freshly-dirtied `/sync` request's own flush
1712    /// epoch open before deciding whether to act) and then throws the
1713    /// return value away strands every request in that batch permanently
1714    /// in flight, since nothing else will ever release it. Call this
1715    /// exactly once per tick, and always drive every request it returns
1716    /// through to a response.
1717    pub fn releasable_requests(&mut self, now_ms: i64) -> Vec<OutgoingRequest> {
1718        self.ensure_sync_enqueued();
1719        self.outgoing.releasable(&self.store, now_ms)
1720    }
1721
1722    fn ensure_sync_enqueued(&mut self) {
1723        if self.sync_request_id.is_some() {
1724            return;
1725        }
1726        let Ok(request_id) = self.next_request_id() else { return };
1727        let since = self.store.sync_token().ok().flatten().map(str::to_string);
1728        // The first `/sync` of a session must never long-poll. An initial
1729        // sync (no `since`) is answered at once by Synapse whatever `timeout`
1730        // says, but a server that treats a brand-new, still-empty account's
1731        // initial view as "nothing new" holds it for the full timeout -- and
1732        // this core publishes its device keys only once that first response
1733        // has landed. A resumed session (persisted `since`) has the same
1734        // need: the catch-up answer must not wait for news that may never
1735        // come. Live run, 2026-09-29: fresh accounts sat 30 s before their
1736        // keys went up.
1737        let timeout_ms = if since.is_some() && self.sync_caught_up { SYNC_TIMEOUT_MS } else { 0 };
1738        let request = OutgoingRequest::sync(request_id.clone(), since.as_deref(), Some(timeout_ms));
1739        if self.enqueue_request(request, Lane::Sync).is_ok() {
1740            self.sync_request_id = Some(request_id);
1741        }
1742    }
1743
1744    /// Feeds one HTTP response back in, keyed by the id the original
1745    /// [`OutgoingRequest`] carried. Returns whatever became visible as a
1746    /// direct result of processing it (equivalent to calling
1747    /// [`MessengerCore::events`] immediately afterwards — there is exactly
1748    /// one underlying event queue behind both).
1749    pub fn on_response(&mut self, request_id: RequestId, resp: HttpResponseDescriptor, now_ms: i64) -> Vec<MessengerEvent> {
1750        let kind = self.pending_kinds.get(&request_id).copied();
1751        let room_messages_room = self.pending_room_messages.get(&request_id).cloned();
1752        let create_room_kind = self.pending_create_room.get(&request_id).cloned();
1753        let send_owner = self.send_request_owner.get(&request_id).cloned();
1754
1755        let outcome = self.outgoing.on_response(&mut self.store, &request_id, resp, now_ms, self.jitter.as_mut());
1756        let (is_terminal, terminal_response, failed_errcode) = match outcome {
1757            Ok(Some(ResponseOutcome::Done(response))) => (true, Some(response), None),
1758            Ok(Some(ResponseOutcome::Failed { errcode, .. })) => (true, None, Some(errcode)),
1759            _ => (false, None, None),
1760        };
1761
1762        if is_terminal {
1763            self.pending_kinds.remove(&request_id);
1764            if matches!(kind, Some(OutgoingRequestKind::Sync)) {
1765                self.sync_request_id = None;
1766                if terminal_response.is_some() {
1767                    self.sync_caught_up = true;
1768                }
1769            }
1770            if room_messages_room.is_some() {
1771                self.pending_room_messages.remove(&request_id);
1772            }
1773            self.pending_create_room.remove(&request_id);
1774            self.send_request_owner.remove(&request_id);
1775            if let Some(key) = self.account_data_write_keys.remove(&request_id) {
1776                self.settle_account_data_write(&key, terminal_response.is_some());
1777            }
1778        }
1779
1780        if let Some(response) = &terminal_response {
1781            // A parse/logic failure against an otherwise-successful HTTP
1782            // response is dropped, not applied: the one invariant that
1783            // matters -- never advancing past a partially-applied `/sync` --
1784            // holds structurally, since `ingest_sync` only persists the new
1785            // sync token as its very last step. The failure is kept for
1786            // [`MessengerCore::take_ingest_error`] so a shell can show it.
1787            if let Err(error) = self.handle_terminal_success(&request_id, kind, room_messages_room, create_room_kind, response) {
1788                self.ingest_error = Some(match kind {
1789                    Some(kind) => format!("{kind:?} response not ingested: {error}"),
1790                    None => format!("response not ingested: {error}"),
1791                });
1792            }
1793        }
1794
1795        if is_terminal
1796            && terminal_response.is_none()
1797            && matches!(kind, Some(OutgoingRequestKind::SigningKeysUpload) | Some(OutgoingRequestKind::SignaturesUpload))
1798        {
1799            self.xsign_inflight = false;
1800        }
1801
1802        if let Some(txn_id) = send_owner {
1803            if let Some(response) = &terminal_response {
1804                self.advance_send_on_success(&txn_id, &request_id, kind, response, now_ms);
1805            } else if let Some(errcode) = failed_errcode.clone() {
1806                self.fail_send(&txn_id, errcode, now_ms);
1807            }
1808        }
1809
1810        // A refused `/keys/upload` must not look like a quiet success: the
1811        // account stays unpublished and the shell can surface the errcode
1812        // instead of reporting mail as sent under a rejected identity.
1813        if matches!(kind, Some(OutgoingRequestKind::KeysUpload)) {
1814            if let Some(errcode) = failed_errcode {
1815                self.ingest_error = Some(format!("keys/upload failed: {errcode}"));
1816            }
1817        }
1818
1819        self.drain_events()
1820    }
1821
1822    /// Feeds a transport-level failure back in (a timeout, a connection
1823    /// reset, ...) — always resolves to a retry with this core's own
1824    /// backoff (`crate::outgoing_queue`'s own doc); a no-op if `request_id`
1825    /// is not (or no longer) pending.
1826    pub fn on_transport_error(&mut self, request_id: &RequestId, now_ms: i64) {
1827        let _ = self.outgoing.on_transport_error(request_id, now_ms, self.jitter.as_mut());
1828    }
1829
1830    fn handle_terminal_success(
1831        &mut self,
1832        request_id: &RequestId,
1833        kind: Option<OutgoingRequestKind>,
1834        room_messages_room: Option<RoomId>,
1835        create_room_kind: Option<CreateRoomKind>,
1836        response: &HttpResponseDescriptor,
1837    ) -> Result<(), MessengerError> {
1838        match kind {
1839            Some(OutgoingRequestKind::Sync) => {
1840                let parsed = parse_sync_response(&response.body)?;
1841                self.ingest_sync(&parsed)?;
1842            }
1843            Some(OutgoingRequestKind::KeysUpload) => {
1844                self.account.on_keys_upload_response(&mut self.store)?;
1845            }
1846            Some(OutgoingRequestKind::KeysClaim) if self.history_claims.contains_key(request_id) => {
1847                if let Some(devices) = self.history_claims.remove(request_id) {
1848                    let refs: Vec<&StoredDevice> = devices.iter().collect();
1849                    let _ = OlmSessionManager::on_keys_claim_response(&mut self.store, &self.account, &refs, &response.body);
1850                }
1851                self.retry_missing_room_keys();
1852            }
1853            Some(OutgoingRequestKind::SigningKeysUpload) => {
1854                self.xsign_inflight = false;
1855                if let Some(state) = self.xsign.as_mut() {
1856                    state.uploaded = true;
1857                    let snapshot = state.clone();
1858                    snapshot.save(&mut self.store)?;
1859                }
1860                self.maybe_enqueue_cross_signing()?;
1861            }
1862            Some(OutgoingRequestKind::SignaturesUpload) => {
1863                self.xsign_inflight = false;
1864                let failures_empty = serde_json::from_slice::<serde_json::Value>(&response.body)
1865                    .ok()
1866                    .and_then(|v| v.get("failures").and_then(|f| f.as_object()).map(|f| f.is_empty()))
1867                    .unwrap_or(true);
1868                if failures_empty {
1869                    if let Some(state) = self.xsign.as_mut() {
1870                        state.device_signed = true;
1871                        let snapshot = state.clone();
1872                        snapshot.save(&mut self.store)?;
1873                    }
1874                }
1875            }
1876            Some(OutgoingRequestKind::KeysQuery) => {
1877                let outcome = DeviceTracker::on_keys_query_response(&mut self.store, &response.body)?;
1878                if !outcome.key_changes.is_empty() {
1879                    let room_ids: Vec<RoomId> = self.rooms.keys().cloned().collect();
1880                    for change in &outcome.key_changes {
1881                        let _ = GroupSessionManager::forget_shared_device(
1882                            &mut self.store,
1883                            &room_ids,
1884                            &change.user_id,
1885                            &change.device_id,
1886                        );
1887                        self.emit(MessengerEvent::DeviceKeyChanged {
1888                            user_id: change.user_id.clone(),
1889                            device_id: change.device_id.clone(),
1890                        });
1891                    }
1892                }
1893                self.retry_unknown_sender_queue()?;
1894            }
1895            Some(OutgoingRequestKind::RoomMessages) => {
1896                if let Some(room_id) = room_messages_room {
1897                    self.ingest_room_messages_response(&room_id, &response.body)?;
1898                }
1899            }
1900            Some(OutgoingRequestKind::CreateRoom) => {
1901                if let Some(kind) = create_room_kind {
1902                    self.on_create_room_response(kind, response)?;
1903                }
1904            }
1905            // A response to a search this account has already moved past (a
1906            // later `SearchPublicRooms` was dispatched since) fails the
1907            // guard and is dropped -- only the LATEST search's own result
1908            // set is ever visible (this module's own doc).
1909            Some(OutgoingRequestKind::PublicRooms) if self.latest_public_rooms_request.as_ref() == Some(request_id) => {
1910                let body: PublicRoomsResponseBody = serde_json::from_slice(&response.body)?;
1911                self.public_rooms_result = body
1912                    .chunk
1913                    .into_iter()
1914                    .map(|entry| PublicRoomsResultEntry {
1915                        room_id: entry.room_id,
1916                        name: entry.name,
1917                        topic: entry.topic,
1918                        num_joined_members: entry.num_joined_members,
1919                    })
1920                    .collect();
1921                self.emit(MessengerEvent::PublicRoomsChanged);
1922            }
1923            // Same supersession rule as `PublicRooms` above.
1924            Some(OutgoingRequestKind::UserDirectorySearch) if self.latest_user_search_request.as_ref() == Some(request_id) => {
1925                let body: UserDirectorySearchResponseBody = serde_json::from_slice(&response.body)?;
1926                self.user_search_result = body
1927                    .results
1928                    .into_iter()
1929                    .map(|entry| UserDirectoryResultEntry { user_id: entry.user_id, display_name: entry.display_name })
1930                    .collect();
1931                self.emit(MessengerEvent::UserSearchChanged);
1932            }
1933            _ => {}
1934        }
1935        Ok(())
1936    }
1937
1938    /// A `POST /createRoom` succeeded. For [`CreateRoomKind::Dm`], also
1939    /// merges the new room into this account's own `m.direct` account data
1940    /// (M13b send-pipeline doc: "DM also merges `m.direct` account data")
1941    /// — every other kind needs nothing further here (room state/timeline
1942    /// populate off the next `/sync`, same as any other room).
1943    fn on_create_room_response(&mut self, kind: CreateRoomKind, response: &HttpResponseDescriptor) -> Result<(), MessengerError> {
1944        let CreateRoomKind::Dm { peer } = kind else { return Ok(()) };
1945        let body: CreateRoomResponseBody = serde_json::from_slice(&response.body)?;
1946        self.merge_direct_account_data(body.room_id, peer)
1947    }
1948
1949    /// Merges `room_id` into this account's own `m.direct` account data
1950    /// under `peer`, optimistically, then sends the full updated value
1951    /// (the account-data endpoint replaces the whole value, same as
1952    /// [`MessengerCore::enqueue_tag_update`]'s own doc). Shared by
1953    /// [`MessengerCore::on_create_room_response`] (the CREATOR side of a
1954    /// [`CreateRoomKind::Dm`]) and [`MessengerCommand::JoinRoom`]'s own
1955    /// dispatch arm (the INVITEE side, this module's own doc: a DM
1956    /// invite's `is_direct` flag is not carried forward once THIS
1957    /// account's own join overwrites that membership event, so `m.direct`
1958    /// is the only durable "this is a DM" signal this account keeps of
1959    /// its own past that point).
1960    fn merge_direct_account_data(&mut self, room_id: RoomId, peer: UserId) -> Result<(), MessengerError> {
1961        let mut direct = self.direct_account_data.clone().unwrap_or_default();
1962        let rooms_for_peer = direct.0.entry(peer).or_default();
1963        if !rooms_for_peer.contains(&room_id) {
1964            rooms_for_peer.push(room_id);
1965        }
1966        let value = serde_json::to_value(&direct)?;
1967        self.write_account_data(None, "m.direct".to_string(), value)
1968    }
1969
1970    fn cached_account_data(&self, key: &AccountDataKey) -> Option<&serde_json::Value> {
1971        match &key.0 {
1972            Some(room_id) => self.room_account_data(room_id, &key.1),
1973            None => self.global_account_data(&key.1),
1974        }
1975    }
1976
1977    /// Sets (`Some`) or removes (`None`) the cached value for `key`,
1978    /// keeping the typed `m.direct` view in step with it.
1979    fn store_account_data_cache(&mut self, key: &AccountDataKey, value: Option<serde_json::Value>) {
1980        if key.0.is_none() && key.1 == "m.direct" {
1981            self.direct_account_data = value.as_ref().and_then(|v| serde_json::from_value::<DirectContent>(v.clone()).ok());
1982        }
1983        match (&key.0, value) {
1984            (None, Some(v)) => {
1985                self.global_account_data.insert(key.1.clone(), v);
1986            }
1987            (None, None) => {
1988                self.global_account_data.remove(&key.1);
1989            }
1990            (Some(room_id), Some(v)) => {
1991                self.room_account_data.entry(room_id.clone()).or_default().insert(key.1.clone(), v);
1992            }
1993            (Some(room_id), None) => {
1994                if let Some(by_type) = self.room_account_data.get_mut(room_id) {
1995                    by_type.remove(&key.1);
1996                }
1997            }
1998        }
1999    }
2000
2001    /// The one path every account-data write takes: builds the PUT, applies
2002    /// `value` to the cache optimistically (the next edit builds on it),
2003    /// guards the key against stale `/sync` echoes for as long as the
2004    /// write is outstanding ([`AccountDataGuard`]), and queues it on the
2005    /// strictly ordered [`Lane::AccountData`].
2006    fn write_account_data(&mut self, room_id: Option<RoomId>, event_type: String, value: serde_json::Value) -> Result<(), MessengerError> {
2007        let request_id = self.next_request_id()?;
2008        let request = match &room_id {
2009            Some(room_id) => OutgoingRequest::room_account_data(request_id.clone(), &self.config.user_id, room_id, &event_type, value.clone()),
2010            None => OutgoingRequest::account_data(request_id.clone(), &self.config.user_id, &event_type, value.clone()),
2011        };
2012        let key: AccountDataKey = (room_id, event_type);
2013        let known = self.cached_account_data(&key).cloned();
2014        let guard = self.account_data_guard.entry(key.clone()).or_insert(AccountDataGuard { pending: 0, server_value: known });
2015        guard.pending += 1;
2016        self.store_account_data_cache(&key, Some(value));
2017        match self.enqueue_request(request, Lane::AccountData) {
2018            Ok(()) => {
2019                self.account_data_write_keys.insert(request_id, key);
2020                Ok(())
2021            }
2022            Err(error) => {
2023                self.settle_account_data_write(&key, false);
2024                Err(error)
2025            }
2026        }
2027    }
2028
2029    /// One outstanding write for `key` reached its end (`succeeded`: a 2xx;
2030    /// otherwise a terminal failure). Once the last one settles, a failure
2031    /// restores the server's value over the now-wrong optimistic cache and
2032    /// tells the UI to re-render; a success keeps the optimistic value.
2033    fn settle_account_data_write(&mut self, key: &AccountDataKey, succeeded: bool) {
2034        let Some(guard) = self.account_data_guard.get_mut(key) else { return };
2035        guard.pending = guard.pending.saturating_sub(1);
2036        if guard.pending > 0 {
2037            return;
2038        }
2039        let Some(guard) = self.account_data_guard.remove(key) else { return };
2040        if !succeeded {
2041            self.store_account_data_cache(key, guard.server_value);
2042            self.emit(MessengerEvent::RoomsChanged);
2043        }
2044    }
2045
2046    /// Folds one account-data event `/sync` reported into the cache -- or,
2047    /// while writes for the same key are outstanding, only into that key's
2048    /// [`AccountDataGuard::server_value`].
2049    fn apply_synced_account_data(&mut self, room_id: Option<&RoomId>, event_type: &str, content: serde_json::Value) {
2050        let key: AccountDataKey = (room_id.cloned(), event_type.to_string());
2051        if let Some(guard) = self.account_data_guard.get_mut(&key) {
2052            guard.server_value = Some(content);
2053            return;
2054        }
2055        self.store_account_data_cache(&key, Some(content));
2056    }
2057
2058    /// Drains everything dirtied since the previous call into one
2059    /// [`FlushBatch`] for a shell to write durably.
2060    pub fn take_flush_batch(&mut self) -> Option<FlushBatch> {
2061        if let Some(batch) = self.pending_flush_batches.pop_front() {
2062            return Some(batch);
2063        }
2064        self.store.take_flush_batch()
2065    }
2066
2067    /// Acknowledges that batch `id` has been durably written.
2068    pub fn ack_flush(&mut self, id: u64) {
2069        self.store.ack_flush(id);
2070    }
2071
2072    /// Drains every [`MessengerEvent`] queued since the last call to this
2073    /// method or to [`MessengerCore::on_response`].
2074    pub fn events(&mut self) -> Vec<MessengerEvent> {
2075        self.drain_events()
2076    }
2077
2078    /// The last response this core failed to ingest, once (cleared by
2079    /// reading it) -- see the `ingest_error` field's own doc.
2080    pub fn take_ingest_error(&mut self) -> Option<String> {
2081        self.ingest_error.take()
2082    }
2083
2084    /// Monotonically increasing count of visible changes — a cheap
2085    /// "did anything change at all since I last checked" signal for a
2086    /// provider that only wants to know whether to re-read its snapshot
2087    /// getters, without keeping its own copy of every [`MessengerEvent`].
2088    pub fn change_counter(&self) -> u64 {
2089        self.change_counter
2090    }
2091
2092    /// Every room this core currently holds state for.
2093    pub fn room_ids(&self) -> impl Iterator<Item = &RoomId> {
2094        self.rooms.keys()
2095    }
2096
2097    /// `room_id`'s current state, if this core has one.
2098    pub fn room_state(&self, room_id: &RoomId) -> Option<&RoomState> {
2099        self.rooms.get(room_id)
2100    }
2101
2102    /// `room_id`'s current timeline, if this core has one.
2103    pub fn timeline(&self, room_id: &RoomId) -> Option<&Timeline> {
2104        self.timelines.get(room_id)
2105    }
2106
2107    /// `room_id`'s derived [`RoomKind`] (plan manager decision #5's DM/
2108    /// channel/group precedence), using this core's current global
2109    /// `m.direct` account data.
2110    pub fn room_kind(&self, room_id: &RoomId) -> Option<RoomKind> {
2111        self.rooms.get(room_id).map(|state| state.derive_room_kind(room_id, self.direct_account_data.as_ref()))
2112    }
2113
2114    /// Every user currently typing in `room_id`, per the most recent
2115    /// `/sync` ephemeral batch — empty if none, or if this core has never
2116    /// seen a typing event for this room.
2117    pub fn typing_users(&self, room_id: &RoomId) -> &[UserId] {
2118        self.typing.get(room_id).map(Vec::as_slice).unwrap_or(&[])
2119    }
2120
2121    /// `room_id`'s most recently received `m.receipt` account data, if any.
2122    pub fn receipts(&self, room_id: &RoomId) -> Option<&ReceiptContent> {
2123        self.receipts.get(room_id)
2124    }
2125
2126    /// One piece of `room_id`'s room-scoped account data (e.g.
2127    /// `"m.fully_read"`), if this core has seen one.
2128    pub fn room_account_data(&self, room_id: &RoomId, event_type: &str) -> Option<&serde_json::Value> {
2129        self.room_account_data.get(room_id).and_then(|by_type| by_type.get(event_type))
2130    }
2131
2132    /// One piece of this account's own global account data, if this core
2133    /// has seen one.
2134    pub fn global_account_data(&self, event_type: &str) -> Option<&serde_json::Value> {
2135        self.global_account_data.get(event_type)
2136    }
2137
2138    /// How many missing Megolm sessions this process has already requested.
2139    pub fn key_request_count(&self) -> usize {
2140        self.key_requests_sent.len()
2141    }
2142
2143    /// This account's own user id, exactly as configured at
2144    /// [`MessengerCore::open`] -- a UI-facing adapter needs this to tell
2145    /// its own account apart from every other room member (a DM's peer, a
2146    /// reaction's `by_me`, a typing-user list minus self, ...).
2147    pub fn user_id(&self) -> &UserId {
2148        &self.config.user_id
2149    }
2150
2151    /// Whether this core currently has at least one verified device on
2152    /// file for `user_id` (`/keys/query` results already persisted by
2153    /// [`crate::crypto::device_tracker::DeviceTracker`]) -- a UI-facing
2154    /// adapter's own "has a messaging key" check for a member list, without
2155    /// duplicating that module's storage format.
2156    pub fn has_tracked_device(&self, user_id: &UserId) -> bool {
2157        DeviceTracker::devices_for_user(&self.store, user_id).map(|devices| !devices.is_empty()).unwrap_or(false)
2158    }
2159
2160    /// The latest [`MessengerCommand::SearchPublicRooms`] result set, empty
2161    /// until one has landed.
2162    pub fn public_rooms_result(&self) -> &[PublicRoomsResultEntry] {
2163        &self.public_rooms_result
2164    }
2165
2166    /// The latest [`MessengerCommand::SearchUsers`] result set, empty until
2167    /// one has landed.
2168    pub fn user_search_result(&self) -> &[UserDirectoryResultEntry] {
2169        &self.user_search_result
2170    }
2171
2172    /// The Matrix `errcode` a failed send (`txn_id`) ended with, if
2173    /// `txn_id` currently names a [`crate::room::timeline::SendState::Failed`]
2174    /// send this core is still tracking — `None` for a send that never
2175    /// failed, already succeeded, or that this core has never seen.
2176    pub fn send_failure_reason(&self, txn_id: &TxnId) -> Option<&str> {
2177        match self.send_state.get(txn_id).map(|pending| &pending.phase) {
2178            Some(SendPhase::Failed { errcode }) => Some(errcode.as_str()),
2179            _ => None,
2180        }
2181    }
2182
2183    /// The "in" edge — mailbox command (single-writer-core doctrine's Law
2184    /// 3). See [`MessengerCommand`]'s own doc for every variant this piece
2185    /// implements. `now_ms` is this call's own clock reading (this crate's
2186    /// "no clock inside" rule) — used for a fresh send's local-echo
2187    /// timestamp, [`MessengerCommand::SetTyping`]'s debounce window, and
2188    /// Megolm rotation timing.
2189    pub fn dispatch(&mut self, cmd: MessengerCommand, now_ms: i64) -> Result<(), MessengerError> {
2190        match cmd {
2191            MessengerCommand::SendMessage { room_id, message, txn_id } => {
2192                if let Some(edit_of) = &message.edit_of {
2193                    let owned = self
2194                        .timelines
2195                        .get(&room_id)
2196                        .and_then(|timeline| timeline.item_by_event_id(edit_of))
2197                        .is_some_and(|item| item.sender == self.config.user_id);
2198                    if !owned {
2199                        return Err(MessengerError::EditNotOwned { event_id: edit_of.clone() });
2200                    }
2201                }
2202                self.start_new_send(room_id, SendPayload::Message(message), txn_id, now_ms)?;
2203            }
2204            MessengerCommand::Forward { from_room, event_id, to_room, txn_id } => {
2205                let payload = self.build_forward_payload(&from_room, &event_id, &to_room)?;
2206                self.start_new_send(to_room, payload, txn_id, now_ms)?;
2207            }
2208            MessengerCommand::React { room_id, target, key } => {
2209                self.start_new_send(room_id, SendPayload::Reaction { target, key }, None, now_ms)?;
2210            }
2211            MessengerCommand::Redact { room_id, target, reason } => {
2212                self.start_new_send(room_id, SendPayload::Redaction { target, reason }, None, now_ms)?;
2213            }
2214            MessengerCommand::RetrySend { room_id, txn_id } => {
2215                self.retry_send(&room_id, &txn_id, now_ms)?;
2216            }
2217            MessengerCommand::CreateRoom { kind } => {
2218                self.dispatch_create_room(kind)?;
2219            }
2220            MessengerCommand::JoinRoom { room_id } => {
2221                let request_id = self.next_request_id()?;
2222                let request = OutgoingRequest::join_room(request_id, room_id.as_str());
2223                self.enqueue_request(request, Lane::Other)?;
2224
2225                // A DM invite's `is_direct` flag lives on this account's own
2226                // invite membership event and is overwritten by the join
2227                // itself, so accepting it records the room in this account's
2228                // own `m.direct` (same as the creator side does on
2229                // `/createRoom`) -- otherwise the invitee's room kind falls
2230                // back to Group once the join lands.
2231                let dm_peer = self.rooms.get(&room_id).and_then(|room| {
2232                    let invited_as_direct = room.members.get(&self.config.user_id).is_some_and(|member| member.is_direct);
2233                    if !invited_as_direct {
2234                        return None;
2235                    }
2236                    room.members.keys().find(|user_id| **user_id != self.config.user_id).cloned()
2237                });
2238                if let Some(peer) = dm_peer {
2239                    self.merge_direct_account_data(room_id, peer)?;
2240                }
2241            }
2242            MessengerCommand::LeaveRoom { room_id } => {
2243                let request_id = self.next_request_id()?;
2244                let request = OutgoingRequest::leave_room(request_id, &room_id);
2245                self.enqueue_request(request, Lane::Other)?;
2246            }
2247            MessengerCommand::Invite { room_id, user_id } => {
2248                let request_id = self.next_request_id()?;
2249                let request = OutgoingRequest::invite(request_id, &room_id, &user_id);
2250                self.enqueue_request(request, Lane::Other)?;
2251            }
2252            MessengerCommand::Kick { room_id, user_id, reason } => {
2253                let request_id = self.next_request_id()?;
2254                let request = OutgoingRequest::kick(request_id, &room_id, &user_id, reason.as_deref());
2255                self.enqueue_request(request, Lane::Other)?;
2256            }
2257            MessengerCommand::SetPowerLevel { room_id, user_id, level } => {
2258                let mut content = self.room_state(&room_id).map(|st| st.power_levels.clone()).ok_or_else(|| MessengerError::IntentRefused(format!("unknown room {}", room_id.as_str())))?;
2259                content.users.insert(user_id, level);
2260                let body = serde_json::to_value(&content)?;
2261                let request_id = self.next_request_id()?;
2262                let request = OutgoingRequest::set_state(request_id, &room_id, "m.room.power_levels", "", body);
2263                self.enqueue_request(request, Lane::Other)?;
2264            }
2265            MessengerCommand::SetEventsDefault { room_id, level } => {
2266                let mut content = self.room_state(&room_id).map(|st| st.power_levels.clone()).ok_or_else(|| MessengerError::IntentRefused(format!("unknown room {}", room_id.as_str())))?;
2267                content.events_default = level;
2268                let body = serde_json::to_value(&content)?;
2269                let request_id = self.next_request_id()?;
2270                let request = OutgoingRequest::set_state(request_id, &room_id, "m.room.power_levels", "", body);
2271                self.enqueue_request(request, Lane::Other)?;
2272            }
2273            MessengerCommand::SetTag { room_id, tag, order } => {
2274                let mut content = self.current_tag_content(&room_id);
2275                content.tags.insert(tag, TagInfo { order });
2276                self.enqueue_tag_update(room_id, content)?;
2277            }
2278            MessengerCommand::RemoveTag { room_id, tag } => {
2279                let mut content = self.current_tag_content(&room_id);
2280                content.tags.remove(&tag);
2281                self.enqueue_tag_update(room_id, content)?;
2282            }
2283            MessengerCommand::MarkRead { room_id, event_id } => {
2284                let request_id = self.next_request_id()?;
2285                let body = serde_json::json!({ "m.fully_read": event_id.as_str(), "m.read": event_id.as_str() });
2286                let request = OutgoingRequest::read_markers(request_id, &room_id, body);
2287                self.enqueue_request(request, Lane::Other)?;
2288            }
2289            MessengerCommand::SetTyping { room_id, typing } => {
2290                if typing {
2291                    let should_send = match self.typing_debounce.get(&room_id) {
2292                        Some(&last_sent_ms) => now_ms.saturating_sub(last_sent_ms) >= TYPING_DEBOUNCE_MS,
2293                        None => true,
2294                    };
2295                    if !should_send {
2296                        return Ok(());
2297                    }
2298                    self.typing_debounce.insert(room_id.clone(), now_ms);
2299                } else {
2300                    self.typing_debounce.remove(&room_id);
2301                }
2302                let request_id = self.next_request_id()?;
2303                let timeout_ms = typing.then_some(TYPING_TIMEOUT_MS);
2304                let request = OutgoingRequest::typing(request_id, &room_id, &self.config.user_id, typing, timeout_ms);
2305                self.enqueue_request(request, Lane::Other)?;
2306            }
2307            MessengerCommand::RetryDecryption { room_id, event_id } => {
2308                if self.retry_decrypt_one(&room_id, &event_id) {
2309                    self.emit(MessengerEvent::TimelineChanged { room_id });
2310                }
2311            }
2312            MessengerCommand::LoadOlder { room_id } => {
2313                let from = self
2314                    .timelines
2315                    .get(&room_id)
2316                    .and_then(|timeline| {
2317                        timeline
2318                            .older_token()
2319                            .map(str::to_string)
2320                            .or_else(|| timeline.gap().and_then(|gap| gap.prev_batch.clone()))
2321                    })
2322                    // A known room with no pagination token yet -- a resumed
2323                    // session, whose timelines are not persisted and whose
2324                    // first `/sync` carries only what is new -- pages back
2325                    // from its own sync position (a `/sync` `next_batch` token
2326                    // is a valid `/messages` `from`).
2327                    .or_else(|| {
2328                        if self.rooms.contains_key(&room_id) {
2329                            self.store.sync_token().ok().flatten().map(str::to_string)
2330                        } else {
2331                            None
2332                        }
2333                    });
2334                let Some(from) = from else { return Ok(()) };
2335                let request_id = self.next_request_id()?;
2336                let request =
2337                    OutgoingRequest::room_messages(request_id.clone(), &room_id, &from, "b", Some(LOAD_OLDER_PAGE_SIZE));
2338                self.enqueue_request(request, Lane::Room(room_id.clone()))?;
2339                self.pending_room_messages.insert(request_id, room_id);
2340            }
2341            MessengerCommand::SetAccountData { event_type, content } => {
2342                self.write_account_data(None, event_type, content)?;
2343            }
2344            MessengerCommand::SetRoomAccountData { room_id, event_type, content } => {
2345                self.write_account_data(Some(room_id), event_type, content)?;
2346            }
2347            MessengerCommand::SearchPublicRooms { term } => {
2348                let request_id = self.next_request_id()?;
2349                let body = serde_json::json!({ "filter": { "generic_search_term": term }, "limit": 50 });
2350                let request = OutgoingRequest::public_rooms(request_id.clone(), body);
2351                self.latest_public_rooms_request = Some(request_id);
2352                self.enqueue_request(request, Lane::Other)?;
2353            }
2354            MessengerCommand::SearchUsers { term } => {
2355                let request_id = self.next_request_id()?;
2356                let body = serde_json::json!({ "search_term": term, "limit": 20 });
2357                let request = OutgoingRequest::user_directory_search(request_id.clone(), body);
2358                self.latest_user_search_request = Some(request_id);
2359                self.enqueue_request(request, Lane::Other)?;
2360            }
2361        }
2362        Ok(())
2363    }
2364
2365    /// `POST /createRoom`'s own body per [`CreateRoomKind`] (server plan
2366    /// §5: the server derives `kind`/power levels from `visibility`+
2367    /// `is_direct` itself — a client only ever sends those two fields plus
2368    /// `invite`/`name`/`topic`).
2369    fn dispatch_create_room(&mut self, kind: CreateRoomKind) -> Result<(), MessengerError> {
2370        let body = match &kind {
2371            CreateRoomKind::Dm { peer } => serde_json::json!({
2372                "is_direct": true,
2373                "invite": [peer.as_str()],
2374            }),
2375            CreateRoomKind::Group { name, invite, members_can_invite } => serde_json::json!({
2376                "visibility": "private",
2377                "is_direct": false,
2378                "name": name,
2379                "invite": invite.iter().map(UserId::as_str).collect::<Vec<_>>(),
2380                "members_can_invite": members_can_invite,
2381            }),
2382            CreateRoomKind::Channel { name, topic } => {
2383                let mut value = serde_json::json!({
2384                    "visibility": "public",
2385                    "is_direct": false,
2386                    "name": name,
2387                });
2388                if let Some(topic) = topic {
2389                    value["topic"] = serde_json::Value::String(topic.clone());
2390                }
2391                value
2392            }
2393        };
2394        let request_id = self.next_request_id()?;
2395        let request = OutgoingRequest::create_room(request_id.clone(), body);
2396        self.pending_create_room.insert(request_id.clone(), kind);
2397        self.enqueue_request(request, Lane::Other)?;
2398        Ok(())
2399    }
2400
2401    /// `room_id`'s current `m.tag` content, or an empty one if this core
2402    /// has never seen one — [`MessengerCommand::SetTag`]/`RemoveTag` both
2403    /// mutate a full copy of this (the account-data endpoint replaces the
2404    /// whole value, there is no partial-update verb).
2405    fn current_tag_content(&self, room_id: &RoomId) -> TagContent {
2406        self.room_account_data(room_id, "m.tag")
2407            .and_then(|value| serde_json::from_value(value.clone()).ok())
2408            .unwrap_or_default()
2409    }
2410
2411    /// Optimistic and guarded like every account-data write
2412    /// ([`MessengerCore::write_account_data`]): the very next tag edit must
2413    /// build on THIS content, not on whatever `/sync` last echoed, or two
2414    /// back-to-back edits would each start from the same stale copy and the
2415    /// later PUT would drop the earlier one's change.
2416    fn enqueue_tag_update(&mut self, room_id: RoomId, content: TagContent) -> Result<(), MessengerError> {
2417        let value = serde_json::to_value(&content)?;
2418        self.write_account_data(Some(room_id), "m.tag".to_string(), value)
2419    }
2420
2421    /// The [`SendPayload::Event`] a [`MessengerCommand::Forward`] sends: the
2422    /// source item's current decrypted content, stripped and marked
2423    /// ([`forwardable_content`]) per that command's own rules, or the
2424    /// refusal naming `event_id`.
2425    fn build_forward_payload(
2426        &self,
2427        from_room: &RoomId,
2428        event_id: &EventId,
2429        to_room: &RoomId,
2430    ) -> Result<SendPayload, MessengerError> {
2431        let refuse = |reason: &dyn std::fmt::Display| {
2432            MessengerError::IntentRefused(format!("cannot forward {event_id} from {from_room}: {reason}"))
2433        };
2434        if !self.rooms.contains_key(to_room) {
2435            return Err(refuse(&format!("the target room {to_room} is unknown")));
2436        }
2437        let source = match self.timelines.get(from_room) {
2438            Some(timeline) => timeline.forward_source(event_id),
2439            None => Err(ForwardRefusal::Missing),
2440        }
2441        .map_err(|reason| refuse(&reason))?;
2442        let (marker_key, marker) = self.forward_marker(from_room);
2443        Ok(SendPayload::Event {
2444            event_type: source.event_type,
2445            content: forwardable_content(source.content, marker_key, marker),
2446        })
2447    }
2448
2449    /// The marker a forward out of `from_room` carries: the bare `forwarded:
2450    /// true` unless that room is a public channel, in which case
2451    /// `forwarded_from` names it (Slack-like attribution). Channels are
2452    /// still E2E; an unknown room is treated as private -- fail-safe.
2453    fn forward_marker(&self, from_room: &RoomId) -> (&'static str, serde_json::Value) {
2454        let public_channel = self.room_kind(from_room) == Some(RoomKind::Channel);
2455        if public_channel {
2456            let room_name = self.forward_room_name(from_room);
2457            (KEY_FORWARDED_FROM, serde_json::json!({ "room_id": from_room.as_str(), "room_name": room_name }))
2458        } else {
2459            (KEY_FORWARDED, serde_json::Value::Bool(true))
2460        }
2461    }
2462
2463    /// `room_id`'s name as a forward attribution: its `m.room.name`, or a
2464    /// generic label by kind. Never the members' names an unnamed room's
2465    /// chat-list row would list -- an attribution goes to third parties.
2466    fn forward_room_name(&self, room_id: &RoomId) -> String {
2467        if let Some(name) = self.rooms.get(room_id).and_then(|room| room.name.clone()) {
2468            return name;
2469        }
2470        match self.room_kind(room_id) {
2471            Some(RoomKind::Dm) => "Direct",
2472            Some(RoomKind::Channel) => "Channel",
2473            Some(RoomKind::Group) | None => "Group",
2474        }
2475        .to_string()
2476    }
2477
2478    /// Re-attempts decrypting one currently-pending Megolm item, applying
2479    /// the result to its timeline on success. Returns `true` iff the item
2480    /// was found and successfully decrypted (i.e. something a caller should
2481    /// treat as a visible change) — `false` for an unknown item or a
2482    /// decrypt that still fails.
2483    fn retry_decrypt_one(&mut self, room_id: &RoomId, event_id: &EventId) -> bool {
2484        let Some(item) = self.pending_decrypt.get(room_id).and_then(|by_event| by_event.get(event_id)).cloned() else {
2485            return false;
2486        };
2487        match GroupSessionManager::decrypt_event(
2488            &mut self.store,
2489            room_id,
2490            event_id,
2491            item.origin_server_ts,
2492            &item.sender,
2493            &item.content,
2494        ) {
2495            Ok(plaintext) => {
2496                self.apply_decrypted_plaintext(room_id, event_id, &item.sender, item.origin_server_ts, &plaintext);
2497            }
2498            Err(GroupDecryptError::MissingSession { .. }) => {
2499                let _ = self.request_missing_room_key(room_id, &item.sender, &item.content);
2500                return false;
2501            }
2502            Err(_) => return false,
2503        }
2504        if let Some(by_event) = self.pending_decrypt.get_mut(room_id) {
2505            by_event.remove(event_id);
2506        }
2507        true
2508    }
2509
2510    /// One backward page from the tip, not from the stored sync token.
2511    /// Used when a push names an event the timeline does not have as text.
2512    pub fn pull_latest_page(&mut self, room_id: RoomId) -> Result<(), MessengerError> {
2513        let request_id = self.next_request_id()?;
2514        let request = OutgoingRequest::room_messages_latest(request_id.clone(), &room_id, 20);
2515        self.enqueue_request(request, Lane::Room(room_id.clone()))?;
2516        self.pending_room_messages.insert(request_id, room_id);
2517        Ok(())
2518    }
2519
2520    /// Decrypts Megolm rows already on the timeline. `pending_decrypt` is
2521    /// memory-only, so a restarted client still holds the ciphertext and
2522    /// would otherwise never turn it into text the leader trigger can see.
2523    pub fn decrypt_loaded_timeline(&mut self) {
2524        let pending: Vec<(RoomId, Vec<EventId>)> = self
2525            .pending_decrypt
2526            .iter()
2527            .map(|(room_id, by_event)| (room_id.clone(), by_event.keys().cloned().collect()))
2528            .collect();
2529        for (room_id, event_ids) in pending {
2530            for event_id in event_ids {
2531                self.retry_decrypt_one(&room_id, &event_id);
2532            }
2533        }
2534        let rooms: Vec<RoomId> = self.timelines.keys().cloned().collect();
2535        for room_id in rooms {
2536            let sealed: Vec<(EventId, i64, UserId, MegolmEncryptedContent)> = {
2537                let Some(timeline) = self.timelines.get(&room_id) else {
2538                    continue;
2539                };
2540                timeline
2541                    .items()
2542                    .iter()
2543                    .filter_map(|item| {
2544                        let event_id = item.event_id.clone()?;
2545                        let (session_id, ciphertext) = match &item.content {
2546                            ItemContent::Encrypted { session_id, ciphertext_b64 } => {
2547                                (session_id.clone(), ciphertext_b64.clone())
2548                            }
2549                            _ => return None,
2550                        };
2551                        Some((
2552                            event_id,
2553                            item.origin_server_ts,
2554                            item.sender.clone(),
2555                            MegolmEncryptedContent {
2556                                ciphertext,
2557                                session_id,
2558                                sender_key: None,
2559                                device_id: None,
2560                                relates_to: None,
2561                            },
2562                        ))
2563                    })
2564                    .collect()
2565            };
2566            for (event_id, origin_server_ts, sender, content) in sealed {
2567                let plaintext = match GroupSessionManager::decrypt_event(
2568                    &mut self.store,
2569                    &room_id,
2570                    &event_id,
2571                    origin_server_ts,
2572                    &sender,
2573                    &content,
2574                ) {
2575                    Ok(plaintext) => plaintext,
2576                    Err(GroupDecryptError::MissingSession { .. }) => {
2577                        let _ = self.request_missing_room_key(&room_id, &sender, &content);
2578                        continue;
2579                    }
2580                    Err(_) => continue,
2581                };
2582                self.apply_decrypted_plaintext(
2583                    &room_id,
2584                    &event_id,
2585                    &sender,
2586                    origin_server_ts,
2587                    &plaintext,
2588                );
2589            }
2590        }
2591    }
2592
2593    /// Applies one successfully-decrypted Megolm plaintext to `room_id`'s
2594    /// timeline entry for `event_id` — as an ordinary message
2595    /// ([`Timeline::set_decrypted`]), or, when the plaintext itself carries
2596    /// an `m.annotation`/`m.replace` relation, via
2597    /// [`Timeline::apply_decrypted_relation`] instead (`room::timeline`'s
2598    /// own doc: an encrypted relation's real content — its `m.new_content`
2599    /// — only exists post-decrypt, unlike its `m.relates_to` pointer, which
2600    /// the outer envelope already copies in cleartext; a still-ciphertext
2601    /// event is therefore always given an ordinary placeholder row first,
2602    /// and this is the one place that later resolves what it actually was).
2603    /// Shared by forward decrypt ([`MessengerCore::decrypt_new_timeline_events`])
2604    /// and [`MessengerCore::retry_decrypt_one`].
2605    fn apply_decrypted_plaintext(
2606        &mut self,
2607        room_id: &RoomId,
2608        event_id: &EventId,
2609        sender: &UserId,
2610        origin_server_ts: i64,
2611        plaintext: &RoomEventPlaintext,
2612    ) {
2613        let Some(timeline) = self.timelines.get_mut(room_id) else { return };
2614        if let Some(relation) = RelatesTo::from_content(&plaintext.content) {
2615            if matches!(relation, RelatesTo::Annotation { .. } | RelatesTo::Replace { .. }) {
2616                let new_content = plaintext.content.get("m.new_content").cloned();
2617                timeline.apply_decrypted_relation(event_id, sender.clone(), origin_server_ts, relation, new_content);
2618                return;
2619            }
2620        }
2621        timeline.set_decrypted_event(event_id, &plaintext.event_type, &plaintext.content);
2622    }
2623
2624    fn retry_pending_for_session(&mut self, room_id: &RoomId, session_id: &str) {
2625        let Some(by_event) = self.pending_decrypt.get(room_id) else { return };
2626        let candidates: Vec<EventId> =
2627            by_event.iter().filter(|(_, item)| item.session_id == session_id).map(|(event_id, _)| event_id.clone()).collect();
2628        let mut changed = false;
2629        for event_id in candidates {
2630            if self.retry_decrypt_one(room_id, &event_id) {
2631                changed = true;
2632            }
2633        }
2634        if changed {
2635            self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
2636        }
2637    }
2638
2639    /// Asks for the device identity keys to be published again with the next sync. See
2640    /// [`crate::crypto::account::OlmAccountState::forget_published_device_keys`].
2641    pub fn republish_device_keys(&mut self) {
2642        self.account.forget_published_device_keys();
2643    }
2644
2645    fn ingest_sync(&mut self, response: &crate::wire::sync::SyncResponse) -> Result<(), MessengerError> {
2646        let mut newly_received_sessions: Vec<(RoomId, String)> = Vec::new();
2647
2648        // 1. to-device.
2649        for event in &response.to_device.events {
2650            self.process_to_device_event(event, &mut newly_received_sessions)?;
2651        }
2652
2653        // 2. device_lists.
2654        DeviceTracker::on_device_lists(&mut self.store, &response.device_lists.changed, &response.device_lists.left)?;
2655
2656        // 3. OTK counts / unused fallback types.
2657        let published_otk_count = response.device_one_time_keys_count.get("signed_curve25519").copied().unwrap_or(0);
2658        self.account.on_sync_counts(&mut self.store, published_otk_count, &response.device_unused_fallback_key_types)?;
2659        let keys_upload_request_id = self.next_request_id()?;
2660        if let Some(request) =
2661            self.account.keys_upload_request(keys_upload_request_id, &self.config.user_id, &self.config.device_id)?
2662        {
2663            self.enqueue_request(request, Lane::Other)?;
2664        }
2665        self.maybe_enqueue_cross_signing()?;
2666
2667        // 4. any outdated tracked user -> one keys/query.
2668        let outdated_before_rooms = self.outdated_tracked_users()?;
2669        if !outdated_before_rooms.is_empty() {
2670            self.enqueue_keys_query()?;
2671        }
2672
2673        // 5. rooms.
2674        let rooms_touched =
2675            !response.rooms.join.is_empty() || !response.rooms.invite.is_empty() || !response.rooms.leave.is_empty();
2676        for (room_id, joined) in &response.rooms.join {
2677            self.ingest_joined_room(room_id, joined)?;
2678        }
2679        for (room_id, invited) in &response.rooms.invite {
2680            self.ingest_invited_room(room_id, invited)?;
2681        }
2682        for (room_id, left) in &response.rooms.leave {
2683            self.ingest_left_room(room_id, left)?;
2684        }
2685        if rooms_touched {
2686            self.emit(MessengerEvent::RoomsChanged);
2687        }
2688        // 5b. A member who joined an encrypted room was marked outdated while
2689        // the room was ingested (`track_joined_members`): query them now, not
2690        // at the next sync or send.
2691        if !self.outdated_tracked_users()?.is_subset(&outdated_before_rooms) {
2692            self.enqueue_keys_query()?;
2693        }
2694
2695        // 6. global account data.
2696        for event in &response.account_data.events {
2697            self.apply_synced_account_data(None, &event.event_type, event.content.clone());
2698        }
2699
2700        for (room_id, session_id) in &newly_received_sessions {
2701            self.retry_pending_for_session(room_id, session_id);
2702        }
2703
2704        // 7. the new sync token is persisted LAST -- see this module's own
2705        // doc for why every earlier `?` above matters for this ordering.
2706        self.store.save_sync_token(response.next_batch.clone())?;
2707        Ok(())
2708    }
2709
2710    fn process_to_device_event(
2711        &mut self,
2712        event: &ToDeviceEvent,
2713        newly_received_sessions: &mut Vec<(RoomId, String)>,
2714    ) -> Result<(), MessengerError> {
2715        if event.event_type != "m.room.encrypted" {
2716            return Ok(());
2717        }
2718        let Ok(RoomEncryptedContent::Olm(content)) = serde_json::from_value::<RoomEncryptedContent>(event.content.clone())
2719        else {
2720            return Ok(());
2721        };
2722        match OlmSessionManager::decrypt_to_device(&mut self.store, &mut self.account, &self.config.user_id, &event.sender, &content) {
2723            Ok(decrypted) => self.route_decrypted_to_device(decrypted, newly_received_sessions),
2724            Err(OlmDecryptError::UnknownSenderDevice { .. }) => {
2725                self.queue_unknown_sender_retry(event.clone());
2726                Ok(())
2727            }
2728            // Every other reason (a replay, a malformed envelope, a
2729            // payload-binding mismatch, ...) is dropped silently -- this
2730            // module's own doc, and `crypto::olm_sessions`'s: a redelivered
2731            // to-device message after a crash is legitimate, and this
2732            // crate has no logging dependency to report a genuine attack
2733            // attempt through instead.
2734            Err(_) => Ok(()),
2735        }
2736    }
2737
2738    fn route_decrypted_to_device(
2739        &mut self,
2740        decrypted: DecryptedToDevice,
2741        newly_received_sessions: &mut Vec<(RoomId, String)>,
2742    ) -> Result<(), MessengerError> {
2743        match decrypted.event_type.as_str() {
2744            "m.room_key" => {
2745                if let Ok(content) = serde_json::from_value::<RoomKeyContent>(decrypted.content.clone()) {
2746                    if content.algorithm == "m.megolm.v1.aes-sha2" {
2747                        GroupSessionManager::receive_room_key(
2748                            &mut self.store,
2749                            &decrypted.sender,
2750                            decrypted.sender_device_curve25519,
2751                            decrypted.sender_ed25519,
2752                            &content,
2753                        )?;
2754                        newly_received_sessions.push((content.room_id, content.session_id));
2755                    }
2756                }
2757            }
2758            "m.forwarded_room_key" => {
2759                if let Ok(content) = serde_json::from_value::<ForwardedRoomKeyContent>(decrypted.content.clone()) {
2760                    if let Some((user_id, curve, ed)) = self.forward_binding(&decrypted, &content)? {
2761                        let stored = GroupSessionManager::receive_forwarded_room_key(
2762                            &mut self.store,
2763                            &user_id,
2764                            curve,
2765                            ed,
2766                            &content,
2767                        )?;
2768                        if stored {
2769                            newly_received_sessions.push((content.room_id, content.session_id));
2770                        }
2771                    }
2772                }
2773            }
2774            "m.room_key_request" => {
2775                if let Ok(content) = serde_json::from_value::<RoomKeyRequestContent>(decrypted.content.clone()) {
2776                    self.answer_room_key_request(&decrypted, &content)?;
2777                }
2778            }
2779            "m.room_key.withheld" => {
2780                if let Ok(content) = serde_json::from_value::<RoomKeyWithheldContent>(decrypted.content.clone()) {
2781                    if let (Some(room_id), Some(session_id)) = (content.room_id, content.session_id) {
2782                        self.withheld_sessions.insert((room_id, session_id));
2783                    }
2784                }
2785            }
2786            _ => {}
2787        }
2788        Ok(())
2789    }
2790
2791    /// Who a forwarded Megolm session is bound to. The Olm sender is
2792    /// authenticated. The session is bound to the device named by
2793    /// `sender_key` when that device is the forwarder, or when the
2794    /// forwarder is this account (another of our devices). A room peer
2795    /// cannot install a session bound to somebody else.
2796    fn forward_binding(
2797        &self,
2798        decrypted: &DecryptedToDevice,
2799        content: &ForwardedRoomKeyContent,
2800    ) -> Result<Option<(UserId, mail4agent_vodozemac::Curve25519PublicKey, mail4agent_vodozemac::Ed25519PublicKey)>, MessengerError>
2801    {
2802        let members = self.encrypted_room_member_ids(&content.room_id);
2803        // A room admin (power >= 100) may forward any member's session when history is shared: that is
2804        // how a new member gets the history its senders can no longer send (offline, gone).
2805        let forwarder_is_us = decrypted.sender == self.config.user_id
2806            || (self.history_forwarding_allowed(&content.room_id) && self.room_admins(&content.room_id).contains(&decrypted.sender));
2807        if !forwarder_is_us && !members.contains(&decrypted.sender) {
2808            return Ok(None);
2809        }
2810        for user_id in &members {
2811            for device in DeviceTracker::devices_for_user(&self.store, user_id)? {
2812                if device.blocked || device.curve25519.to_base64() != content.sender_key {
2813                    continue;
2814                }
2815                let same_user = decrypted.sender == device.user_id;
2816                if same_user || forwarder_is_us {
2817                    return Ok(Some((device.user_id, device.curve25519, device.ed25519)));
2818                }
2819            }
2820        }
2821        if decrypted.sender_device_curve25519.to_base64() == content.sender_key {
2822            return Ok(Some((
2823                decrypted.sender.clone(),
2824                decrypted.sender_device_curve25519,
2825                decrypted.sender_ed25519,
2826            )));
2827        }
2828        Ok(None)
2829    }
2830
2831    /// Re-share a session this device holds with the device that asked.
2832    /// Inbound export (first known index) is preferred: the outbound
2833    /// `session_key` is the current ratchet and cannot decrypt earlier
2834    /// messages.
2835    fn answer_room_key_request(
2836        &mut self,
2837        decrypted: &DecryptedToDevice,
2838        content: &RoomKeyRequestContent,
2839    ) -> Result<(), MessengerError> {
2840        if matches!(content.action, RoomKeyRequestAction::RequestCancellation) {
2841            self.key_requests_answered.remove(&(
2842                decrypted.sender.clone(),
2843                content.requesting_device_id.clone(),
2844                content.request_id.clone(),
2845            ));
2846            return Ok(());
2847        }
2848        if !matches!(content.action, RoomKeyRequestAction::Request) {
2849            return Ok(());
2850        }
2851        let Some(body) = content.body.as_ref() else { return Ok(()) };
2852        if body.algorithm != "m.megolm.v1.aes-sha2" {
2853            return Ok(());
2854        }
2855        let members = self.encrypted_room_member_ids(&body.room_id);
2856        if !members.contains(&decrypted.sender) {
2857            return Ok(());
2858        }
2859        if decrypted.sender != self.config.user_id && !self.history_forwarding_allowed(&body.room_id) {
2860            return Ok(());
2861        }
2862        if decrypted.sender == self.config.user_id && content.requesting_device_id == self.config.device_id {
2863            return Ok(());
2864        }
2865        let dedup = (decrypted.sender.clone(), content.requesting_device_id.clone(), content.request_id.clone());
2866        if self.key_requests_answered.contains(&dedup) {
2867            return Ok(());
2868        }
2869        let Some(target) = self.requester_device(decrypted, &content.requesting_device_id)? else {
2870            return Ok(());
2871        };
2872        let our_curve = self.account.identity_keys().curve25519.to_base64();
2873        let (inner_type, payload) =
2874            if let Some(forwarded) = GroupSessionManager::forwarded_room_key_content(
2875                &self.store,
2876                &body.room_id,
2877                &body.session_id,
2878                &our_curve,
2879            )? {
2880                ("m.forwarded_room_key", serde_json::to_value(&forwarded)?)
2881            } else if let Some(room_key) =
2882                GroupSessionManager::outbound_room_key_content(&self.store, &body.room_id, &body.session_id)?
2883            {
2884                ("m.room_key", serde_json::to_value(&room_key)?)
2885            } else {
2886                return Ok(());
2887            };
2888        let sent = self.enqueue_olm_to_devices(std::slice::from_ref(&target), inner_type, payload)?;
2889        if sent > 0 {
2890            self.key_requests_answered.insert(dedup);
2891        }
2892        Ok(())
2893    }
2894
2895    /// The device a key request should be encrypted back to. Prefers the
2896    /// stored device with `requesting_device_id`, then the Olm sender's
2897    /// curve key. A blocked device is not answered.
2898    fn requester_device(
2899        &self,
2900        decrypted: &DecryptedToDevice,
2901        requesting_device_id: &DeviceId,
2902    ) -> Result<Option<StoredDevice>, MessengerError> {
2903        let devices = DeviceTracker::devices_for_user(&self.store, &decrypted.sender)?;
2904        if let Some(device) = devices.iter().find(|device| &device.device_id == requesting_device_id) {
2905            return Ok((!device.blocked).then(|| device.clone()));
2906        }
2907        let curve = decrypted.sender_device_curve25519.to_base64();
2908        if let Some(device) = devices.iter().find(|device| device.curve25519.to_base64() == curve) {
2909            return Ok((!device.blocked).then(|| device.clone()));
2910        }
2911        Ok(None)
2912    }
2913
2914    /// Ask the event sender and this account's other devices for one
2915    /// missing Megolm session. Does nothing when this process already
2916    /// asked, or when the peer withheld it. A send that reaches no device
2917    /// stays unmarked so a later drive tries again.
2918    fn request_missing_room_key(
2919        &mut self,
2920        room_id: &RoomId,
2921        sender: &UserId,
2922        content: &MegolmEncryptedContent,
2923    ) -> Result<(), MessengerError> {
2924        let key = (room_id.clone(), content.session_id.clone());
2925        if self.key_requests_sent.contains(&key) || self.withheld_sessions.contains(&key) {
2926            return Ok(());
2927        }
2928        let sender_key = match content.sender_key.clone() {
2929            Some(sender_key) => sender_key,
2930            None => {
2931                let devices = DeviceTracker::devices_for_user(&self.store, sender)?;
2932                let found = content
2933                    .device_id
2934                    .as_ref()
2935                    .and_then(|device_id| devices.iter().find(|device| &device.device_id == device_id))
2936                    .or_else(|| devices.iter().find(|device| !device.blocked));
2937                match found {
2938                    Some(device) => device.curve25519.to_base64(),
2939                    None => return Ok(()),
2940                }
2941            }
2942        };
2943        let mut targets = DeviceTracker::devices_for_user(&self.store, sender)?;
2944        targets.extend(DeviceTracker::devices_for_user(&self.store, &self.config.user_id)?);
2945        // History for a new member: the original sender may be offline for good, so also ask the
2946        // room's admins (power >= 100), whose forwards this client accepts when history is shared.
2947        if self.history_forwarding_allowed(room_id) {
2948            for admin in self.room_admins(room_id) {
2949                if &admin != sender && admin != self.config.user_id {
2950                    targets.extend(DeviceTracker::devices_for_user(&self.store, &admin)?);
2951                }
2952            }
2953        }
2954        if targets.is_empty() {
2955            return Ok(());
2956        }
2957        // M4: reach devices we share no Olm session with yet (we just joined
2958        // and never talked to them) by claiming one-time keys first; the
2959        // claim response re-runs this request.
2960        let missing: Vec<StoredDevice> = OlmSessionManager::sessions_missing_for(&self.store, &targets)?
2961            .into_iter()
2962            .filter(|device| {
2963                !device.blocked
2964                    && !(device.user_id == self.config.user_id && device.device_id == self.config.device_id)
2965                    && !self.history_claim_tried.contains(&(device.user_id.clone(), device.device_id.clone()))
2966            })
2967            .cloned()
2968            .collect();
2969        if !missing.is_empty() {
2970            let claim_id = self.next_request_id()?;
2971            let refs: Vec<&StoredDevice> = missing.iter().collect();
2972            if let Some(request) = OlmSessionManager::keys_claim_request(claim_id.clone(), &refs) {
2973                for device in &missing {
2974                    self.history_claim_tried.insert((device.user_id.clone(), device.device_id.clone()));
2975                }
2976                self.history_claims.insert(claim_id, missing);
2977                self.enqueue_request(request, Lane::Other)?;
2978                return Ok(());
2979            }
2980        }
2981        let request_id = self.next_txn_id()?;
2982        let body = RoomKeyRequestContent {
2983            action: RoomKeyRequestAction::Request,
2984            body: Some(RoomKeyRequestBody {
2985                algorithm: "m.megolm.v1.aes-sha2".to_string(),
2986                room_id: room_id.clone(),
2987                session_id: content.session_id.clone(),
2988                sender_key,
2989            }),
2990            request_id: request_id.as_str().to_string(),
2991            requesting_device_id: self.config.device_id.clone(),
2992        };
2993        let sent = self.enqueue_olm_to_devices(&targets, "m.room_key_request", serde_json::to_value(&body)?)?;
2994        if sent > 0 {
2995            self.key_requests_sent.insert(key);
2996        }
2997        Ok(())
2998    }
2999
3000    /// Olm-encrypt `content` as `inner_type` to every device that already
3001    /// has a session, and enqueue one `sendToDevice` per chunk. Devices
3002    /// with no Olm session are skipped. Returns how many devices were
3003    /// addressed.
3004    fn enqueue_olm_to_devices(
3005        &mut self,
3006        devices: &[StoredDevice],
3007        inner_type: &str,
3008        content: serde_json::Value,
3009    ) -> Result<usize, MessengerError> {
3010        let mut unique = Vec::new();
3011        for device in devices {
3012            if device.blocked || (device.user_id == self.config.user_id && device.device_id == self.config.device_id) {
3013                continue;
3014            }
3015            if unique.iter().any(|have: &StoredDevice| have.user_id == device.user_id && have.device_id == device.device_id)
3016            {
3017                continue;
3018            }
3019            unique.push(device.clone());
3020        }
3021        let missing = OlmSessionManager::sessions_missing_for(&self.store, &unique)?;
3022        let missing_ids: BTreeSet<(UserId, DeviceId)> =
3023            missing.into_iter().map(|device| (device.user_id.clone(), device.device_id.clone())).collect();
3024        let ready: Vec<StoredDevice> = unique
3025            .into_iter()
3026            .filter(|device| !missing_ids.contains(&(device.user_id.clone(), device.device_id.clone())))
3027            .collect();
3028        let mut sent = 0;
3029        for chunk in GroupSessionManager::chunk_recipients_for_send_to_device(&ready) {
3030            let body = GroupSessionManager::build_encrypted_to_device_body(
3031                &mut self.store,
3032                &self.account,
3033                &self.config.user_id,
3034                &self.config.device_id,
3035                chunk,
3036                inner_type,
3037                content.clone(),
3038            )?;
3039            let request_id = self.next_request_id()?;
3040            let share_txn = self.next_txn_id()?;
3041            let request = OutgoingRequest::send_to_device(request_id, ROOM_KEY_SHARE_EVENT_TYPE, &share_txn, body);
3042            self.enqueue_request(request, Lane::ToDevice)?;
3043            sent += chunk.len();
3044        }
3045        Ok(sent)
3046    }
3047
3048    fn queue_unknown_sender_retry(&mut self, event: ToDeviceEvent) {
3049        self.unknown_sender_retry.push_back(event);
3050        while self.unknown_sender_retry.len() > UNKNOWN_SENDER_RETRY_CAPACITY {
3051            self.unknown_sender_retry.pop_front();
3052        }
3053    }
3054
3055    fn retry_unknown_sender_queue(&mut self) -> Result<(), MessengerError> {
3056        let pending: Vec<ToDeviceEvent> = std::mem::take(&mut self.unknown_sender_retry).into_iter().collect();
3057        let mut newly_received_sessions = Vec::new();
3058        for event in pending {
3059            self.process_to_device_event(&event, &mut newly_received_sessions)?;
3060        }
3061        for (room_id, session_id) in &newly_received_sessions {
3062            self.retry_pending_for_session(room_id, session_id);
3063        }
3064        Ok(())
3065    }
3066
3067    /// Every tracked user whose device list is currently outdated.
3068    fn outdated_tracked_users(&self) -> Result<BTreeSet<UserId>, MessengerError> {
3069        Ok(self.store.tracked_users()?.into_iter().filter_map(|(user_id, outdated)| outdated.then_some(user_id)).collect())
3070    }
3071
3072    /// Enqueues one `/keys/query` for every outdated tracked user (nothing if none is).
3073    fn enqueue_keys_query(&mut self) -> Result<(), MessengerError> {
3074        let request_id = self.next_request_id()?;
3075        if let Some(request) = DeviceTracker::keys_query_request(&self.store, request_id)? {
3076            self.enqueue_request(request, Lane::Other)?;
3077        }
3078        Ok(())
3079    }
3080
3081    /// Marks every other member that `events` show joining `room_id` (an
3082    /// encrypted room) as a tracked user with an outdated device list. The
3083    /// server answers `/keys/query` only for users who share a JOINED room with
3084    /// the caller and reports a device list in `device_lists.changed` only when
3085    /// it changes -- never when a user merely starts sharing a room. So a peer
3086    /// asked about while still only invited comes back with no devices and
3087    /// would otherwise stay "up to date" with none after joining, and no later
3088    /// message would ever be shared with them.
3089    /// M4: re-run key requests for every undecryptable (MissingSession) row.
3090    fn retry_missing_room_keys(&mut self) {
3091        let work: Vec<(RoomId, EventId)> = self
3092            .pending_decrypt
3093            .iter()
3094            .flat_map(|(room, by_event)| by_event.keys().map(move |event| (room.clone(), event.clone())))
3095            .collect();
3096        for (room_id, event_id) in work {
3097            let _ = self.retry_decrypt_one(&room_id, &event_id);
3098        }
3099    }
3100
3101    /// M4 policy: forward old room keys only when the room's history is
3102    /// shared (our rooms default to `shared`); a `joined`/`invited` room keeps
3103    /// pre-join history away from later members.
3104    /// Joined members whose power level in the room is at least 100.
3105    fn room_admins(&self, room_id: &RoomId) -> BTreeSet<UserId> {
3106        let Some(state) = self.room_state(room_id) else { return BTreeSet::new() };
3107        self.encrypted_room_member_ids(room_id)
3108            .into_iter()
3109            .filter(|u| state.power_levels.users.get(u).copied().unwrap_or(state.power_levels.users_default) >= 100)
3110            .collect()
3111    }
3112
3113    fn history_forwarding_allowed(&self, room_id: &RoomId) -> bool {
3114        !matches!(
3115            self.rooms.get(room_id).and_then(|room| room.history_visibility.clone()),
3116            Some(crate::wire::events::HistoryVisibility::Joined) | Some(crate::wire::events::HistoryVisibility::Invited)
3117        )
3118    }
3119
3120    /// M3: one step of automatic cross-signing. Keys first, then this
3121    /// device's self-signature. At most a few attempts per process.
3122    fn maybe_enqueue_cross_signing(&mut self) -> Result<(), MessengerError> {
3123        if self.xsign_inflight || self.xsign_attempts >= 4 {
3124            return Ok(());
3125        }
3126        if self.xsign.is_none() {
3127            self.xsign = Some(CrossSigningState::load_or_create(&mut self.store)?);
3128        }
3129        let Some(state) = self.xsign.clone() else { return Ok(()) };
3130        let user_id = self.config.user_id.clone();
3131        let request_id;
3132        let request = if !state.uploaded {
3133            request_id = self.next_request_id()?;
3134            OutgoingRequest::signing_keys_upload(request_id, state.upload_body(&user_id)?)
3135        } else if !state.device_signed {
3136            let device_id = self.config.device_id.clone();
3137            let device_keys = self.account.device_keys_json(&user_id, &device_id)?;
3138            request_id = self.next_request_id()?;
3139            OutgoingRequest::signatures_upload(request_id, state.device_signature_body(&user_id, &device_id, device_keys)?)
3140        } else {
3141            return Ok(());
3142        };
3143        self.xsign_attempts += 1;
3144        self.xsign_inflight = true;
3145        self.enqueue_request(request, Lane::Other)
3146    }
3147
3148    fn track_joined_members<'a>(&mut self, room_id: &RoomId, events: impl Iterator<Item = &'a RawEvent>) -> Result<(), MessengerError> {
3149        if self.rooms.get(room_id).is_none_or(|room| room.encryption.is_none()) {
3150            return Ok(());
3151        }
3152        let joined: Vec<UserId> = events
3153            .filter(|event| event.event_type == "m.room.member")
3154            .filter(|event| event.content.get("membership").and_then(serde_json::Value::as_str) == Some("join"))
3155            .filter_map(|event| event.state_key.as_deref().and_then(|state_key| UserId::parse(state_key).ok()))
3156            .filter(|user_id| *user_id != self.config.user_id)
3157            .collect();
3158        if joined.is_empty() {
3159            return Ok(());
3160        }
3161        DeviceTracker::on_device_lists(&mut self.store, &joined, &[])
3162    }
3163
3164    fn ingest_joined_room(&mut self, room_id: &RoomId, joined: &JoinedRoom) -> Result<(), MessengerError> {
3165        for event in joined.state.events.iter().chain(joined.timeline.events.iter()) {
3166            self.rooms.entry(room_id.clone()).or_default().apply_state_event(event)?;
3167        }
3168
3169        self.track_joined_members(room_id, joined.state.events.iter().chain(joined.timeline.events.iter()))?;
3170
3171        let unread_before = self.rooms.get(room_id).map(RoomState::unread_count);
3172        self.rooms.entry(room_id.clone()).or_default().apply_summary(&joined.summary, &joined.unread_notifications);
3173        let unread_after = self.rooms.get(room_id).map(RoomState::unread_count);
3174        if unread_before != unread_after {
3175            self.emit(MessengerEvent::UnreadChanged { room_id: room_id.clone() });
3176        }
3177
3178        for event in &joined.account_data.events {
3179            self.apply_synced_account_data(Some(room_id), &event.event_type, event.content.clone());
3180        }
3181
3182        if let Some(typing_event) = joined.ephemeral.events.iter().find(|event| event.event_type == "m.typing") {
3183            if let Ok(typing) = serde_json::from_value::<TypingContent>(typing_event.content.clone()) {
3184                self.typing.insert(room_id.clone(), typing.user_ids);
3185                self.emit(MessengerEvent::TypingChanged { room_id: room_id.clone() });
3186            }
3187        }
3188        if let Some(receipt_event) = joined.ephemeral.events.iter().find(|event| event.event_type == "m.receipt") {
3189            if let Ok(receipts) = serde_json::from_value::<ReceiptContent>(receipt_event.content.clone()) {
3190                self.receipts.insert(room_id.clone(), receipts);
3191                self.emit(MessengerEvent::ReceiptsChanged { room_id: room_id.clone() });
3192            }
3193        }
3194
3195        if !joined.timeline.events.is_empty() {
3196            self.timelines.entry(room_id.clone()).or_default().apply_timeline_batch(
3197                &joined.timeline.events,
3198                joined.timeline.limited,
3199                joined.timeline.prev_batch.clone(),
3200            );
3201            self.decrypt_new_timeline_events(room_id, &joined.timeline.events);
3202            self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
3203        }
3204
3205        self.persist_room_state(room_id)
3206    }
3207
3208    fn ingest_invited_room(&mut self, room_id: &RoomId, invited: &InvitedRoom) -> Result<(), MessengerError> {
3209        for stripped in &invited.invite_state.events {
3210            let synthetic = stripped_state_to_raw_event(stripped);
3211            self.rooms.entry(room_id.clone()).or_default().apply_state_event(&synthetic)?;
3212        }
3213        self.persist_room_state(room_id)
3214    }
3215
3216    fn ingest_left_room(&mut self, room_id: &RoomId, left: &LeftRoom) -> Result<(), MessengerError> {
3217        for event in left.state.events.iter().chain(left.timeline.events.iter()) {
3218            self.rooms.entry(room_id.clone()).or_default().apply_state_event(event)?;
3219        }
3220        for event in &left.account_data.events {
3221            self.apply_synced_account_data(Some(room_id), &event.event_type, event.content.clone());
3222        }
3223        if !left.timeline.events.is_empty() {
3224            self.timelines.entry(room_id.clone()).or_default().apply_timeline_batch(
3225                &left.timeline.events,
3226                left.timeline.limited,
3227                left.timeline.prev_batch.clone(),
3228            );
3229            self.decrypt_new_timeline_events(room_id, &left.timeline.events);
3230            self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
3231        }
3232        self.persist_room_state(room_id)
3233    }
3234
3235    fn persist_room_state(&mut self, room_id: &RoomId) -> Result<(), MessengerError> {
3236        let Some(state) = self.rooms.get(room_id) else { return Ok(()) };
3237        let bytes = serde_json::to_vec(state)
3238            .map_err(|source| MessengerError::Crypto(format!("encode room state for {room_id}: {source}")))?;
3239        self.store.save_room_state(room_id, bytes)?;
3240        Ok(())
3241    }
3242
3243    /// Attempts to decrypt every well-formed Megolm `m.room.encrypted`
3244    /// event in `events` (already applied to `room_id`'s timeline as
3245    /// `Encrypted{session_id}` placeholders by the caller), patching each
3246    /// one in place via [`MessengerCore::apply_decrypted_plaintext`] — shared
3247    /// by forward sync ([`MessengerCore::ingest_joined_room`]/
3248    /// [`MessengerCore::ingest_left_room`]) and back-pagination
3249    /// ([`MessengerCore::ingest_room_messages_response`]).
3250    fn decrypt_new_timeline_events(&mut self, room_id: &RoomId, events: &[RawEvent]) {
3251        for event in events {
3252            if event.event_type != "m.room.encrypted" || event.state_key.is_some() || event.unsigned.redacted_because.is_some() {
3253                continue;
3254            }
3255            let Ok(RoomEncryptedContent::Megolm(content)) = serde_json::from_value::<RoomEncryptedContent>(event.content.clone())
3256            else {
3257                continue;
3258            };
3259            match GroupSessionManager::decrypt_event(
3260                &mut self.store,
3261                room_id,
3262                &event.event_id,
3263                event.origin_server_ts,
3264                &event.sender,
3265                &content,
3266            ) {
3267                Ok(plaintext) => {
3268                    self.apply_decrypted_plaintext(room_id, &event.event_id, &event.sender, event.origin_server_ts, &plaintext);
3269                    if let Some(by_event) = self.pending_decrypt.get_mut(room_id) {
3270                        by_event.remove(&event.event_id);
3271                    }
3272                }
3273                Err(err) => {
3274                    if matches!(err, GroupDecryptError::MissingSession { .. }) {
3275                        let _ = self.request_missing_room_key(room_id, &event.sender, &content);
3276                    }
3277                    // Stored as the coarse `UtdReason` variant's own name
3278                    // (never the full, per-error-variant `Display` message)
3279                    // -- this is the only place a UI-facing adapter can
3280                    // recover which of the crate's own [`crate::crypto::
3281                    // group_sessions::UtdReason`] variants this failure
3282                    // maps to (`GroupDecryptError::utd_reason`'s own doc),
3283                    // since [`ItemContent::Undecryptable`] carries a plain
3284                    // `String`, not the enum itself.
3285                    let reason = format!("{:?}", err.utd_reason());
3286                    if let Some(timeline) = self.timelines.get_mut(room_id) {
3287                        timeline.set_decrypted(&event.event_id, ItemContent::Undecryptable { reason });
3288                    }
3289                    self.pending_decrypt.entry(room_id.clone()).or_default().insert(
3290                        event.event_id.clone(),
3291                        PendingDecryptItem {
3292                            session_id: content.session_id.clone(),
3293                            sender: event.sender.clone(),
3294                            origin_server_ts: event.origin_server_ts,
3295                            content,
3296                        },
3297                    );
3298                }
3299            }
3300        }
3301    }
3302
3303    fn ingest_room_messages_response(&mut self, room_id: &RoomId, body: &[u8]) -> Result<(), MessengerError> {
3304        let parsed: RoomMessagesResponseBody = serde_json::from_slice(body)?;
3305        let mut events = parsed.chunk;
3306        events.reverse();
3307        self.timelines.entry(room_id.clone()).or_default().prepend_back_page(&events, parsed.end);
3308        self.decrypt_new_timeline_events(room_id, &events);
3309        self.emit(MessengerEvent::TimelineChanged { room_id: room_id.clone() });
3310        self.persist_room_state(room_id)
3311    }
3312}
3313
3314impl MessengerCore<SealedRecordCodec> {
3315    /// The production entry point (M14): opens a core whose store seals
3316    /// every record with AES-256-GCM under `secrets.store_seal_key` — the
3317    /// constructor a shell calls, in place of the generic
3318    /// [`MessengerCore::open`] this crate's own tests use with
3319    /// [`crate::store::InsecurePlainCodecForTests`].
3320    ///
3321    /// Fails with [`MessengerError::Crypto`] if `secrets.store_seal_key` is
3322    /// `None`. The caller supplies that key. There is no unsealed fallback
3323    /// here by design (an unsealed messenger record store is exactly what
3324    /// [`SealedRecordCodec`] exists to keep off disk).
3325    pub fn open_sealed(
3326        records: impl IntoIterator<Item = SealedRecord>,
3327        secrets: CoreSecrets,
3328        config: CoreConfig,
3329        now_ms: i64,
3330        jitter: Box<dyn Jitter>,
3331    ) -> Result<Self, MessengerError> {
3332        let seal_key = secrets.store_seal_key.clone().ok_or_else(|| {
3333            MessengerError::Crypto(
3334                "MessengerCore::open_sealed requires CoreSecrets::store_seal_key"
3335                    .to_string(),
3336            )
3337        })?;
3338        let codec = SealedRecordCodec::new(seal_key);
3339        Self::open(records, codec, config, secrets, now_ms, jitter)
3340    }
3341}
3342
3343/// A stable, always-well-formed placeholder [`EventId`] for a synthetic
3344/// [`RawEvent`] built from an invite's [`StrippedStateEvent`] (which itself
3345/// carries no event id) — [`RoomState::apply_state_event`] never reads
3346/// `event_id` at all, so its exact value is irrelevant; it only has to
3347/// parse.
3348fn stripped_placeholder_event_id() -> EventId {
3349    EventId::parse("$stripped-preview").expect("this literal is a well-formed, hardcoded $-prefixed opaque id")
3350}
3351
3352/// Reshapes one invite-preview [`StrippedStateEvent`] into the
3353/// [`RawEvent`] shape [`RoomState::apply_state_event`] accepts —
3354/// `origin_server_ts`/`unsigned`/`event_id` are unused by that method for a
3355/// state event, so a placeholder is enough (this function's own doc).
3356fn stripped_state_to_raw_event(stripped: &StrippedStateEvent) -> RawEvent {
3357    RawEvent {
3358        event_id: stripped_placeholder_event_id(),
3359        event_type: stripped.event_type.clone(),
3360        sender: stripped.sender.clone(),
3361        origin_server_ts: 0,
3362        state_key: Some(stripped.state_key.clone()),
3363        content: stripped.content.clone(),
3364        unsigned: Unsigned::default(),
3365    }
3366}
3367
3368/// `content` as a forward re-sends it: `m.relates_to` (a reply or edit
3369/// pointer into the source room), `m.mentions` and `m.new_content` removed,
3370/// any forward marker the source itself carried replaced by this hop's own
3371/// (`marker_key: marker`) -- so an earlier hop's origin never travels on.
3372/// Everything else (including an unrecognized `msgtype`'s own fields) passes
3373/// through untouched.
3374fn forwardable_content(mut content: serde_json::Value, marker_key: &str, marker: serde_json::Value) -> serde_json::Value {
3375    if let Some(object) = content.as_object_mut() {
3376        for key in ["m.relates_to", "m.mentions", "m.new_content", KEY_FORWARDED, KEY_FORWARDED_FROM] {
3377            object.remove(key);
3378        }
3379        object.insert(marker_key.to_string(), marker);
3380    }
3381    content
3382}
3383
3384/// The local-echo [`ItemContent`] for a not-yet-sent [`OutgoingMessage`] —
3385/// always the plain new body, regardless of `reply_to`/`edit_of` (this
3386/// crate's timeline has no dedicated "echo of an edit" rendering; the
3387/// confirmed event, once it arrives back off `/sync`, carries the full
3388/// `m.relates_to`-aware rendering via the ordinary
3389/// [`crate::room::relations`] aggregation — this module's own doc).
3390fn message_local_echo_content(message: &OutgoingMessage) -> ItemContent {
3391    let body = TextLikeMessageContent { body: message.body.clone(), format: None, formatted_body: None };
3392    match message.kind {
3393        MessageKind::Text => ItemContent::Text(body),
3394        MessageKind::Notice => ItemContent::Notice(body),
3395        MessageKind::Emote => ItemContent::Emote(body),
3396    }
3397}
3398
3399/// The `m.room.message` wire content for `message` — plain
3400/// `{msgtype, body}` for an ordinary send; a reply adds the legacy
3401/// `m.in_reply_to` pointer; an edit instead prefixes the outer `body` with
3402/// `"* "` (the client-side fallback convention for a client that does not
3403/// understand `m.replace`) and carries the real replacement under
3404/// `m.new_content`, with `m.relates_to: m.replace` naming the edited event
3405/// (research doc §1.3; `reply_to` and `edit_of` are mutually exclusive at
3406/// the wire level — [`OutgoingMessage`]'s own doc).
3407fn message_wire_content(message: &OutgoingMessage) -> serde_json::Value {
3408    let msgtype = message.kind.msgtype();
3409    let new_content = serde_json::json!({ "msgtype": msgtype, "body": message.body });
3410    let mut content = new_content.clone();
3411    if let Some(edit_of) = &message.edit_of {
3412        content["body"] = serde_json::Value::String(format!("* {}", message.body));
3413        content["m.new_content"] = new_content;
3414        content["m.relates_to"] = serde_json::json!({ "rel_type": "m.replace", "event_id": edit_of.as_str() });
3415    } else if let Some(reply_to) = &message.reply_to {
3416        content["m.relates_to"] = serde_json::json!({ "m.in_reply_to": { "event_id": reply_to.as_str() } });
3417    }
3418    content
3419}
3420
3421/// The outer, unencrypted-envelope `m.relates_to` a Megolm-encrypted
3422/// `message` must carry alongside its ciphertext (`crate::wire::events::
3423/// MegolmEncryptedContent`'s own doc: copied verbatim from the cleartext
3424/// plaintext's own relation, so the server can index it without ever
3425/// decrypting). `None` for a plain send with neither a reply nor an edit.
3426fn message_relates_to(message: &OutgoingMessage) -> Option<RelatesTo> {
3427    if let Some(edit_of) = &message.edit_of {
3428        Some(RelatesTo::Replace { event_id: edit_of.clone() })
3429    } else {
3430        message.reply_to.clone().map(|event_id| RelatesTo::InReplyTo(InReplyTo { event_id }))
3431    }
3432}
3433
3434#[cfg(test)]
3435mod tests {
3436    use super::*;
3437    use crate::room::timeline::Forwarded;
3438    use crate::store::InsecurePlainCodecForTests;
3439    use crate::wire::HttpMethod;
3440
3441    struct FixedJitter(f64);
3442
3443    impl Jitter for FixedJitter {
3444        fn next_unit(&mut self) -> f64 {
3445            self.0
3446        }
3447    }
3448
3449    fn device_id() -> DeviceId {
3450        DeviceId::parse("DEV1").expect("valid device id")
3451    }
3452
3453    fn user_id() -> UserId {
3454        UserId::parse("@alice:example.org").expect("valid user id")
3455    }
3456
3457    fn test_config() -> CoreConfig {
3458        CoreConfig { user_id: user_id(), device_id: device_id(), server_name: "example.org".to_string() }
3459    }
3460
3461    fn open_fresh_core() -> MessengerCore<InsecurePlainCodecForTests> {
3462        MessengerCore::open(Vec::new(), InsecurePlainCodecForTests, test_config(), CoreSecrets::default(), 0, Box::new(FixedJitter(0.0)))
3463            .expect("open succeeds on a brand-new device")
3464    }
3465
3466    /// Drains and acks every currently-pending flush batch -- the same
3467    /// "persist, then ack" tick a real shell performs once per loop
3468    /// iteration. Tests call this explicitly wherever they need a
3469    /// mutation's own flush epoch satisfied before checking
3470    /// [`MessengerCore::releasable_requests`] (the flush-before-send
3471    /// barrier, `crate::persist`'s own doc, applies just as much to a
3472    /// counter bump as to an Olm/Megolm ratchet advance).
3473    fn flush_and_ack(core: &mut MessengerCore<InsecurePlainCodecForTests>) {
3474        while let Some(batch) = core.take_flush_batch() {
3475            core.ack_flush(batch.id);
3476        }
3477    }
3478
3479    #[test]
3480    fn counters_survive_restart_and_never_repeat_txn_ids() {
3481        let device = device_id();
3482        let mut store = Store::new(device.clone(), InsecurePlainCodecForTests);
3483        store.save_counters(Counters { next_request_id: 3, next_txn_id: 7 }).expect("save");
3484        let batch = store.take_flush_batch().expect("counters dirtied the store");
3485        store.ack_flush(batch.id);
3486
3487        let reloaded = Store::load(batch.records, InsecurePlainCodecForTests, device).expect("reload");
3488        let restored = reloaded.counters().expect("no error").expect("counters were persisted");
3489        assert_eq!(restored.next_txn_id, 7, "a restart resumes the txn-id sequence exactly where it left off");
3490        assert_eq!(restored.next_request_id, 3);
3491
3492        // The restore rule itself: a request still pending with a HIGHER
3493        // seed than whatever was last durably persisted must push the
3494        // resumed counter past it too, so a freshly minted id can never
3495        // collide with one already embedded in an in-flight request.
3496        assert_eq!(restore_counter(3, Some(10)), 11, "resumes past the highest pending id, never repeating it");
3497        assert_eq!(restore_counter(3, None), 3, "nothing pending: the persisted value alone is authoritative");
3498    }
3499
3500    #[test]
3501    fn open_restores_the_request_counter_past_a_still_pending_requests_id() {
3502        let device = device_id();
3503        let mut store = Store::new(device.clone(), InsecurePlainCodecForTests);
3504        let mut queue = OutgoingQueue::new();
3505        let pending = OutgoingRequest::sync(RequestId::next(5), None, None);
3506        queue.enqueue(&mut store, pending, Lane::Sync).expect("enqueue");
3507        // A stale, lower counter value -- as if the process crashed after
3508        // minting seed 5 but before this record was ever written.
3509        store.save_counters(Counters { next_request_id: 2, next_txn_id: 0 }).expect("save");
3510        let batch = store.take_flush_batch().expect("pending");
3511        store.ack_flush(batch.id);
3512
3513        let core = MessengerCore::open(
3514            batch.records,
3515            InsecurePlainCodecForTests,
3516            test_config(),
3517            CoreSecrets::default(),
3518            0,
3519            Box::new(FixedJitter(0.0)),
3520        )
3521        .expect("open succeeds");
3522        assert_eq!(
3523            core.counters.next_request_id, 6,
3524            "resumes past the highest id already in flight, not the stale persisted value"
3525        );
3526    }
3527
3528    #[test]
3529    fn sync_token_persisted_after_everything_it_covers() {
3530        let mut core = open_fresh_core();
3531        flush_and_ack(&mut core);
3532        // The first call mints and enqueues the sync request, which itself
3533        // dirties the counter record -- not releasable until that flush is
3534        // acked (the flush-before-send barrier). The second call, after
3535        // acking, actually returns it.
3536        core.releasable_requests(0);
3537        flush_and_ack(&mut core);
3538        let requests = core.releasable_requests(0);
3539        let sync_request = requests.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("a sync request was enqueued");
3540
3541        assert!(core.store.sync_token().expect("no error").is_none(), "no token before the first response");
3542
3543        let body = serde_json::json!({
3544            "next_batch": "s1",
3545            "device_one_time_keys_count": { "signed_curve25519": 0 },
3546            "device_unused_fallback_key_types": []
3547        })
3548        .to_string();
3549        core.on_response(sync_request.id.clone(), HttpResponseDescriptor { status: 200, body: body.into_bytes() }, 0);
3550
3551        assert_eq!(core.store.sync_token().expect("no error"), Some("s1"));
3552    }
3553
3554    #[test]
3555    fn only_one_sync_in_flight() {
3556        let mut core = open_fresh_core();
3557        flush_and_ack(&mut core);
3558        core.releasable_requests(0);
3559        flush_and_ack(&mut core);
3560        let first = core.releasable_requests(0);
3561        assert_eq!(first.iter().filter(|r| r.kind == OutgoingRequestKind::Sync).count(), 1);
3562
3563        // Calling again before any response arrives must not mint a
3564        // second `/sync` -- the lane is occupied and this core's own
3565        // `sync_request_id` tracking prevents a duplicate enqueue too.
3566        let second = core.releasable_requests(0);
3567        assert!(second.iter().all(|r| r.kind != OutgoingRequestKind::Sync), "no second sync request released");
3568    }
3569
3570    #[test]
3571    fn first_sync_of_a_session_never_long_polls_then_the_loop_does() {
3572        let mut core = open_fresh_core();
3573        flush_and_ack(&mut core);
3574        core.releasable_requests(0);
3575        flush_and_ack(&mut core);
3576        let first = core.releasable_requests(0);
3577        let first_sync = first.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("a sync request was enqueued");
3578        assert!(first_sync.query.contains(&("timeout".to_string(), "0".to_string())), "the initial sync must answer at once");
3579        assert!(first_sync.query.iter().all(|(key, _)| key != "since"), "a fresh device has no since token");
3580
3581        let body = serde_json::json!({
3582            "next_batch": "s1",
3583            "device_one_time_keys_count": { "signed_curve25519": 0 },
3584            "device_unused_fallback_key_types": []
3585        })
3586        .to_string();
3587        core.on_response(first_sync.id.clone(), HttpResponseDescriptor { status: 200, body: body.into_bytes() }, 0);
3588        flush_and_ack(&mut core);
3589
3590        // The next `/sync` is minted lazily (then needs its own flush round
3591        // before it is releasable) -- and only now does it long-poll.
3592        core.releasable_requests(0);
3593        flush_and_ack(&mut core);
3594        let second = core.releasable_requests(0);
3595        let second_sync = second.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("the follow-up sync was enqueued");
3596        assert!(second_sync.query.contains(&("since".to_string(), "s1".to_string())));
3597        assert!(second_sync.query.contains(&("timeout".to_string(), SYNC_TIMEOUT_MS.to_string())), "once caught up the loop long-polls");
3598    }
3599
3600    #[test]
3601    fn room_key_arrival_retries_undecryptable_items() {
3602        use crate::crypto::group_sessions::GroupSessionManager;
3603        use mail4agent_vodozemac::megolm::{GroupSession, SessionConfig};
3604        use mail4agent_vodozemac::olm::Account;
3605
3606        let mut core = open_fresh_core();
3607        let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3608        let sender = UserId::parse("@bob:example.org").expect("valid user id");
3609        let identity = Account::new().identity_keys();
3610
3611        let mut group_session = GroupSession::new(SessionConfig::version_1());
3612        let session_id = group_session.session_id();
3613        // Captured BEFORE encrypting: Megolm's ratchet only advances
3614        // forward, so a `session_key` exported AFTER `encrypt` would only
3615        // let a recipient decrypt messages from that later point onward,
3616        // never the one encrypted just before it.
3617        let session_key = group_session.session_key();
3618        let plaintext = crate::crypto::group_sessions::RoomEventPlaintext {
3619            event_type: "m.room.message".to_string(),
3620            content: serde_json::json!({ "msgtype": "m.text", "body": "hi" }),
3621            room_id: room_id.clone(),
3622        };
3623        let message = group_session.encrypt(serde_json::to_vec(&plaintext).expect("valid JSON"));
3624
3625        let event: RawEvent = serde_json::from_value(serde_json::json!({
3626            "event_id": "$evt:example.org",
3627            "type": "m.room.encrypted",
3628            "sender": sender.as_str(),
3629            "origin_server_ts": 1,
3630            "content": {
3631                "algorithm": "m.megolm.v1.aes-sha2",
3632                "ciphertext": message.to_base64(),
3633                "session_id": session_id,
3634            }
3635        }))
3636        .expect("valid event");
3637
3638        // The event arrives with no session known yet -- it lands as
3639        // `Undecryptable`. `apply_timeline_batch` first, exactly like
3640        // `ingest_joined_room` does, since `decrypt_new_timeline_events`
3641        // only ever patches an item [`Timeline::set_decrypted`] already
3642        // knows about -- it never inserts one itself.
3643        let mut timeline = Timeline::new();
3644        timeline.apply_timeline_batch(std::slice::from_ref(&event), false, None);
3645        core.timelines.insert(room_id.clone(), timeline);
3646        core.decrypt_new_timeline_events(&room_id, std::slice::from_ref(&event));
3647        {
3648            let timeline = core.timeline(&room_id).expect("timeline exists");
3649            let item = timeline.item_by_event_id(&EventId::parse("$evt:example.org").expect("valid event id")).expect("item present");
3650            assert!(matches!(item.content, ItemContent::Undecryptable { .. }));
3651        }
3652
3653        // The room key now arrives (e.g. via a to-device `m.room_key`) --
3654        // simulated directly against the manager, mirroring what
3655        // `route_decrypted_to_device` itself does.
3656        let content = RoomKeyContent {
3657            algorithm: "m.megolm.v1.aes-sha2".to_string(),
3658            room_id: room_id.clone(),
3659            session_id: session_id.clone(),
3660            session_key: session_key.to_base64(),
3661        };
3662        GroupSessionManager::receive_room_key(&mut core.store, &sender, identity.curve25519, identity.ed25519, &content)
3663            .expect("receive room key");
3664        core.retry_pending_for_session(&room_id, &session_id);
3665
3666        let timeline = core.timeline(&room_id).expect("timeline exists");
3667        let item = timeline.item_by_event_id(&EventId::parse("$evt:example.org").expect("valid event id")).expect("item present");
3668        match &item.content {
3669            ItemContent::Text(text) => assert_eq!(text.body, "hi"),
3670            other => panic!("expected the retried decrypt to succeed, got {other:?}"),
3671        }
3672    }
3673
3674    #[test]
3675    fn redelivered_to_device_after_restart_is_dropped_silently() {
3676        use crate::crypto::olm_sessions::OlmSessionManager;
3677
3678        // Alice's own store/account -- the sender.
3679        let alice_user = UserId::parse("@alice:example.org").expect("valid user id");
3680        let alice_device = DeviceId::parse("ALICEDEV").expect("valid device id");
3681        let mut alice_store = Store::new(alice_device.clone(), InsecurePlainCodecForTests);
3682        let alice_account = OlmAccountState::load_or_create(&mut alice_store).expect("create alice's account");
3683        // Signed once, up front -- see `unknown_sender_device_is_retried_after_keys_query`'s
3684        // own comment on this same call for why the order matters here.
3685        let alice_device_keys =
3686            alice_account.device_keys_json(&alice_user, &alice_device).expect("sign alice's device_keys");
3687
3688        // Bob's core is the one under test.
3689        let mut bob_store = Store::new(device_id(), InsecurePlainCodecForTests);
3690        let mut bob_account = OlmAccountState::load_or_create(&mut bob_store).expect("create bob's account");
3691        bob_account.on_sync_counts(&mut bob_store, 0, &["signed_curve25519".to_string()]).expect("top up bob's OTKs");
3692        let upload = bob_account
3693            .keys_upload_request(RequestId::next(0), &user_id(), &device_id())
3694            .expect("builds a request")
3695            .expect("something to upload");
3696        let one_time_keys = upload.body.expect("upload has a body");
3697        let one_time_keys = one_time_keys.get("one_time_keys").and_then(serde_json::Value::as_object).expect("OTKs present");
3698        let (key_id, key_value) = one_time_keys.iter().next().expect("at least one OTK");
3699        let claim_body = serde_json::to_vec(&serde_json::json!({
3700            "one_time_keys": { user_id().as_str(): { device_id().as_str(): { key_id: key_value } } }
3701        }))
3702        .expect("valid JSON");
3703        let bob_device_stored = crate::crypto::device_tracker::StoredDevice {
3704            user_id: user_id(),
3705            device_id: device_id(),
3706            curve25519: bob_account.identity_keys().curve25519,
3707            ed25519: bob_account.identity_keys().ed25519,
3708            algorithms: vec!["m.olm.v1.curve25519-aes-sha2".to_string(), "m.megolm.v1.aes-sha2".to_string()],
3709            display_name: None,
3710            verified: false,
3711            blocked: false,
3712        };
3713        OlmSessionManager::on_keys_claim_response(&mut alice_store, &alice_account, &[&bob_device_stored], &claim_body)
3714            .expect("alice establishes a session with bob");
3715
3716        // Bob learns alice's device ahead of time, so the FIRST decrypt
3717        // below is a genuine, fully-validated success (not merely an
3718        // `Ok(())` a caller can't tell apart from a silently-dropped
3719        // failure) -- this test's whole point is a real replay, not an
3720        // incidental one.
3721        DeviceTracker::on_keys_query_response(
3722            &mut bob_store,
3723            &serde_json::to_vec(&serde_json::json!({
3724                "device_keys": { alice_user.as_str(): { alice_device.as_str(): alice_device_keys } }
3725            }))
3726            .expect("valid JSON"),
3727        )
3728        .expect("bob learns alice's device");
3729
3730        let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3731        let group_session = mail4agent_vodozemac::megolm::GroupSession::new(mail4agent_vodozemac::megolm::SessionConfig::version_1());
3732        let room_key_content = RoomKeyContent {
3733            algorithm: "m.megolm.v1.aes-sha2".to_string(),
3734            room_id,
3735            session_id: group_session.session_id(),
3736            session_key: group_session.session_key().to_base64(),
3737        };
3738        let encrypted = OlmSessionManager::encrypt_to_device(
3739            &mut alice_store,
3740            &alice_account,
3741            &alice_user,
3742            &alice_device,
3743            &bob_device_stored,
3744            "m.room_key",
3745            serde_json::to_value(&room_key_content).expect("valid JSON"),
3746        )
3747        .expect("encrypt to bob");
3748        let to_device_event = ToDeviceEvent {
3749            sender: alice_user.clone(),
3750            event_type: "m.room.encrypted".to_string(),
3751            content: serde_json::to_value(RoomEncryptedContent::Olm(encrypted)).expect("valid JSON"),
3752        };
3753
3754        let mut core =
3755            MessengerCore::open(Vec::new(), InsecurePlainCodecForTests, test_config(), CoreSecrets::default(), 0, Box::new(FixedJitter(0.0)))
3756                .expect("open succeeds");
3757        core.store = bob_store;
3758        core.account = bob_account;
3759
3760        let mut sessions = Vec::new();
3761        core.process_to_device_event(&to_device_event, &mut sessions).expect("first decrypt succeeds");
3762        assert_eq!(
3763            sessions,
3764            vec![(room_key_content.room_id.clone(), room_key_content.session_id.clone())],
3765            "the first, genuine delivery is fully validated and accepted"
3766        );
3767
3768        // Simulate a restart: flush, ack, and rebuild the store/account
3769        // from just the durable records -- the exact scenario a redelivery
3770        // after a crash looks like.
3771        let batch = core.store.take_flush_batch().expect("decrypting dirtied the store");
3772        core.store.ack_flush(batch.id);
3773        let reloaded_store = Store::load(batch.records, InsecurePlainCodecForTests, device_id()).expect("reload succeeds");
3774        core.store = reloaded_store;
3775        core.account = OlmAccountState::load_or_create(&mut core.store).expect("reload reuses the persisted account");
3776
3777        // The exact same to-device event, redelivered -- must not error the
3778        // whole ingestion path, and must not resurrect a second plaintext
3779        // anywhere a caller could observe.
3780        let mut sessions_after_restart = Vec::new();
3781        let result = core.process_to_device_event(&to_device_event, &mut sessions_after_restart);
3782        assert!(result.is_ok(), "a redelivered to-device event is dropped, not surfaced as an ingestion error");
3783        assert!(sessions_after_restart.is_empty(), "the replay never produces a second accepted room key");
3784    }
3785
3786    #[test]
3787    fn unknown_sender_device_is_retried_after_keys_query() {
3788        use crate::crypto::olm_sessions::OlmSessionManager;
3789
3790        // Alice's own store/account -- the sender.
3791        let alice_user = UserId::parse("@alice:example.org").expect("valid user id");
3792        let alice_device = DeviceId::parse("ALICEDEV").expect("valid device id");
3793        let mut alice_store = Store::new(alice_device.clone(), InsecurePlainCodecForTests);
3794        let alice_account = OlmAccountState::load_or_create(&mut alice_store).expect("create alice's account");
3795        // Signed once now (not only later, right before building the
3796        // `/keys/query` response) -- this is the exact same signed
3797        // `device_keys` payload either way, computed here so bob's own
3798        // `/keys/claim` handshake below observes it already having
3799        // happened, matching the sequencing every other multi-device e2e
3800        // test in this crate (`crypto::olm_sessions`'s own
3801        // `olm_session_rejects_out_of_order_ratchet_message`) already uses.
3802        let alice_device_keys =
3803            alice_account.device_keys_json(&alice_user, &alice_device).expect("sign alice's device_keys");
3804
3805        // Bob's own store/account -- a genuinely distinct user (not this
3806        // module's shared `user_id()`/`device_id()` test fixture, which is
3807        // "alice" for every OTHER test in this file) -- built standalone
3808        // first (same order as
3809        // `redelivered_to_device_after_restart_is_dropped_silently`) and
3810        // only assigned into a `MessengerCore` once the whole Olm handshake
3811        // is done.
3812        let bob_user = UserId::parse("@bob:example.org").expect("valid user id");
3813        let bob_device = DeviceId::parse("BOBDEV").expect("valid device id");
3814        let mut bob_store = Store::new(bob_device.clone(), InsecurePlainCodecForTests);
3815        let mut bob_account = OlmAccountState::load_or_create(&mut bob_store).expect("create bob's account");
3816        bob_account.on_sync_counts(&mut bob_store, 0, &["signed_curve25519".to_string()]).expect("top up bob's OTKs");
3817        let upload = bob_account
3818            .keys_upload_request(RequestId::next(0), &bob_user, &bob_device)
3819            .expect("builds a request")
3820            .expect("something to upload");
3821        let one_time_keys = upload.body.expect("upload has a body");
3822        let one_time_keys = one_time_keys.get("one_time_keys").and_then(serde_json::Value::as_object).expect("OTKs present");
3823        let (key_id, key_value) = one_time_keys.iter().next().expect("at least one OTK");
3824        let claim_body = serde_json::to_vec(&serde_json::json!({
3825            "one_time_keys": { bob_user.as_str(): { bob_device.as_str(): { key_id: key_value } } }
3826        }))
3827        .expect("valid JSON");
3828        let bob_device_stored = crate::crypto::device_tracker::StoredDevice {
3829            user_id: bob_user.clone(),
3830            device_id: bob_device.clone(),
3831            curve25519: bob_account.identity_keys().curve25519,
3832            ed25519: bob_account.identity_keys().ed25519,
3833            algorithms: vec!["m.olm.v1.curve25519-aes-sha2".to_string(), "m.megolm.v1.aes-sha2".to_string()],
3834            display_name: None,
3835            verified: false,
3836            blocked: false,
3837        };
3838        OlmSessionManager::on_keys_claim_response(&mut alice_store, &alice_account, &[&bob_device_stored], &claim_body)
3839            .expect("alice establishes a session with bob");
3840        let encrypted = OlmSessionManager::encrypt_to_device(
3841            &mut alice_store,
3842            &alice_account,
3843            &alice_user,
3844            &alice_device,
3845            &bob_device_stored,
3846            "m.dummy",
3847            serde_json::json!({}),
3848        )
3849        .expect("encrypt to bob");
3850        let to_device_event = ToDeviceEvent {
3851            sender: alice_user.clone(),
3852            event_type: "m.room.encrypted".to_string(),
3853            content: serde_json::to_value(RoomEncryptedContent::Olm(encrypted)).expect("valid JSON"),
3854        };
3855
3856        let bob_config = CoreConfig { user_id: bob_user.clone(), device_id: bob_device.clone(), server_name: "example.org".to_string() };
3857        let mut core =
3858            MessengerCore::open(Vec::new(), InsecurePlainCodecForTests, bob_config, CoreSecrets::default(), 0, Box::new(FixedJitter(0.0)))
3859                .expect("open succeeds");
3860        core.store = bob_store;
3861        core.account = bob_account;
3862
3863        // Bob does not know alice's device yet: the decrypt is refused with
3864        // `UnknownSenderDevice` before any session is created, and the event
3865        // is queued for a retry.
3866        let mut sessions = Vec::new();
3867        core.process_to_device_event(&to_device_event, &mut sessions).expect("queues for retry, does not error");
3868        assert_eq!(core.unknown_sender_retry.len(), 1, "queued for a retry after the next keys/query");
3869        let alice_curve = alice_account.identity_keys().curve25519.to_base64();
3870        assert!(
3871            core.store.olm_sessions_for_device(&alice_curve).expect("no error").is_empty(),
3872            "the refused attempt created no Olm session (it would have consumed the one-time key)"
3873        );
3874
3875        // Bob learns alice's device via a `/keys/query` response --
3876        // `MessengerCore::handle_terminal_success`'s own `KeysQuery` arm
3877        // calls `retry_unknown_sender_queue` exactly once, right after
3878        // applying the response.
3879        let query_body = serde_json::to_vec(&serde_json::json!({
3880            "device_keys": { alice_user.as_str(): { alice_device.as_str(): alice_device_keys } }
3881        }))
3882        .expect("valid JSON");
3883        DeviceTracker::on_keys_query_response(&mut core.store, &query_body).expect("bob learns alice's device");
3884        core.retry_unknown_sender_queue().expect("no error");
3885        assert!(core.unknown_sender_retry.is_empty(), "the queued entry is drained by the retry attempt");
3886        assert_eq!(
3887            core.store.olm_sessions_for_device(&alice_curve).expect("no error").len(),
3888            1,
3889            "the retry decrypted the very same pre-key message and established the inbound session"
3890        );
3891    }
3892
3893    #[test]
3894    fn dispatch_load_older_produces_an_outgoing_request() {
3895        let mut core = open_fresh_core();
3896        let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3897        let mut timeline = Timeline::new();
3898        let event: RawEvent = serde_json::from_value(serde_json::json!({
3899            "event_id": "$1:example.org",
3900            "type": "m.room.message",
3901            "sender": "@alice:example.org",
3902            "origin_server_ts": 1,
3903            "content": { "msgtype": "m.text", "body": "hi" }
3904        }))
3905        .expect("valid event");
3906        timeline.apply_timeline_batch(&[event], true, Some("t1".to_string()));
3907        core.timelines.insert(room_id.clone(), timeline);
3908        flush_and_ack(&mut core);
3909
3910        core.dispatch(MessengerCommand::LoadOlder { room_id: room_id.clone() }, 0).expect("dispatch succeeds");
3911        flush_and_ack(&mut core);
3912        let requests = core.releasable_requests(0);
3913        let load_older = requests.iter().find(|r| r.kind == OutgoingRequestKind::RoomMessages);
3914        assert!(load_older.is_some(), "a RoomMessages request was enqueued using the room's own gap token");
3915    }
3916
3917    #[test]
3918    fn load_older_on_a_resumed_room_pages_back_from_the_sync_token() {
3919        let mut core = open_fresh_core();
3920        let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3921        core.store.save_sync_token("s42_7".to_string()).expect("save the sync token");
3922        flush_and_ack(&mut core);
3923
3924        // Unknown room and no timeline: nothing to page.
3925        core.dispatch(MessengerCommand::LoadOlder { room_id: room_id.clone() }, 0).expect("dispatch succeeds");
3926        flush_and_ack(&mut core);
3927        assert!(core.releasable_requests(0).iter().all(|r| r.kind != OutgoingRequestKind::RoomMessages));
3928
3929        // The room is known (its state record was replayed) but its timeline is empty.
3930        core.rooms.insert(room_id.clone(), RoomState::default());
3931        core.dispatch(MessengerCommand::LoadOlder { room_id: room_id.clone() }, 0).expect("dispatch succeeds");
3932        flush_and_ack(&mut core);
3933        let requests = core.releasable_requests(0);
3934        let load_older = requests.iter().find(|r| r.kind == OutgoingRequestKind::RoomMessages).expect("a RoomMessages request was enqueued");
3935        assert!(load_older.query.contains(&("from".to_string(), "s42_7".to_string())), "pages back from the sync token: {:?}", load_older.query);
3936    }
3937
3938    /// A `/sync` body: `!r:example.org` where bob is seen joining, with an
3939    /// `m.room.encryption` state event only if `encrypted`.
3940    fn bob_joins_room_body(encrypted: bool) -> serde_json::Value {
3941        let mut state = Vec::new();
3942        if encrypted {
3943            state.push(serde_json::json!({
3944                "event_id": "$enc:example.org", "type": "m.room.encryption", "sender": "@alice:example.org",
3945                "origin_server_ts": 1, "state_key": "", "content": { "algorithm": "m.megolm.v1.aes-sha2" }
3946            }));
3947        }
3948        serde_json::json!({
3949            "next_batch": "s1",
3950            "rooms": { "join": { "!r:example.org": {
3951                "state": { "events": state },
3952                "timeline": { "events": [{
3953                    "event_id": "$join:example.org", "type": "m.room.member", "sender": "@bob:example.org",
3954                    "origin_server_ts": 2, "state_key": "@bob:example.org", "content": { "membership": "join" }
3955                }] }
3956            } } }
3957        })
3958    }
3959
3960    #[test]
3961    fn a_member_joining_an_encrypted_room_is_tracked_and_queried_right_away() {
3962        let mut core = open_fresh_core();
3963        let bob = UserId::parse("@bob:example.org").expect("valid user id");
3964        deliver_sync(&mut core, bob_joins_room_body(true));
3965        let tracked = core.store.tracked_users().expect("no error");
3966        assert!(tracked.iter().any(|(user_id, outdated)| *user_id == bob && *outdated), "bob is tracked with an outdated device list: {tracked:?}");
3967
3968        flush_and_ack(&mut core);
3969        let requests = core.releasable_requests(0);
3970        let query = requests.iter().find(|r| r.kind == OutgoingRequestKind::KeysQuery).expect("a /keys/query was enqueued for the new member");
3971        assert!(query.body.as_ref().is_some_and(|body| body["device_keys"].get(bob.as_str()).is_some()), "the query names bob: {:?}", query.body);
3972    }
3973
3974    #[test]
3975    fn a_member_joining_a_plaintext_room_is_not_tracked() {
3976        let mut core = open_fresh_core();
3977        deliver_sync(&mut core, bob_joins_room_body(false));
3978        assert!(core.store.tracked_users().expect("no error").is_empty());
3979        flush_and_ack(&mut core);
3980        assert!(core.releasable_requests(0).iter().all(|r| r.kind != OutgoingRequestKind::KeysQuery));
3981    }
3982
3983    #[test]
3984    fn dispatch_mark_read_and_set_typing_produce_outgoing_requests() {
3985        let mut core = open_fresh_core();
3986        flush_and_ack(&mut core);
3987        let room_id = RoomId::parse("!r:example.org").expect("valid room id");
3988        let event_id = EventId::parse("$1:example.org").expect("valid event id");
3989
3990        core.dispatch(MessengerCommand::MarkRead { room_id: room_id.clone(), event_id }, 0).expect("dispatch succeeds");
3991        core.dispatch(MessengerCommand::SetTyping { room_id: room_id.clone(), typing: true }, 0).expect("dispatch succeeds");
3992        flush_and_ack(&mut core);
3993
3994        let requests = core.releasable_requests(0);
3995        assert!(requests.iter().any(|r| r.kind == OutgoingRequestKind::ReadMarkers));
3996        assert!(requests.iter().any(|r| r.kind == OutgoingRequestKind::Typing));
3997    }
3998
3999    #[test]
4000    fn snapshot_reflects_the_latest_applied_sync() {
4001        let mut core = open_fresh_core();
4002        flush_and_ack(&mut core);
4003        core.releasable_requests(0);
4004        flush_and_ack(&mut core);
4005        let requests = core.releasable_requests(0);
4006        let sync_request = requests.iter().find(|r| r.kind == OutgoingRequestKind::Sync).expect("sync enqueued");
4007
4008        let body = serde_json::json!({
4009            "next_batch": "s1",
4010            "rooms": {
4011                "join": {
4012                    "!r:example.org": {
4013                        "state": { "events": [] },
4014                        "timeline": {
4015                            "events": [
4016                                {
4017                                    "event_id": "$1:example.org",
4018                                    "type": "m.room.message",
4019                                    "sender": "@alice:example.org",
4020                                    "origin_server_ts": 1,
4021                                    "content": { "msgtype": "m.text", "body": "hi" }
4022                                }
4023                            ]
4024                        },
4025                        "unread_notifications": { "notification_count": 1, "highlight_count": 0 }
4026                    }
4027                }
4028            }
4029        })
4030        .to_string();
4031        let events = core.on_response(sync_request.id.clone(), HttpResponseDescriptor { status: 200, body: body.into_bytes() }, 0);
4032
4033        assert!(events.contains(&MessengerEvent::RoomsChanged));
4034        let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4035        assert!(events.contains(&MessengerEvent::TimelineChanged { room_id: room_id.clone() }));
4036        assert!(events.contains(&MessengerEvent::UnreadChanged { room_id: room_id.clone() }));
4037
4038        let timeline = core.timeline(&room_id).expect("timeline present");
4039        assert_eq!(timeline.items().len(), 1);
4040        assert_eq!(core.room_state(&room_id).expect("room present").unread_count(), 1);
4041        assert_eq!(core.change_counter(), events.len() as u64);
4042        // `events()`/`on_response`'s own inline return share one queue --
4043        // nothing left to drain right after.
4044        assert!(core.events().is_empty());
4045    }
4046
4047    #[test]
4048    fn dispatch_set_account_data_updates_optimistically_and_sends_a_put() {
4049        let mut core = open_fresh_core();
4050        flush_and_ack(&mut core);
4051        let content = serde_json::json!({ "note": "keep" });
4052        core.dispatch(
4053            MessengerCommand::SetAccountData { event_type: "com.example.prefs".to_string(), content: content.clone() },
4054            0,
4055        )
4056        .expect("dispatch succeeds");
4057
4058        assert_eq!(
4059            core.global_account_data("com.example.prefs"),
4060            Some(&content),
4061            "SetAccountData updates the core's own cache before any round trip"
4062        );
4063
4064        flush_and_ack(&mut core);
4065        let requests = core.releasable_requests(0);
4066        let request = requests.iter().find(|r| r.kind == OutgoingRequestKind::AccountData).expect("an AccountData request was enqueued");
4067        assert_eq!(request.method, HttpMethod::Put);
4068        assert_eq!(request.path, "/_matrix/client/v3/user/%40alice%3Aexample.org/account_data/com.example.prefs");
4069        assert_eq!(request.body.as_ref(), Some(&content));
4070    }
4071
4072    #[test]
4073    fn dispatch_set_room_account_data_updates_optimistically_and_sends_a_put() {
4074        let mut core = open_fresh_core();
4075        flush_and_ack(&mut core);
4076        let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4077        let content = serde_json::json!({ "flag": true });
4078        core.dispatch(
4079            MessengerCommand::SetRoomAccountData {
4080                room_id: room_id.clone(),
4081                event_type: "com.example.flag".to_string(),
4082                content: content.clone(),
4083            },
4084            0,
4085        )
4086        .expect("dispatch succeeds");
4087
4088        assert_eq!(
4089            core.room_account_data(&room_id, "com.example.flag"),
4090            Some(&content),
4091            "SetRoomAccountData updates the core's own cache before any round trip"
4092        );
4093
4094        flush_and_ack(&mut core);
4095        let requests = core.releasable_requests(0);
4096        let request =
4097            requests.iter().find(|r| r.kind == OutgoingRequestKind::RoomAccountData).expect("a RoomAccountData request was enqueued");
4098        assert_eq!(request.method, HttpMethod::Put);
4099        assert_eq!(
4100            request.path,
4101            "/_matrix/client/v3/user/%40alice%3Aexample.org/rooms/%21r%3Aexample.org/account_data/com.example.flag"
4102        );
4103        assert_eq!(request.body.as_ref(), Some(&content));
4104    }
4105
4106    #[test]
4107    fn dispatch_search_public_rooms_sends_the_expected_request() {
4108        let mut core = open_fresh_core();
4109        flush_and_ack(&mut core);
4110        core.dispatch(MessengerCommand::SearchPublicRooms { term: "trading".to_string() }, 0).expect("dispatch succeeds");
4111        flush_and_ack(&mut core);
4112        let requests = core.releasable_requests(0);
4113        let request = requests.iter().find(|r| r.kind == OutgoingRequestKind::PublicRooms).expect("a PublicRooms request was enqueued");
4114        assert_eq!(request.method, HttpMethod::Post);
4115        assert_eq!(request.path, "/_matrix/client/v3/publicRooms");
4116        assert_eq!(request.body, Some(serde_json::json!({ "filter": { "generic_search_term": "trading" }, "limit": 50 })));
4117    }
4118
4119    #[test]
4120    fn dispatch_search_users_sends_the_expected_request() {
4121        let mut core = open_fresh_core();
4122        flush_and_ack(&mut core);
4123        core.dispatch(MessengerCommand::SearchUsers { term: "bob".to_string() }, 0).expect("dispatch succeeds");
4124        flush_and_ack(&mut core);
4125        let requests = core.releasable_requests(0);
4126        let request =
4127            requests.iter().find(|r| r.kind == OutgoingRequestKind::UserDirectorySearch).expect("a UserDirectorySearch request was enqueued");
4128        assert_eq!(request.method, HttpMethod::Post);
4129        assert_eq!(request.path, "/_matrix/client/v3/user_directory/search");
4130        assert_eq!(request.body, Some(serde_json::json!({ "search_term": "bob", "limit": 20 })));
4131    }
4132
4133    #[test]
4134    fn public_rooms_response_superseded_by_a_later_search_is_dropped() {
4135        let mut core = open_fresh_core();
4136        flush_and_ack(&mut core);
4137        core.dispatch(MessengerCommand::SearchPublicRooms { term: "first".to_string() }, 0).expect("dispatch succeeds");
4138        flush_and_ack(&mut core);
4139        let first_request = core
4140            .releasable_requests(0)
4141            .into_iter()
4142            .find(|r| r.kind == OutgoingRequestKind::PublicRooms)
4143            .expect("the first search's own request");
4144
4145        // A second search supersedes the first before the first's response
4146        // ever lands.
4147        core.dispatch(MessengerCommand::SearchPublicRooms { term: "second".to_string() }, 0).expect("dispatch succeeds");
4148        flush_and_ack(&mut core);
4149        let second_request = core
4150            .releasable_requests(0)
4151            .into_iter()
4152            .find(|r| r.kind == OutgoingRequestKind::PublicRooms)
4153            .expect("the second search's own request");
4154
4155        // The stale response (naming the FIRST request id) is dropped.
4156        let stale_body = serde_json::json!({ "chunk": [{ "room_id": "!stale:example.org", "num_joined_members": 1 }] });
4157        core.on_response(first_request.id, HttpResponseDescriptor { status: 200, body: serde_json::to_vec(&stale_body).expect("json") }, 0);
4158        assert!(core.public_rooms_result().is_empty(), "a response to a superseded search is dropped");
4159
4160        // The current response is applied.
4161        let fresh_body = serde_json::json!({ "chunk": [{ "room_id": "!fresh:example.org", "num_joined_members": 2 }] });
4162        core.on_response(second_request.id, HttpResponseDescriptor { status: 200, body: serde_json::to_vec(&fresh_body).expect("json") }, 0);
4163        assert_eq!(core.public_rooms_result().len(), 1);
4164        assert_eq!(core.public_rooms_result()[0].room_id, RoomId::parse("!fresh:example.org").expect("valid room id"));
4165    }
4166
4167    fn is_account_data_write(request: &OutgoingRequest) -> bool {
4168        matches!(request.kind, OutgoingRequestKind::AccountData | OutgoingRequestKind::RoomAccountData)
4169    }
4170
4171    #[test]
4172    fn pin_then_archive_back_to_back_keeps_both_tags() {
4173        let mut core = open_fresh_core();
4174        flush_and_ack(&mut core);
4175        let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4176        for tag in ["m.favourite", "u.example.archived"] {
4177            core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: tag.to_string(), order: None }, 0)
4178                .expect("dispatch succeeds");
4179        }
4180
4181        // Optimistic: the cache already holds both tags, before any round trip.
4182        let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4183        assert!(cached["tags"].get("m.favourite").is_some());
4184        assert!(cached["tags"].get("u.example.archived").is_some());
4185
4186        flush_and_ack(&mut core);
4187        let first_release = core.releasable_requests(0);
4188        let first_writes: Vec<&OutgoingRequest> = first_release.iter().filter(|r| is_account_data_write(r)).collect();
4189        assert_eq!(first_writes.len(), 1, "the second PUT is not released while the first is in flight");
4190        let first_body = first_writes[0].body.clone().expect("a body");
4191        assert!(first_body["tags"].get("m.favourite").is_some());
4192        assert!(first_body["tags"].get("u.example.archived").is_none(), "the first PUT carries only what existed when it was built");
4193
4194        core.on_response(first_writes[0].id.clone(), HttpResponseDescriptor { status: 200, body: b"{}".to_vec() }, 0);
4195        flush_and_ack(&mut core);
4196        let second_release = core.releasable_requests(0);
4197        let second = second_release.iter().find(|r| is_account_data_write(r)).expect("the second PUT is released after the first completed");
4198        let second_body = second.body.clone().expect("a body");
4199        assert!(second_body["tags"].get("m.favourite").is_some(), "the later PUT still carries the earlier tag");
4200        assert!(second_body["tags"].get("u.example.archived").is_some());
4201    }
4202
4203    #[test]
4204    fn account_data_lane_is_fifo_single_flight() {
4205        let mut core = open_fresh_core();
4206        flush_and_ack(&mut core);
4207        let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4208        core.dispatch(MessengerCommand::SetAccountData { event_type: "org.t.one".to_string(), content: serde_json::json!({}) }, 0)
4209            .expect("dispatch succeeds");
4210        core.dispatch(
4211            MessengerCommand::SetRoomAccountData {
4212                room_id: room_id.clone(),
4213                event_type: "org.t.two".to_string(),
4214                content: serde_json::json!({}),
4215            },
4216            0,
4217        )
4218        .expect("dispatch succeeds");
4219        core.dispatch(MessengerCommand::SetAccountData { event_type: "org.t.three".to_string(), content: serde_json::json!({}) }, 0)
4220            .expect("dispatch succeeds");
4221        core.dispatch(MessengerCommand::RemoveTag { room_id: room_id.clone(), tag: "m.favourite".to_string() }, 0)
4222            .expect("dispatch succeeds");
4223        flush_and_ack(&mut core);
4224
4225        let mut released_paths = Vec::new();
4226        for _ in 0..4 {
4227            let release = core.releasable_requests(0);
4228            let writes: Vec<&OutgoingRequest> = release.iter().filter(|r| is_account_data_write(r)).collect();
4229            assert_eq!(writes.len(), 1, "exactly one account-data write in flight at a time");
4230            released_paths.push(writes[0].path.clone());
4231            core.on_response(writes[0].id.clone(), HttpResponseDescriptor { status: 200, body: b"{}".to_vec() }, 0);
4232            flush_and_ack(&mut core);
4233        }
4234        assert!(released_paths[0].ends_with("/account_data/org.t.one"));
4235        assert!(released_paths[1].ends_with("/account_data/org.t.two"));
4236        assert!(released_paths[2].ends_with("/account_data/org.t.three"));
4237        assert!(released_paths[3].ends_with("/account_data/m.tag"));
4238        assert!(core.releasable_requests(0).iter().all(|r| !is_account_data_write(r)), "the lane is drained");
4239    }
4240
4241    /// Answers the outstanding `/sync` request with `body` (minting and
4242    /// flushing it first if needed) and returns every OTHER request the
4243    /// rounds released, unanswered -- the tests below keep account-data
4244    /// writes pending on purpose.
4245    fn deliver_sync(core: &mut MessengerCore<InsecurePlainCodecForTests>, body: serde_json::Value) -> Vec<OutgoingRequest> {
4246        let mut others = Vec::new();
4247        for _ in 0..3 {
4248            flush_and_ack(core);
4249            let mut sync_request = None;
4250            for request in core.releasable_requests(0) {
4251                if request.kind == OutgoingRequestKind::Sync && sync_request.is_none() {
4252                    sync_request = Some(request);
4253                } else {
4254                    others.push(request);
4255                }
4256            }
4257            if let Some(sync_request) = sync_request {
4258                let body = serde_json::to_vec(&body).expect("valid JSON");
4259                core.on_response(sync_request.id, HttpResponseDescriptor { status: 200, body }, 0);
4260                return others;
4261            }
4262        }
4263        panic!("no sync request became releasable");
4264    }
4265
4266    fn tag_sync_body(room_id: &str, tags: serde_json::Value, next_batch: &str) -> serde_json::Value {
4267        let mut join = serde_json::Map::new();
4268        join.insert(
4269            room_id.to_string(),
4270            serde_json::json!({ "account_data": { "events": [{ "type": "m.tag", "content": { "tags": tags } }] } }),
4271        );
4272        serde_json::json!({ "next_batch": next_batch, "rooms": { "join": serde_json::Value::Object(join) } })
4273    }
4274
4275    fn ok_response() -> HttpResponseDescriptor {
4276        HttpResponseDescriptor { status: 200, body: b"{}".to_vec() }
4277    }
4278
4279    #[test]
4280    fn sync_echo_of_an_older_tag_write_does_not_clobber_a_newer_optimistic_value() {
4281        let mut core = open_fresh_core();
4282        let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4283        for tag in ["m.favourite", "u.example.archived"] {
4284            core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: tag.to_string(), order: None }, 0)
4285                .expect("dispatch succeeds");
4286        }
4287
4288        // The server echoes the FIRST write only, while both are outstanding.
4289        let released = deliver_sync(&mut core, tag_sync_body("!r:example.org", serde_json::json!({ "m.favourite": {} }), "s1"));
4290        let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4291        assert!(cached["tags"].get("u.example.archived").is_some(), "the older echo must not clobber the newer optimistic value");
4292        assert!(cached["tags"].get("m.favourite").is_some());
4293        let key = (Some(room_id.clone()), "m.tag".to_string());
4294        assert_eq!(
4295            core.account_data_guard.get(&key).expect("writes are outstanding").server_value,
4296            Some(serde_json::json!({ "tags": { "m.favourite": {} } })),
4297            "the echo is remembered as the server's value"
4298        );
4299
4300        // Both writes settle; the optimistic value survives (it is what the
4301        // server holds now) and the guard is gone.
4302        let first = released.iter().find(|r| r.kind == OutgoingRequestKind::RoomAccountData).expect("first write released");
4303        core.on_response(first.id.clone(), ok_response(), 0);
4304        flush_and_ack(&mut core);
4305        let second = core
4306            .releasable_requests(0)
4307            .into_iter()
4308            .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4309            .expect("second write released after the first");
4310        core.on_response(second.id, ok_response(), 0);
4311        assert!(core.account_data_guard.is_empty());
4312        let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4313        assert!(cached["tags"].get("m.favourite").is_some() && cached["tags"].get("u.example.archived").is_some());
4314
4315        // With nothing outstanding, sync applies normally again.
4316        deliver_sync(
4317            &mut core,
4318            tag_sync_body("!r:example.org", serde_json::json!({ "m.favourite": {}, "u.example.archived": {} }), "s2"),
4319        );
4320        let cached = core.room_account_data(&room_id, "m.tag").expect("m.tag cached");
4321        assert!(cached["tags"].get("u.example.archived").is_some());
4322    }
4323
4324    #[test]
4325    fn terminal_account_data_failure_restores_the_server_value() {
4326        let mut core = open_fresh_core();
4327        let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4328        deliver_sync(&mut core, tag_sync_body("!r:example.org", serde_json::json!({ "m.favourite": {} }), "s1"));
4329
4330        core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: "u.example.archived".to_string(), order: None }, 0)
4331            .expect("dispatch succeeds");
4332        assert!(core.room_account_data(&room_id, "m.tag").expect("cached")["tags"].get("u.example.archived").is_some());
4333        flush_and_ack(&mut core);
4334        let write = core
4335            .releasable_requests(0)
4336            .into_iter()
4337            .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4338            .expect("the write is released");
4339
4340        let counter_before = core.change_counter();
4341        let forbidden =
4342            HttpResponseDescriptor { status: 403, body: br#"{"errcode":"M_FORBIDDEN","error":"nope"}"#.to_vec() };
4343        let events = core.on_response(write.id, forbidden, 0);
4344        assert!(core.account_data_guard.is_empty());
4345        let restored = core.room_account_data(&room_id, "m.tag").expect("the server's value is back");
4346        assert!(restored["tags"].get("u.example.archived").is_none(), "the failed write's optimistic value is gone");
4347        assert!(restored["tags"].get("m.favourite").is_some());
4348        assert!(events.contains(&MessengerEvent::RoomsChanged));
4349        assert!(core.change_counter() > counter_before, "the UI is told to re-render");
4350
4351        // A failure while a later write is still outstanding leaves the cache
4352        // alone (the later write carries the full content anyway); once that
4353        // one succeeds its value stands.
4354        for tag in ["u.a", "u.b"] {
4355            core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: tag.to_string(), order: None }, 0)
4356                .expect("dispatch succeeds");
4357        }
4358        flush_and_ack(&mut core);
4359        let first = core
4360            .releasable_requests(0)
4361            .into_iter()
4362            .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4363            .expect("first write released");
4364        let forbidden =
4365            HttpResponseDescriptor { status: 403, body: br#"{"errcode":"M_FORBIDDEN","error":"nope"}"#.to_vec() };
4366        core.on_response(first.id, forbidden, 0);
4367        let cached = core.room_account_data(&room_id, "m.tag").expect("cached");
4368        assert!(cached["tags"].get("u.a").is_some() && cached["tags"].get("u.b").is_some(), "cache untouched while a write is pending");
4369        flush_and_ack(&mut core);
4370        let second = core
4371            .releasable_requests(0)
4372            .into_iter()
4373            .find(|r| r.kind == OutgoingRequestKind::RoomAccountData)
4374            .expect("second write released");
4375        core.on_response(second.id, ok_response(), 0);
4376        let cached = core.room_account_data(&room_id, "m.tag").expect("cached");
4377        assert!(cached["tags"].get("u.b").is_some(), "the last write succeeded: its optimistic value stands");
4378    }
4379
4380    #[test]
4381    fn pending_account_data_writes_rebuild_their_guard_after_a_restart() {
4382        let mut core = open_fresh_core();
4383        let room_id = RoomId::parse("!r:example.org").expect("valid room id");
4384        core.dispatch(MessengerCommand::SetTag { room_id: room_id.clone(), tag: "m.favourite".to_string(), order: None }, 0)
4385            .expect("dispatch succeeds");
4386        core.dispatch(
4387            MessengerCommand::SetAccountData { event_type: "org.t.one".to_string(), content: serde_json::json!({ "n": 1 }) },
4388            0,
4389        )
4390        .expect("dispatch succeeds");
4391
4392        // Everything dirtied so far, including both pending PUT records.
4393        let mut records = Vec::new();
4394        while let Some(batch) = core.take_flush_batch() {
4395            records.extend(batch.records.clone());
4396            core.ack_flush(batch.id);
4397        }
4398        let reopened = MessengerCore::open(
4399            records,
4400            InsecurePlainCodecForTests,
4401            test_config(),
4402            CoreSecrets::default(),
4403            0,
4404            Box::new(FixedJitter(0.0)),
4405        )
4406        .expect("reopen succeeds");
4407
4408        assert_eq!(reopened.account_data_guard.len(), 2, "one guard per key with a pending write");
4409        assert!(reopened.account_data_guard.values().all(|guard| guard.pending == 1 && guard.server_value.is_none()));
4410        assert_eq!(reopened.global_account_data("org.t.one"), Some(&serde_json::json!({ "n": 1 })));
4411        assert!(reopened.room_account_data(&room_id, "m.tag").expect("cached")["tags"].get("m.favourite").is_some());
4412    }
4413
4414    // -----------------------------------------------------------------
4415    // Stickers and forwarding
4416    // -----------------------------------------------------------------
4417
4418    type TestCore = MessengerCore<InsecurePlainCodecForTests>;
4419
4420    fn room(id: &str) -> RoomId {
4421        RoomId::parse(id).expect("valid room id")
4422    }
4423
4424    fn event(id: &str) -> EventId {
4425        EventId::parse(id).expect("valid event id")
4426    }
4427
4428    /// Registers `room_id` in `core`: Megolm-encrypted or not, optionally
4429    /// named, with an (empty) timeline.
4430    fn add_room(core: &mut TestCore, room_id: &RoomId, encrypted: bool, name: Option<&str>) {
4431        let mut state = RoomState::new();
4432        if encrypted {
4433            state.encryption = Some(crate::wire::events::RoomEncryptionContent {
4434                algorithm: "m.megolm.v1.aes-sha2".to_string(),
4435                rotation_period_ms: 604_800_000,
4436                rotation_period_msgs: 100,
4437            });
4438        }
4439        state.name = name.map(str::to_string);
4440        core.rooms.insert(room_id.clone(), state);
4441        core.timelines.entry(room_id.clone()).or_default();
4442    }
4443
4444    /// Marks `room_id` as a public channel for [`RoomState::derive_room_kind`]
4445    /// (join_rule public + raised `events_default`).
4446    #[test]
4447    fn set_events_default_opens_a_channel_for_every_member() {
4448        let mut core = open_fresh_core();
4449        let chan = room("!chan:example.org");
4450        add_room(&mut core, &chan, false, Some("Talk"));
4451        mark_as_channel(&mut core, &chan);
4452        core.dispatch(MessengerCommand::SetEventsDefault { room_id: chan.clone(), level: 0 }, 1_000).expect("dispatch");
4453        core.releasable_requests(1_000);
4454        flush_and_ack(&mut core);
4455        let sent = core.releasable_requests(1_000);
4456        let put = sent.iter().find(|r| r.kind == OutgoingRequestKind::SetState).expect("a state PUT");
4457        assert!(put.path.ends_with("/state/m.room.power_levels/"), "{}", put.path);
4458        let body = put.body.clone().expect("body");
4459        assert_eq!(body["events_default"], 0);
4460        assert_eq!(body["users_default"], 0);
4461    }
4462
4463    #[test]
4464    fn set_events_default_refuses_an_unknown_room() {
4465        let mut core = open_fresh_core();
4466        let err = core.dispatch(MessengerCommand::SetEventsDefault { room_id: room("!nope:example.org"), level: 0 }, 1_000);
4467        assert!(err.is_err());
4468    }
4469
4470    fn mark_as_channel(core: &mut TestCore, room_id: &RoomId) {
4471        let Some(state) = core.rooms.get_mut(room_id) else { return };
4472        state
4473            .apply_state_event(&state_event_raw(
4474                "m.room.join_rules",
4475                "",
4476                "@alice:example.org",
4477                serde_json::json!({ "join_rule": "public" }),
4478            ))
4479            .expect("join_rules");
4480        state
4481            .apply_state_event(&state_event_raw(
4482                "m.room.power_levels",
4483                "",
4484                "@alice:example.org",
4485                serde_json::json!({ "events_default": 50, "users_default": 0 }),
4486            ))
4487            .expect("power_levels");
4488    }
4489
4490    fn state_event_raw(event_type: &str, state_key: &str, sender: &str, content: serde_json::Value) -> RawEvent {
4491        serde_json::from_value(serde_json::json!({
4492            "event_id": format!("${event_type}:example.org"),
4493            "type": event_type,
4494            "sender": sender,
4495            "origin_server_ts": 1,
4496            "state_key": state_key,
4497            "content": content,
4498        }))
4499        .expect("valid raw event")
4500    }
4501
4502    fn raw_event(event_id: &str, event_type: &str, sender: &str, content: serde_json::Value) -> RawEvent {
4503        serde_json::from_value(serde_json::json!({
4504            "event_id": event_id,
4505            "type": event_type,
4506            "sender": sender,
4507            "origin_server_ts": 1,
4508            "content": content,
4509        }))
4510        .expect("valid raw event")
4511    }
4512
4513    /// An event that arrived in `room_id` as plaintext.
4514    fn seed_plain(core: &mut TestCore, room_id: &RoomId, event_id: &str, sender: &str, event_type: &str, content: serde_json::Value) {
4515        let raw = raw_event(event_id, event_type, sender, content);
4516        core.timelines.entry(room_id.clone()).or_default().apply_timeline_batch(&[raw], false, None);
4517    }
4518
4519    /// A Megolm envelope that arrived in `room_id` and has not been opened.
4520    fn seed_sealed(core: &mut TestCore, room_id: &RoomId, event_id: &str, sender: &str) {
4521        let content = serde_json::json!({ "algorithm": "m.megolm.v1.aes-sha2", "ciphertext": "AAAA", "session_id": "s1" });
4522        seed_plain(core, room_id, event_id, sender, "m.room.encrypted", content);
4523    }
4524
4525    /// A Megolm envelope that arrived in `room_id` and decrypted to
4526    /// `{event_type, content}`.
4527    fn seed_decrypted(
4528        core: &mut TestCore,
4529        room_id: &RoomId,
4530        event_id_str: &str,
4531        sender: &str,
4532        event_type: &str,
4533        content: serde_json::Value,
4534    ) {
4535        seed_sealed(core, room_id, event_id_str, sender);
4536        if let Some(timeline) = core.timelines.get_mut(room_id) {
4537            timeline.set_decrypted_event(&event(event_id_str), event_type, &content);
4538        }
4539    }
4540
4541    /// Flushes, releases, and returns the `(path, body)` of the one
4542    /// `RoomSend` the core just queued -- after answering it with a fresh
4543    /// event id, so the room's next send is free to start.
4544    fn sent_room_event(core: &mut TestCore) -> (String, serde_json::Value) {
4545        flush_and_ack(core);
4546        let request = core
4547            .releasable_requests(0)
4548            .into_iter()
4549            .find(|request| request.kind == OutgoingRequestKind::RoomSend)
4550            .expect("a room send is releasable");
4551        let confirmed = format!("$sent{}:example.org", core.counters.next_request_id);
4552        let body = serde_json::json!({ "event_id": confirmed }).to_string().into_bytes();
4553        core.on_response(request.id.clone(), HttpResponseDescriptor { status: 200, body }, 0);
4554        (request.path, request.body.expect("a room send has a body"))
4555    }
4556
4557    fn forward(core: &mut TestCore, from: &RoomId, event_id: &str, to: &RoomId) -> Result<(), MessengerError> {
4558        core.dispatch(
4559            MessengerCommand::Forward { from_room: from.clone(), event_id: event(event_id), to_room: to.clone(), txn_id: None },
4560            0,
4561        )
4562    }
4563
4564    fn last_item(core: &TestCore, room_id: &RoomId) -> crate::room::timeline::TimelineItem {
4565        core.timeline(room_id).and_then(|timeline| timeline.items().last()).expect("the room has an item").clone()
4566    }
4567
4568    #[test]
4569    fn forward_from_encrypted_room_carries_only_the_forwarded_flag() {
4570        let mut core = open_fresh_core();
4571        let dm = room("!dm:example.org");
4572        let target = room("!target:example.org");
4573        add_room(&mut core, &dm, true, Some("Secret DM name"));
4574        add_room(&mut core, &target, false, None);
4575        seed_decrypted(
4576            &mut core,
4577            &dm,
4578            "$src:example.org",
4579            "@bob:example.org",
4580            "m.room.message",
4581            serde_json::json!({
4582                "msgtype": "m.text",
4583                "body": "quarterly numbers",
4584                "m.relates_to": { "m.in_reply_to": { "event_id": "$parent:example.org" } },
4585                "m.mentions": { "user_ids": ["@carol:example.org"] },
4586            }),
4587        );
4588
4589        forward(&mut core, &dm, "$src:example.org", &target).expect("a decrypted message is forwardable");
4590
4591        let (path, body) = sent_room_event(&mut core);
4592        assert!(path.contains("/send/m.room.message/"), "the event type is kept: {path}");
4593        assert_eq!(
4594            body,
4595            serde_json::json!({ "msgtype": "m.text", "body": "quarterly numbers", "forwarded": true }),
4596            "the text plus the bare flag; the reply pointer and the mentions are stripped"
4597        );
4598        let wire = body.to_string();
4599        for leaked in ["bob", "carol", "!dm", "Secret DM name", "$parent"] {
4600            assert!(!wire.contains(leaked), "{leaked:?} must not travel with a forward out of an encrypted room: {wire}");
4601        }
4602        let echo = last_item(&core, &target);
4603        assert_eq!(echo.forwarded, Some(Forwarded::Hidden), "the forwarder's own echo shows the same marker");
4604    }
4605
4606    #[test]
4607    fn forward_from_public_channel_carries_room_id_and_name_no_sender() {
4608        let mut core = open_fresh_core();
4609        let channel = room("!chan:example.org");
4610        let target = room("!target:example.org");
4611        add_room(&mut core, &channel, true, Some("Announcements"));
4612        mark_as_channel(&mut core, &channel);
4613        add_room(&mut core, &target, false, None);
4614        seed_plain(
4615            &mut core,
4616            &channel,
4617            "$post:example.org",
4618            "@dave:example.org",
4619            "m.room.message",
4620            serde_json::json!({
4621                "msgtype": "m.text",
4622                "body": "BTC breaks out",
4623                "m.mentions": { "room": true },
4624                "m.relates_to": { "m.in_reply_to": { "event_id": "$older:example.org" } },
4625            }),
4626        );
4627
4628        forward(&mut core, &channel, "$post:example.org", &target).expect("a channel post is forwardable");
4629
4630        let (_, body) = sent_room_event(&mut core);
4631        assert_eq!(
4632            body,
4633            serde_json::json!({
4634                "msgtype": "m.text",
4635                "body": "BTC breaks out",
4636                "forwarded_from": { "room_id": "!chan:example.org", "room_name": "Announcements" },
4637            })
4638        );
4639        assert!(!body.to_string().contains("dave"), "the original sender never travels");
4640        assert_eq!(
4641            last_item(&core, &target).forwarded,
4642            Some(Forwarded::Channel { room_id: channel.clone(), room_name: "Announcements".to_string() })
4643        );
4644    }
4645
4646    #[test]
4647    fn forward_from_an_unnamed_room_never_lists_member_names() {
4648        let mut core = open_fresh_core();
4649        let channel = room("!chan:example.org");
4650        let target = room("!target:example.org");
4651        add_room(&mut core, &channel, true, None);
4652        mark_as_channel(&mut core, &channel);
4653        if let Some(state) = core.rooms.get_mut(&channel) {
4654            state.members.insert(
4655                UserId::parse("@erin:example.org").expect("valid user id"),
4656                crate::room::state::MemberState {
4657                    membership: Membership::Join,
4658                    displayname: Some("Erin Private".to_string()),
4659                    is_direct: false,
4660                    invited_by: None,
4661                },
4662            );
4663        }
4664        add_room(&mut core, &target, false, None);
4665        seed_plain(&mut core, &channel, "$post:example.org", "@erin:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "hi" }));
4666
4667        forward(&mut core, &channel, "$post:example.org", &target).expect("forwardable");
4668
4669        let (_, body) = sent_room_event(&mut core);
4670        let name = body["forwarded_from"]["room_name"].as_str().expect("a name is sent");
4671        assert!(!name.contains("Erin"), "an unnamed room is attributed by a generic label, not by its members: {name}");
4672    }
4673
4674    #[test]
4675    fn forward_replaces_the_sources_own_forward_marker() {
4676        let mut core = open_fresh_core();
4677        let dm = room("!dm:example.org");
4678        let target = room("!target:example.org");
4679        add_room(&mut core, &dm, true, None);
4680        add_room(&mut core, &target, false, None);
4681        seed_decrypted(
4682            &mut core,
4683            &dm,
4684            "$src:example.org",
4685            "@bob:example.org",
4686            "m.room.message",
4687            serde_json::json!({
4688                "msgtype": "m.text",
4689                "body": "second hop",
4690                "forwarded_from": { "room_id": "!older:example.org", "room_name": "Older channel" },
4691            }),
4692        );
4693
4694        forward(&mut core, &dm, "$src:example.org", &target).expect("forwardable");
4695
4696        let (_, body) = sent_room_event(&mut core);
4697        assert_eq!(body, serde_json::json!({ "msgtype": "m.text", "body": "second hop", "forwarded": true }));
4698    }
4699
4700    #[test]
4701    fn forward_of_edited_item_sends_the_latest_text() {
4702        let mut core = open_fresh_core();
4703        let channel = room("!chan:example.org");
4704        let dm = room("!dm:example.org");
4705        let target = room("!target:example.org");
4706        add_room(&mut core, &channel, false, Some("Announcements"));
4707        add_room(&mut core, &dm, true, None);
4708        add_room(&mut core, &target, false, None);
4709
4710        // Plaintext room: the edit is an ordinary event folded onto its target.
4711        seed_plain(&mut core, &channel, "$orig:example.org", "@dave:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "typo text" }));
4712        seed_plain(
4713            &mut core,
4714            &channel,
4715            "$edit:example.org",
4716            "@dave:example.org",
4717            "m.room.message",
4718            serde_json::json!({
4719                "msgtype": "m.text",
4720                "body": "* fixed text",
4721                "m.new_content": { "msgtype": "m.text", "body": "fixed text" },
4722                "m.relates_to": { "rel_type": "m.replace", "event_id": "$orig:example.org" },
4723            }),
4724        );
4725        forward(&mut core, &channel, "$orig:example.org", &target).expect("forwardable");
4726        let (_, body) = sent_room_event(&mut core);
4727        assert_eq!(body["body"], "fixed text", "the latest edit's text, not the original and not the `* ` fallback");
4728        assert!(body.get("m.new_content").is_none() && body.get("m.relates_to").is_none());
4729
4730        // Encrypted room: the edit is only recognised once decrypted.
4731        seed_decrypted(&mut core, &dm, "$secret:example.org", "@bob:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "typo secret" }));
4732        seed_sealed(&mut core, &dm, "$secret-edit:example.org", "@bob:example.org");
4733        let applied = core.timelines.get_mut(&dm).expect("timeline").apply_decrypted_relation(
4734            &event("$secret-edit:example.org"),
4735            UserId::parse("@bob:example.org").expect("valid user id"),
4736            2,
4737            RelatesTo::Replace { event_id: event("$secret:example.org") },
4738            Some(serde_json::json!({ "msgtype": "m.text", "body": "fixed secret" })),
4739        );
4740        assert!(applied, "the decrypted edit folds onto its target");
4741        forward(&mut core, &dm, "$secret:example.org", &target).expect("forwardable");
4742        let (_, body) = sent_room_event(&mut core);
4743        assert_eq!(body, serde_json::json!({ "msgtype": "m.text", "body": "fixed secret", "forwarded": true }));
4744    }
4745
4746    #[test]
4747    fn forward_of_unknown_msgtype_keeps_its_fields() {
4748        let mut core = open_fresh_core();
4749        let dm = room("!dm:example.org");
4750        let target = room("!target:example.org");
4751        add_room(&mut core, &dm, true, None);
4752        add_room(&mut core, &target, false, None);
4753        let card = serde_json::json!({
4754            "msgtype": "com.example.card",
4755            "body": "opaque",
4756            "note": "kept raw",
4757        });
4758        seed_decrypted(&mut core, &dm, "$card:example.org", "@bob:example.org", "m.room.message", card.clone());
4759
4760        forward(&mut core, &dm, "$card:example.org", &target).expect("an unrecognized msgtype is still an m.room.message");
4761
4762        let (path, body) = sent_room_event(&mut core);
4763        assert!(path.contains("/send/m.room.message/"));
4764        let mut expected = card;
4765        expected["forwarded"] = serde_json::Value::Bool(true);
4766        assert_eq!(body, expected, "unrecognized msgtype fields pass through plus the marker");
4767        assert!(matches!(last_item(&core, &target).content, ItemContent::Unknown), "the echo does not invent a card type");
4768    }
4769
4770    #[test]
4771    fn forward_refuses_redacted_sealed_unknown_and_missing() {
4772        let mut core = open_fresh_core();
4773        let dm = room("!dm:example.org");
4774        let target = room("!target:example.org");
4775        add_room(&mut core, &dm, true, None);
4776        add_room(&mut core, &target, false, None);
4777
4778        seed_decrypted(&mut core, &dm, "$redacted:example.org", "@bob:example.org", "m.room.message", serde_json::json!({ "msgtype": "m.text", "body": "gone" }));
4779        core.timelines.get_mut(&dm).expect("timeline").apply_redaction(&event("$redacted:example.org"));
4780        seed_sealed(&mut core, &dm, "$sealed:example.org", "@bob:example.org");
4781        seed_sealed(&mut core, &dm, "$utd:example.org", "@bob:example.org");
4782        core.timelines
4783            .get_mut(&dm)
4784            .expect("timeline")
4785            .set_decrypted(&event("$utd:example.org"), ItemContent::Undecryptable { reason: "MissingSession".to_string() });
4786        seed_plain(&mut core, &dm, "$call:example.org", "@bob:example.org", "m.call.invite", serde_json::json!({ "call_id": "c1" }));
4787
4788        for id in ["$redacted:example.org", "$sealed:example.org", "$utd:example.org", "$call:example.org", "$missing:example.org"] {
4789            match forward(&mut core, &dm, id, &target) {
4790                Err(MessengerError::IntentRefused(reason)) => {
4791                    assert!(reason.contains(id), "the refusal names the event {id}: {reason}");
4792                }
4793                other => panic!("forwarding {id} must be refused, got {other:?}"),
4794            }
4795        }
4796        match forward(&mut core, &room("!nowhere:example.org"), "$redacted:example.org", &target) {
4797            Err(MessengerError::IntentRefused(reason)) => assert!(reason.contains("$redacted:example.org"), "{reason}"),
4798            other => panic!("an unknown source room means a missing event, got {other:?}"),
4799        }
4800        match forward(&mut core, &dm, "$sealed:example.org", &room("!nowhere:example.org")) {
4801            Err(MessengerError::IntentRefused(reason)) => assert!(reason.contains("unknown"), "{reason}"),
4802            other => panic!("an unknown target room is refused, got {other:?}"),
4803        }
4804
4805        assert!(core.send_state.is_empty(), "a refused forward queues nothing");
4806        assert!(core.timeline(&target).is_some_and(|timeline| timeline.items().is_empty()), "and leaves no echo");
4807    }
4808}