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