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