atagen

macltok

public
Star0
Watch0

Commit 31ce2453c82f

tarball

atagen <boss@atagen.co>2026-09-24 23:43 UTC

32 files changed+5086-6553
engine: answer every command directly and keep the event stream for state

Correlating CommandIds against a shared stream made every client rebuild request and response, and a web backend could not serve media bytes per request at all. Each Command now carries a Responder, and dropping one answers Interrupted, so panics, closed rooms and shutdown need no bookkeeping. Media, profiles and space hierarchies became handle queries, and RoomSummary grew the fields a room list needs, so Malice needs no direct matrix-sdk. One cloneable Error replaces the four error types.
Modified file: Cargo.toml+1 additions−1 deletions
@@ -11,7 +11,7 @@ sqlite = ["matrix-sdk/sqlite", "serde", "dep:url"]
1111 bundled-sqlite = ["sqlite", "matrix-sdk/bundled-sqlite"]
1212
1313 [dependencies]
14-futures-util = { version = "0.3.31", default-features = false }
14+futures-util = { version = "0.3.31", default-features = false, features = ["std"] }
1515 http = "1"
1616 matrix-sdk = { version = "0.18.0", default-features = false, features = ["e2e-encryption"] }
1717 matrix-sdk-crypto = { version = "0.18.0", default-features = false }
Modified file: README.md+10 additions−10 deletions
@@ -21,16 +21,16 @@ already have remote event IDs.
2121
2222 ## Consuming the engine
2323
24-Run `Engine::run` and drain its single `EventStream` independently of command
25-submission. `ClientHandle::send` returns an enqueue ID; match that ID against
26-`CommandSucceeded`, `CommandSucceededWithRoom`, or `CommandFailed` before
27-reporting completion. A successful message command only enters the SDK send
28-queue; the timeline item's send state reports subsequent network delivery.
29-
30-Start a timeline from `TimelineReset`, then apply `TimelineChanged` diffs with
31-`TimelineDiff::try_apply`. If a diff does not match the current snapshot, stop
32-applying diffs for that timeline and send `TimelineCommand::Resync`. Resume only
33-after its fresh `TimelineReset`; the reset precedes the command completion.
24+spawn `Engine::run`, drain the `EventStream` from its own task, and talk to the
25+engine through `ClientHandle`. every command answers once, awaited
26+(`handle.send_message(..).await?`) or as a `Pending` you can await later or
27+drop. a sent message answers when the sdk send queue has it, delivery shows up
28+as the timeline item's send state. reads like `rooms`, `members` and `media`
29+go straight to the sdk. the stream only carries state
30+
31+start a timeline from `TimelineReset`, then apply `TimelineChanged` diffs with
32+`TimelineDiff::try_apply`. when one doesn't fit, stop applying, call `resync`,
33+and pick up from the next `TimelineReset`
3434
3535 With `sqlite`, persist the initial `Session::record()` separately from its
3636 encrypted SQLite store. On `SessionTokensChanged`, obtain a new record from
Modified file: docs/design-decisions.md+137 additions−170 deletions
@@ -1,172 +1,139 @@
11 # macltok design decisions
22
3-This document describes the contract implemented in the working tree.
4-
5-## Engine execution
6-
7-The engine keeps a contextual task registry. Every spawned task is registered
8-with either its command id and kind, a watcher name, or a room id and
9-generation. The run loop continuously joins completed tasks and removes their
10-context. A command-task panic produces its correlated `CommandFailed` event.
11-A watcher or current room-actor panic is terminal to the engine. Shutdown aborts
12-and joins the remaining registered tasks.
13-
14-The engine emits `Authenticated`, then `ConnectionChanged(Connecting)`, before
15-starting the sync service. `ConnectionChanged` also represents the later SDK
16-states and the final `Stopped` transition. `Engine::run` returns an
17-`EngineError` for terminal failure and emits `Failed` before `Stopped` when the
18-event stream remains available.
19-
20-The driver command channel remains lossless and clients must drain
21-`EventStream` independently of command submission. `ClientHandle` also exposes
22-a `watch` receiver for the current connection state. `RoomsChanged` is an
23-invalidation event. Call `ClientHandle::rooms` for current, normalised room
24-summaries.
25-
26-`ClientHandle` sends `Command` values and receives a `CommandId`. Completion is
27-reported as `CommandSucceeded`, `CommandSucceededWithRoom`, or
28-`CommandFailed`. `CommandFailure` contains the id, outer `CommandKind`, typed
29-`CommandErrorKind`, and display message. Direct request and response methods
30-return `CommandResultError`, which is either a delivered `CommandFailure` or a
31-closed reply channel.
32-
33-For timeline sends, success means the SDK send queue accepted the event;
34-network delivery is reported later through timeline send state.
35-
36-`send` reports enqueueing, not completion. Consumers retain the returned id
37-until a terminal result with that id arrives; they match `CommandFailure::kind`
38-rather than parsing its display message. The event stream must be drained while
39-commands are submitted, even when several frontend clients share one engine.
40-
41-## Room lifetime and ordering
42-
43-An open room has an explicit generation and exactly one of three states.
44-
45-- `Opening` owns the generation and actor request sender
46-- `Open` owns the generation and actor request sender
47-- `Closing` owns the generation and a bounded FIFO of deferred open and close commands
48-
49-An `Open` command starts an actor immediately and returns its terminal result
50-only after the actor reports readiness. There is no separate readiness waiter or
51-readiness task. Repeated opens while opening are retained by the actor and each
52-gets one terminal result. A close changes the room to `Closing`, rejects work
53-that needs an open actor, and waits for that generation to exit. Opens and
54-closes during closing are queued in order; after exit, each deferred command is
55-replayed, with a fresh generation started when needed. Events from earlier
56-generations are ignored.
57-
58-Each room actor serialises its timeline work in FIFO order. Different rooms run
59-concurrently. Room mutations which do not require an open timeline use a
60-separate keyed FIFO scheduler. Jobs with the same room, target, or direct-chat
61-key run in submission order while unrelated keys run concurrently. Notification
62-commands and encryption commands each have one dedicated serial worker. The
63-encryption worker owns the verification controller and cancels it before
64-shutdown completes.
65-
66-Pagination has a per-timeline mutex gate and a session-owned completion path.
67-Pagination tasks are reaped as they finish. Closing a timeline aborts its
68-pagination work and emits one terminal result for every still-pending command.
69-There is no public pagination handle. The internal result channel is only the
70-session completion mechanism and is not an engine-wide routing design.
71-
72-`TimelineDiff::try_apply` rejects indices that do not match the consumer's
73-snapshot without modifying that snapshot. A consumer then stops applying diffs
74-for that timeline and sends `TimelineCommand::Resync`. The timeline session
75-aborts and joins its old watcher, atomically subscribes to a fresh SDK snapshot
76-and diff stream, and emits `TimelineReset` before the resync command succeeds.
77-This works for rooms and threads without closing their timelines or pagination.
78-
79-Timeline diffs and resets carry `TimelineId`, which distinguishes a room from a
80-thread by its root event. Thread actors and pagination state belong to the room
81-actor. Stateful timeline commands require the room actor. Standalone room
82-commands such as typing, receipts, send-queue control, membership actions, and
83-moderation use the SDK `Room` directly.
84-
85-## Account construction and secrets
86-
87-`Session::login_password`, `Session::begin_oauth`, and `Session::restore` use
88-one shared account client builder. It configures encrypted SQLite storage,
89-encryption settings, refresh-token handling, and enabled threading without
90-thread subscriptions. `Session::build` passes the authenticated client to the
91-engine.
92-
93-Secret-bearing input types do not derive `Clone`. The store passphrase is
94-borrowed by the shared builder, password login passes its password by reference,
95-and authentication records are cloned only where SDK ownership requires it.
96-
97-The caller owns durable authentication records. Save `Session::record()` after
98-login, then consume `SessionTokensChanged` and atomically replace that saved
99-record with `ClientHandle::session_record()`. A previously captured record may
100-hold an exhausted refresh token; failing to persist a rotation must be surfaced
101-rather than silently treating the stale record as safe to restore. Restore with
102-the same `StoreConfig` path and passphrase. logout and a rejected token both
103-stop the engine. logout and a hard rejection kill the record and the store, a
104-soft logout keeps the store for a same-device login, `Stopped` alone kills
105-nothing. Neither cookies nor application-specific secret storage belong in the
106-toolkit.
107-
108-## Notifications and room data
109-
110-The notification client is constructed lazily in an `OnceCell` and shares the
111-engine sync service. It is created only when notification resolution is first
112-requested. `PushNotification` exposes typed room and sender metadata and a
113-typed `PushNotificationEvent`. The event is `Raw<AnySyncTimelineEvent>` for a
114-timeline notification or `Raw<StrippedRoomMemberEvent>` for an invite. SDK UI
115-types do not cross the public boundary.
116-
117-Room summaries are normalised once per `rooms` query. `ClientHandle::rooms_filtered`
118-normalises its name search term once before filtering. Summary generation runs
119-display-name requests with bounded concurrency. Sorting is descending latest
120-event timestamp, then case-insensitive name, then room id as a deterministic tie
121-break.
122-
123-Global and room account data retain typed Matrix event-type parameters and raw
124-event content. The handle offers direct raw reads and engine-backed raw writes.
125-
126-## Commands and errors
127-
128-`Command` is grouped into `Account`, `Room`, `Timeline`, `Media`,
129-`Encryption`, `Notification`, and `Lifecycle` domains. `CommandKind` identifies
130-the outer domain in result events.
131-
132-`RoomCommand::InviteUser` contains only room and user ids. It deliberately does
133-not expose a reason because the SDK invite helper accepts none. Kick, ban, and
134-unban retain optional reasons where the protocol supports them.
135-
136-`TimelineCommand::Edit` accepts `MessageEditContent`. Its `Text` and `Emote`
137-variants carry `RichText`, so edited text and emotes preserve plain text,
138-optional Matrix HTML, and mentions. Empty edited bodies fail with `EmptyBody`.
139-
140-`VerificationAction` contains `Confirm`, `Reject`, and `Cancel`. Starting a
141-verification is a target-bearing encryption command rather than an action.
142-Verification progress uses `Waiting`, `Comparison`, `Confirmed`, `Done`,
143-`Cancelled`, and typed `Failed(VerificationFailure)`. Request acceptance, SAS
144-startup and acceptance failures retain their message, while stream termination
145-has dedicated variants. A cancelled verification can be started again.
146-The watcher and action paths claim terminal emission so a racing cancellation
147-cannot replace `Done` with a local `Cancelled` event.
148-
149-`CommandErrorKind` is the stable typed failure category. It includes room and
150-timeline availability, queue pressure, validation, media, verification,
151-authentication, network, server, store, timeline, encryption, and fallback
152-categories. The rendered message preserves operation context while internal
153-errors retain their source chains.
154-
155-## Power levels
156-
157-Power-level reads return the complete local `RoomPowerLevels` mapping.
158-`PowerLevelChanges` applies selected common and well-known event thresholds,
159-and `SetUserPowerLevel` changes one user mapping.
160-
161-The SDK trait that applies a power-level event is crate-private. macltok keeps
162-an atomic local mapping, applies all requested changes to it, and sends one
163-state event. It does not patch upstream and does not send separate events for
164-individual fields.
165-
166-## Public model boundaries
167-
168-The public model uses owned ruma identifiers and typed data wherever it has a
169-stable representation. Raw Matrix event payloads remain `Raw` where arbitrary
170-event content must be preserved. Public output models are non-exhaustive where
171-they are expected to grow. SDK clients remain available through the handle for
172-direct state queries that do not need engine coordination.
3+the contract the working tree actually implements. if the code disagrees with
4+this, one of them is lying and it should be fixed
5+
6+## two ways in, one way out
7+
8+state changes are commands. `ClientHandle::send` queues one and hands back a
9+`Pending`, `execute` queues and waits, and there's an awaited convenience method
10+for every command. each command answers exactly once with
11+`Result<CommandOutput, Error>`. drop the `Pending` if you don't care, the
12+command still runs
13+
14+pure reads skip the engine and ask the sdk directly through the handle. rooms,
15+members, power levels, account data, devices, push notification resolution,
16+and media bytes all live here
17+
18+the event stream carries state and only state. timelines, history, typing,
19+rooms, connection, encryption, verification, upload progress, session. no
20+command results ride on it. drain it from its own task, it's bounded and the
21+engine waits for you
22+
23+one error type for everything, `Error { kind, message, challenge }`. match on
24+`kind`. `Interrupted` means the engine stopped, the room closed, or the task
25+doing the work died before it could answer. `challenge` turns up with
26+`InteractiveAuthRequired`
27+
28+## engine
29+
30+the run loop routes and does nothing slow. every task it spawns is registered
31+with a context. a command task that panics costs its caller an `Interrupted`
32+and gets logged. a watcher or a room actor that panics takes the engine down,
33+emitting `Failed` then `Stopped`
34+
35+sync restarts itself with backoff after an error or an unexpected stop, and
36+`ConnectionChanged` shows `Offline` meanwhile. every return to `Online` re-arms
37+the send queues, since the sdk parks a room's queue on any send failure and
38+never unparks it. a recoverable failure while online gets the room another go
39+after a few seconds
40+
41+logout and a rejected token both stop the engine. `Shutdown` answers once
42+`Stopped` has gone out
43+
44+## rooms
45+
46+one actor per open room. the engine holds a slot per room, `Active` with a
47+sender or `Closing` with a fifo of opens and closes that arrived while it wound
48+down. those replay in order once the actor exits. a room you leave, or get
49+kicked or banned from, closes itself
50+
51+opening sends `TimelineReset` on the stream before it answers. opening an
52+already open room sends a fresh reset, so a second frontend gets a snapshot
53+
54+sends, edits and replies run on the actor in order, so they reach the send
55+queue in the order they were asked for. redactions, reactions and read
56+receipts are network calls, so they run beside the actor and a slow one
57+doesn't queue the room behind it. closing lets them finish rather than lie
58+
59+threads belong to their room's actor. commands for a thread that's still
60+opening wait behind the open, up to the actor's queue capacity
61+
62+pagination runs one request at a time per timeline and emits `HistoryChanged`
63+around each. the sdk remembers where history starts, we don't, because a gappy
64+sync can unload it again
65+
66+every task inside an actor is supervised, and a panic in any of them panics the
67+actor
68+
69+## timelines
70+
71+`TimelineDiff::try_apply` refuses a diff whose indices don't fit your snapshot
72+and leaves the snapshot alone. stop applying, `resync`, and carry on from the
73+next `TimelineReset`. resync kills the old watcher before the new reset goes
74+out, so no stale diff can land after it
75+
76+`TimelineId` tells a room from a thread by its root event
77+
78+## mutations
79+
80+membership, moderation, power levels, room creation and account data writes go
81+through one keyed fifo scheduler. same room, same alias, same dm partner or
82+same account data type run in order, everything else runs at once.
83+`create_direct` hands back the existing dm when there is one
84+
85+notifications and encryption each get one serial worker. the encryption worker
86+owns the verification controller outright
87+
88+power level changes build one state event from the local mapping, since the
89+sdk's own patcher is private
90+
91+## media
92+
93+media is a read. `ClientHandle::media` takes a `MediaId` a retained timeline
94+handed you, `mxc_media` takes any plain mxc uri. both share one bounded
95+download path with the byte cap the sdk's own download lacks, and
96+concurrent asks for the same bytes share one flight
97+
98+the cache holds plaintext, keyed like the sdk keys it, so the sdk and macltok
99+can read each other's entries. that's how your own encrypted uploads come back
100+
101+media ids are registered as timelines render and forgotten when their timeline
102+closes, least recently seen first once the cap is hit
103+
104+uploads are commands that answer once the send queue has them. progress and
105+failure arrive as events from one watcher on the sdk's global send queue
106+
107+## sessions and secrets
108+
109+`Session::login_password`, `begin_oauth` and `restore` share one client builder
110+with encrypted sqlite, refresh tokens and threads. secret-bearing inputs don't
111+derive `Clone`
112+
113+the caller owns the saved record. save `Session::record()` after login and
114+replace it atomically with `ClientHandle::session_record()` on every
115+`SessionTokensChanged`. logout and a hard rejection kill the record and the
116+store. a soft logout keeps the store for a same-device login, or the e2ee keys
117+go with it. a bare `Stopped` kills nothing. one store directory per login
118+
119+## notifications and room data
120+
121+the notification client is built lazily and shares the engine's sync service.
122+sdk ui types stay behind the boundary
123+
124+`rooms` normalises on demand with bounded concurrency and sorts by latest event,
125+then name, then id. `RoomsChanged` is an invalidation, coalesced so one sync
126+touching fifty rooms is one event
127+
128+## verification
129+
130+`Confirm`, `Reject` and `Cancel`. reject tells the other side the emoji didn't
131+match. the watcher and the action paths claim the terminal event, so a racing
132+cancel can't turn `Done` into `Cancelled`
133+
134+## public model
135+
136+owned ruma ids and typed data wherever there's a stable shape, `Raw` where
137+arbitrary content has to survive. anything expected to grow is
138+`#[non_exhaustive]`. `Engine::from_client` stays public for people who built
139+their own client
Modified file: docs/review-2026-09-24.md+13 additions−11 deletions
@@ -68,7 +68,7 @@ ciphertext as if it were the image. The media tests force plaintext rooms
6868 Fix direction. Cache plaintext, matching the SDK, and keep the byte bound by
6969 decrypting in a streaming fashion before the write.
7070
71-### C5. Media single-flight is poisoned by a leader panic. folded into the media query rewrite under q1
71+### C5. Media single-flight is poisoned by a leader panic. done, shared-future single flight in the media query
7272
7373 In `spawn_media_fetch` (`src/engine.rs:1870-1911`) followers push their id and
7474 return at once, trusting the leader to answer for them. If `fetch_media`
@@ -132,7 +132,7 @@ Fix direction. When the room is open, route through the actor and use
132132 already talk to creates a duplicate every time. `Client::get_dm_room` exists
133133 for the lookup (`client/mod.rs:1852`).
134134
135-### C11. Unregistered tasks swallow panics, contradicting the design doc. folded into q1 and the actor restructure
135+### C11. Unregistered tasks swallow panics, contradicting the design doc. done, command tasks answer `Interrupted`, actor internals are supervised and panic the actor
136136
137137 The design doc says a watcher panic is terminal. But the timeline watcher
138138 (`src/engine.rs:589`), pagination worker (`446`), pagination result task
@@ -161,7 +161,7 @@ loaded in a room that is still open are the first evicted, and fetching them
161161 then fails with `MediaUnknown`. Closed timelines never release their entries,
162162 so they compete with open ones.
163163
164-### C14. Smaller correctness issues. mostly done. `open_room` on an open room waits for q1 snapshots, the lagged watcher stays as is
164+### C14. Smaller correctness issues. done, bar the lagged session watcher which stays as is
165165
166166 - `CommandResultError::Failed` renders its `Display` through `Debug`
167167 (`src/transport.rs:840`), so users see a struct dump.
@@ -185,7 +185,7 @@ so they compete with open ones.
185185
186186 ## A. API shape and client-builder friendliness
187187
188-### A1. Three completion mechanisms, and the useful one is rationed. OPEN
188+### A1. Three completion mechanisms, and the useful one is rationed. done, one awaited path and one error type
189189
190190 A client must know which of three patterns each operation uses.
191191
@@ -202,7 +202,7 @@ The headless example is mostly bookkeeping of ids. Malice gap 4 comes straight
202202 from this. The one pattern a web backend can use, an awaited result per
203203 request, is not offered for most operations.
204204
205-### A2. Pure reads modelled as commands, against the doc's own rule. OPEN
205+### A2. Pure reads modelled as commands, against the doc's own rule. done, `media` and `mxc_media` are handle queries
206206
207207 The design doc says operations are split by whether they change state. Media
208208 fetch changes no state, yet it is a `MediaCommand` whose bytes arrive as a
@@ -210,7 +210,7 @@ fetch changes no state, yet it is a `MediaCommand` whose bytes arrive as a
210210 other frontend's image bytes, up to 20 MiB each, and the requester has to
211211 filter by `MediaId`.
212212
213-### A3. Every timeline item is a `Message`, with presentation strings in `body`. OPEN
213+### A3. Every timeline item is a `Message`, with presentation strings in `body`. open, q3
214214
215215 `normalise_timeline_item` (`src/normalise/timeline.rs`) flattens every event
216216 into `TimelineMessage`. The consequences for a client are these.
@@ -235,7 +235,7 @@ This needs a content enum, for example `Message { msgtype, ... }`, `Redacted`,
235235 `UnableToDecrypt`, `Membership`, `ProfileChange`, `RoomState`, `Poll` and
236236 `Other { event_type }`. It is the largest single rendering gap.
237237
238-### A4. Timeline commands cannot target local echoes. OPEN
238+### A4. Timeline commands cannot target local echoes. open, lands with q3
239239
240240 `Edit`, `Redact` and `ToggleReaction` take an `OwnedEventId` and wrap it in
241241 `TimelineEventItemId::EventId`. The SDK accepts transaction ids for all three,
@@ -243,13 +243,13 @@ but a client cannot edit, redact or react to a message it sent until the
243243 server echo arrives. The commands should take the identifier that the model
244244 already exposes.
245245
246-### A5. `RoomSummary` is too thin for a room list. OPEN
246+### A5. `RoomSummary` is too thin for a room list. open, lands with the malice queries and q2
247247
248248 Missing fields, all cheap SDK reads. Topic, canonical alias, joined and invited
249249 member counts, heroes, tombstone and successor, inviter for invites, a latest
250250 event preview, and space parents and children (Malice gap 3).
251251
252-### A6. No incoming verification. OPEN
252+### A6. No incoming verification. open
253253
254254 Only outgoing `StartVerification` exists. A request started from another
255255 device, which is the normal "verify this new login" flow, is never observed.
@@ -257,7 +257,7 @@ macltok registers no handler for it, and the SDK has no stream, so the app
257257 pattern is an event handler plus `Encryption::get_verification_request`
258258 (`encryption/mod.rs:1060`).
259259
260-### A7. Missing `#[non_exhaustive]`. OPEN
260+### A7. Missing `#[non_exhaustive]`. done
261261
262262 Adding the attribute later is itself a breaking change, so it belongs now. Output types that will
263263 grow are `TimelineItem`, `CommandKind`, `RoomMembership`, `VerificationState`,
@@ -265,7 +265,7 @@ grow are `TimelineItem`, `CommandKind`, `RoomMembership`, `VerificationState`,
265265 grow need a constructor plus `#[non_exhaustive]`. These are
266266 `CreateRoomOptions`, `PasswordLogin`, `OAuthLogin` and `UploadThumbnail`.
267267
268-### A8. Smaller API issues. OPEN
268+### A8. Smaller API issues. mostly done. `HistoryState::Failed(String)`, stringly power level events and fixed encryption defaults remain
269269
270270 - `MediaCommand::Upload` duplicates `UploadAttachment`, and the handle exposes
271271 both `upload_media` and `upload_attachment`.
@@ -284,6 +284,8 @@ grow need a constructor plus `#[non_exhaustive]`. These are
284284
285285 ## D. Duplication and antipatterns
286286
287+mostly done by the engine split. the actor thread registry, one spawn helper, one responder, one media request path, one test support module. `get_room` lookups went from twenty one hand rolled errors to a helper per module
288+
287289 Line references are from the mechanical inventory taken during this review.
288290
289291 - `RoomActor::run` is about 700 lines. The "queue while the thread is opening,
Modified file: examples/headless.rs+54 additions−54 deletions
@@ -1,76 +1,69 @@
11 use macltok::{
2- Command, CommandKind, EngineConfig, Event, LifecycleCommand, PasswordLogin, RoomCommand,
3- RoomMembership, Session, StoreConfig, TimelineCommand,
2+ EngineConfig, Event, PasswordLogin, RoomMembership, Session, StoreConfig, TimelineId,
3+ TimelineItem,
44 };
55
66 #[tokio::main]
77 async fn main() -> Result<(), Box<dyn std::error::Error>> {
8- let homeserver = required("MATRIX_HOMESERVER")?;
9- let username = required("MATRIX_USERNAME")?;
10- let password = required("MATRIX_PASSWORD")?;
11- let store_path = required("MATRIX_STORE_PATH")?;
12- let store_passphrase = required("MATRIX_STORE_PASSPHRASE")?;
138 let session = Session::login_password(
14- PasswordLogin {
15- homeserver,
16- username,
17- password,
18- device_display_name: Some("macltok headless example".into()),
19- device_id: None,
20- },
21- StoreConfig::new(store_path, store_passphrase),
9+ PasswordLogin::new(
10+ required("MATRIX_HOMESERVER")?,
11+ required("MATRIX_USERNAME")?,
12+ required("MATRIX_PASSWORD")?,
13+ )
14+ .with_device_display_name("macltok headless example"),
15+ StoreConfig::new(
16+ required("MATRIX_STORE_PATH")?,
17+ required("MATRIX_STORE_PASSPHRASE")?,
18+ ),
2219 )
2320 .await?;
2421 let (engine, handle, mut events) = session.build(EngineConfig::default()).await?;
2522 let running = tokio::spawn(engine.run());
26- let mut room_open_id = None;
27- let mut resync_id = None;
28- let mut timeline_items = Vec::new();
23+ let mut opened: Option<TimelineId> = None;
24+ let mut items = Vec::new();
25+ let mut stale = false;
2926
3027 loop {
3128 tokio::select! {
3229 event = events.recv() => {
33- let Some(event) = event else { break; };
30+ let Some(event) = event else { break };
3431 match event {
35- Event::RoomsChanged if room_open_id.is_none() => {
36- if let Some(room) = handle
32+ Event::RoomsChanged if opened.is_none() => {
33+ let Some(room) = handle
3734 .rooms()
3835 .await
3936 .into_iter()
4037 .find(|room| room.membership == RoomMembership::Joined)
41- {
42- room_open_id = Some(handle
43- .send(Command::Room(RoomCommand::Open(room.room_id)))
44- .await?);
45- }
38+ else {
39+ continue;
40+ };
41+ opened = Some(room.room_id.clone().into());
42+ // answers arrive on their own, the event loop keeps draining meanwhile
43+ let handle = handle.clone();
44+ tokio::spawn(async move {
45+ if let Err(error) = handle.open_room(room.room_id).await {
46+ eprintln!("could not open {error}");
47+ }
48+ });
4649 }
47- Event::TimelineReset { items, .. } => {
48- timeline_items = items;
49- resync_id = None;
50+ Event::TimelineReset { timeline_id, items: snapshot }
51+ if opened.as_ref() == Some(&timeline_id) =>
52+ {
53+ items = snapshot;
54+ stale = false;
55+ print_messages(&items);
5056 }
51- Event::TimelineChanged { timeline_id, diffs } if resync_id.is_none() => {
52- for diff in diffs {
53- if diff.try_apply(&mut timeline_items).is_err() {
54- resync_id = Some(handle.send(Command::Timeline(
55- TimelineCommand::Resync { timeline_id },
56- )).await?);
57- break;
58- }
59- }
60- if resync_id.is_none() {
61- for item in &timeline_items {
62- if let macltok::TimelineItem::Message(message) = item {
63- println!("{} {}", message.sender_name, message.body);
64- }
65- }
57+ Event::TimelineChanged { timeline_id, diffs }
58+ if !stale && opened.as_ref() == Some(&timeline_id) =>
59+ {
60+ if diffs.into_iter().any(|diff| diff.try_apply(&mut items).is_err()) {
61+ stale = true;
62+ let handle = handle.clone();
63+ tokio::spawn(async move { handle.resync(timeline_id).await });
64+ continue;
6665 }
67- }
68- Event::CommandSucceeded { id, command: CommandKind::Room }
69- if room_open_id == Some(id) => println!("room ready"),
70- Event::CommandFailed(error) if room_open_id == Some(error.id)
71- || resync_id == Some(error.id) => return Err(error.message.into()),
72- Event::CommandFailed(error) => {
73- eprintln!("{:?} {}", error.command, error.message);
66+ print_messages(&items);
7467 }
7568 Event::Stopped => break,
7669 _ => {}
@@ -78,9 +71,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
7871 }
7972 result = tokio::signal::ctrl_c() => {
8073 result?;
81- handle
82- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
83- .await?;
74+ let handle = handle.clone();
75+ tokio::spawn(async move { handle.shutdown().await });
8476 }
8577 }
8678 }
@@ -88,6 +80,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
8880 Ok(())
8981 }
9082
83+fn print_messages(items: &[TimelineItem]) {
84+ for item in items {
85+ if let TimelineItem::Message(message) = item {
86+ println!("{} {}", message.sender_name, message.body);
87+ }
88+ }
89+}
90+
9191 fn required(name: &str) -> Result<String, Box<dyn std::error::Error>> {
9292 std::env::var(name).map_err(|_| format!("{name} is required").into())
9393 }
Modified file: src/account.rs+71 additions
@@ -27,6 +27,7 @@ pub struct Session {
2727 record: SessionRecord,
2828 }
2929
30+#[non_exhaustive]
3031 pub struct PasswordLogin {
3132 pub homeserver: String,
3233 pub username: String,
@@ -36,6 +37,36 @@ pub struct PasswordLogin {
3637 pub device_id: Option<OwnedDeviceId>,
3738 }
3839
40+impl PasswordLogin {
41+ #[must_use]
42+ pub fn new(
43+ homeserver: impl Into<String>,
44+ username: impl Into<String>,
45+ password: impl Into<String>,
46+ ) -> Self {
47+ Self {
48+ homeserver: homeserver.into(),
49+ username: username.into(),
50+ password: password.into(),
51+ device_display_name: None,
52+ device_id: None,
53+ }
54+ }
55+
56+ #[must_use]
57+ pub fn with_device_display_name(mut self, name: impl Into<String>) -> Self {
58+ self.device_display_name = Some(name.into());
59+ self
60+ }
61+
62+ #[must_use]
63+ pub fn with_device_id(mut self, device_id: OwnedDeviceId) -> Self {
64+ self.device_id = Some(device_id);
65+ self
66+ }
67+}
68+
69+#[non_exhaustive]
3970 pub struct OAuthLogin {
4071 pub homeserver: String,
4172 pub redirect_uri: String,
@@ -46,7 +77,47 @@ pub struct OAuthLogin {
4677 pub application_type: OAuthApplicationType,
4778 }
4879
80+impl OAuthLogin {
81+ #[must_use]
82+ pub fn new(
83+ homeserver: impl Into<String>,
84+ redirect_uri: impl Into<String>,
85+ client_uri: impl Into<String>,
86+ application_type: OAuthApplicationType,
87+ ) -> Self {
88+ Self {
89+ homeserver: homeserver.into(),
90+ redirect_uri: redirect_uri.into(),
91+ client_uri: client_uri.into(),
92+ client_name: None,
93+ client_id: None,
94+ device_id: None,
95+ application_type,
96+ }
97+ }
98+
99+ #[must_use]
100+ pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
101+ self.client_name = Some(name.into());
102+ self
103+ }
104+
105+ /// a statically registered client id, skipping dynamic registration
106+ #[must_use]
107+ pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
108+ self.client_id = Some(client_id.into());
109+ self
110+ }
111+
112+ #[must_use]
113+ pub fn with_device_id(mut self, device_id: OwnedDeviceId) -> Self {
114+ self.device_id = Some(device_id);
115+ self
116+ }
117+}
118+
49119 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
120+#[non_exhaustive]
50121 pub enum OAuthApplicationType {
51122 Native,
52123 Web,
Modified file: src/encryption.rs+88 additions−115 deletions
@@ -17,9 +17,9 @@ use ruma::{OwnedDeviceId, UserId};
1717 use tokio::{sync::Mutex, task::JoinHandle};
1818
1919 use crate::{
20- CommandErrorKind, EncryptionState, Event, KeyBackupState, RecoveryState, VerificationAction,
21- VerificationState as UiState, VerificationTarget, error::CommandError,
22- model::VerificationFailure, transport::EventSender,
20+ EncryptionState, Error, ErrorKind, Event, KeyBackupState, RecoveryState, VerificationAction,
21+ VerificationState as UiState, VerificationTarget, model::VerificationFailure,
22+ transport::EventSender,
2323 };
2424
2525 pub(crate) fn encryption_state(
@@ -64,10 +64,9 @@ struct VerificationSession {
6464 impl VerificationController {
6565 pub(crate) async fn apply(
6666 &mut self,
67- _client: &Client,
6867 sender: EventSender,
6968 action: VerificationAction,
70- ) -> Result<(), CommandError> {
69+ ) -> Result<(), Error> {
7170 match action {
7271 VerificationAction::Confirm => {
7372 // Retain a handle across the operation: the watcher clears
@@ -76,9 +75,7 @@ impl VerificationController {
7675 let sas = self.active.lock().await.sas.clone();
7776 match self.confirm().await {
7877 Ok(()) => Ok(()),
79- Err(error) if error.kind == CommandErrorKind::VerificationInactive => {
80- Err(error)
81- }
78+ Err(error) if error.kind == ErrorKind::VerificationInactive => Err(error),
8279 // The confirm advanced local SDK state before its send failed,
8380 // so the retained session could report success on retry without
8481 // network traffic. Best-effort cancel (no lock held across the
@@ -153,8 +150,8 @@ impl VerificationController {
153150 )))
154151 .await;
155152 }
156- return Err(CommandError::new(
157- CommandErrorKind::VerificationInactive,
153+ return Err(Error::new(
154+ ErrorKind::VerificationInactive,
158155 info.reason().to_owned(),
159156 ));
160157 }
@@ -185,8 +182,8 @@ impl VerificationController {
185182 )))
186183 .await
187184 .map_err(|source| {
188- CommandError::with_source(
189- CommandErrorKind::EventReceiverClosed,
185+ Error::with_source(
186+ ErrorKind::Interrupted,
190187 "event receiver has closed",
191188 source,
192189 )
@@ -202,14 +199,14 @@ impl VerificationController {
202199 client: &Client,
203200 sender: EventSender,
204201 target: VerificationTarget,
205- ) -> Result<(), CommandError> {
202+ ) -> Result<(), Error> {
206203 client
207204 .encryption()
208205 .wait_for_e2ee_initialization_tasks()
209206 .await;
210207 let own_user_id = client.user_id().ok_or_else(|| {
211- CommandError::new(
212- CommandErrorKind::Unauthenticated,
208+ Error::new(
209+ ErrorKind::Unauthenticated,
213210 "the Matrix client is not authenticated",
214211 )
215212 })?;
@@ -231,15 +228,15 @@ impl VerificationController {
231228 .get_device(user_id, device_id)
232229 .await
233230 .map_err(|source| {
234- CommandError::with_source(
235- CommandErrorKind::Encryption,
231+ Error::with_source(
232+ ErrorKind::Encryption,
236233 "reading the verification device failed",
237234 source,
238235 )
239236 })?
240237 .ok_or_else(|| {
241- CommandError::new(
242- CommandErrorKind::VerificationUnsupported,
238+ Error::new(
239+ ErrorKind::VerificationUnsupported,
243240 "the verification device is unavailable",
244241 )
245242 })?;
@@ -247,7 +244,7 @@ impl VerificationController {
247244 .request_verification_with_methods(vec![VerificationMethod::SasV1])
248245 .await
249246 .map_err(|source| {
250- CommandError::from_sdk("starting device verification failed", source)
247+ Error::from_sdk("starting device verification failed", source)
251248 })?
252249 }
253250 };
@@ -256,21 +253,13 @@ impl VerificationController {
256253 .send(Event::VerificationTargetChanged(target))
257254 .await
258255 .map_err(|source| {
259- CommandError::with_source(
260- CommandErrorKind::EventReceiverClosed,
261- "event receiver has closed",
262- source,
263- )
256+ Error::with_source(ErrorKind::Interrupted, "event receiver has closed", source)
264257 })?;
265258 sender
266259 .send(Event::VerificationChanged(UiState::Waiting))
267260 .await
268261 .map_err(|source| {
269- CommandError::with_source(
270- CommandErrorKind::EventReceiverClosed,
271- "event receiver has closed",
272- source,
273- )
262+ Error::with_source(ErrorKind::Interrupted, "event receiver has closed", source)
274263 })?;
275264 let active = self.active.clone();
276265 self.task = Some(tokio::spawn(async move {
@@ -279,24 +268,24 @@ impl VerificationController {
279268 Ok(())
280269 }
281270
282- async fn confirm(&self) -> Result<(), CommandError> {
271+ async fn confirm(&self) -> Result<(), Error> {
283272 let sas = self.active.lock().await.sas.clone().ok_or_else(|| {
284- CommandError::new(
285- CommandErrorKind::VerificationInactive,
273+ Error::new(
274+ ErrorKind::VerificationInactive,
286275 "verification has not reached comparison",
287276 )
288277 })?;
289278 // Report a settled flow truthfully instead of as "not reached
290279 // comparison": the SDK would no-op the confirm with success.
291280 if sas.is_done() {
292- return Err(CommandError::new(
293- CommandErrorKind::VerificationInactive,
281+ return Err(Error::new(
282+ ErrorKind::VerificationInactive,
294283 "verification is already complete",
295284 ));
296285 }
297286 if let Some(info) = sas.cancel_info() {
298- return Err(CommandError::new(
299- CommandErrorKind::VerificationInactive,
287+ return Err(Error::new(
288+ ErrorKind::VerificationInactive,
300289 info.reason().to_owned(),
301290 ));
302291 }
@@ -304,8 +293,8 @@ impl VerificationController {
304293 // remote-MAC-arrived state through the same presentable flag, while an
305294 // early confirm would locally no-op and falsely report success.
306295 if !sas.can_be_presented() {
307- return Err(CommandError::new(
308- CommandErrorKind::VerificationInactive,
296+ return Err(Error::new(
297+ ErrorKind::VerificationInactive,
309298 "verification has not reached comparison",
310299 ));
311300 }
@@ -315,11 +304,11 @@ impl VerificationController {
315304 let was_done = sas.is_done();
316305 sas.confirm()
317306 .await
318- .map_err(|source| CommandError::from_sdk("confirming verification failed", source))?;
307+ .map_err(|source| Error::from_sdk("confirming verification failed", source))?;
319308 if sas.is_done() {
320309 if was_done {
321- return Err(CommandError::new(
322- CommandErrorKind::VerificationInactive,
310+ return Err(Error::new(
311+ ErrorKind::VerificationInactive,
323312 "verification is already complete",
324313 ));
325314 }
@@ -328,52 +317,49 @@ impl VerificationController {
328317 .cancel_info()
329318 .map(|info| info.reason().to_owned())
330319 .unwrap_or_else(|| "verification was cancelled".to_owned());
331- return Err(CommandError::new(
332- CommandErrorKind::VerificationInactive,
333- reason,
334- ));
320+ return Err(Error::new(ErrorKind::VerificationInactive, reason));
335321 }
336322 Ok(())
337323 }
338324
339- async fn reject(&self) -> Result<(), CommandError> {
325+ async fn reject(&self) -> Result<(), Error> {
340326 let sas = self.active.lock().await.sas.clone().ok_or_else(|| {
341- CommandError::new(
342- CommandErrorKind::VerificationInactive,
327+ Error::new(
328+ ErrorKind::VerificationInactive,
343329 "verification has not reached comparison",
344330 )
345331 })?;
346332 // Report a settled flow truthfully instead of as "not reached
347333 // comparison": the SDK would no-op the mismatch with success.
348334 if sas.is_done() {
349- return Err(CommandError::new(
350- CommandErrorKind::VerificationInactive,
335+ return Err(Error::new(
336+ ErrorKind::VerificationInactive,
351337 "verification is already complete",
352338 ));
353339 }
354340 if let Some(info) = sas.cancel_info() {
355- return Err(CommandError::new(
356- CommandErrorKind::VerificationInactive,
341+ return Err(Error::new(
342+ ErrorKind::VerificationInactive,
357343 info.reason().to_owned(),
358344 ));
359345 }
360346 // Rejecting before the comparison stage would cancel a flow the user
361347 // never compared, so it requires the same presentable state.
362348 if !sas.can_be_presented() {
363- return Err(CommandError::new(
364- CommandErrorKind::VerificationInactive,
349+ return Err(Error::new(
350+ ErrorKind::VerificationInactive,
365351 "verification has not reached comparison",
366352 ));
367353 }
368354 sas.mismatch()
369355 .await
370- .map_err(|source| CommandError::from_sdk("rejecting verification failed", source))?;
356+ .map_err(|source| Error::from_sdk("rejecting verification failed", source))?;
371357 // A peer completing the flow concurrently must not be reported as
372358 // rejected. A remote cancellation racing the send turns the SDK call
373359 // into a no-op success, so report it instead of a false success.
374360 if sas.is_done() {
375- return Err(CommandError::new(
376- CommandErrorKind::VerificationInactive,
361+ return Err(Error::new(
362+ ErrorKind::VerificationInactive,
377363 "verification is already complete",
378364 ));
379365 }
@@ -382,29 +368,29 @@ impl VerificationController {
382368 // expected reject path and stays Ok. Only a peer cancel reports an
383369 // error.
384370 if !info.cancelled_by_us() {
385- return Err(CommandError::new(
386- CommandErrorKind::VerificationInactive,
371+ return Err(Error::new(
372+ ErrorKind::VerificationInactive,
387373 info.reason().to_owned(),
388374 ));
389375 }
390376 }
391377 Ok(())
392378 }
393- pub(crate) async fn cancel(&mut self) -> Result<(), CommandError> {
379+ pub(crate) async fn cancel(&mut self) -> Result<(), Error> {
394380 self.cancel_claimed(None).await.map(|_| ())
395381 }
396382
397383 /// Return whether this action won the claim for a local cancellation.
398384 /// The optional sender reports a peer terminal outcome when it races after
399385 /// the claim; starting a replacement verification does not publish it.
400- async fn cancel_claimed(&mut self, sender: Option<&EventSender>) -> Result<bool, CommandError> {
386+ async fn cancel_claimed(&mut self, sender: Option<&EventSender>) -> Result<bool, Error> {
401387 let (sas, request) = {
402388 let active = self.active.lock().await;
403389 (active.sas.clone(), active.request.clone())
404390 };
405391 if sas.is_none() && request.is_none() {
406- return Err(CommandError::new(
407- CommandErrorKind::VerificationInactive,
392+ return Err(Error::new(
393+ ErrorKind::VerificationInactive,
408394 "no verification is active",
409395 ));
410396 }
@@ -415,27 +401,27 @@ impl VerificationController {
415401 // SDK no-op success regress Done to "cancelled by this client".
416402 if let Some(sas) = sas.as_ref() {
417403 if sas.is_done() {
418- return Err(CommandError::new(
419- CommandErrorKind::VerificationInactive,
404+ return Err(Error::new(
405+ ErrorKind::VerificationInactive,
420406 "verification is already complete",
421407 ));
422408 }
423409 if let Some(info) = sas.cancel_info() {
424- return Err(CommandError::new(
425- CommandErrorKind::VerificationInactive,
410+ return Err(Error::new(
411+ ErrorKind::VerificationInactive,
426412 info.reason().to_owned(),
427413 ));
428414 }
429415 } else if let Some(request) = request.as_ref() {
430416 if request.is_done() {
431- return Err(CommandError::new(
432- CommandErrorKind::VerificationInactive,
417+ return Err(Error::new(
418+ ErrorKind::VerificationInactive,
433419 "verification is already complete",
434420 ));
435421 }
436422 if let Some(info) = request.cancel_info() {
437- return Err(CommandError::new(
438- CommandErrorKind::VerificationInactive,
423+ return Err(Error::new(
424+ ErrorKind::VerificationInactive,
439425 info.reason().to_owned(),
440426 ));
441427 }
@@ -451,33 +437,33 @@ impl VerificationController {
451437 // flow and must not be disturbed.
452438 if let Some(sas) = sas.as_ref() {
453439 if sas.is_done() {
454- return Err(CommandError::new(
455- CommandErrorKind::VerificationInactive,
440+ return Err(Error::new(
441+ ErrorKind::VerificationInactive,
456442 "verification is already complete",
457443 ));
458444 }
459445 if let Some(info) = sas.cancel_info() {
460- return Err(CommandError::new(
461- CommandErrorKind::VerificationInactive,
446+ return Err(Error::new(
447+ ErrorKind::VerificationInactive,
462448 info.reason().to_owned(),
463449 ));
464450 }
465451 } else if let Some(request) = request.as_ref() {
466452 if request.is_done() {
467- return Err(CommandError::new(
468- CommandErrorKind::VerificationInactive,
453+ return Err(Error::new(
454+ ErrorKind::VerificationInactive,
469455 "verification is already complete",
470456 ));
471457 }
472458 if let Some(info) = request.cancel_info() {
473- return Err(CommandError::new(
474- CommandErrorKind::VerificationInactive,
459+ return Err(Error::new(
460+ ErrorKind::VerificationInactive,
475461 info.reason().to_owned(),
476462 ));
477463 }
478464 }
479- return Err(CommandError::new(
480- CommandErrorKind::VerificationInactive,
465+ return Err(Error::new(
466+ ErrorKind::VerificationInactive,
481467 "no verification is active",
482468 ));
483469 }
@@ -486,8 +472,8 @@ impl VerificationController {
486472 let result = sas.cancel().await;
487473 if sas.is_done() {
488474 report_claimed_terminal(sender, UiState::Done).await;
489- return Err(CommandError::new(
490- CommandErrorKind::VerificationInactive,
475+ return Err(Error::new(
476+ ErrorKind::VerificationInactive,
491477 "verification is already complete",
492478 ));
493479 }
@@ -496,13 +482,10 @@ impl VerificationController {
496482 {
497483 let reason = info.reason().to_owned();
498484 report_claimed_terminal(sender, UiState::Cancelled(reason.clone())).await;
499- return Err(CommandError::new(
500- CommandErrorKind::VerificationInactive,
501- reason,
502- ));
485+ return Err(Error::new(ErrorKind::VerificationInactive, reason));
503486 }
504487 if let Err(source) = result {
505- let error = CommandError::from_sdk("cancelling verification failed", source);
488+ let error = Error::from_sdk("cancelling verification failed", source);
506489 report_claimed_terminal(sender, UiState::Cancelled(error.to_string())).await;
507490 return Err(error);
508491 }
@@ -511,8 +494,8 @@ impl VerificationController {
511494 let result = request.cancel().await;
512495 if request.is_done() {
513496 report_claimed_terminal(sender, UiState::Done).await;
514- return Err(CommandError::new(
515- CommandErrorKind::VerificationInactive,
497+ return Err(Error::new(
498+ ErrorKind::VerificationInactive,
516499 "verification is already complete",
517500 ));
518501 }
@@ -521,13 +504,10 @@ impl VerificationController {
521504 {
522505 let reason = info.reason().to_owned();
523506 report_claimed_terminal(sender, UiState::Cancelled(reason.clone())).await;
524- return Err(CommandError::new(
525- CommandErrorKind::VerificationInactive,
526- reason,
527- ));
507+ return Err(Error::new(ErrorKind::VerificationInactive, reason));
528508 }
529509 if let Err(source) = result {
530- let error = CommandError::from_sdk("cancelling verification failed", source);
510+ let error = Error::from_sdk("cancelling verification failed", source);
531511 report_claimed_terminal(sender, UiState::Cancelled(error.to_string())).await;
532512 return Err(error);
533513 }
@@ -548,37 +528,36 @@ impl VerificationController {
548528 let _ = task.await;
549529 }
550530 }
531+}
551532
552- pub(crate) async fn stop(&mut self) {
553- // Shutdown path: terminate the watcher without touching the SDK so a
554- // shutdown under lock cannot hang on network sends. No terminal event
555- // is emitted; the session is simply forgotten locally.
556- self.stop_task().await;
557- clear_verification(&self.active).await;
533+/// shutdown forgets the flow without telling the sdk, a network send could hang it
534+impl Drop for VerificationController {
535+ fn drop(&mut self) {
536+ if let Some(task) = self.task.take() {
537+ task.abort();
538+ }
558539 }
559540 }
560541
561542 async fn request_identity_verification(
562543 client: &Client,
563544 user_id: &UserId,
564-) -> Result<VerificationRequest, CommandError> {
545+) -> Result<VerificationRequest, Error> {
565546 let identity = client
566547 .encryption()
567548 .request_user_identity(user_id)
568549 .await
569- .map_err(|source| CommandError::from_sdk("requesting the user identity failed", source))?
550+ .map_err(|source| Error::from_sdk("requesting the user identity failed", source))?
570551 .ok_or_else(|| {
571- CommandError::new(
572- CommandErrorKind::NoCrossSigningIdentity,
552+ Error::new(
553+ ErrorKind::NoCrossSigningIdentity,
573554 "the user has no cross-signing identity",
574555 )
575556 })?;
576557 identity
577558 .request_verification_with_methods(vec![VerificationMethod::SasV1])
578559 .await
579- .map_err(|source| {
580- CommandError::from_verification_request("starting verification failed", source)
581- })
560+ .map_err(|source| Error::from_verification_request("starting verification failed", source))
582561 }
583562
584563 async fn follow_request(
@@ -787,12 +766,6 @@ async fn report_claimed_terminal(sender: Option<&EventSender>, state: UiState) {
787766 }
788767 }
789768
790-async fn clear_verification(active: &Arc<Mutex<VerificationSession>>) {
791- let mut active = active.lock().await;
792- active.sas.take();
793- active.request.take();
794-}
795-
796769 /// Claim the active session for terminal emission, if one is still present.
797770 ///
798771 /// Exactly one party (watcher or action path) wins the claim and may emit the
Deleted file: src/engine.rs−4364 deletions
@@ -1,4364 +0,0 @@
1-use std::{
2- borrow::Cow,
3- collections::{HashMap, HashSet, VecDeque},
4- future::Future,
5- io::Read as _,
6- panic::AssertUnwindSafe,
7- sync::Arc,
8- time::Duration,
9-};
10-
11-use futures_util::{FutureExt, StreamExt};
12-use matrix_sdk::{
13- Client, Room, SessionChange,
14- media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
15- room::edit::EditedContent,
16- room::reply::{EnforceThread, Reply as SdkReply},
17- ruma::{
18- OwnedDeviceId, OwnedEventId, OwnedRoomId, RoomId,
19- api::client::receipt::create_receipt::v3::ReceiptType,
20- events::{
21- AnyGlobalAccountDataEventContent, AnyMessageLikeEventContent,
22- GlobalAccountDataEventType, Mentions, RoomAccountDataEventType,
23- receipt::ReceiptThread,
24- room::message::{
25- AddMentions, MessageType, Relation, ReplyWithinThread,
26- RoomMessageEventContentWithoutRelation,
27- },
28- },
29- serde::Raw,
30- },
31- send_queue::{LocalEchoContent, RoomSendQueueUpdate, SendHandle},
32-};
33-use matrix_sdk_ui::{
34- sync_service::{State as SyncState, SyncService},
35- timeline::{
36- RoomExt, Timeline, TimelineEventItemId, TimelineFocus, TimelineReadReceiptTracking,
37- },
38-};
39-use thiserror::Error;
40-use tokio::{
41- sync::{Mutex, mpsc, oneshot, watch},
42- task::{AbortHandle, Id as TaskId, JoinHandle, JoinSet},
43-};
44-
45-use crate::{
46- AccountCommand, AttachmentUpload, ClientHandle, Command, CommandErrorKind, CommandFailure,
47- CommandId, CommandKind, ConnectionState, CreateRoomOptions, EncryptionCommand, EngineErrorKind,
48- EngineFailure, Event, EventStream, HistoryState, InteractiveAuth, InteractiveAuthChallenge,
49- InteractiveAuthOutcome, LifecycleCommand, MediaCommand, MediaId, MediaSize, MessageContent,
50- MessageEditContent, MessageMentions, NotificationCommand, PowerLevelChanges, RichText,
51- RoomCommand, RoomNotificationMode, RoomPreset, RoomVisibility, TimelineCommand, TimelineId,
52- TypingState,
53- encryption::{VerificationController, encryption_state},
54- error::{CommandError, classify_sdk_error},
55- normalise::{MediaRegistration, MediaScope, normalise_timeline, normalise_timeline_diffs},
56- transport::{
57- ClientQueryError, CommandEnvelope, CommandResultError, DriverPort, DriverRequest,
58- EventSender, SendEventError, transport_pair,
59- },
60- upload::prepare_upload,
61-};
62-use matrix_sdk::ruma::api::{Metadata, OutgoingRequest, auth_scheme::SendAccessToken};
63-use matrix_sdk_crypto::{AttachmentDecryptor, MediaEncryptionInfo};
64-
65-#[derive(Clone, Debug, Eq, PartialEq)]
66-#[non_exhaustive]
67-pub struct EngineConfig {
68- pub command_capacity: usize,
69- pub event_capacity: usize,
70- pub initial_history_limit: u16,
71- pub max_media_bytes: usize,
72- pub max_media_sources: usize,
73-}
74-
75-impl EngineConfig {
76- #[must_use]
77- pub const fn with_command_capacity(mut self, capacity: usize) -> Self {
78- self.command_capacity = capacity;
79- self
80- }
81-
82- #[must_use]
83- pub const fn with_event_capacity(mut self, capacity: usize) -> Self {
84- self.event_capacity = capacity;
85- self
86- }
87-
88- #[must_use]
89- pub const fn with_initial_history_limit(mut self, limit: u16) -> Self {
90- self.initial_history_limit = limit;
91- self
92- }
93-
94- #[must_use]
95- pub const fn with_max_media_bytes(mut self, bytes: usize) -> Self {
96- self.max_media_bytes = bytes;
97- self
98- }
99-
100- #[must_use]
101- pub const fn with_max_media_sources(mut self, sources: usize) -> Self {
102- self.max_media_sources = sources;
103- self
104- }
105-}
106-
107-impl Default for EngineConfig {
108- fn default() -> Self {
109- Self {
110- command_capacity: 64,
111- event_capacity: 128,
112- initial_history_limit: 30,
113- max_media_bytes: 20 * 1024 * 1024,
114- max_media_sources: 2_048,
115- }
116- }
117-}
118-
119-#[derive(Debug, Error)]
120-pub enum EngineError {
121- #[error("the Matrix client is not authenticated")]
122- Unauthenticated,
123- #[error("command, event, and media source capacities must be nonzero")]
124- ZeroCapacity,
125- #[error("could not build the sync service")]
126- SyncService(#[source] matrix_sdk_ui::sync_service::Error),
127- #[error("the event receiver has closed")]
128- EventReceiverClosed,
129- #[error("engine task {task} panicked")]
130- TaskPanicked { task: String },
131-}
132-
133-impl EngineError {
134- fn failure(&self) -> EngineFailure {
135- EngineFailure {
136- kind: match self {
137- Self::EventReceiverClosed => EngineErrorKind::EventReceiverClosed,
138- Self::Unauthenticated
139- | Self::ZeroCapacity
140- | Self::SyncService(_)
141- | Self::TaskPanicked { .. } => EngineErrorKind::Other,
142- },
143- message: self.to_string(),
144- }
145- }
146-}
147-
148-type UploadWatchers = HashMap<
149- OwnedRoomId,
150- (
151- AbortHandle,
152- watch::Receiver<Option<Result<(), CommandErrorKind>>>,
153- ),
154->;
155-const SYNC_RESTART_MIN: Duration = Duration::from_secs(1);
156-const SYNC_RESTART_MAX: Duration = Duration::from_secs(60);
157-const SEND_QUEUE_RETRY: Duration = Duration::from_secs(5);
158-
159-type MediaFlights = Arc<Mutex<HashMap<(MediaId, MediaSize), Vec<CommandId>>>>;
160-
161-pub struct Engine {
162- client: Client,
163- sync: Arc<SyncService>,
164- port: DriverPort,
165- rooms: HashMap<OwnedRoomId, RoomActorState>,
166- next_room_generation: u64,
167- room_actor_events: mpsc::UnboundedReceiver<RoomActorEvent>,
168- room_actor_event_sender: mpsc::UnboundedSender<RoomActorEvent>,
169- upload_watchers: UploadWatchers,
170- tasks: TaskRegistry,
171- session_changes: Option<tokio::sync::broadcast::Receiver<SessionChange>>,
172- initial_history_limit: u16,
173- room_command_capacity: usize,
174- max_media_bytes: usize,
175- media: Arc<Mutex<MediaRegistry>>,
176- notification_commands: mpsc::Sender<CommandJob<NotificationCommand>>,
177- notification_command_receiver: Option<mpsc::Receiver<CommandJob<NotificationCommand>>>,
178- encryption_commands: mpsc::Sender<EncryptionWork>,
179- encryption_command_receiver: Option<mpsc::Receiver<EncryptionWork>>,
180- verification: Arc<Mutex<VerificationController>>,
181- room_commands: mpsc::Sender<RoomJob>,
182- room_command_receiver: Option<mpsc::Receiver<RoomJob>>,
183- global_account_gates: HashMap<String, oneshot::Receiver<()>>,
184- room_account_gates: HashMap<(OwnedRoomId, String), oneshot::Receiver<()>>,
185- media_flights: MediaFlights,
186- stop: Arc<tokio::sync::Notify>,
187-}
188-
189-struct CommandJob<T> {
190- id: CommandId,
191- command: T,
192-}
193-
194-enum EncryptionWork {
195- Command(CommandJob<EncryptionCommand>),
196-}
197-
198-struct RoomJob {
199- id: CommandId,
200- command: RoomCommand,
201- reply: Option<oneshot::Sender<Result<OwnedRoomId, CommandResultError>>>,
202-}
203-
204-#[derive(Clone, Debug, Eq, Hash, PartialEq)]
205-enum RoomMutationKey {
206- Room(OwnedRoomId),
207- Target(String),
208- Direct(ruma::OwnedUserId),
209- Independent(CommandId),
210-}
211-
212-struct RoomJobCompletion {
213- key: RoomMutationKey,
214- id: CommandId,
215- reply: Option<oneshot::Sender<Result<OwnedRoomId, CommandResultError>>>,
216- result: Result<Option<OwnedRoomId>, CommandError>,
217-}
218-
219-#[derive(Clone)]
220-enum TaskContext {
221- Command {
222- id: CommandId,
223- kind: CommandKind,
224- },
225- Watcher(&'static str),
226- RoomActor {
227- room_id: OwnedRoomId,
228- generation: u64,
229- },
230-}
231-
232-impl TaskContext {
233- fn label(&self) -> String {
234- match self {
235- Self::Command { id, kind } => format!("{kind:?} command {}", id.get()),
236- Self::Watcher(name) => (*name).to_owned(),
237- Self::RoomActor {
238- room_id,
239- generation,
240- } => format!("room actor {room_id} generation {generation}"),
241- }
242- }
243-}
244-
245-struct TaskRegistry {
246- tasks: JoinSet<()>,
247- contexts: HashMap<TaskId, TaskContext>,
248-}
249-
250-impl TaskRegistry {
251- fn new() -> Self {
252- Self {
253- tasks: JoinSet::new(),
254- contexts: HashMap::new(),
255- }
256- }
257-
258- fn is_empty(&self) -> bool {
259- self.tasks.is_empty()
260- }
261-
262- fn spawn(
263- &mut self,
264- context: TaskContext,
265- future: impl Future<Output = ()> + Send + 'static,
266- ) -> AbortHandle {
267- let handle = self.tasks.spawn(future);
268- self.contexts.insert(handle.id(), context);
269- handle
270- }
271-
272- async fn join_next(&mut self) -> Option<(TaskContext, Result<(), tokio::task::JoinError>)> {
273- let result = self.tasks.join_next_with_id().await?;
274- let id = match &result {
275- Ok((id, ())) => *id,
276- Err(error) => error.id(),
277- };
278- let context = self
279- .contexts
280- .remove(&id)
281- .expect("every registered task has context");
282- Some((context, result.map(|(_, ())| ())))
283- }
284-
285- async fn shutdown(&mut self) {
286- self.tasks.abort_all();
287- while self.tasks.join_next().await.is_some() {}
288- self.contexts.clear();
289- }
290-}
291-
292-enum RoomActorState {
293- Opening {
294- generation: u64,
295- sender: mpsc::Sender<RoomActorRequest>,
296- },
297- Open {
298- generation: u64,
299- sender: mpsc::Sender<RoomActorRequest>,
300- },
301- Closing {
302- generation: u64,
303- pending: VecDeque<DeferredRoomLifecycle>,
304- },
305-}
306-
307-enum DeferredRoomLifecycle {
308- Open(CommandId),
309- Close(CommandId),
310-}
311-
312-impl RoomActorState {
313- const fn generation(&self) -> u64 {
314- match self {
315- Self::Opening { generation, .. }
316- | Self::Open { generation, .. }
317- | Self::Closing { generation, .. } => *generation,
318- }
319- }
320-
321- fn sender(&self) -> Option<&mpsc::Sender<RoomActorRequest>> {
322- match self {
323- Self::Opening { sender, .. } | Self::Open { sender, .. } => Some(sender),
324- Self::Closing { .. } => None,
325- }
326- }
327-}
328-
329-enum RoomActorEvent {
330- Ready {
331- room_id: OwnedRoomId,
332- generation: u64,
333- },
334- Exited {
335- room_id: OwnedRoomId,
336- generation: u64,
337- },
338-}
339-
340-enum EngineActivity {
341- Request(Option<Box<DriverRequest>>),
342- Task(Option<(TaskContext, Result<(), tokio::task::JoinError>)>),
343- RoomActor(Option<RoomActorEvent>),
344-}
345-
346-/// least recently seen goes first. stale order slots are skipped by stamp rather than hunted down
347-struct MediaRegistry {
348- entries: HashMap<MediaId, (MediaRegistration, u64)>,
349- order: VecDeque<(MediaId, u64)>,
350- next_stamp: u64,
351- limit: usize,
352-}
353-
354-impl MediaRegistry {
355- fn new(limit: usize) -> Self {
356- Self {
357- entries: HashMap::new(),
358- order: VecDeque::new(),
359- next_stamp: 0,
360- limit,
361- }
362- }
363-
364- fn get(&self, id: &MediaId) -> Option<&MediaRegistration> {
365- self.entries.get(id).map(|(media, _)| media)
366- }
367-
368- fn insert_all(&mut self, media: Vec<MediaRegistration>) {
369- for media in media {
370- let stamp = self.next_stamp;
371- self.next_stamp += 1;
372- self.order.push_back((media.id.clone(), stamp));
373- self.entries.insert(media.id.clone(), (media, stamp));
374- }
375- while self.entries.len() > self.limit {
376- let Some((id, stamp)) = self.order.pop_front() else {
377- break;
378- };
379- if self
380- .entries
381- .get(&id)
382- .is_some_and(|(_, current)| *current == stamp)
383- {
384- self.entries.remove(&id);
385- }
386- }
387- if self.order.len() > self.limit.saturating_mul(4) {
388- let entries = &self.entries;
389- self.order
390- .retain(|(id, stamp)| entries.get(id).is_some_and(|(_, current)| current == stamp));
391- }
392- }
393-
394- fn release(&mut self, scope: &MediaScope) {
395- self.entries.retain(|id, _| !scope.owns(id));
396- let entries = &self.entries;
397- self.order.retain(|(id, _)| entries.contains_key(id));
398- }
399-}
400-
401-enum RoomActorRequest {
402- Opened {
403- id: CommandId,
404- },
405- Execute {
406- id: CommandId,
407- command: TimelineCommand,
408- },
409- Close {
410- id: CommandId,
411- },
412-}
413-
414-struct RoomActor {
415- room_id: OwnedRoomId,
416- generation: u64,
417- room: Room,
418- receiver: mpsc::Receiver<RoomActorRequest>,
419- events: EventSender,
420- engine_events: mpsc::UnboundedSender<RoomActorEvent>,
421- media: Arc<Mutex<MediaRegistry>>,
422- initial_history_limit: u16,
423- open_ids: Vec<CommandId>,
424- background: JoinSet<(CommandId, Result<(), CommandError>)>,
425- background_ids: HashMap<TaskId, CommandId>,
426-}
427-
428-type BackgroundResult =
429- Result<(TaskId, (CommandId, Result<(), CommandError>)), tokio::task::JoinError>;
430-type BackgroundWork = std::pin::Pin<Box<dyn Future<Output = Result<(), CommandError>> + Send>>;
431-
432-/// network-bound work runs beside the actor so one slow redaction doesn't queue the room behind it
433-enum TimelineOutcome {
434- Done(Result<(), CommandError>),
435- Background(BackgroundWork),
436-}
437-
438-struct TimelineSession {
439- id: TimelineId,
440- timeline: Arc<Timeline>,
441- events: EventSender,
442- watcher: JoinHandle<()>,
443- media: Arc<Mutex<MediaRegistry>>,
444- current_user_id: Option<ruma::OwnedUserId>,
445- scope: MediaScope,
446- pagination_ids: Arc<Mutex<HashSet<CommandId>>>,
447- pagination_sender: mpsc::Sender<PaginationRequest>,
448- pagination_worker: JoinHandle<()>,
449- pagination_results: Option<mpsc::UnboundedSender<(CommandId, Result<(), CommandError>)>>,
450- pagination_result_task: JoinHandle<()>,
451-}
452-
453-struct PaginationRequest {
454- timeline: Arc<Timeline>,
455- events: EventSender,
456- timeline_id: TimelineId,
457- limit: u16,
458- command_id: Option<CommandId>,
459- results: mpsc::UnboundedSender<(CommandId, Result<(), CommandError>)>,
460-}
461-
462-impl TimelineSession {
463- async fn start(
464- id: TimelineId,
465- timeline: Timeline,
466- events: EventSender,
467- media: Arc<Mutex<MediaRegistry>>,
468- current_user_id: Option<ruma::OwnedUserId>,
469- initial_history_limit: u16,
470- ) -> Result<Self, SendEventError> {
471- let timeline = Arc::new(timeline);
472- let scope = MediaScope::new();
473- let watcher = start_timeline_watcher(
474- id.clone(),
475- timeline.clone(),
476- events.clone(),
477- media.clone(),
478- current_user_id.clone(),
479- scope.clone(),
480- )
481- .await?;
482-
483- let (pagination_sender, mut pagination_receiver) = mpsc::channel::<PaginationRequest>(32);
484- let pagination_worker = tokio::spawn(async move {
485- while let Some(request) = pagination_receiver.recv().await {
486- let command_id = request.command_id;
487- let results = request.results.clone();
488- if AssertUnwindSafe(run_pagination(request))
489- .catch_unwind()
490- .await
491- .is_err()
492- {
493- tracing::warn!(?command_id, "pagination task panicked");
494- if let Some(id) = command_id {
495- let _ = results.send((
496- id,
497- Err(CommandError::new(
498- CommandErrorKind::Timeline,
499- "pagination failed",
500- )),
501- ));
502- }
503- }
504- }
505- });
506- let pagination_ids = Arc::new(Mutex::new(HashSet::new()));
507- let (pagination_results, mut result_receiver) = mpsc::unbounded_channel();
508- let result_events = events.clone();
509- let result_ids = pagination_ids.clone();
510- let pagination_result_task = tokio::spawn(async move {
511- while let Some((id, result)) = result_receiver.recv().await {
512- if result_ids.lock().await.remove(&id) {
513- let _ = emit_command_result(&result_events, id, CommandKind::Timeline, result)
514- .await;
515- }
516- }
517- });
518-
519- let mut session = Self {
520- id,
521- timeline,
522- events,
523- watcher,
524- media,
525- current_user_id,
526- scope,
527- pagination_sender,
528- pagination_worker,
529- pagination_ids,
530- pagination_results: Some(pagination_results),
531- pagination_result_task,
532- };
533- if initial_history_limit > 0 {
534- session.paginate(initial_history_limit, None).await;
535- }
536- Ok(session)
537- }
538-
539- async fn resync(&mut self) -> Result<(), SendEventError> {
540- self.watcher.abort();
541- let _ = (&mut self.watcher).await;
542- self.watcher = start_timeline_watcher(
543- self.id.clone(),
544- self.timeline.clone(),
545- self.events.clone(),
546- self.media.clone(),
547- self.current_user_id.clone(),
548- self.scope.clone(),
549- )
550- .await?;
551- Ok(())
552- }
553-
554- async fn paginate(&mut self, limit: u16, command_id: Option<CommandId>) {
555- if let Some(id) = command_id {
556- self.pagination_ids.lock().await.insert(id);
557- }
558- let request = PaginationRequest {
559- timeline: self.timeline.clone(),
560- events: self.events.clone(),
561- timeline_id: self.id.clone(),
562- limit,
563- command_id,
564- results: self
565- .pagination_results
566- .as_ref()
567- .expect("pagination session is open")
568- .clone(),
569- };
570- if let Err(error) = self.pagination_sender.try_send(request) {
571- let kind = match error {
572- mpsc::error::TrySendError::Full(_) => CommandErrorKind::QueueFull,
573- mpsc::error::TrySendError::Closed(_) => CommandErrorKind::TimelineNotOpen,
574- };
575- if let Some(id) = command_id {
576- self.pagination_ids.lock().await.remove(&id);
577- let _ = emit_command_result(
578- &self.events,
579- id,
580- CommandKind::Timeline,
581- Err(CommandError::new(kind, "pagination queue is unavailable")),
582- )
583- .await;
584- }
585- }
586- }
587-
588- async fn close(mut self, kind: CommandErrorKind, message: &'static str) {
589- self.media.lock().await.release(&self.scope);
590- self.pagination_worker.abort();
591- let _ = (&mut self.pagination_worker).await;
592- drop(self.pagination_results.take());
593- let _ = (&mut self.pagination_result_task).await;
594- let pending = self.pagination_ids.lock().await.drain().collect::<Vec<_>>();
595- for id in pending {
596- let _ = emit_command_result(
597- &self.events,
598- id,
599- CommandKind::Timeline,
600- Err(CommandError::new(kind, message)),
601- )
602- .await;
603- }
604- self.watcher.abort();
605- let _ = (&mut self.watcher).await;
606- }
607-}
608-
609-async fn start_timeline_watcher(
610- id: TimelineId,
611- timeline: Arc<Timeline>,
612- events: EventSender,
613- media: Arc<Mutex<MediaRegistry>>,
614- current_user_id: Option<ruma::OwnedUserId>,
615- scope: MediaScope,
616-) -> Result<JoinHandle<()>, SendEventError> {
617- let (items, mut stream) = timeline.subscribe().await;
618- let normalised = normalise_timeline(items.iter(), current_user_id.as_deref(), &scope);
619- media.lock().await.insert_all(normalised.media);
620- events
621- .send(Event::TimelineReset {
622- timeline_id: id.clone(),
623- items: normalised.items,
624- })
625- .await?;
626- Ok(tokio::spawn(async move {
627- while let Some(diffs) = stream.next().await {
628- let normalised = normalise_timeline_diffs(diffs, current_user_id.as_deref(), &scope);
629- media.lock().await.insert_all(normalised.media);
630- if events
631- .send(Event::TimelineChanged {
632- timeline_id: id.clone(),
633- diffs: normalised.diffs,
634- })
635- .await
636- .is_err()
637- {
638- tracing::debug!(
639- ?id,
640- "timeline watcher stopped because the event receiver closed"
641- );
642- return;
643- }
644- }
645- tracing::debug!(?id, "timeline watcher stopped");
646- }))
647-}
648-
649-impl Drop for TimelineSession {
650- fn drop(&mut self) {
651- self.watcher.abort();
652- self.pagination_worker.abort();
653- self.pagination_result_task.abort();
654- }
655-}
656-
657-impl Engine {
658- pub async fn from_client(
659- client: Client,
660- config: EngineConfig,
661- ) -> Result<(Self, ClientHandle, EventStream), EngineError> {
662- if client.user_id().is_none() {
663- return Err(EngineError::Unauthenticated);
664- }
665- if config.command_capacity == 0
666- || config.event_capacity == 0
667- || config.max_media_sources == 0
668- {
669- return Err(EngineError::ZeroCapacity);
670- }
671- let session_changes = client.subscribe_to_session_changes();
672- let sync = Arc::new(
673- SyncService::builder(client.clone())
674- .with_offline_mode()
675- .build()
676- .await
677- .map_err(EngineError::SyncService)?,
678- );
679- let (handle, port, events) = transport_pair(
680- client.clone(),
681- sync.clone(),
682- config.command_capacity,
683- config.event_capacity,
684- );
685- client.send_queue().enable_upload_progress(true);
686- let (room_actor_event_sender, room_actor_events) = mpsc::unbounded_channel();
687- let (notification_commands, notification_command_receiver) =
688- mpsc::channel(config.command_capacity);
689- let (encryption_commands, encryption_command_receiver) =
690- mpsc::channel(config.command_capacity);
691- let (room_commands, room_command_receiver) = mpsc::channel(config.command_capacity);
692- Ok((
693- Self {
694- client,
695- sync,
696- port,
697- rooms: HashMap::new(),
698- next_room_generation: 0,
699- room_actor_events,
700- room_actor_event_sender,
701- upload_watchers: HashMap::new(),
702- tasks: TaskRegistry::new(),
703- session_changes: Some(session_changes),
704- initial_history_limit: config.initial_history_limit,
705- room_command_capacity: config.command_capacity,
706- max_media_bytes: config.max_media_bytes,
707- media: Arc::new(Mutex::new(MediaRegistry::new(config.max_media_sources))),
708- notification_commands,
709- notification_command_receiver: Some(notification_command_receiver),
710- encryption_commands,
711- encryption_command_receiver: Some(encryption_command_receiver),
712- verification: Arc::new(Mutex::new(VerificationController::default())),
713- room_commands,
714- room_command_receiver: Some(room_command_receiver),
715- global_account_gates: HashMap::new(),
716- room_account_gates: HashMap::new(),
717- media_flights: Arc::new(Mutex::new(HashMap::new())),
718- stop: Arc::new(tokio::sync::Notify::new()),
719- },
720- handle,
721- events,
722- ))
723- }
724-
725- pub async fn run(mut self) -> Result<(), EngineError> {
726- let sender = self.port.event_sender();
727- let user_id = self
728- .client
729- .user_id()
730- .expect("authentication was checked during construction")
731- .to_owned();
732- if sender.send(Event::Authenticated { user_id }).await.is_err() {
733- self.shutdown().await;
734- return Err(EngineError::EventReceiverClosed);
735- }
736- self.port
737- .connection_sender()
738- .send_replace(ConnectionState::Connecting);
739- if sender
740- .send(Event::ConnectionChanged(ConnectionState::Connecting))
741- .await
742- .is_err()
743- {
744- self.shutdown().await;
745- return Err(EngineError::EventReceiverClosed);
746- }
747- self.start_watchers();
748- self.start_domain_workers();
749- self.sync.start().await;
750- let mut terminal_error = None;
751-
752- loop {
753- let activity = tokio::select! {
754- biased;
755- _ = sender.closed() => {
756- terminal_error = Some(EngineError::EventReceiverClosed);
757- break;
758- }
759- () = self.stop.notified() => break,
760- request = self.port.recv() => EngineActivity::Request(request.map(Box::new)),
761- completed = self.tasks.join_next(), if !self.tasks.is_empty() => {
762- EngineActivity::Task(completed)
763- }
764- event = self.room_actor_events.recv() => EngineActivity::RoomActor(event),
765- };
766- match activity {
767- EngineActivity::Request(Some(request)) => {
768- let Some(envelope) = self.dispatch_request(*request).await else {
769- continue;
770- };
771- let id = envelope.id;
772- let command = envelope.command;
773- if matches!(command, Command::Lifecycle(LifecycleCommand::Shutdown)) {
774- let _ =
775- emit_command_result(&sender, id, CommandKind::Lifecycle, Ok(())).await;
776- break;
777- }
778- let kind = command.kind();
779- tracing::debug!(?id, ?kind, "dispatching command");
780- let result = self.execute(id, command).await;
781- if Self::emit_sync_result(&sender, id, kind, result)
782- .await
783- .is_err()
784- {
785- terminal_error = Some(EngineError::EventReceiverClosed);
786- break;
787- }
788- }
789- EngineActivity::Request(None) => break,
790- EngineActivity::Task(Some((context, result))) => {
791- if let Err(error) = result
792- && error.is_panic()
793- {
794- match context {
795- TaskContext::Command { id, kind } => {
796- if sender
797- .send(Event::CommandFailed(CommandFailure {
798- id,
799- command: kind,
800- kind: CommandErrorKind::Other,
801- message: "command task panicked".to_owned(),
802- }))
803- .await
804- .is_err()
805- {
806- terminal_error = Some(EngineError::EventReceiverClosed);
807- break;
808- }
809- }
810- context => {
811- terminal_error = Some(EngineError::TaskPanicked {
812- task: context.label(),
813- });
814- break;
815- }
816- }
817- }
818- }
819- EngineActivity::Task(None) => {}
820- EngineActivity::RoomActor(Some(event)) => {
821- if let Err(error) = self.handle_room_actor_event(event).await {
822- terminal_error = Some(error);
823- break;
824- }
825- }
826- EngineActivity::RoomActor(None) => {
827- terminal_error = Some(EngineError::TaskPanicked {
828- task: "room actor event channel".to_owned(),
829- });
830- break;
831- }
832- }
833- }
834-
835- if let Some(error) = terminal_error.as_ref() {
836- let _ = sender.send(Event::Failed(error.failure())).await;
837- }
838- self.shutdown().await;
839- let _ = sender.send(Event::Stopped).await;
840- if let Some(error) = terminal_error {
841- Err(error)
842- } else {
843- Ok(())
844- }
845- }
846-
847- async fn emit_sync_result(
848- sender: &EventSender,
849- id: CommandId,
850- kind: CommandKind,
851- result: Result<bool, CommandError>,
852- ) -> Result<(), SendEventError> {
853- match result {
854- Ok(true) => Ok(()),
855- Ok(false) => emit_command_result(sender, id, kind, Ok(())).await,
856- Err(error) => {
857- tracing::warn!(?id, ?kind, error = ?error, "command failed");
858- emit_command_result(sender, id, kind, Err(error)).await?;
859- Ok(())
860- }
861- }
862- }
863-
864- fn spawn_logout(&mut self, id: CommandId) {
865- let events = self.port.event_sender();
866- let client = self.client.clone();
867- let stop = self.stop.clone();
868- self.tasks.spawn(
869- TaskContext::Command {
870- id,
871- kind: CommandKind::Account,
872- },
873- async move {
874- match client
875- .logout()
876- .await
877- .map(|_| ())
878- .map_err(|source| CommandError::from_sdk("logout failed", source))
879- {
880- Ok(()) => {
881- let _ =
882- emit_command_result(&events, id, CommandKind::Account, Ok(())).await;
883- let _ = events.send(Event::LoggedOut).await;
884- stop.notify_one();
885- }
886- Err(error) => {
887- let _ = emit_command_result(&events, id, CommandKind::Account, Err(error))
888- .await;
889- }
890- }
891- },
892- );
893- }
894-
895- async fn dispatch_request(&mut self, request: DriverRequest) -> Option<CommandEnvelope> {
896- match request {
897- DriverRequest::Command(command) => Some(command),
898- DriverRequest::DeleteDevices {
899- id,
900- device_ids,
901- auth,
902- reply,
903- } => {
904- self.spawn_delete_devices(id, device_ids, auth, Some(reply));
905- None
906- }
907- DriverRequest::RoomResult { id, command, reply } => {
908- if let Err(error) = self.spawn_room_command_with_reply(id, command, Some(reply)) {
909- tracing::warn!(?id, error = ?error, "room result request failed");
910- }
911- None
912- }
913- DriverRequest::SetGlobalAccountData {
914- event_type,
915- content,
916- reply,
917- } => {
918- self.spawn_global_account_data_write(event_type, content, reply);
919- None
920- }
921- DriverRequest::SetRoomAccountData {
922- room_id,
923- event_type,
924- content,
925- reply,
926- } => {
927- self.spawn_room_account_data_write(room_id, event_type, content, reply);
928- None
929- }
930- }
931- }
932-
933- fn spawn_mark_read(&mut self, id: CommandId, timeline_id: TimelineId, event_id: OwnedEventId) {
934- let events = self.port.event_sender();
935- let client = self.client.clone();
936- self.tasks.spawn(
937- TaskContext::Command {
938- id,
939- kind: CommandKind::Room,
940- },
941- async move {
942- let result = mark_read(&client, &timeline_id, event_id).await;
943- let _ = emit_command_result(&events, id, CommandKind::Room, result).await;
944- },
945- );
946- }
947-
948- fn spawn_typing(&mut self, id: CommandId, room_id: OwnedRoomId, typing: bool) {
949- let events = self.port.event_sender();
950- let client = self.client.clone();
951- self.tasks.spawn(
952- TaskContext::Command {
953- id,
954- kind: CommandKind::Room,
955- },
956- async move {
957- let result = set_typing(&client, &room_id, typing).await;
958- let _ = emit_command_result(&events, id, CommandKind::Room, result).await;
959- },
960- );
961- }
962-
963- fn spawn_cancel_send(
964- &mut self,
965- id: CommandId,
966- room_id: OwnedRoomId,
967- transaction_id: matrix_sdk::ruma::OwnedTransactionId,
968- ) {
969- let events = self.port.event_sender();
970- let client = self.client.clone();
971- self.tasks.spawn(
972- TaskContext::Command {
973- id,
974- kind: CommandKind::Room,
975- },
976- async move {
977- let result = cancel_send(&client, &room_id, &transaction_id).await;
978- let _ = emit_command_result(&events, id, CommandKind::Room, result).await;
979- },
980- );
981- }
982-
983- fn spawn_retry_send(
984- &mut self,
985- id: CommandId,
986- room_id: OwnedRoomId,
987- transaction_id: matrix_sdk::ruma::OwnedTransactionId,
988- ) {
989- let events = self.port.event_sender();
990- let client = self.client.clone();
991- self.tasks.spawn(
992- TaskContext::Command {
993- id,
994- kind: CommandKind::Room,
995- },
996- async move {
997- let result = retry_send(&client, &room_id, &transaction_id).await;
998- let _ = emit_command_result(&events, id, CommandKind::Room, result).await;
999- },
1000- );
1001- }
1002-
1003- fn spawn_delete_devices(
1004- &mut self,
1005- id: CommandId,
1006- device_ids: Vec<OwnedDeviceId>,
1007- auth: Option<InteractiveAuth>,
1008- reply: Option<oneshot::Sender<Result<InteractiveAuthOutcome, CommandResultError>>>,
1009- ) {
1010- let events = self.port.event_sender();
1011- let client = self.client.clone();
1012- self.tasks.spawn(
1013- TaskContext::Command {
1014- id,
1015- kind: CommandKind::Account,
1016- },
1017- async move {
1018- let auth = match auth {
1019- Some(auth) => match auth.into_sdk(
1020- client
1021- .user_id()
1022- .expect("authentication was checked during construction"),
1023- ) {
1024- Ok(auth) => Some(auth),
1025- Err(message) => {
1026- let failure = CommandFailure {
1027- id,
1028- command: CommandKind::Account,
1029- kind: CommandErrorKind::Other,
1030- message: format!(
1031- "interactive authentication data is invalid, {message}"
1032- ),
1033- };
1034- if let Some(reply) = reply {
1035- let _ =
1036- reply.send(Err(CommandResultError::Failed(failure.clone())));
1037- } else {
1038- let _ = events.send(Event::CommandFailed(failure)).await;
1039- }
1040- return;
1041- }
1042- },
1043- None => None,
1044- };
1045- let result = client.delete_devices(&device_ids, auth).await;
1046-
1047- match result {
1048- Ok(_) => {
1049- if let Some(reply) = reply {
1050- let _ = reply.send(Ok(InteractiveAuthOutcome::Completed));
1051- } else {
1052- let _ = emit_command_result(&events, id, CommandKind::Account, Ok(()))
1053- .await;
1054- }
1055- }
1056- Err(source) => {
1057- if let Some(info) = source.as_uiaa_response() {
1058- let challenge = InteractiveAuthChallenge::from(info);
1059- if let Some(reply) = reply {
1060- let _ =
1061- reply.send(Ok(InteractiveAuthOutcome::Challenge(challenge)));
1062- } else {
1063- let _ = events
1064- .send(Event::InteractiveAuthRequired {
1065- id,
1066- command: CommandKind::Account,
1067- challenge: challenge.clone(),
1068- })
1069- .await;
1070- let _ = events
1071- .send(Event::CommandFailed(CommandFailure {
1072- id,
1073- command: CommandKind::Account,
1074- kind: CommandErrorKind::InteractiveAuthRequired,
1075- message: "interactive authentication is required"
1076- .to_owned(),
1077- }))
1078- .await;
1079- }
1080- } else {
1081- let error = CommandError::from_http("deleting devices failed", source);
1082- let failure = CommandFailure {
1083- id,
1084- command: CommandKind::Account,
1085- kind: error.kind,
1086- message: error.to_string(),
1087- };
1088- if let Some(reply) = reply {
1089- let _ =
1090- reply.send(Err(CommandResultError::Failed(failure.clone())));
1091- } else {
1092- let _ = events.send(Event::CommandFailed(failure)).await;
1093- }
1094- }
1095- }
1096- }
1097- },
1098- );
1099- }
1100-
1101- fn spawn_rename_device(
1102- &mut self,
1103- id: CommandId,
1104- device_id: OwnedDeviceId,
1105- display_name: String,
1106- ) {
1107- let events = self.port.event_sender();
1108- let client = self.client.clone();
1109- self.tasks.spawn(
1110- TaskContext::Command {
1111- id,
1112- kind: CommandKind::Account,
1113- },
1114- async move {
1115- match client.rename_device(&device_id, &display_name).await {
1116- Ok(_) => {
1117- let _ = events
1118- .send(Event::CommandSucceeded {
1119- id,
1120- command: CommandKind::Account,
1121- })
1122- .await;
1123- }
1124- Err(source) => {
1125- let error = CommandError::from_http("renaming device failed", source);
1126- let _ = events
1127- .send(Event::CommandFailed(CommandFailure {
1128- id,
1129- command: CommandKind::Account,
1130- kind: error.kind,
1131- message: error.to_string(),
1132- }))
1133- .await;
1134- }
1135- }
1136- },
1137- );
1138- }
1139-
1140- fn spawn_global_account_data_write(
1141- &mut self,
1142- event_type: GlobalAccountDataEventType,
1143- content: Raw<AnyGlobalAccountDataEventContent>,
1144- reply: oneshot::Sender<Result<(), ClientQueryError>>,
1145- ) {
1146- let (done, next) = oneshot::channel();
1147- let previous = self
1148- .global_account_gates
1149- .insert(event_type.to_string(), next);
1150- let client = self.client.clone();
1151- self.tasks.spawn(
1152- TaskContext::Watcher("global account data writer"),
1153- async move {
1154- if let Some(previous) = previous {
1155- let _ = previous.await;
1156- }
1157- let result = client
1158- .account()
1159- .set_account_data_raw(event_type, content)
1160- .await
1161- .map(|_| ())
1162- .map_err(ClientQueryError::Sdk);
1163- let _ = reply.send(result);
1164- let _ = done.send(());
1165- },
1166- );
1167- }
1168-
1169- fn spawn_room_account_data_write(
1170- &mut self,
1171- room_id: OwnedRoomId,
1172- event_type: RoomAccountDataEventType,
1173- content: Raw<matrix_sdk::ruma::events::AnyRoomAccountDataEventContent>,
1174- reply: oneshot::Sender<Result<(), ClientQueryError>>,
1175- ) {
1176- let (done, next) = oneshot::channel();
1177- let previous = self
1178- .room_account_gates
1179- .insert((room_id.clone(), event_type.to_string()), next);
1180- let client = self.client.clone();
1181- self.tasks.spawn(
1182- TaskContext::Watcher("room account data writer"),
1183- async move {
1184- if let Some(previous) = previous {
1185- let _ = previous.await;
1186- }
1187- let result = match client.get_room(&room_id) {
1188- Some(room) => room
1189- .set_account_data_raw(event_type, content)
1190- .await
1191- .map(|_| ())
1192- .map_err(ClientQueryError::Sdk),
1193- None => Err(ClientQueryError::RoomUnknown(room_id)),
1194- };
1195- let _ = reply.send(result);
1196- let _ = done.send(());
1197- },
1198- );
1199- }
1200-
1201- fn start_domain_workers(&mut self) {
1202- let mut notification_commands = self
1203- .notification_command_receiver
1204- .take()
1205- .expect("notification worker has not started");
1206- let notification_client = self.client.clone();
1207- let notification_events = self.port.event_sender();
1208- self.tasks.spawn(
1209- TaskContext::Watcher("notification command worker"),
1210- async move {
1211- while let Some(CommandJob { id, command }) = notification_commands.recv().await {
1212- let result = AssertUnwindSafe(execute_notification_command(
1213- &notification_client,
1214- command,
1215- ))
1216- .catch_unwind()
1217- .await
1218- .unwrap_or_else(|_| {
1219- Err(CommandError::new(
1220- CommandErrorKind::Other,
1221- "notification command panicked",
1222- ))
1223- });
1224- let _ = emit_command_result(
1225- &notification_events,
1226- id,
1227- CommandKind::Notification,
1228- result,
1229- )
1230- .await;
1231- }
1232- },
1233- );
1234-
1235- let mut encryption_commands = self
1236- .encryption_command_receiver
1237- .take()
1238- .expect("encryption worker has not started");
1239- let encryption_client = self.client.clone();
1240- let encryption_events = self.port.event_sender();
1241- let verification = self.verification.clone();
1242- self.tasks.spawn(
1243- TaskContext::Watcher("encryption command worker"),
1244- async move {
1245- while let Some(work) = encryption_commands.recv().await {
1246- match work {
1247- EncryptionWork::Command(CommandJob { id, command }) => {
1248- if matches!(
1249- command,
1250- EncryptionCommand::Recover(_)
1251- | EncryptionCommand::RecoverKeyBackup(_)
1252- ) {
1253- let result = AssertUnwindSafe(execute_encryption_command(
1254- &encryption_client,
1255- &encryption_events,
1256- &mut VerificationController::default(),
1257- command,
1258- ))
1259- .catch_unwind()
1260- .await
1261- .unwrap_or_else(|_| {
1262- Err(CommandError::new(
1263- CommandErrorKind::Other,
1264- "encryption command panicked",
1265- ))
1266- });
1267- let _ = emit_command_result(
1268- &encryption_events,
1269- id,
1270- CommandKind::Encryption,
1271- result,
1272- )
1273- .await;
1274- continue;
1275- }
1276- let is_key_backup = matches!(
1277- command,
1278- EncryptionCommand::CreateKeyBackup
1279- | EncryptionCommand::EnableKeyBackup
1280- | EncryptionCommand::DisableKeyBackup { .. }
1281- );
1282- let result = if is_key_backup {
1283- AssertUnwindSafe(execute_key_backup_command(
1284- &encryption_client,
1285- command,
1286- ))
1287- .catch_unwind()
1288- .await
1289- .unwrap_or_else(|_| {
1290- Err(CommandError::new(
1291- CommandErrorKind::Other,
1292- "encryption command panicked",
1293- ))
1294- })
1295- } else {
1296- let mut verification = verification.lock().await;
1297- let result = AssertUnwindSafe(execute_encryption_command(
1298- &encryption_client,
1299- &encryption_events,
1300- &mut verification,
1301- command,
1302- ))
1303- .catch_unwind()
1304- .await
1305- .unwrap_or_else(|_| {
1306- Err(CommandError::new(
1307- CommandErrorKind::Other,
1308- "encryption command panicked",
1309- ))
1310- });
1311- drop(verification);
1312- result
1313- };
1314- let _ = emit_command_result(
1315- &encryption_events,
1316- id,
1317- CommandKind::Encryption,
1318- result,
1319- )
1320- .await;
1321- }
1322- }
1323- }
1324- },
1325- );
1326-
1327- let room_commands = self
1328- .room_command_receiver
1329- .take()
1330- .expect("room command worker has not started");
1331- let room_client = self.client.clone();
1332- let room_events = self.port.event_sender();
1333- self.tasks.spawn(
1334- TaskContext::Watcher("room command worker"),
1335- run_room_command_worker(
1336- room_client,
1337- room_events,
1338- room_commands,
1339- self.room_command_capacity,
1340- ),
1341- );
1342- }
1343-
1344- fn start_watchers(&mut self) {
1345- let mut session_changes = self
1346- .session_changes
1347- .take()
1348- .expect("session watcher has not started");
1349- let session_sender = self.port.event_sender();
1350- let stop = self.stop.clone();
1351- self.tasks
1352- .spawn(TaskContext::Watcher("session change watcher"), async move {
1353- loop {
1354- let event = match session_changes.recv().await {
1355- Ok(SessionChange::TokensRefreshed) => Event::SessionTokensChanged,
1356- Ok(SessionChange::UnknownToken(data)) => {
1357- let _ = session_sender
1358- .send(Event::SessionAccessRejected {
1359- soft_logout: data.soft_logout,
1360- })
1361- .await;
1362- stop.notify_one();
1363- break;
1364- }
1365- Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
1366- Event::SessionTokensChanged
1367- }
1368- Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
1369- };
1370- if session_sender.send(event).await.is_err() {
1371- break;
1372- }
1373- }
1374- });
1375-
1376- let mut sync_states = self.sync.state();
1377- let state_sender = self.port.event_sender();
1378- let connection_sender = self.port.connection_sender();
1379- let sync = self.sync.clone();
1380- let client = self.client.clone();
1381- self.tasks.spawn(
1382- TaskContext::Watcher("connection state watcher"),
1383- async move {
1384- let mut backoff = SYNC_RESTART_MIN;
1385- while let Some(state) = sync_states.next().await {
1386- let (state, restart) = match state {
1387- SyncState::Idle => (ConnectionState::Idle, false),
1388- SyncState::Running => {
1389- backoff = SYNC_RESTART_MIN;
1390- // the sdk parks a room queue on any send error and never unparks it
1391- client.send_queue().set_enabled(true).await;
1392- (ConnectionState::Online, false)
1393- }
1394- SyncState::Offline => (ConnectionState::Offline, false),
1395- SyncState::Terminated => (ConnectionState::Offline, true),
1396- SyncState::Error(error) => {
1397- tracing::warn!(error = %error, "sync service reported an error");
1398- (ConnectionState::Offline, true)
1399- }
1400- };
1401- if *connection_sender.borrow() != state {
1402- connection_sender.send_replace(state);
1403- if state_sender
1404- .send(Event::ConnectionChanged(state))
1405- .await
1406- .is_err()
1407- {
1408- break;
1409- }
1410- }
1411- if restart {
1412- tokio::time::sleep(backoff).await;
1413- backoff = (backoff * 2).min(SYNC_RESTART_MAX);
1414- sync.start().await;
1415- }
1416- }
1417- },
1418- );
1419-
1420- let client = self.client.clone();
1421- let online = self.port.connection_sender().subscribe();
1422- self.tasks
1423- .spawn(TaskContext::Watcher("send queue watcher"), async move {
1424- let mut errors = client.send_queue().subscribe_errors();
1425- let mut retries = JoinSet::new();
1426- loop {
1427- let error = tokio::select! {
1428- error = errors.recv() => error,
1429- Some(_) = retries.join_next(), if !retries.is_empty() => continue,
1430- };
1431- match error {
1432- Ok(error) if error.is_recoverable => {
1433- let client = client.clone();
1434- let online = online.clone();
1435- retries.spawn(async move {
1436- tokio::time::sleep(SEND_QUEUE_RETRY).await;
1437- if *online.borrow() == ConnectionState::Online
1438- && let Some(room) = client.get_room(&error.room_id)
1439- {
1440- room.send_queue().set_enabled(true);
1441- }
1442- });
1443- }
1444- Ok(_) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
1445- Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
1446- }
1447- }
1448- });
1449-
1450- let client = self.client.clone();
1451- let encryption_sender = self.port.event_sender();
1452- self.tasks.spawn(
1453- TaskContext::Watcher("encryption state watcher"),
1454- async move {
1455- let Some(device_id) = client.device_id().map(ToOwned::to_owned) else {
1456- tracing::warn!("encryption watcher stopped because the client has no device id");
1457- return;
1458- };
1459- let recovery = client.encryption().recovery();
1460- let backups = client.encryption().backups();
1461- let mut recovery_stream = recovery.state_stream();
1462- let mut backup_stream = backups.state_stream();
1463- let mut verification_stream = client.encryption().verification_state();
1464- let mut recovery_state = recovery.state();
1465- let mut backup_state = backups.state();
1466- let mut verification_state = verification_stream.get();
1467- if encryption_sender
1468- .send(Event::EncryptionChanged(encryption_state(
1469- device_id.clone(),
1470- recovery_state,
1471- verification_state,
1472- backup_state,
1473- )))
1474- .await
1475- .is_err()
1476- {
1477- tracing::debug!(
1478- "encryption watcher stopped because the event receiver closed"
1479- );
1480- return;
1481- }
1482- loop {
1483- tokio::select! {
1484- update = recovery_stream.next() => {
1485- let Some(update) = update else {
1486- tracing::debug!("encryption watcher stopped because the recovery stream ended");
1487- break;
1488- };
1489- recovery_state = update;
1490- }
1491- update = verification_stream.next() => {
1492- let Some(update) = update else {
1493- tracing::debug!("encryption watcher stopped because the verification stream ended");
1494- break;
1495- };
1496- verification_state = update;
1497- }
1498- update = backup_stream.next() => {
1499- let Some(update) = update else {
1500- tracing::debug!("backup watcher stopped because the backup state stream ended");
1501- break;
1502- };
1503- backup_state = update.unwrap_or_else(|_| backups.state());
1504- }
1505- }
1506- if encryption_sender
1507- .send(Event::EncryptionChanged(encryption_state(
1508- device_id.clone(),
1509- recovery_state,
1510- verification_state,
1511- backup_state,
1512- )))
1513- .await
1514- .is_err()
1515- {
1516- tracing::debug!(
1517- "encryption watcher stopped because the event receiver closed"
1518- );
1519- break;
1520- }
1521- }
1522- },
1523- );
1524-
1525- let client = self.client.clone();
1526- let room_sender = self.port.event_sender();
1527- self.tasks.spawn(
1528- TaskContext::Watcher("room list watcher"),
1529- async move {
1530- let (_, mut room_stream) = client.rooms_stream();
1531- let mut info_stream = client.room_info_notable_update_receiver();
1532- if room_sender.send(Event::RoomsChanged).await.is_err() {
1533- tracing::debug!("room watcher stopped because the event receiver closed");
1534- return;
1535- }
1536- loop {
1537- tokio::select! {
1538- update = room_stream.next() => {
1539- if update.is_none() {
1540- tracing::debug!("room watcher stopped because the room stream ended");
1541- break;
1542- }
1543- }
1544- update = info_stream.recv() => {
1545- if update.is_err() && info_stream.is_closed() {
1546- tracing::debug!("room watcher stopped because the room info stream ended");
1547- break;
1548- }
1549- }
1550- }
1551- if room_sender.send(Event::RoomsChanged).await.is_err() {
1552- tracing::debug!("room watcher stopped because the event receiver closed");
1553- break;
1554- }
1555- }
1556- },
1557- );
1558-
1559- let client = self.client.clone();
1560- let notification_sender = self.port.event_sender();
1561- self.tasks.spawn(
1562- TaskContext::Watcher("notification settings watcher"),
1563- async move {
1564- let settings = client.notification_settings().await;
1565- let mut changes = settings.subscribe_to_changes();
1566- while let Ok(()) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) =
1567- changes.recv().await
1568- {
1569- if notification_sender
1570- .send(Event::NotificationSettingsChanged)
1571- .await
1572- .is_err()
1573- {
1574- break;
1575- }
1576- }
1577- },
1578- );
1579- }
1580-
1581- async fn execute(&mut self, id: CommandId, command: Command) -> Result<bool, CommandError> {
1582- match command {
1583- Command::Account(AccountCommand::Logout) => {
1584- self.spawn_logout(id);
1585- Ok(true)
1586- }
1587- Command::Account(AccountCommand::RenameDevice {
1588- device_id,
1589- display_name,
1590- }) => {
1591- self.spawn_rename_device(id, device_id, display_name);
1592- Ok(true)
1593- }
1594- Command::Account(AccountCommand::DeleteDevices { device_ids, auth }) => {
1595- self.spawn_delete_devices(id, device_ids, auth, None);
1596- Ok(true)
1597- }
1598- Command::Room(RoomCommand::Open(room_id)) => self.open_room(id, room_id).await,
1599- Command::Room(RoomCommand::Close(room_id)) => self.close_room(id, &room_id).await,
1600- Command::Room(RoomCommand::SetTyping { room_id, typing }) => {
1601- self.spawn_typing(id, room_id, typing);
1602- Ok(true)
1603- }
1604- Command::Room(RoomCommand::Join { room, via }) => {
1605- self.spawn_room_command(id, RoomCommand::Join { room, via })
1606- }
1607- Command::Room(RoomCommand::Leave(room_id)) => {
1608- self.spawn_room_command(id, RoomCommand::Leave(room_id))
1609- }
1610- Command::Room(RoomCommand::Forget(room_id)) => {
1611- self.spawn_room_command(id, RoomCommand::Forget(room_id))
1612- }
1613- Command::Room(RoomCommand::AcceptInvite(room_id)) => {
1614- self.spawn_room_command(id, RoomCommand::AcceptInvite(room_id))
1615- }
1616- Command::Room(RoomCommand::RejectInvite(room_id)) => {
1617- self.spawn_room_command(id, RoomCommand::RejectInvite(room_id))
1618- }
1619- Command::Room(command @ RoomCommand::InviteUser { .. })
1620- | Command::Room(command @ RoomCommand::KickUser { .. })
1621- | Command::Room(command @ RoomCommand::BanUser { .. })
1622- | Command::Room(command @ RoomCommand::UnbanUser { .. })
1623- | Command::Room(command @ RoomCommand::SetPowerLevels { .. })
1624- | Command::Room(command @ RoomCommand::SetUserPowerLevel { .. }) => {
1625- self.spawn_room_command(id, command)
1626- }
1627- Command::Room(RoomCommand::Create(options)) => {
1628- self.spawn_room_command(id, RoomCommand::Create(options))
1629- }
1630- Command::Room(RoomCommand::CreateDirect(user_id)) => {
1631- self.spawn_room_command(id, RoomCommand::CreateDirect(user_id))
1632- }
1633- Command::Room(RoomCommand::CancelSend {
1634- room_id,
1635- transaction_id,
1636- }) => {
1637- self.spawn_cancel_send(id, room_id, transaction_id);
1638- Ok(true)
1639- }
1640- Command::Room(RoomCommand::RetrySend {
1641- room_id,
1642- transaction_id,
1643- }) => {
1644- self.spawn_retry_send(id, room_id, transaction_id);
1645- Ok(true)
1646- }
1647- Command::Timeline(command) => self.route_timeline(id, command),
1648- Command::Media(MediaCommand::Fetch { id: media_id, size }) => {
1649- self.spawn_media_fetch(id, media_id, size);
1650- Ok(true)
1651- }
1652- Command::Media(MediaCommand::Upload {
1653- timeline_id,
1654- filename,
1655- mimetype,
1656- bytes,
1657- caption,
1658- }) => {
1659- let mut upload = AttachmentUpload::new(timeline_id, filename, mimetype, bytes);
1660- upload.caption = caption;
1661- self.spawn_media_upload(id, upload).await.map(|()| true)
1662- }
1663- Command::Media(MediaCommand::UploadAttachment(upload)) => {
1664- self.spawn_media_upload(id, upload).await.map(|()| true)
1665- }
1666- Command::Encryption(command) => {
1667- self.encryption_commands
1668- .try_send(EncryptionWork::Command(CommandJob { id, command }))
1669- .map_err(|error| domain_queue_error(error, "encryption"))?;
1670- Ok(true)
1671- }
1672- Command::Notification(command) => {
1673- self.notification_commands
1674- .try_send(CommandJob { id, command })
1675- .map_err(|error| domain_queue_error(error, "notification"))?;
1676- Ok(true)
1677- }
1678- Command::Lifecycle(LifecycleCommand::Shutdown) => Ok(false),
1679- }
1680- }
1681-
1682- async fn open_room(
1683- &mut self,
1684- id: CommandId,
1685- room_id: OwnedRoomId,
1686- ) -> Result<bool, CommandError> {
1687- match self.rooms.get_mut(&room_id) {
1688- Some(RoomActorState::Opening { sender, .. }) => {
1689- send_room_actor_request(sender, RoomActorRequest::Opened { id })?;
1690- return Ok(true);
1691- }
1692- Some(RoomActorState::Open { .. }) => return Ok(false),
1693- Some(RoomActorState::Closing { pending, .. }) => {
1694- if pending.len() >= self.room_command_capacity {
1695- return Err(CommandError::new(
1696- CommandErrorKind::RoomBusy,
1697- "room lifecycle queue is full",
1698- ));
1699- }
1700- pending.push_back(DeferredRoomLifecycle::Open(id));
1701- return Ok(true);
1702- }
1703- None => {}
1704- }
1705- self.start_room_actor(room_id, vec![id]).await?;
1706- Ok(true)
1707- }
1708-
1709- async fn start_room_actor(
1710- &mut self,
1711- room_id: OwnedRoomId,
1712- open_ids: Vec<CommandId>,
1713- ) -> Result<(), CommandError> {
1714- let room = self.client.get_room(&room_id).ok_or_else(|| {
1715- CommandError::new(
1716- CommandErrorKind::RoomUnknown,
1717- "room is not available in the local store",
1718- )
1719- })?;
1720- self.next_room_generation = self.next_room_generation.wrapping_add(1);
1721- let generation = self.next_room_generation;
1722- let (sender, receiver) = mpsc::channel(self.room_command_capacity);
1723- let actor = RoomActor {
1724- room_id: room_id.clone(),
1725- generation,
1726- room,
1727- receiver,
1728- events: self.port.event_sender(),
1729- engine_events: self.room_actor_event_sender.clone(),
1730- media: self.media.clone(),
1731- initial_history_limit: self.initial_history_limit,
1732- open_ids,
1733- background: JoinSet::new(),
1734- background_ids: HashMap::new(),
1735- };
1736- let current_user_id = self.client.user_id().map(ToOwned::to_owned);
1737- self.rooms.insert(
1738- room_id.clone(),
1739- RoomActorState::Opening { generation, sender },
1740- );
1741- let actor_room_id = room_id.clone();
1742- let engine_events = self.room_actor_event_sender.clone();
1743- self.tasks.spawn(
1744- TaskContext::RoomActor {
1745- room_id: room_id.clone(),
1746- generation,
1747- },
1748- async move {
1749- actor.run(current_user_id).await;
1750- let _ = engine_events.send(RoomActorEvent::Exited {
1751- room_id: actor_room_id,
1752- generation,
1753- });
1754- },
1755- );
1756- self.update_room_subscriptions().await;
1757- Ok(())
1758- }
1759-
1760- fn spawn_room_command(
1761- &mut self,
1762- id: CommandId,
1763- command: RoomCommand,
1764- ) -> Result<bool, CommandError> {
1765- self.spawn_room_command_with_reply(id, command, None)?;
1766- Ok(true)
1767- }
1768-
1769- fn spawn_room_command_with_reply(
1770- &mut self,
1771- id: CommandId,
1772- command: RoomCommand,
1773- reply: Option<oneshot::Sender<Result<OwnedRoomId, CommandResultError>>>,
1774- ) -> Result<(), CommandError> {
1775- let job = RoomJob { id, command, reply };
1776- self.room_commands.try_send(job).map_err(|error| {
1777- let (job, kind, message) = match error {
1778- mpsc::error::TrySendError::Full(job) => (
1779- job,
1780- CommandErrorKind::QueueFull,
1781- "room command queue is full",
1782- ),
1783- mpsc::error::TrySendError::Closed(job) => (
1784- job,
1785- CommandErrorKind::Other,
1786- "room command worker has stopped",
1787- ),
1788- };
1789- let error = CommandError::new(kind, message);
1790- if let Some(reply) = job.reply {
1791- let _ = reply.send(Err(CommandResultError::Failed(CommandFailure {
1792- id: job.id,
1793- command: CommandKind::Room,
1794- kind: error.kind,
1795- message: error.to_string(),
1796- })));
1797- }
1798- error
1799- })
1800- }
1801-
1802- fn route_timeline(
1803- &mut self,
1804- id: CommandId,
1805- command: TimelineCommand,
1806- ) -> Result<bool, CommandError> {
1807- let sender = self
1808- .rooms
1809- .get(command.room_id())
1810- .and_then(RoomActorState::sender)
1811- .cloned();
1812- match (sender, command) {
1813- (Some(sender), command) => {
1814- send_room_actor_request(&sender, RoomActorRequest::Execute { id, command })?;
1815- }
1816- (
1817- None,
1818- TimelineCommand::MarkRead {
1819- timeline_id,
1820- event_id,
1821- },
1822- ) => self.spawn_mark_read(id, timeline_id, event_id),
1823- (None, _) => {
1824- return Err(CommandError::new(
1825- CommandErrorKind::RoomNotOpen,
1826- "room is not open",
1827- ));
1828- }
1829- }
1830- Ok(true)
1831- }
1832-
1833- async fn close_room(&mut self, id: CommandId, room_id: &RoomId) -> Result<bool, CommandError> {
1834- let Some(state) = self.rooms.get_mut(room_id) else {
1835- return Ok(false);
1836- };
1837- if let RoomActorState::Closing { pending, .. } = state {
1838- if pending.len() >= self.room_command_capacity {
1839- return Err(CommandError::new(
1840- CommandErrorKind::RoomBusy,
1841- "room lifecycle queue is full",
1842- ));
1843- }
1844- pending.push_back(DeferredRoomLifecycle::Close(id));
1845- return Ok(true);
1846- }
1847- let Some(sender) = state.sender().cloned() else {
1848- return Ok(false);
1849- };
1850- let generation = state.generation();
1851- send_room_actor_request(&sender, RoomActorRequest::Close { id })?;
1852- self.rooms.insert(
1853- room_id.to_owned(),
1854- RoomActorState::Closing {
1855- generation,
1856- pending: VecDeque::new(),
1857- },
1858- );
1859- self.update_room_subscriptions().await;
1860- Ok(true)
1861- }
1862-
1863- async fn handle_room_actor_event(&mut self, event: RoomActorEvent) -> Result<(), EngineError> {
1864- match event {
1865- RoomActorEvent::Ready {
1866- room_id,
1867- generation,
1868- } => {
1869- let stale = self
1870- .rooms
1871- .get(&room_id)
1872- .is_none_or(|state| state.generation() != generation);
1873- if stale {
1874- return Ok(());
1875- }
1876- let Some(state) = self.rooms.remove(&room_id) else {
1877- return Ok(());
1878- };
1879- match state {
1880- RoomActorState::Opening {
1881- generation: current,
1882- sender,
1883- } if current == generation => {
1884- self.rooms
1885- .insert(room_id, RoomActorState::Open { generation, sender });
1886- }
1887- state => {
1888- self.rooms.insert(room_id, state);
1889- }
1890- }
1891- }
1892- RoomActorEvent::Exited {
1893- room_id,
1894- generation,
1895- } => {
1896- if self
1897- .rooms
1898- .get(&room_id)
1899- .is_none_or(|state| state.generation() != generation)
1900- {
1901- return Ok(());
1902- }
1903- let state = self
1904- .rooms
1905- .remove(&room_id)
1906- .expect("room generation was checked");
1907- let pending = match state {
1908- RoomActorState::Closing { pending, .. } => pending,
1909- RoomActorState::Opening { .. } | RoomActorState::Open { .. } => VecDeque::new(),
1910- };
1911- for operation in pending {
1912- let (id, result) = match operation {
1913- DeferredRoomLifecycle::Open(id) => {
1914- (id, self.open_room(id, room_id.clone()).await)
1915- }
1916- DeferredRoomLifecycle::Close(id) => {
1917- (id, self.close_room(id, &room_id).await)
1918- }
1919- };
1920- match result {
1921- Ok(true) => {}
1922- Ok(false) => emit_command_result(
1923- &self.port.event_sender(),
1924- id,
1925- CommandKind::Room,
1926- Ok(()),
1927- )
1928- .await
1929- .map_err(|_| EngineError::EventReceiverClosed)?,
1930- Err(error) => emit_command_result(
1931- &self.port.event_sender(),
1932- id,
1933- CommandKind::Room,
1934- Err(error),
1935- )
1936- .await
1937- .map_err(|_| EngineError::EventReceiverClosed)?,
1938- }
1939- }
1940- self.update_room_subscriptions().await;
1941- }
1942- }
1943- Ok(())
1944- }
1945-
1946- async fn update_room_subscriptions(&self) {
1947- let room_ids = self
1948- .rooms
1949- .iter()
1950- .filter(|(_, state)| !matches!(state, RoomActorState::Closing { .. }))
1951- .map(|(room_id, _)| room_id.clone())
1952- .collect::<Vec<_>>();
1953- let room_refs = room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>();
1954- self.sync
1955- .room_list_service()
1956- .subscribe_to_rooms(&room_refs)
1957- .await;
1958- }
1959-
1960- fn spawn_media_fetch(&mut self, command_id: CommandId, media_id: MediaId, size: MediaSize) {
1961- let client = self.client.clone();
1962- let media = self.media.clone();
1963- let events = self.port.event_sender();
1964- let max_media_bytes = self.max_media_bytes;
1965- let flights = self.media_flights.clone();
1966- self.tasks.spawn(
1967- TaskContext::Command {
1968- id: command_id,
1969- kind: CommandKind::Media,
1970- },
1971- async move {
1972- let leader = {
1973- let mut flights = flights.lock().await;
1974- let waiters = flights.entry((media_id.clone(), size)).or_default();
1975- waiters.push(command_id);
1976- waiters.len() == 1
1977- };
1978- if !leader {
1979- return;
1980- }
1981- let result = fetch_media(
1982- client,
1983- media,
1984- events.clone(),
1985- max_media_bytes,
1986- media_id.clone(),
1987- size,
1988- )
1989- .await;
1990- let waiters = {
1991- let mut flights = flights.lock().await;
1992- flights.remove(&(media_id, size)).unwrap_or_default()
1993- };
1994- let result = match result {
1995- Ok(()) => Ok(()),
1996- Err(error) => {
1997- let kind = error.kind;
1998- let message = error.to_string();
1999- Err((kind, message))
2000- }
2001- };
2002- for waiter in waiters {
2003- let waiter_result = match &result {
2004- Ok(()) => Ok(()),
2005- Err((kind, message)) => Err(CommandError::new(*kind, message.clone())),
2006- };
2007- if emit_command_result(&events, waiter, CommandKind::Media, waiter_result)
2008- .await
2009- .is_err()
2010- {
2011- tracing::debug!(?waiter, "media fetch result was not delivered");
2012- }
2013- }
2014- },
2015- );
2016- }
2017-
2018- async fn spawn_media_upload(
2019- &mut self,
2020- command_id: CommandId,
2021- upload: AttachmentUpload,
2022- ) -> Result<(), CommandError> {
2023- let room_id = upload.timeline_id.room_id().clone();
2024- let room = self.client.get_room(&room_id).ok_or_else(|| {
2025- CommandError::new(
2026- CommandErrorKind::RoomUnknown,
2027- "room is not available in the local store",
2028- )
2029- })?;
2030- if self
2031- .upload_watchers
2032- .get(&room_id)
2033- .is_some_and(|(watcher, _)| watcher.is_finished())
2034- {
2035- self.upload_watchers.remove(&room_id);
2036- }
2037- let ready = if let Some((_, ready)) = self.upload_watchers.get(&room_id) {
2038- ready.clone()
2039- } else {
2040- let (ready_sender, ready) = watch::channel(None);
2041- let watched_room_id = room_id.clone();
2042- let progress_room_id = room_id.clone();
2043- let queued_room = room.clone();
2044- let events = self.port.event_sender();
2045- let watcher =
2046- self.tasks
2047- .spawn(TaskContext::Watcher("media upload watcher"), async move {
2048- let (_, mut updates) = match queued_room.send_queue().subscribe().await {
2049- Ok(subscription) => subscription,
2050- Err(source) => {
2051- tracing::warn!(room_id = %progress_room_id, error = %source, "reading media send queue failed");
2052- ready_sender.send_replace(Some(Err(CommandErrorKind::Store)));
2053- return;
2054- }
2055- };
2056- ready_sender.send_replace(Some(Ok(())));
2057- loop {
2058- match updates.recv().await {
2059- Ok(RoomSendQueueUpdate::MediaUpload {
2060- related_to,
2061- file,
2062- progress,
2063- ..
2064- }) => {
2065- if events
2066- .send(Event::MediaUploadProgress {
2067- room_id: progress_room_id.clone(),
2068- transaction_id: related_to,
2069- uploaded_bytes: progress.current as u64,
2070- total_bytes: progress.total as u64,
2071- complete: file.is_some()
2072- && progress.current == progress.total,
2073- })
2074- .await
2075- .is_err()
2076- {
2077- break;
2078- }
2079- }
2080- Ok(RoomSendQueueUpdate::SendError {
2081- transaction_id,
2082- error,
2083- is_recoverable,
2084- }) => {
2085- let is_attachment = queued_room
2086- .send_queue()
2087- .subscribe()
2088- .await
2089- .is_ok_and(|(echoes, _)| {
2090- echoes.iter().any(|echo| {
2091- echo.transaction_id == transaction_id
2092- && is_attachment_echo(&echo.content)
2093- })
2094- });
2095- if is_attachment
2096- && events
2097- .send(Event::MediaUploadFailed {
2098- room_id: progress_room_id.clone(),
2099- transaction_id,
2100- kind: classify_sdk_error(&error),
2101- recoverable: is_recoverable,
2102- })
2103- .await
2104- .is_err()
2105- {
2106- break;
2107- }
2108- }
2109- Ok(_)
2110- | Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
2111- Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
2112- }
2113- }
2114- });
2115- self.upload_watchers
2116- .insert(watched_room_id, (watcher, ready.clone()));
2117- ready
2118- };
2119- let max_media_bytes = self.max_media_bytes;
2120- let events = self.port.event_sender();
2121- self.tasks.spawn(
2122- TaskContext::Command {
2123- id: command_id,
2124- kind: CommandKind::Media,
2125- },
2126- async move {
2127- let mut ready = ready;
2128- let result = async {
2129- loop {
2130- if let Some(result) = *ready.borrow_and_update() {
2131- result.map_err(|kind| {
2132- CommandError::new(kind, "reading media send queue failed")
2133- })?;
2134- break;
2135- }
2136- ready.changed().await.map_err(|_| {
2137- CommandError::new(
2138- CommandErrorKind::Store,
2139- "media send queue watcher stopped",
2140- )
2141- })?;
2142- }
2143- queue_media_upload(room, upload, max_media_bytes).await
2144- }
2145- .await;
2146- if emit_command_result(&events, command_id, CommandKind::Media, result)
2147- .await
2148- .is_err()
2149- {
2150- tracing::debug!(?command_id, "media upload result was not delivered");
2151- }
2152- },
2153- );
2154- Ok(())
2155- }
2156-
2157- async fn shutdown(&mut self) {
2158- self.tasks.shutdown().await;
2159- self.verification.lock().await.stop().await;
2160- self.rooms.clear();
2161- self.upload_watchers.clear();
2162- let mut media = self.media.lock().await;
2163- media.entries.clear();
2164- media.order.clear();
2165- drop(media);
2166- self.sync.stop().await;
2167- self.port
2168- .connection_sender()
2169- .send_replace(ConnectionState::Stopped);
2170- let _ = self
2171- .port
2172- .send(Event::ConnectionChanged(ConnectionState::Stopped))
2173- .await;
2174- }
2175-}
2176-
2177-async fn mark_read(
2178- client: &Client,
2179- timeline_id: &TimelineId,
2180- event_id: OwnedEventId,
2181-) -> Result<(), CommandError> {
2182- let room = client
2183- .get_room(timeline_id.room_id())
2184- .ok_or_else(room_unknown)?;
2185- let receipt_thread = match timeline_id {
2186- TimelineId::Room(_) => ReceiptThread::Main,
2187- TimelineId::Thread { root_event_id, .. } => ReceiptThread::Thread(root_event_id.clone()),
2188- };
2189- room.send_single_receipt(ReceiptType::Read, receipt_thread, event_id.clone())
2190- .await
2191- .map_err(|source| CommandError::from_sdk("sending read receipt failed", source))?;
2192- if matches!(timeline_id, TimelineId::Room(_)) {
2193- room.send_single_receipt(ReceiptType::FullyRead, ReceiptThread::Unthreaded, event_id)
2194- .await
2195- .map_err(|source| {
2196- CommandError::from_sdk("sending fully read receipt failed", source)
2197- })?;
2198- }
2199- Ok(())
2200-}
2201-
2202-async fn set_typing(client: &Client, room_id: &RoomId, typing: bool) -> Result<(), CommandError> {
2203- let room = client.get_room(room_id).ok_or_else(room_unknown)?;
2204- room.typing_notice(typing)
2205- .await
2206- .map_err(|source| CommandError::from_sdk("typing update failed", source))
2207-}
2208-
2209-fn is_attachment_echo(content: &LocalEchoContent) -> bool {
2210- let LocalEchoContent::Event {
2211- serialized_event, ..
2212- } = content
2213- else {
2214- return false;
2215- };
2216- matches!(
2217- serialized_event.deserialize(),
2218- Ok(AnyMessageLikeEventContent::RoomMessage(content))
2219- if matches!(
2220- content.msgtype,
2221- MessageType::Audio(_) | MessageType::File(_) | MessageType::Image(_) | MessageType::Video(_)
2222- )
2223- )
2224-}
2225-
2226-async fn send_handle(
2227- client: &Client,
2228- room_id: &RoomId,
2229- transaction_id: &matrix_sdk::ruma::TransactionId,
2230-) -> Result<SendHandle, CommandError> {
2231- let room = client.get_room(room_id).ok_or_else(room_unknown)?;
2232- let (echoes, _) = room.send_queue().subscribe().await.map_err(|source| {
2233- CommandError::with_source(CommandErrorKind::Store, "reading send queue failed", source)
2234- })?;
2235- echoes
2236- .into_iter()
2237- .find_map(|echo| {
2238- if echo.transaction_id.as_str() != transaction_id.as_str() {
2239- return None;
2240- }
2241- match echo.content {
2242- LocalEchoContent::Event { send_handle, .. } => Some(send_handle),
2243- _ => None,
2244- }
2245- })
2246- .ok_or_else(|| {
2247- CommandError::new(
2248- CommandErrorKind::Other,
2249- "send is no longer pending or is not a message",
2250- )
2251- })
2252-}
2253-
2254-async fn cancel_send(
2255- client: &Client,
2256- room_id: &RoomId,
2257- transaction_id: &matrix_sdk::ruma::TransactionId,
2258-) -> Result<(), CommandError> {
2259- let handle = send_handle(client, room_id, transaction_id).await?;
2260- match handle.abort().await {
2261- Ok(true) => Ok(()),
2262- Ok(false) => Err(CommandError::new(
2263- CommandErrorKind::Other,
2264- "send could not be cancelled because it has already been sent",
2265- )),
2266- Err(source) => Err(CommandError::with_source(
2267- CommandErrorKind::Store,
2268- "cancelling send failed",
2269- source,
2270- )),
2271- }
2272-}
2273-
2274-async fn retry_send(
2275- client: &Client,
2276- room_id: &RoomId,
2277- transaction_id: &matrix_sdk::ruma::TransactionId,
2278-) -> Result<(), CommandError> {
2279- let handle = send_handle(client, room_id, transaction_id).await?;
2280- handle.unwedge().await.map_err(|source| {
2281- CommandError::with_source(CommandErrorKind::Store, "retrying send failed", source)
2282- })?;
2283- if let Some(room) = client.get_room(room_id) {
2284- room.send_queue().set_enabled(true);
2285- }
2286- Ok(())
2287-}
2288-
2289-fn send_room_actor_request(
2290- sender: &mpsc::Sender<RoomActorRequest>,
2291- request: RoomActorRequest,
2292-) -> Result<(), CommandError> {
2293- sender.try_send(request).map_err(|error| match error {
2294- mpsc::error::TrySendError::Full(_) => {
2295- CommandError::new(CommandErrorKind::RoomBusy, "room command queue is full")
2296- }
2297- mpsc::error::TrySendError::Closed(_) => {
2298- CommandError::new(CommandErrorKind::RoomNotOpen, "room is not open")
2299- }
2300- })
2301-}
2302-
2303-fn domain_queue_error<T>(error: mpsc::error::TrySendError<T>, domain: &str) -> CommandError {
2304- match error {
2305- mpsc::error::TrySendError::Full(_) => CommandError::new(
2306- CommandErrorKind::QueueFull,
2307- format!("{domain} command queue is full"),
2308- ),
2309- mpsc::error::TrySendError::Closed(_) => CommandError::new(
2310- CommandErrorKind::Other,
2311- format!("{domain} command worker has stopped"),
2312- ),
2313- }
2314-}
2315-
2316-async fn run_room_command_worker(
2317- client: Client,
2318- events: EventSender,
2319- mut commands: mpsc::Receiver<RoomJob>,
2320- queue_limit: usize,
2321-) {
2322- let mut queues = HashMap::<RoomMutationKey, VecDeque<RoomJob>>::new();
2323- let mut running = HashSet::<RoomMutationKey>::new();
2324- let mut tasks = JoinSet::<RoomJobCompletion>::new();
2325- let mut receiving = true;
2326- let mut queued = 0_usize;
2327- while receiving || !tasks.is_empty() {
2328- tokio::select! {
2329- biased;
2330- completed = tasks.join_next(), if !tasks.is_empty() => {
2331- let completion = match completed {
2332- Some(Ok(completion)) => completion,
2333- Some(Err(error)) => panic!("room command task failed: {error}"),
2334- None => continue,
2335- };
2336- let key = completion.key.clone();
2337- emit_room_job_result(&events, completion).await;
2338- let next = queues.get_mut(&key).and_then(VecDeque::pop_front);
2339- if next.is_some() {
2340- queued -= 1;
2341- }
2342- if queues.get(&key).is_some_and(VecDeque::is_empty) {
2343- queues.remove(&key);
2344- }
2345- if let Some(job) = next {
2346- spawn_room_job(&mut tasks, client.clone(), key, job);
2347- } else {
2348- running.remove(&key);
2349- }
2350- }
2351- job = commands.recv(), if receiving => {
2352- match job {
2353- Some(job) => {
2354- let key = room_mutation_key(job.id, &job.command);
2355- if running.insert(key.clone()) {
2356- spawn_room_job(&mut tasks, client.clone(), key, job);
2357- } else if queued < queue_limit {
2358- queues.entry(key).or_default().push_back(job);
2359- queued += 1;
2360- } else {
2361- let failure = CommandFailure {
2362- id: job.id,
2363- command: CommandKind::Room,
2364- kind: CommandErrorKind::QueueFull,
2365- message: "room command queue is full".to_owned(),
2366- };
2367- if let Some(reply) = job.reply {
2368- let _ = reply.send(Err(CommandResultError::Failed(failure)));
2369- } else {
2370- let _ = events.send(Event::CommandFailed(failure)).await;
2371- }
2372- }
2373- }
2374- None => receiving = false,
2375- }
2376- }
2377- }
2378- }
2379-}
2380-
2381-fn spawn_room_job(
2382- tasks: &mut JoinSet<RoomJobCompletion>,
2383- client: Client,
2384- key: RoomMutationKey,
2385- job: RoomJob,
2386-) {
2387- tasks.spawn(async move {
2388- let RoomJob { id, command, reply } = job;
2389- let result = AssertUnwindSafe(execute_room_command(&client, command))
2390- .catch_unwind()
2391- .await
2392- .unwrap_or_else(|_| {
2393- Err(CommandError::new(
2394- CommandErrorKind::Other,
2395- "room command panicked",
2396- ))
2397- });
2398- RoomJobCompletion {
2399- key,
2400- id,
2401- reply,
2402- result,
2403- }
2404- });
2405-}
2406-
2407-fn room_mutation_key(id: CommandId, command: &RoomCommand) -> RoomMutationKey {
2408- match command {
2409- RoomCommand::Join { room, .. } if room.is_room_id() => RoomMutationKey::Room(
2410- OwnedRoomId::try_from(room.clone()).expect("room ID variant was checked"),
2411- ),
2412- RoomCommand::Join { room, .. } => RoomMutationKey::Target(room.to_string()),
2413- RoomCommand::Leave(room_id)
2414- | RoomCommand::Forget(room_id)
2415- | RoomCommand::AcceptInvite(room_id)
2416- | RoomCommand::RejectInvite(room_id)
2417- | RoomCommand::InviteUser { room_id, .. }
2418- | RoomCommand::KickUser { room_id, .. }
2419- | RoomCommand::BanUser { room_id, .. }
2420- | RoomCommand::UnbanUser { room_id, .. }
2421- | RoomCommand::SetPowerLevels { room_id, .. }
2422- | RoomCommand::SetUserPowerLevel { room_id, .. } => RoomMutationKey::Room(room_id.clone()),
2423- RoomCommand::CreateDirect(user_id) => RoomMutationKey::Direct(user_id.clone()),
2424- _ => RoomMutationKey::Independent(id),
2425- }
2426-}
2427-
2428-async fn emit_room_job_result(events: &EventSender, completion: RoomJobCompletion) {
2429- let RoomJobCompletion {
2430- id, reply, result, ..
2431- } = completion;
2432- match result {
2433- Ok(Some(room_id)) => {
2434- if let Some(reply) = reply {
2435- let _ = reply.send(Ok(room_id.clone()));
2436- } else {
2437- let _ = emit_command_result_with_room(events, id, room_id).await;
2438- }
2439- }
2440- Ok(None) => {
2441- if let Some(reply) = reply {
2442- let _ = reply.send(Err(CommandResultError::Failed(CommandFailure {
2443- id,
2444- command: CommandKind::Room,
2445- kind: CommandErrorKind::Other,
2446- message: "room operation did not return a room ID".to_owned(),
2447- })));
2448- } else {
2449- let _ = emit_command_result(events, id, CommandKind::Room, Ok(())).await;
2450- }
2451- }
2452- Err(error) => {
2453- let failure = CommandFailure {
2454- id,
2455- command: CommandKind::Room,
2456- kind: error.kind,
2457- message: error.to_string(),
2458- };
2459- if let Some(reply) = reply {
2460- let _ = reply.send(Err(CommandResultError::Failed(failure)));
2461- } else {
2462- let _ = events.send(Event::CommandFailed(failure)).await;
2463- }
2464- }
2465- }
2466-}
2467-
2468-async fn execute_room_command(
2469- client: &Client,
2470- command: RoomCommand,
2471-) -> Result<Option<OwnedRoomId>, CommandError> {
2472- match command {
2473- RoomCommand::Join { room, via } => client
2474- .join_room_by_id_or_alias(&room, &via)
2475- .await
2476- .map(|room| Some(room.room_id().to_owned()))
2477- .map_err(|source| CommandError::from_sdk("joining room failed", source)),
2478- RoomCommand::Leave(room_id) => match client.get_room(&room_id) {
2479- Some(room) => room
2480- .leave()
2481- .await
2482- .map(|_| None)
2483- .map_err(|source| CommandError::from_sdk("leaving room failed", source)),
2484- None => Err(room_unknown()),
2485- },
2486- RoomCommand::Forget(room_id) => match client.get_room(&room_id) {
2487- Some(room) => room
2488- .forget()
2489- .await
2490- .map(|_| None)
2491- .map_err(|source| CommandError::from_sdk("forgetting room failed", source)),
2492- None => Err(room_unknown()),
2493- },
2494- RoomCommand::AcceptInvite(room_id) => match client.get_room(&room_id) {
2495- Some(room) => room
2496- .join()
2497- .await
2498- .map(|_| None)
2499- .map_err(|source| CommandError::from_sdk("accepting invite failed", source)),
2500- None => Err(room_unknown()),
2501- },
2502- RoomCommand::RejectInvite(room_id) => match client.get_room(&room_id) {
2503- Some(room) => room
2504- .leave()
2505- .await
2506- .map(|_| None)
2507- .map_err(|source| CommandError::from_sdk("rejecting invite failed", source)),
2508- None => Err(room_unknown()),
2509- },
2510- RoomCommand::InviteUser { room_id, user_id } => match client.get_room(&room_id) {
2511- Some(room) => room
2512- .invite_user_by_id(&user_id)
2513- .await
2514- .map(|()| None)
2515- .map_err(|source| CommandError::from_sdk("inviting user failed", source)),
2516- None => Err(room_unknown()),
2517- },
2518- RoomCommand::KickUser {
2519- room_id,
2520- user_id,
2521- reason,
2522- } => match client.get_room(&room_id) {
2523- Some(room) => room
2524- .kick_user(&user_id, reason.as_deref())
2525- .await
2526- .map(|_| None)
2527- .map_err(|source| CommandError::from_sdk("kicking user failed", source)),
2528- None => Err(room_unknown()),
2529- },
2530- RoomCommand::BanUser {
2531- room_id,
2532- user_id,
2533- reason,
2534- } => match client.get_room(&room_id) {
2535- Some(room) => room
2536- .ban_user(&user_id, reason.as_deref())
2537- .await
2538- .map(|_| None)
2539- .map_err(|source| CommandError::from_sdk("banning user failed", source)),
2540- None => Err(room_unknown()),
2541- },
2542- RoomCommand::UnbanUser {
2543- room_id,
2544- user_id,
2545- reason,
2546- } => match client.get_room(&room_id) {
2547- Some(room) => room
2548- .unban_user(&user_id, reason.as_deref())
2549- .await
2550- .map(|_| None)
2551- .map_err(|source| CommandError::from_sdk("unbanning user failed", source)),
2552- None => Err(room_unknown()),
2553- },
2554- RoomCommand::SetPowerLevels { room_id, changes } => match client.get_room(&room_id) {
2555- Some(room) => apply_power_level_changes(&room, changes)
2556- .await
2557- .map(|_| None),
2558- None => Err(room_unknown()),
2559- },
2560- RoomCommand::SetUserPowerLevel {
2561- room_id,
2562- user_id,
2563- level,
2564- } => match client.get_room(&room_id) {
2565- Some(room) => match matrix_sdk::ruma::Int::try_from(level) {
2566- Ok(level) => room
2567- .update_power_levels(vec![(&user_id, level)])
2568- .await
2569- .map(|_| None)
2570- .map_err(|source| {
2571- CommandError::from_sdk("updating user power level failed", source)
2572- }),
2573- Err(_) => Err(CommandError::new(
2574- CommandErrorKind::Other,
2575- "power level is outside the Matrix integer range",
2576- )),
2577- },
2578- None => Err(room_unknown()),
2579- },
2580- RoomCommand::Create(options) => client
2581- .create_room(create_room_request(options))
2582- .await
2583- .map(|room| Some(room.room_id().to_owned()))
2584- .map_err(|source| CommandError::from_sdk("creating room failed", source)),
2585- RoomCommand::CreateDirect(user_id) => match client.get_dm_room(&user_id) {
2586- Some(room) => Ok(Some(room.room_id().to_owned())),
2587- None => client
2588- .create_dm(&user_id)
2589- .await
2590- .map(|room| Some(room.room_id().to_owned()))
2591- .map_err(|source| CommandError::from_sdk("creating direct room failed", source)),
2592- },
2593- _ => Err(CommandError::new(
2594- CommandErrorKind::Other,
2595- "unsupported room command",
2596- )),
2597- }
2598-}
2599-
2600-fn room_unknown() -> CommandError {
2601- CommandError::new(
2602- CommandErrorKind::RoomUnknown,
2603- "room is not available in the local store",
2604- )
2605-}
2606-
2607-const fn sdk_notification_mode(
2608- mode: RoomNotificationMode,
2609-) -> matrix_sdk::notification_settings::RoomNotificationMode {
2610- match mode {
2611- RoomNotificationMode::AllMessages => {
2612- matrix_sdk::notification_settings::RoomNotificationMode::AllMessages
2613- }
2614- RoomNotificationMode::MentionsAndKeywordsOnly => {
2615- matrix_sdk::notification_settings::RoomNotificationMode::MentionsAndKeywordsOnly
2616- }
2617- RoomNotificationMode::Mute => matrix_sdk::notification_settings::RoomNotificationMode::Mute,
2618- }
2619-}
2620-
2621-async fn execute_notification_command(
2622- client: &Client,
2623- command: NotificationCommand,
2624-) -> Result<(), CommandError> {
2625- use matrix_sdk::notification_settings::{IsEncrypted, IsOneToOne};
2626-
2627- let settings = client.notification_settings().await;
2628- let result = match command {
2629- NotificationCommand::SetRoomMode { room_id, mode } => {
2630- settings
2631- .set_room_notification_mode(&room_id, sdk_notification_mode(mode))
2632- .await
2633- }
2634- NotificationCommand::ResetRoomMode(room_id) => {
2635- settings.delete_user_defined_room_rules(&room_id).await
2636- }
2637- NotificationCommand::SetDefaultMode {
2638- encrypted,
2639- one_to_one,
2640- mode,
2641- } => {
2642- settings
2643- .set_default_room_notification_mode(
2644- IsEncrypted::from(encrypted),
2645- IsOneToOne::from(one_to_one),
2646- sdk_notification_mode(mode),
2647- )
2648- .await
2649- }
2650- NotificationCommand::AddKeyword(keyword) => settings.add_keyword(keyword).await,
2651- NotificationCommand::RemoveKeyword(keyword) => settings.remove_keyword(&keyword).await,
2652- };
2653- result.map_err(|source| {
2654- CommandError::with_source(
2655- CommandErrorKind::Server,
2656- "updating notification settings failed",
2657- source,
2658- )
2659- })
2660-}
2661-
2662-async fn execute_encryption_command(
2663- client: &Client,
2664- events: &EventSender,
2665- verification: &mut VerificationController,
2666- command: EncryptionCommand,
2667-) -> Result<(), CommandError> {
2668- match command {
2669- EncryptionCommand::Recover(key) => client
2670- .encryption()
2671- .recovery()
2672- .recover(&key)
2673- .await
2674- .map_err(|source| CommandError::from_recovery("account recovery failed", source)),
2675- EncryptionCommand::StartVerification(target) => {
2676- verification.start(client, events.clone(), target).await
2677- }
2678- EncryptionCommand::Verify(action) => {
2679- verification.apply(client, events.clone(), action).await
2680- }
2681- command => execute_key_backup_command(client, command).await,
2682- }
2683-}
2684-
2685-async fn execute_key_backup_command(
2686- client: &Client,
2687- command: EncryptionCommand,
2688-) -> Result<(), CommandError> {
2689- match command {
2690- EncryptionCommand::CreateKeyBackup => client
2691- .encryption()
2692- .backups()
2693- .create()
2694- .await
2695- .map_err(|source| CommandError::from_sdk("creating key backup failed", source)),
2696- EncryptionCommand::EnableKeyBackup => client
2697- .encryption()
2698- .recovery()
2699- .enable_backup()
2700- .await
2701- .map_err(|source| CommandError::from_recovery("enabling key backup failed", source)),
2702- EncryptionCommand::DisableKeyBackup { delete_from_server } => {
2703- let backups = client.encryption().backups();
2704- if delete_from_server {
2705- backups.disable_and_delete().await
2706- } else {
2707- backups.disable().await
2708- }
2709- .map_err(|source| CommandError::from_sdk("disabling key backup failed", source))
2710- }
2711- EncryptionCommand::RecoverKeyBackup(key) => client
2712- .encryption()
2713- .recovery()
2714- .recover_and_fix_backup(&key)
2715- .await
2716- .map_err(|source| CommandError::from_recovery("recovering key backup failed", source)),
2717- _ => unreachable!("only key backup commands are spawned here"),
2718- }
2719-}
2720-
2721-async fn apply_power_level_changes(
2722- room: &Room,
2723- changes: PowerLevelChanges,
2724-) -> Result<(), CommandError> {
2725- use matrix_sdk::ruma::events::{
2726- MessageLikeEventType, StateEventType, room::power_levels::RoomPowerLevelsEventContent,
2727- };
2728-
2729- let mut levels = room
2730- .power_levels()
2731- .await
2732- .map_err(|source| CommandError::from_sdk("reading power levels failed", source.into()))?;
2733- if let Some(value) = changes.ban {
2734- levels.ban = matrix_power_level(value)?;
2735- }
2736- if let Some(value) = changes.invite {
2737- levels.invite = matrix_power_level(value)?;
2738- }
2739- if let Some(value) = changes.kick {
2740- levels.kick = matrix_power_level(value)?;
2741- }
2742- if let Some(value) = changes.redact {
2743- levels.redact = matrix_power_level(value)?;
2744- }
2745- if let Some(value) = changes.events_default {
2746- levels.events_default = matrix_power_level(value)?;
2747- }
2748- if let Some(value) = changes.state_default {
2749- levels.state_default = matrix_power_level(value)?;
2750- }
2751- if let Some(value) = changes.users_default {
2752- levels.users_default = matrix_power_level(value)?;
2753- }
2754- if let Some(value) = changes.room_notification {
2755- levels.notifications.room = matrix_power_level(value)?;
2756- }
2757- for (event_type, value) in [
2758- (StateEventType::RoomName.into(), changes.room_name),
2759- (StateEventType::RoomAvatar.into(), changes.room_avatar),
2760- (StateEventType::RoomTopic.into(), changes.room_topic),
2761- (StateEventType::SpaceChild.into(), changes.space_child),
2762- (MessageLikeEventType::Beacon.into(), changes.beacon),
2763- (StateEventType::BeaconInfo.into(), changes.beacon_info),
2764- ] {
2765- if let Some(value) = value {
2766- levels.events.insert(event_type, matrix_power_level(value)?);
2767- }
2768- }
2769- let content = RoomPowerLevelsEventContent::try_from(levels).map_err(|source| {
2770- CommandError::with_source(
2771- CommandErrorKind::Other,
2772- "normalizing power levels failed",
2773- source,
2774- )
2775- })?;
2776- room.send_state_event(content)
2777- .await
2778- .map(|_| ())
2779- .map_err(|source| CommandError::from_sdk("updating power levels failed", source))
2780-}
2781-
2782-fn matrix_power_level(value: i64) -> Result<matrix_sdk::ruma::Int, CommandError> {
2783- value.try_into().map_err(|source| {
2784- CommandError::with_source(
2785- CommandErrorKind::Other,
2786- "power level is outside the Matrix integer range",
2787- source,
2788- )
2789- })
2790-}
2791-
2792-fn create_room_request(
2793- options: CreateRoomOptions,
2794-) -> matrix_sdk::ruma::api::client::room::create_room::v3::Request {
2795- use matrix_sdk::ruma::api::client::room::{
2796- Visibility as SdkVisibility,
2797- create_room::v3::{Request, RoomPreset as SdkPreset},
2798- };
2799- let mut request = Request::new();
2800- request.name = options.name;
2801- request.topic = options.topic;
2802- request.room_alias_name = options.alias_localpart;
2803- request.invite = options.invite;
2804- request.is_direct = options.direct;
2805- request.visibility = match options.visibility {
2806- RoomVisibility::Private => SdkVisibility::Private,
2807- RoomVisibility::Public => SdkVisibility::Public,
2808- };
2809- request.preset = options.preset.map(|preset| match preset {
2810- RoomPreset::PrivateChat => SdkPreset::PrivateChat,
2811- RoomPreset::TrustedPrivateChat => SdkPreset::TrustedPrivateChat,
2812- RoomPreset::PublicChat => SdkPreset::PublicChat,
2813- });
2814- request
2815-}
2816-
2817-async fn queue_media_upload(
2818- room: Room,
2819- upload: AttachmentUpload,
2820- max_media_bytes: usize,
2821-) -> Result<(), CommandError> {
2822- let (upload, mime, mut config) = prepare_upload(upload, max_media_bytes)?;
2823- if let TimelineId::Thread { root_event_id, .. } = &upload.timeline_id {
2824- config.reply = Some(SdkReply {
2825- event_id: root_event_id.clone(),
2826- enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
2827- add_mentions: AddMentions::No,
2828- });
2829- }
2830- room.send_queue()
2831- .send_attachment(upload.filename, mime, upload.bytes, config)
2832- .await
2833- .map(|_| ())
2834- .map_err(|source| {
2835- CommandError::with_source(
2836- CommandErrorKind::MediaUnavailable,
2837- "media upload could not be queued",
2838- source,
2839- )
2840- })
2841-}
2842-
2843-async fn fetch_media(
2844- client: Client,
2845- registry: Arc<Mutex<MediaRegistry>>,
2846- events: EventSender,
2847- max_media_bytes: usize,
2848- media_id: MediaId,
2849- size: MediaSize,
2850-) -> Result<(), CommandError> {
2851- let media = registry
2852- .lock()
2853- .await
2854- .get(&media_id)
2855- .cloned()
2856- .ok_or_else(|| {
2857- CommandError::new(
2858- CommandErrorKind::MediaUnknown,
2859- "media is not registered in a retained timeline",
2860- )
2861- })?;
2862- let (source, format, mimetype, declared_size) = match size {
2863- MediaSize::Full => (
2864- media.full.source,
2865- MediaFormat::File,
2866- media.full.mimetype,
2867- media.full.declared_size,
2868- ),
2869- MediaSize::Thumbnail => {
2870- if let Some(thumbnail) = media.thumbnail {
2871- (
2872- thumbnail.source,
2873- MediaFormat::File,
2874- thumbnail.mimetype,
2875- thumbnail.declared_size,
2876- )
2877- } else if matches!(
2878- media.kind,
2879- crate::MediaKind::Image | crate::MediaKind::Video
2880- ) && matches!(
2881- &media.full.source,
2882- matrix_sdk::ruma::events::room::MediaSource::Plain(_)
2883- ) {
2884- (
2885- media.full.source,
2886- MediaFormat::Thumbnail(MediaThumbnailSettings::new(
2887- 800_u32.into(),
2888- 800_u32.into(),
2889- )),
2890- media.full.mimetype,
2891- None,
2892- )
2893- } else {
2894- return Err(CommandError::new(
2895- CommandErrorKind::MediaUnavailable,
2896- "the requested media size is unavailable",
2897- ));
2898- }
2899- }
2900- };
2901- if declared_size.is_some_and(|size| size > max_media_bytes as u64) {
2902- return Err(CommandError::new(
2903- CommandErrorKind::MediaTooLarge,
2904- "media exceeds the configured byte limit",
2905- ));
2906- }
2907- let request = MediaRequestParameters { source, format };
2908- if let Some(cached) = cached_media(&client, &request).await? {
2909- if cached.len() > max_media_bytes {
2910- remove_oversize_media(&client, &request).await;
2911- return Err(CommandError::new(
2912- CommandErrorKind::MediaTooLarge,
2913- "cached media exceeds the configured byte limit",
2914- ));
2915- }
2916- events
2917- .send(Event::MediaReady {
2918- media_id,
2919- size,
2920- bytes: Arc::from(cached),
2921- mimetype: mimetype.unwrap_or_else(|| "application/octet-stream".into()),
2922- })
2923- .await
2924- .map_err(send_error)?;
2925- return Ok(());
2926- }
2927- let bytes = download_bounded(&client, &request, max_media_bytes).await?;
2928- // the sdk caches plaintext under the encrypted key, so we do too or nobody can read anything
2929- let bytes = maybe_decrypt_media(bytes, &request, max_media_bytes)?;
2930- cache_media(&client, &request, &bytes).await;
2931- events
2932- .send(Event::MediaReady {
2933- media_id,
2934- size,
2935- bytes: Arc::from(bytes),
2936- mimetype: mimetype.unwrap_or_else(|| "application/octet-stream".into()),
2937- })
2938- .await
2939- .map_err(send_error)
2940-}
2941-async fn cached_media(
2942- client: &Client,
2943- request: &MediaRequestParameters,
2944-) -> Result<Option<Vec<u8>>, CommandError> {
2945- client
2946- .media_store()
2947- .lock()
2948- .await
2949- .map_err(|source| {
2950- CommandError::with_source(
2951- CommandErrorKind::Store,
2952- "reading media cache failed",
2953- source,
2954- )
2955- })?
2956- .get_media_content(request)
2957- .await
2958- .map_err(|source| {
2959- CommandError::with_source(
2960- CommandErrorKind::Store,
2961- "reading media cache failed",
2962- source,
2963- )
2964- })
2965-}
2966-
2967-async fn remove_oversize_media(client: &Client, request: &MediaRequestParameters) {
2968- if let Ok(store) = client.media_store().lock().await {
2969- let _ = store.remove_media_content(request).await;
2970- }
2971-}
2972-
2973-async fn cache_media(client: &Client, request: &MediaRequestParameters, bytes: &[u8]) {
2974- if let Ok(store) = client.media_store().lock().await {
2975- let _ = store
2976- .add_media_content(
2977- request,
2978- bytes.to_vec(),
2979- matrix_sdk::media::store::IgnoreMediaRetentionPolicy::No,
2980- )
2981- .await;
2982- }
2983-}
2984-
2985-fn maybe_decrypt_media(
2986- bytes: Vec<u8>,
2987- request: &MediaRequestParameters,
2988- max_media_bytes: usize,
2989-) -> Result<Vec<u8>, CommandError> {
2990- let matrix_sdk::ruma::events::room::MediaSource::Encrypted(file) = &request.source else {
2991- return Ok(bytes);
2992- };
2993- let info: MediaEncryptionInfo = (**file).clone().into();
2994- let mut cursor = std::io::Cursor::new(bytes);
2995- let mut decryptor = AttachmentDecryptor::new(&mut cursor, info).map_err(|source| {
2996- CommandError::with_source(
2997- CommandErrorKind::MediaUnavailable,
2998- "media decryption failed",
2999- source,
3000- )
3001- })?;
3002- let mut decrypted = Vec::new();
3003- let mut chunk = [0_u8; 8192];
3004- loop {
3005- match decryptor.read(&mut chunk) {
3006- Ok(0) => break,
3007- Ok(read) => {
3008- if read > max_media_bytes.saturating_sub(decrypted.len()) {
3009- return Err(CommandError::new(
3010- CommandErrorKind::MediaTooLarge,
3011- "decrypted media exceeds the configured byte limit",
3012- ));
3013- }
3014- decrypted.extend_from_slice(&chunk[..read]);
3015- }
3016- Err(source) => {
3017- return Err(CommandError::with_source(
3018- CommandErrorKind::MediaUnavailable,
3019- "media decryption failed",
3020- source,
3021- ));
3022- }
3023- }
3024- }
3025- Ok(decrypted)
3026-}
3027-
3028-async fn download_bounded(
3029- client: &Client,
3030- request: &MediaRequestParameters,
3031- max_media_bytes: usize,
3032-) -> Result<Vec<u8>, CommandError> {
3033- if is_local_media(&request.source) {
3034- return Err(CommandError::new(
3035- CommandErrorKind::MediaUnavailable,
3036- "local media is not in the media cache",
3037- ));
3038- }
3039- let mut refreshed = false;
3040- let request = loop {
3041- let http_request = media_http_request(client, request).await?;
3042- let response = client
3043- .http_client()
3044- .execute(request_from_http(client, http_request)?)
3045- .await
3046- .map_err(|source| {
3047- CommandError::with_source(
3048- CommandErrorKind::Network,
3049- "media download failed",
3050- source,
3051- )
3052- })?;
3053- if response.status() != matrix_sdk::reqwest::StatusCode::UNAUTHORIZED {
3054- break response;
3055- }
3056- if refreshed
3057- || client
3058- .session_tokens()
3059- .is_none_or(|tokens| tokens.refresh_token.is_none())
3060- {
3061- return Err(CommandError::new(
3062- CommandErrorKind::Unauthenticated,
3063- "media download was not authorized",
3064- ));
3065- }
3066- client.refresh_access_token().await.map_err(|source| {
3067- CommandError::from_refresh_token("refreshing media access token failed", source)
3068- })?;
3069- refreshed = true;
3070- };
3071- let status = request.status();
3072- if !status.is_success() {
3073- return Err(CommandError::new(
3074- CommandErrorKind::MediaUnavailable,
3075- format!("media download failed with status {status}"),
3076- ));
3077- }
3078- if request
3079- .content_length()
3080- .is_some_and(|length| length > max_media_bytes as u64)
3081- {
3082- return Err(CommandError::new(
3083- CommandErrorKind::MediaTooLarge,
3084- "media exceeds the configured byte limit",
3085- ));
3086- }
3087- let mut stream = request.bytes_stream();
3088- let mut bytes = Vec::new();
3089- while let Some(chunk) = stream.next().await {
3090- let chunk = chunk.map_err(|source| {
3091- CommandError::with_source(CommandErrorKind::Network, "media download failed", source)
3092- })?;
3093- if chunk.len() > max_media_bytes.saturating_sub(bytes.len()) {
3094- return Err(CommandError::new(
3095- CommandErrorKind::MediaTooLarge,
3096- "downloaded media exceeds the configured byte limit",
3097- ));
3098- }
3099- bytes.extend_from_slice(&chunk);
3100- }
3101- Ok(bytes)
3102-}
3103-
3104-fn is_local_media(source: &matrix_sdk::ruma::events::room::MediaSource) -> bool {
3105- let uri = match source {
3106- matrix_sdk::ruma::events::room::MediaSource::Plain(uri) => uri,
3107- matrix_sdk::ruma::events::room::MediaSource::Encrypted(file) => &file.url,
3108- };
3109- let Ok(server) = uri.server_name() else {
3110- return false;
3111- };
3112- server.as_str() == "send-queue.localhost"
3113-}
3114-
3115-async fn media_http_request(
3116- client: &Client,
3117- request: &MediaRequestParameters,
3118-) -> Result<http::Request<Vec<u8>>, CommandError> {
3119- use matrix_sdk::ruma::api::client::{authenticated_media, media};
3120-
3121- let versions = client.supported_versions().await.map_err(|source| {
3122- CommandError::with_source(
3123- CommandErrorKind::Network,
3124- "reading supported server versions failed",
3125- source,
3126- )
3127- })?;
3128- let use_auth =
3129- authenticated_media::get_content::v1::Request::PATH_BUILDER.is_supported(&versions);
3130- let base = client.homeserver().to_string();
3131- let token = client.access_token();
3132- let build = |request: http::Request<Vec<u8>>| -> Result<http::Request<Vec<u8>>, CommandError> {
3133- let mut request = request;
3134- if let Some(token) = token.as_deref() {
3135- request.headers_mut().insert(
3136- http::header::AUTHORIZATION,
3137- format!("Bearer {token}").parse().map_err(|_| {
3138- CommandError::new(
3139- CommandErrorKind::Unauthenticated,
3140- "stored access token is invalid",
3141- )
3142- })?,
3143- );
3144- }
3145- Ok(request)
3146- };
3147- match &request.source {
3148- matrix_sdk::ruma::events::room::MediaSource::Encrypted(file) => {
3149- if use_auth {
3150- let endpoint = authenticated_media::get_content::v1::Request::from_uri(&file.url)
3151- .map_err(|source| {
3152- CommandError::with_source(
3153- CommandErrorKind::Other,
3154- "media URI is invalid",
3155- source,
3156- )
3157- })?;
3158- endpoint
3159- .try_into_http_request::<Vec<u8>>(
3160- &base,
3161- SendAccessToken::IfRequired(token.as_deref().unwrap_or("")),
3162- Cow::Borrowed(&versions),
3163- )
3164- .map_err(|_| {
3165- CommandError::new(
3166- CommandErrorKind::Unauthenticated,
3167- "media request could not be authorized",
3168- )
3169- })
3170- .and_then(build)
3171- } else {
3172- #[allow(deprecated)]
3173- let endpoint =
3174- media::get_content::v3::Request::from_url(&file.url).map_err(|source| {
3175- CommandError::with_source(
3176- CommandErrorKind::Other,
3177- "media URI is invalid",
3178- source,
3179- )
3180- })?;
3181- endpoint
3182- .try_into_http_request::<Vec<u8>>(
3183- &base,
3184- SendAccessToken::None,
3185- Cow::Borrowed(&versions),
3186- )
3187- .map_err(|_| {
3188- CommandError::new(CommandErrorKind::Other, "media request is invalid")
3189- })
3190- .and_then(build)
3191- }
3192- }
3193- matrix_sdk::ruma::events::room::MediaSource::Plain(uri) => {
3194- if let MediaFormat::Thumbnail(settings) = &request.format {
3195- if use_auth {
3196- let mut endpoint =
3197- authenticated_media::get_content_thumbnail::v1::Request::from_uri(
3198- uri,
3199- settings.width,
3200- settings.height,
3201- )
3202- .map_err(|source| {
3203- CommandError::with_source(
3204- CommandErrorKind::Other,
3205- "media URI is invalid",
3206- source,
3207- )
3208- })?;
3209- endpoint.method = Some(settings.method.clone());
3210- endpoint.animated = Some(settings.animated);
3211- endpoint
3212- .try_into_http_request::<Vec<u8>>(
3213- &base,
3214- SendAccessToken::IfRequired(token.as_deref().unwrap_or("")),
3215- Cow::Borrowed(&versions),
3216- )
3217- .map_err(|_| {
3218- CommandError::new(
3219- CommandErrorKind::Unauthenticated,
3220- "media request could not be authorized",
3221- )
3222- })
3223- .and_then(build)
3224- } else {
3225- #[allow(deprecated)]
3226- let endpoint = {
3227- let mut endpoint = media::get_content_thumbnail::v3::Request::from_url(
3228- uri,
3229- settings.width,
3230- settings.height,
3231- )
3232- .map_err(|source| {
3233- CommandError::with_source(
3234- CommandErrorKind::Other,
3235- "media URI is invalid",
3236- source,
3237- )
3238- })?;
3239- endpoint.method = Some(settings.method.clone());
3240- endpoint.animated = Some(settings.animated);
3241- endpoint
3242- };
3243- endpoint
3244- .try_into_http_request::<Vec<u8>>(
3245- &base,
3246- SendAccessToken::None,
3247- Cow::Borrowed(&versions),
3248- )
3249- .map_err(|_| {
3250- CommandError::new(CommandErrorKind::Other, "media request is invalid")
3251- })
3252- .and_then(build)
3253- }
3254- } else if use_auth {
3255- let endpoint = authenticated_media::get_content::v1::Request::from_uri(uri)
3256- .map_err(|source| {
3257- CommandError::with_source(
3258- CommandErrorKind::Other,
3259- "media URI is invalid",
3260- source,
3261- )
3262- })?;
3263- endpoint
3264- .try_into_http_request::<Vec<u8>>(
3265- &base,
3266- SendAccessToken::IfRequired(token.as_deref().unwrap_or("")),
3267- Cow::Borrowed(&versions),
3268- )
3269- .map_err(|_| {
3270- CommandError::new(
3271- CommandErrorKind::Unauthenticated,
3272- "media request could not be authorized",
3273- )
3274- })
3275- .and_then(build)
3276- } else {
3277- #[allow(deprecated)]
3278- let endpoint =
3279- media::get_content::v3::Request::from_url(uri).map_err(|source| {
3280- CommandError::with_source(
3281- CommandErrorKind::Other,
3282- "media URI is invalid",
3283- source,
3284- )
3285- })?;
3286- endpoint
3287- .try_into_http_request::<Vec<u8>>(
3288- &base,
3289- SendAccessToken::None,
3290- Cow::Borrowed(&versions),
3291- )
3292- .map_err(|_| {
3293- CommandError::new(CommandErrorKind::Other, "media request is invalid")
3294- })
3295- .and_then(build)
3296- }
3297- }
3298- }
3299-}
3300-
3301-fn request_from_http(
3302- client: &Client,
3303- request: http::Request<Vec<u8>>,
3304-) -> Result<matrix_sdk::reqwest::Request, CommandError> {
3305- let (parts, _) = request.into_parts();
3306- let mut builder = client.http_client().get(parts.uri.to_string());
3307- for (name, value) in parts.headers.iter() {
3308- builder = builder.header(name.as_str(), value.as_bytes());
3309- }
3310- builder
3311- .build()
3312- .map_err(|_| CommandError::new(CommandErrorKind::Network, "media request is invalid"))
3313-}
3314-
3315-impl RoomActor {
3316- async fn finish(
3317- &mut self,
3318- id: CommandId,
3319- outcome: TimelineOutcome,
3320- ) -> Result<(), SendEventError> {
3321- match outcome {
3322- TimelineOutcome::Done(result) => {
3323- emit_command_result(&self.events, id, CommandKind::Timeline, result).await
3324- }
3325- TimelineOutcome::Background(work) => {
3326- let task = self.background.spawn(async move { (id, work.await) });
3327- self.background_ids.insert(task.id(), id);
3328- Ok(())
3329- }
3330- }
3331- }
3332-
3333- async fn reap(&mut self, completed: BackgroundResult) -> Result<(), SendEventError> {
3334- let (id, result) = match completed {
3335- Ok((task_id, completed)) => {
3336- self.background_ids.remove(&task_id);
3337- completed
3338- }
3339- Err(error) => {
3340- let Some(id) = self.background_ids.remove(&error.id()) else {
3341- return Ok(());
3342- };
3343- (
3344- id,
3345- Err(CommandError::new(
3346- CommandErrorKind::Other,
3347- "timeline operation panicked",
3348- )),
3349- )
3350- }
3351- };
3352- emit_command_result(&self.events, id, CommandKind::Timeline, result).await
3353- }
3354-
3355- async fn run(mut self, current_user_id: Option<ruma::OwnedUserId>) {
3356- let timeline = match self
3357- .room
3358- .timeline_builder()
3359- .with_focus(TimelineFocus::Live {
3360- hide_threaded_events: true,
3361- })
3362- .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)
3363- .build()
3364- .await
3365- {
3366- Ok(timeline) => timeline,
3367- Err(error) => {
3368- tracing::warn!(error = %error, room_id = %self.room_id, "room actor could not build its timeline");
3369- let message = format!("room timeline build failed: {error}");
3370- for id in self.open_ids.drain(..) {
3371- let _ = emit_command_result(
3372- &self.events,
3373- id,
3374- CommandKind::Room,
3375- Err(CommandError::new(
3376- CommandErrorKind::Timeline,
3377- message.clone(),
3378- )),
3379- )
3380- .await;
3381- }
3382- self.receiver.close();
3383- while let Some(request) = self.receiver.recv().await {
3384- let (id, command) = match request {
3385- RoomActorRequest::Opened { id } | RoomActorRequest::Close { id } => {
3386- (id, CommandKind::Room)
3387- }
3388- RoomActorRequest::Execute { id, .. } => (id, CommandKind::Timeline),
3389- };
3390- let _ = emit_command_result(
3391- &self.events,
3392- id,
3393- command,
3394- Err(CommandError::new(
3395- CommandErrorKind::Timeline,
3396- "room timeline build failed",
3397- )),
3398- )
3399- .await;
3400- }
3401- return;
3402- }
3403- };
3404- let room_timeline_id = TimelineId::Room(self.room_id.clone());
3405- let mut room_timeline = match TimelineSession::start(
3406- room_timeline_id,
3407- timeline,
3408- self.events.clone(),
3409- self.media.clone(),
3410- current_user_id.clone(),
3411- self.initial_history_limit,
3412- )
3413- .await
3414- {
3415- Ok(timeline) => timeline,
3416- Err(_) => return,
3417- };
3418- let _ = self.engine_events.send(RoomActorEvent::Ready {
3419- room_id: self.room_id.clone(),
3420- generation: self.generation,
3421- });
3422- for id in self.open_ids.drain(..) {
3423- if emit_command_result(&self.events, id, CommandKind::Room, Ok(()))
3424- .await
3425- .is_err()
3426- {
3427- return;
3428- }
3429- }
3430-
3431- let (typing_guard, mut typing_rx) = self.room.subscribe_to_typing_notifications();
3432- let typing_sender = self.events.clone();
3433- let typing_room_id = self.room_id.clone();
3434- let mut room_tasks = JoinSet::new();
3435- room_tasks.spawn(async move {
3436- loop {
3437- match typing_rx.recv().await {
3438- Ok(users) => {
3439- let state = TypingState {
3440- room_id: typing_room_id.clone(),
3441- users,
3442- };
3443- if typing_sender
3444- .send(Event::TypingChanged(state))
3445- .await
3446- .is_err()
3447- {
3448- tracing::debug!(
3449- room_id = %typing_room_id,
3450- "typing watcher stopped because the event receiver closed"
3451- );
3452- break;
3453- }
3454- }
3455- Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
3456- tracing::warn!(room_id = %typing_room_id, skipped, "typing watcher lagged");
3457- }
3458- Err(tokio::sync::broadcast::error::RecvError::Closed) => {
3459- tracing::debug!(room_id = %typing_room_id, "typing watcher stopped");
3460- break;
3461- }
3462- }
3463- }
3464- });
3465-
3466- enum QueuedThreadCommand {
3467- Open {
3468- id: CommandId,
3469- },
3470- Execute {
3471- id: CommandId,
3472- command: TimelineCommand,
3473- },
3474- }
3475-
3476- struct OpeningThread {
3477- generation: u64,
3478- abort: AbortHandle,
3479- task_id: TaskId,
3480- queued: VecDeque<QueuedThreadCommand>,
3481- }
3482-
3483- let mut thread_timelines = HashMap::<OwnedEventId, TimelineSession>::new();
3484- let mut opening_threads = HashMap::<OwnedEventId, OpeningThread>::new();
3485- let mut thread_task_ids = HashMap::<TaskId, OwnedEventId>::new();
3486- let mut thread_open_tasks = JoinSet::new();
3487- let mut next_thread_generation = 0_u64;
3488- let mut close_id = None;
3489- let thread_queue_cap = self.receiver.max_capacity();
3490- let mut queued_thread_count = 0;
3491-
3492- let mut room_info = self.room.subscribe_info();
3493- 'actor: loop {
3494- tokio::select! {
3495- Some(info) = room_info.next() => {
3496- if matches!(info.state(), matrix_sdk::RoomState::Left | matrix_sdk::RoomState::Banned) {
3497- tracing::debug!(room_id = %self.room_id, "room actor stopped because we left the room");
3498- break 'actor;
3499- }
3500- }
3501- request = self.receiver.recv() => {
3502- let Some(request) = request else {
3503- tracing::debug!(room_id = %self.room_id, "room actor stopped because its request channel closed");
3504- break;
3505- };
3506- match request {
3507- RoomActorRequest::Opened { id } => {
3508- if emit_command_result(
3509- &self.events,
3510- id,
3511- CommandKind::Room,
3512- Ok(()),
3513- )
3514- .await
3515- .is_err()
3516- {
3517- break;
3518- }
3519- }
3520- RoomActorRequest::Execute { id, command } => {
3521- match command {
3522- TimelineCommand::OpenThread { root_event_id, .. } => {
3523- if thread_timelines.contains_key(&root_event_id) {
3524- if emit_command_result(
3525- &self.events,
3526- id,
3527- CommandKind::Timeline,
3528- Ok(()),
3529- )
3530- .await
3531- .is_err()
3532- {
3533- break 'actor;
3534- }
3535- } else if let Some(opening) =
3536- opening_threads.get_mut(&root_event_id)
3537- {
3538- if queued_thread_count >= thread_queue_cap {
3539- if emit_command_result(
3540- &self.events,
3541- id,
3542- CommandKind::Timeline,
3543- Err(CommandError::new(
3544- CommandErrorKind::QueueFull,
3545- "thread command queue is full",
3546- )),
3547- )
3548- .await
3549- .is_err()
3550- {
3551- break 'actor;
3552- }
3553- } else {
3554- opening
3555- .queued
3556- .push_back(QueuedThreadCommand::Open { id });
3557- queued_thread_count += 1;
3558- }
3559- } else if queued_thread_count >= thread_queue_cap {
3560- if emit_command_result(
3561- &self.events,
3562- id,
3563- CommandKind::Timeline,
3564- Err(CommandError::new(
3565- CommandErrorKind::QueueFull,
3566- "thread command queue is full",
3567- )),
3568- )
3569- .await
3570- .is_err()
3571- {
3572- break 'actor;
3573- }
3574- } else {
3575- let generation = next_thread_generation;
3576- next_thread_generation =
3577- next_thread_generation.wrapping_add(1);
3578- let task_root = root_event_id.clone();
3579- let room = self.room.clone();
3580- let abort = thread_open_tasks.spawn(async move {
3581- let result = room
3582- .timeline_builder()
3583- .with_focus(TimelineFocus::Thread {
3584- root_event_id: root_event_id.clone(),
3585- })
3586- .track_read_marker_and_receipts(
3587- TimelineReadReceiptTracking::AllEvents,
3588- )
3589- .build()
3590- .await;
3591- (root_event_id, generation, result)
3592- });
3593- let task_id = abort.id();
3594- thread_task_ids.insert(task_id, task_root.clone());
3595- opening_threads.insert(
3596- task_root,
3597- OpeningThread {
3598- generation,
3599- abort,
3600- task_id,
3601- queued: VecDeque::from([
3602- QueuedThreadCommand::Open { id },
3603- ]),
3604- },
3605- );
3606- queued_thread_count += 1;
3607- }
3608- }
3609- TimelineCommand::CloseThread { root_event_id, .. } => {
3610- if let Some(timeline) = thread_timelines.remove(&root_event_id) {
3611- timeline
3612- .close(
3613- CommandErrorKind::TimelineNotOpen,
3614- "thread closed while pagination was in progress",
3615- )
3616- .await;
3617- if emit_command_result(
3618- &self.events,
3619- id,
3620- CommandKind::Timeline,
3621- Ok(()),
3622- )
3623- .await
3624- .is_err()
3625- {
3626- break 'actor;
3627- }
3628- } else if let Some(opening) =
3629- opening_threads.remove(&root_event_id)
3630- {
3631- thread_task_ids.remove(&opening.task_id);
3632- queued_thread_count -= opening.queued.len();
3633- opening.abort.abort();
3634- for queued in opening.queued {
3635- let queued_id = match queued {
3636- QueuedThreadCommand::Open { id } => id,
3637- QueuedThreadCommand::Execute { id, .. } => id,
3638- };
3639- let _ = emit_command_result(
3640- &self.events,
3641- queued_id,
3642- CommandKind::Timeline,
3643- Err(CommandError::new(
3644- CommandErrorKind::TimelineNotOpen,
3645- "thread was closed before it finished opening",
3646- )),
3647- )
3648- .await;
3649- }
3650- if emit_command_result(
3651- &self.events,
3652- id,
3653- CommandKind::Timeline,
3654- Ok(()),
3655- )
3656- .await
3657- .is_err()
3658- {
3659- break 'actor;
3660- }
3661- } else if emit_command_result(
3662- &self.events,
3663- id,
3664- CommandKind::Timeline,
3665- Err(CommandError::new(
3666- CommandErrorKind::TimelineNotOpen,
3667- "thread timeline is not open",
3668- )),
3669- )
3670- .await
3671- .is_err()
3672- {
3673- break 'actor;
3674- }
3675- }
3676- TimelineCommand::Paginate { timeline_id, limit } => {
3677- match timeline_id {
3678- TimelineId::Room(_) => {
3679- room_timeline.paginate(limit, Some(id)).await;
3680- }
3681- TimelineId::Thread { room_id, root_event_id } => {
3682- if let Some(timeline) =
3683- thread_timelines.get_mut(&root_event_id)
3684- {
3685- timeline.paginate(limit, Some(id)).await;
3686- } else if let Some(opening) =
3687- opening_threads.get_mut(&root_event_id)
3688- {
3689- if queued_thread_count >= thread_queue_cap {
3690- if emit_command_result(
3691- &self.events,
3692- id,
3693- CommandKind::Timeline,
3694- Err(CommandError::new(
3695- CommandErrorKind::QueueFull,
3696- "thread command queue is full",
3697- )),
3698- )
3699- .await
3700- .is_err()
3701- {
3702- break 'actor;
3703- }
3704- } else {
3705- opening.queued.push_back(
3706- QueuedThreadCommand::Execute {
3707- id,
3708- command:
3709- TimelineCommand::Paginate {
3710- timeline_id:
3711- TimelineId::Thread {
3712- room_id,
3713- root_event_id,
3714- },
3715- limit,
3716- },
3717- },
3718- );
3719- queued_thread_count += 1;
3720- }
3721- } else if emit_command_result(
3722- &self.events,
3723- id,
3724- CommandKind::Timeline,
3725- Err(CommandError::new(
3726- CommandErrorKind::TimelineNotOpen,
3727- "timeline is not open",
3728- )),
3729- )
3730- .await
3731- .is_err()
3732- {
3733- break 'actor;
3734- }
3735- }
3736- }
3737- }
3738- command => {
3739- let thread_target = match command
3740- .timeline_id()
3741- .expect("non-lifecycle timeline command has a timeline id")
3742- {
3743- TimelineId::Room(_) => None,
3744- TimelineId::Thread { root_event_id, .. } => {
3745- Some(root_event_id.clone())
3746- }
3747- };
3748- match thread_target {
3749- None => {
3750- let result = execute_timeline_command(
3751- &mut room_timeline,
3752- command,
3753- None,
3754- )
3755- .await;
3756- if self.finish(id, result)
3757- .await
3758- .is_err()
3759- {
3760- tracing::debug!(room_id = %self.room_id, "room actor stopped because the event receiver closed");
3761- break;
3762- }
3763- }
3764- Some(root_event_id) => {
3765- if let Some(timeline) =
3766- thread_timelines.get_mut(&root_event_id)
3767- {
3768- let thread_root =
3769- timeline.id.thread_root().cloned();
3770- let result = execute_timeline_command(
3771- timeline,
3772- command,
3773- thread_root.as_ref(),
3774- )
3775- .await;
3776- if self.finish(id, result)
3777- .await
3778- .is_err()
3779- {
3780- tracing::debug!(room_id = %self.room_id, "room actor stopped because the event receiver closed");
3781- break;
3782- }
3783- } else if let Some(opening) =
3784- opening_threads.get_mut(&root_event_id)
3785- {
3786- if queued_thread_count >= thread_queue_cap {
3787- if emit_command_result(
3788- &self.events,
3789- id,
3790- CommandKind::Timeline,
3791- Err(CommandError::new(
3792- CommandErrorKind::QueueFull,
3793- "thread command queue is full",
3794- )),
3795- )
3796- .await
3797- .is_err()
3798- {
3799- break 'actor;
3800- }
3801- } else {
3802- opening.queued.push_back(
3803- QueuedThreadCommand::Execute { id, command },
3804- );
3805- queued_thread_count += 1;
3806- }
3807- } else if emit_command_result(
3808- &self.events,
3809- id,
3810- CommandKind::Timeline,
3811- Err(CommandError::new(
3812- CommandErrorKind::TimelineNotOpen,
3813- "timeline is not open",
3814- )),
3815- )
3816- .await
3817- .is_err()
3818- {
3819- tracing::debug!(room_id = %self.room_id, "room actor stopped because the event receiver closed");
3820- break;
3821- }
3822- }
3823- }
3824- }
3825- }
3826- }
3827- RoomActorRequest::Close { id } => {
3828- close_id = Some(id);
3829- break 'actor;
3830- }
3831- }
3832- }
3833- completed = thread_open_tasks.join_next(), if !thread_open_tasks.is_empty() => {
3834- match completed {
3835- Some(Ok((root_event_id, generation, result))) => {
3836- if opening_threads
3837- .get(&root_event_id)
3838- .is_none_or(|opening| opening.generation != generation)
3839- {
3840- continue;
3841- }
3842- let opening = opening_threads
3843- .remove(&root_event_id)
3844- .expect("thread generation was checked");
3845- thread_task_ids.remove(&opening.task_id);
3846- queued_thread_count -= opening.queued.len();
3847- match result {
3848- Ok(timeline) => {
3849- let timeline_id = TimelineId::thread(
3850- self.room_id.clone(),
3851- root_event_id.clone(),
3852- );
3853- match TimelineSession::start(
3854- timeline_id,
3855- timeline,
3856- self.events.clone(),
3857- self.media.clone(),
3858- current_user_id.clone(),
3859- self.initial_history_limit,
3860- )
3861- .await
3862- {
3863- Ok(session) => {
3864- thread_timelines
3865- .insert(root_event_id.clone(), session);
3866- let timeline = thread_timelines
3867- .get_mut(&root_event_id)
3868- .expect("thread session was inserted");
3869- for queued in opening.queued {
3870- match queued {
3871- QueuedThreadCommand::Open { id } => {
3872- if emit_command_result(
3873- &self.events,
3874- id,
3875- CommandKind::Timeline,
3876- Ok(()),
3877- )
3878- .await
3879- .is_err()
3880- {
3881- break 'actor;
3882- }
3883- }
3884- QueuedThreadCommand::Execute {
3885- id,
3886- command,
3887- } => match command {
3888- TimelineCommand::Paginate {
3889- limit,
3890- ..
3891- } => {
3892- timeline
3893- .paginate(limit, Some(id))
3894- .await;
3895- }
3896- command => {
3897- let thread_root = timeline
3898- .id
3899- .thread_root()
3900- .cloned();
3901- let result =
3902- execute_timeline_command(
3903- timeline,
3904- command,
3905- thread_root.as_ref(),
3906- )
3907- .await;
3908- if self.finish(id, result)
3909- .await
3910- .is_err()
3911- {
3912- break 'actor;
3913- }
3914- }
3915- },
3916- }
3917- }
3918- }
3919- Err(_) => break 'actor,
3920- }
3921- }
3922- Err(error) => {
3923- tracing::warn!(error = %error, room_id = %self.room_id, %root_event_id, "thread timeline build failed");
3924- let message = format!("thread timeline build failed: {error}");
3925- for queued in opening.queued {
3926- let queued_id = match queued {
3927- QueuedThreadCommand::Open { id } => id,
3928- QueuedThreadCommand::Execute { id, .. } => id,
3929- };
3930- if emit_command_result(
3931- &self.events,
3932- queued_id,
3933- CommandKind::Timeline,
3934- Err(CommandError::new(
3935- CommandErrorKind::Timeline,
3936- message.clone(),
3937- )),
3938- )
3939- .await
3940- .is_err()
3941- {
3942- break 'actor;
3943- }
3944- }
3945- }
3946- }
3947- }
3948- Some(Err(error)) => {
3949- if let Some(root_event_id) = thread_task_ids.remove(&error.id()) {
3950- tracing::warn!(error = %error, room_id = %self.room_id, %root_event_id, "thread open task failed");
3951- if let Some(opening) = opening_threads.remove(&root_event_id) {
3952- queued_thread_count -= opening.queued.len();
3953- for queued in opening.queued {
3954- let queued_id = match queued {
3955- QueuedThreadCommand::Open { id } => id,
3956- QueuedThreadCommand::Execute { id, .. } => id,
3957- };
3958- if emit_command_result(
3959- &self.events,
3960- queued_id,
3961- CommandKind::Timeline,
3962- Err(CommandError::new(
3963- CommandErrorKind::Timeline,
3964- "thread timeline build task failed",
3965- )),
3966- )
3967- .await
3968- .is_err()
3969- {
3970- break 'actor;
3971- }
3972- }
3973- }
3974- } else {
3975- tracing::debug!(error = %error, room_id = %self.room_id, "thread open task was aborted");
3976- }
3977- }
3978- None => {}
3979- }
3980- }
3981- Some(completed) = self.background.join_next_with_id(), if !self.background.is_empty() => {
3982- if self.reap(completed).await.is_err() {
3983- break 'actor;
3984- }
3985- }
3986- completed = room_tasks.join_next(), if !room_tasks.is_empty() => {
3987- match completed {
3988- Some(Err(error)) => {
3989- tracing::warn!(error = %error, room_id = %self.room_id, "room actor task failed");
3990- }
3991- Some(Ok(())) => {
3992- tracing::warn!(room_id = %self.room_id, "room actor task stopped unexpectedly");
3993- }
3994- None => {}
3995- }
3996- }
3997- }
3998- }
3999-
4000- while let Some(completed) = self.background.join_next_with_id().await {
4001- let _ = self.reap(completed).await;
4002- }
4003- drop(typing_guard);
4004- room_tasks.abort_all();
4005- while room_tasks.join_next().await.is_some() {}
4006- thread_task_ids.clear();
4007- let pending_opens = opening_threads
4008- .drain()
4009- .flat_map(|(_, opening)| opening.queued)
4010- .collect::<Vec<_>>();
4011- for queued in pending_opens {
4012- let queued_id = match queued {
4013- QueuedThreadCommand::Open { id } => id,
4014- QueuedThreadCommand::Execute { id, .. } => id,
4015- };
4016- let _ = emit_command_result(
4017- &self.events,
4018- queued_id,
4019- CommandKind::Timeline,
4020- Err(CommandError::new(
4021- CommandErrorKind::RoomNotOpen,
4022- "room closed while thread timeline was opening",
4023- )),
4024- )
4025- .await;
4026- }
4027- thread_open_tasks.abort_all();
4028- while thread_open_tasks.join_next().await.is_some() {}
4029- for (_, timeline) in thread_timelines {
4030- timeline
4031- .close(
4032- CommandErrorKind::RoomNotOpen,
4033- "room closed while thread pagination was in progress",
4034- )
4035- .await;
4036- }
4037- room_timeline
4038- .close(
4039- CommandErrorKind::RoomNotOpen,
4040- "room closed while pagination was in progress",
4041- )
4042- .await;
4043- if let Some(id) = close_id
4044- && emit_command_result(&self.events, id, CommandKind::Room, Ok(()))
4045- .await
4046- .is_err()
4047- {
4048- tracing::debug!(room_id = %self.room_id, "room close acknowledgement was not delivered");
4049- }
4050- }
4051-}
4052-
4053-async fn execute_timeline_command(
4054- session: &mut TimelineSession,
4055- command: TimelineCommand,
4056- thread_root: Option<&OwnedEventId>,
4057-) -> TimelineOutcome {
4058- let timeline = session.timeline.clone();
4059- let work: BackgroundWork = match command {
4060- TimelineCommand::Redact {
4061- event_id, reason, ..
4062- } => Box::pin(async move {
4063- timeline
4064- .redact(&TimelineEventItemId::EventId(event_id), reason.as_deref())
4065- .await
4066- .map_err(timeline_error)
4067- }),
4068- TimelineCommand::ToggleReaction { event_id, key, .. } => Box::pin(async move {
4069- timeline
4070- .toggle_reaction(&TimelineEventItemId::EventId(event_id), &key)
4071- .await
4072- .map(|_| ())
4073- .map_err(timeline_error)
4074- }),
4075- TimelineCommand::MarkRead { event_id, .. } => {
4076- let thread = thread_root.is_some();
4077- Box::pin(async move {
4078- let receipt_failed =
4079- |source| CommandError::from_sdk("sending read receipt failed", source);
4080- timeline
4081- .send_single_receipt(ReceiptType::Read, event_id.clone())
4082- .await
4083- .map_err(receipt_failed)?;
4084- if !thread {
4085- timeline
4086- .send_single_receipt(ReceiptType::FullyRead, event_id)
4087- .await
4088- .map_err(receipt_failed)?;
4089- }
4090- Ok(())
4091- })
4092- }
4093- command => {
4094- return TimelineOutcome::Done(
4095- execute_serial_timeline_command(session, command, thread_root).await,
4096- );
4097- }
4098- };
4099- TimelineOutcome::Background(work)
4100-}
4101-
4102-async fn execute_serial_timeline_command(
4103- session: &mut TimelineSession,
4104- command: TimelineCommand,
4105- thread_root: Option<&OwnedEventId>,
4106-) -> Result<(), CommandError> {
4107- let timeline = &session.timeline;
4108- match command {
4109- TimelineCommand::OpenThread { .. }
4110- | TimelineCommand::CloseThread { .. }
4111- | TimelineCommand::Redact { .. }
4112- | TimelineCommand::ToggleReaction { .. }
4113- | TimelineCommand::MarkRead { .. } => Err(CommandError::new(
4114- CommandErrorKind::Other,
4115- "timeline command was misrouted",
4116- )),
4117- TimelineCommand::Resync { .. } => session.resync().await.map_err(|_| {
4118- CommandError::new(
4119- CommandErrorKind::Timeline,
4120- "timeline resync could not emit a reset",
4121- )
4122- }),
4123- TimelineCommand::Paginate { .. } => Ok(()),
4124- TimelineCommand::Send { content, .. } => {
4125- let body = match &content {
4126- MessageContent::Text { content, .. }
4127- | MessageContent::Emote { content, .. }
4128- | MessageContent::Reply { content, .. } => &content.body,
4129- };
4130- if body.trim().is_empty() {
4131- return Err(CommandError::new(
4132- CommandErrorKind::EmptyBody,
4133- "message body is empty",
4134- ));
4135- }
4136- match content {
4137- MessageContent::Text { content, mentions } => timeline
4138- .send(
4139- message_content(content, mentions, false)
4140- .with_relation(None)
4141- .into(),
4142- )
4143- .await
4144- .map(|_| ())
4145- .map_err(timeline_error),
4146- MessageContent::Emote { content, mentions } => timeline
4147- .send(
4148- message_content(content, mentions, true)
4149- .with_relation(None)
4150- .into(),
4151- )
4152- .await
4153- .map(|_| ())
4154- .map_err(timeline_error),
4155- MessageContent::Reply {
4156- event_id,
4157- content,
4158- mentions,
4159- notify_replied_to,
4160- } => {
4161- let content = timeline
4162- .room()
4163- .make_reply_event(
4164- message_content(content, mentions, false),
4165- SdkReply {
4166- event_id,
4167- enforce_thread: if thread_root.is_some() {
4168- EnforceThread::Threaded(ReplyWithinThread::Yes)
4169- } else {
4170- EnforceThread::MaybeThreaded
4171- },
4172- add_mentions: if notify_replied_to {
4173- AddMentions::Yes
4174- } else {
4175- AddMentions::No
4176- },
4177- },
4178- )
4179- .await
4180- .map_err(|source| {
4181- CommandError::with_source(
4182- CommandErrorKind::Timeline,
4183- "reply operation failed",
4184- source,
4185- )
4186- })?;
4187- if let Some(thread_root) = thread_root
4188- && !matches!(
4189- content.relates_to.as_ref(),
4190- Some(Relation::Thread(thread)) if &thread.event_id == thread_root
4191- )
4192- {
4193- return Err(CommandError::new(
4194- CommandErrorKind::Timeline,
4195- "reply target does not belong to the selected thread",
4196- ));
4197- }
4198- timeline
4199- .send(content.into())
4200- .await
4201- .map(|_| ())
4202- .map_err(timeline_error)
4203- }
4204- }
4205- }
4206- TimelineCommand::Edit {
4207- event_id, content, ..
4208- } => {
4209- let (content, mentions, emote) = match content {
4210- MessageEditContent::Text { content, mentions } => (content, mentions, false),
4211- MessageEditContent::Emote { content, mentions } => (content, mentions, true),
4212- };
4213- if content.body.trim().is_empty() {
4214- return Err(CommandError::new(
4215- CommandErrorKind::EmptyBody,
4216- "message body is empty",
4217- ));
4218- }
4219- timeline
4220- .edit(
4221- &TimelineEventItemId::EventId(event_id),
4222- EditedContent::RoomMessage(message_content(content, mentions, emote)),
4223- )
4224- .await
4225- .map_err(timeline_error)
4226- }
4227- }
4228-}
4229-
4230-fn message_content(
4231- content: RichText,
4232- mentions: MessageMentions,
4233- emote: bool,
4234-) -> RoomMessageEventContentWithoutRelation {
4235- let content = match content.formatted_body {
4236- Some(formatted_body) if emote => {
4237- RoomMessageEventContentWithoutRelation::emote_html(content.body, formatted_body)
4238- }
4239- Some(formatted_body) => {
4240- RoomMessageEventContentWithoutRelation::text_html(content.body, formatted_body)
4241- }
4242- None if emote => RoomMessageEventContentWithoutRelation::emote_plain(content.body),
4243- None => RoomMessageEventContentWithoutRelation::text_plain(content.body),
4244- };
4245- let content = content.add_mentions(Mentions::with_user_ids(mentions.user_ids));
4246- if mentions.room {
4247- content.add_mentions(Mentions::with_room_mention())
4248- } else {
4249- content
4250- }
4251-}
4252-
4253-async fn run_pagination(request: PaginationRequest) {
4254- let PaginationRequest {
4255- timeline,
4256- events,
4257- timeline_id,
4258- limit,
4259- command_id,
4260- results,
4261- } = request;
4262- if events
4263- .send(Event::HistoryChanged {
4264- timeline_id: timeline_id.clone(),
4265- state: HistoryState::Loading,
4266- })
4267- .await
4268- .is_err()
4269- {
4270- if let Some(id) = command_id {
4271- let _ = results.send((
4272- id,
4273- Err(CommandError::new(
4274- CommandErrorKind::EventReceiverClosed,
4275- "event receiver has closed",
4276- )),
4277- ));
4278- }
4279- return;
4280- }
4281- let result = timeline
4282- .paginate_backwards(limit.max(1))
4283- .await
4284- .map_err(timeline_error);
4285- match result {
4286- Ok(at_start) => {
4287- let _ = events
4288- .send(Event::HistoryChanged {
4289- timeline_id,
4290- state: if at_start {
4291- HistoryState::StartReached
4292- } else {
4293- HistoryState::Idle
4294- },
4295- })
4296- .await;
4297- if let Some(id) = command_id {
4298- let _ = results.send((id, Ok(())));
4299- }
4300- }
4301- Err(error) => {
4302- let _ = events
4303- .send(Event::HistoryChanged {
4304- timeline_id,
4305- state: HistoryState::Failed(error.to_string()),
4306- })
4307- .await;
4308- if let Some(id) = command_id {
4309- let _ = results.send((id, Err(error)));
4310- }
4311- }
4312- }
4313-}
4314-
4315-async fn emit_command_result_with_room(
4316- events: &EventSender,
4317- id: CommandId,
4318- room_id: OwnedRoomId,
4319-) -> Result<(), SendEventError> {
4320- events
4321- .send(Event::CommandSucceededWithRoom {
4322- id,
4323- command: CommandKind::Room,
4324- room_id,
4325- })
4326- .await
4327-}
4328-
4329-async fn emit_command_result(
4330- events: &EventSender,
4331- id: CommandId,
4332- command: CommandKind,
4333- result: Result<(), CommandError>,
4334-) -> Result<(), SendEventError> {
4335- let event = match result {
4336- Ok(()) => Event::CommandSucceeded { id, command },
4337- Err(error) => {
4338- tracing::warn!(?id, ?command, error = ?error, "room actor command failed");
4339- Event::CommandFailed(CommandFailure {
4340- id,
4341- command,
4342- kind: error.kind,
4343- message: error.to_string(),
4344- })
4345- }
4346- };
4347- events.send(event).await
4348-}
4349-
4350-fn send_error(source: SendEventError) -> CommandError {
4351- CommandError::with_source(
4352- CommandErrorKind::EventReceiverClosed,
4353- "event receiver has closed",
4354- source,
4355- )
4356-}
4357-
4358-fn timeline_error(source: matrix_sdk_ui::timeline::Error) -> CommandError {
4359- CommandError::with_source(
4360- CommandErrorKind::Timeline,
4361- "timeline operation failed",
4362- source,
4363- )
4364-}
Added file: src/engine/commands.rs+319 additions
@@ -0,0 +1,319 @@
1+use std::{future::Future, panic::AssertUnwindSafe};
2+
3+use futures_util::FutureExt;
4+use matrix_sdk::{
5+ Client,
6+ notification_settings::{IsEncrypted, IsOneToOne, RoomNotificationMode as SdkMode},
7+ ruma::{
8+ OwnedDeviceId, OwnedEventId, RoomId, TransactionId,
9+ api::client::receipt::create_receipt::v3::ReceiptType, events::receipt::ReceiptThread,
10+ },
11+ send_queue::{LocalEchoContent, SendHandle},
12+};
13+use tokio::sync::mpsc;
14+
15+use crate::{
16+ EncryptionCommand, Error, ErrorKind, InteractiveAuth, InteractiveAuthChallenge,
17+ NotificationCommand, RoomNotificationMode, TimelineId,
18+ encryption::VerificationController,
19+ transport::{EventSender, Responder},
20+};
21+
22+pub(super) struct Job<T> {
23+ pub(super) command: T,
24+ pub(super) reply: Responder,
25+}
26+
27+/// a panicking command answers with an error instead of taking its worker down with it
28+pub(super) async fn guarded<T>(
29+ label: &'static str,
30+ work: impl Future<Output = Result<T, Error>>,
31+) -> Result<T, Error> {
32+ AssertUnwindSafe(work)
33+ .catch_unwind()
34+ .await
35+ .unwrap_or_else(|_| Err(Error::new(ErrorKind::Other, format!("{label} panicked"))))
36+}
37+
38+pub(super) async fn run_notifications(
39+ client: Client,
40+ mut jobs: mpsc::Receiver<Job<NotificationCommand>>,
41+) {
42+ while let Some(Job { command, reply }) = jobs.recv().await {
43+ reply.done(guarded("notification command", notification(&client, command)).await);
44+ }
45+}
46+
47+pub(super) async fn run_encryption(
48+ client: Client,
49+ events: EventSender,
50+ mut jobs: mpsc::Receiver<Job<EncryptionCommand>>,
51+) {
52+ let mut verification = VerificationController::default();
53+ while let Some(Job { command, reply }) = jobs.recv().await {
54+ let work = encryption(&client, &events, &mut verification, command);
55+ reply.done(guarded("encryption command", work).await);
56+ }
57+}
58+
59+async fn encryption(
60+ client: &Client,
61+ events: &EventSender,
62+ verification: &mut VerificationController,
63+ command: EncryptionCommand,
64+) -> Result<(), Error> {
65+ let encryption = client.encryption();
66+ match command {
67+ EncryptionCommand::Recover(key) => encryption
68+ .recovery()
69+ .recover(&key)
70+ .await
71+ .map_err(|source| Error::from_recovery("account recovery failed", source)),
72+ EncryptionCommand::RecoverKeyBackup(key) => encryption
73+ .recovery()
74+ .recover_and_fix_backup(&key)
75+ .await
76+ .map_err(|source| Error::from_recovery("recovering key backup failed", source)),
77+ EncryptionCommand::CreateKeyBackup => encryption
78+ .backups()
79+ .create()
80+ .await
81+ .map_err(|source| Error::from_sdk("creating key backup failed", source)),
82+ EncryptionCommand::EnableKeyBackup => encryption
83+ .recovery()
84+ .enable_backup()
85+ .await
86+ .map_err(|source| Error::from_recovery("enabling key backup failed", source)),
87+ EncryptionCommand::DisableKeyBackup { delete_from_server } => {
88+ let backups = encryption.backups();
89+ if delete_from_server {
90+ backups.disable_and_delete().await
91+ } else {
92+ backups.disable().await
93+ }
94+ .map_err(|source| Error::from_sdk("disabling key backup failed", source))
95+ }
96+ EncryptionCommand::StartVerification(target) => {
97+ verification.start(client, events.clone(), target).await
98+ }
99+ EncryptionCommand::Verify(action) => verification.apply(events.clone(), action).await,
100+ }
101+}
102+
103+async fn notification(client: &Client, command: NotificationCommand) -> Result<(), Error> {
104+ let settings = client.notification_settings().await;
105+ match command {
106+ NotificationCommand::SetRoomMode { room_id, mode } => {
107+ settings
108+ .set_room_notification_mode(&room_id, sdk_mode(mode))
109+ .await
110+ }
111+ NotificationCommand::ResetRoomMode(room_id) => {
112+ settings.delete_user_defined_room_rules(&room_id).await
113+ }
114+ NotificationCommand::SetDefaultMode {
115+ encrypted,
116+ one_to_one,
117+ mode,
118+ } => {
119+ settings
120+ .set_default_room_notification_mode(
121+ IsEncrypted::from(encrypted),
122+ IsOneToOne::from(one_to_one),
123+ sdk_mode(mode),
124+ )
125+ .await
126+ }
127+ NotificationCommand::AddKeyword(keyword) => settings.add_keyword(keyword).await,
128+ NotificationCommand::RemoveKeyword(keyword) => settings.remove_keyword(&keyword).await,
129+ }
130+ .map_err(|source| {
131+ Error::with_source(
132+ ErrorKind::Server,
133+ "updating notification settings failed",
134+ source,
135+ )
136+ })
137+}
138+
139+const fn sdk_mode(mode: RoomNotificationMode) -> SdkMode {
140+ match mode {
141+ RoomNotificationMode::AllMessages => SdkMode::AllMessages,
142+ RoomNotificationMode::MentionsAndKeywordsOnly => SdkMode::MentionsAndKeywordsOnly,
143+ RoomNotificationMode::Mute => SdkMode::Mute,
144+ }
145+}
146+
147+fn room(client: &Client, room_id: &RoomId) -> Result<matrix_sdk::Room, Error> {
148+ client.get_room(room_id).ok_or_else(Error::room_unknown)
149+}
150+
151+/// the closed-room path. without a timeline nothing stops the marker going backwards
152+pub(super) async fn mark_read(
153+ client: &Client,
154+ timeline_id: &TimelineId,
155+ event_id: OwnedEventId,
156+) -> Result<(), Error> {
157+ let room = room(client, timeline_id.room_id())?;
158+ let receipt_failed = |source| Error::from_sdk("sending read receipt failed", source);
159+ let thread = match timeline_id.thread_root() {
160+ None => ReceiptThread::Main,
161+ Some(root) => ReceiptThread::Thread(root.clone()),
162+ };
163+ room.send_single_receipt(ReceiptType::Read, thread, event_id.clone())
164+ .await
165+ .map_err(receipt_failed)?;
166+ if timeline_id.thread_root().is_none() {
167+ room.send_single_receipt(ReceiptType::FullyRead, ReceiptThread::Unthreaded, event_id)
168+ .await
169+ .map_err(receipt_failed)?;
170+ }
171+ Ok(())
172+}
173+
174+pub(super) async fn set_typing(
175+ client: &Client,
176+ room_id: &RoomId,
177+ typing: bool,
178+) -> Result<(), Error> {
179+ room(client, room_id)?
180+ .typing_notice(typing)
181+ .await
182+ .map_err(|source| Error::from_sdk("typing update failed", source))
183+}
184+
185+async fn send_handle(
186+ client: &Client,
187+ room_id: &RoomId,
188+ transaction_id: &TransactionId,
189+) -> Result<SendHandle, Error> {
190+ let (echoes, _) = room(client, room_id)?
191+ .send_queue()
192+ .subscribe()
193+ .await
194+ .map_err(|source| {
195+ Error::with_source(ErrorKind::Store, "reading send queue failed", source)
196+ })?;
197+ echoes
198+ .into_iter()
199+ .find_map(|echo| match echo.content {
200+ LocalEchoContent::Event { send_handle, .. }
201+ if echo.transaction_id.as_str() == transaction_id.as_str() =>
202+ {
203+ Some(send_handle)
204+ }
205+ _ => None,
206+ })
207+ .ok_or_else(|| {
208+ Error::new(
209+ ErrorKind::Invalid,
210+ "send is no longer pending or is not a message",
211+ )
212+ })
213+}
214+
215+pub(super) async fn cancel_send(
216+ client: &Client,
217+ room_id: &RoomId,
218+ transaction_id: &TransactionId,
219+) -> Result<(), Error> {
220+ match send_handle(client, room_id, transaction_id)
221+ .await?
222+ .abort()
223+ .await
224+ {
225+ Ok(true) => Ok(()),
226+ Ok(false) => Err(Error::new(
227+ ErrorKind::Invalid,
228+ "send could not be cancelled because it has already been sent",
229+ )),
230+ Err(source) => Err(Error::with_source(
231+ ErrorKind::Store,
232+ "cancelling send failed",
233+ source,
234+ )),
235+ }
236+}
237+
238+pub(super) async fn retry_send(
239+ client: &Client,
240+ room_id: &RoomId,
241+ transaction_id: &TransactionId,
242+) -> Result<(), Error> {
243+ send_handle(client, room_id, transaction_id)
244+ .await?
245+ .unwedge()
246+ .await
247+ .map_err(|source| Error::with_source(ErrorKind::Store, "retrying send failed", source))?;
248+ room(client, room_id)?.send_queue().set_enabled(true);
249+ Ok(())
250+}
251+
252+pub(super) async fn upload_avatar(
253+ client: &Client,
254+ mimetype: &str,
255+ bytes: Vec<u8>,
256+ max_bytes: usize,
257+) -> Result<(), Error> {
258+ if bytes.len() > max_bytes {
259+ return Err(Error::new(
260+ ErrorKind::MediaTooLarge,
261+ "avatar exceeds the configured byte limit",
262+ ));
263+ }
264+ let mimetype: mime::Mime = mimetype
265+ .parse()
266+ .map_err(|_| Error::new(ErrorKind::Invalid, "avatar MIME type is invalid"))?;
267+ if mimetype.type_() != mime::IMAGE {
268+ return Err(Error::new(
269+ ErrorKind::Invalid,
270+ "an avatar has to be an image",
271+ ));
272+ }
273+ client
274+ .account()
275+ .upload_avatar(&mimetype, bytes)
276+ .await
277+ .map(drop)
278+ .map_err(|source| Error::from_sdk("uploading avatar failed", source))
279+}
280+
281+pub(super) async fn rename_device(
282+ client: &Client,
283+ device_id: &OwnedDeviceId,
284+ display_name: &str,
285+) -> Result<(), Error> {
286+ client
287+ .rename_device(device_id, display_name)
288+ .await
289+ .map(drop)
290+ .map_err(|source| Error::from_http("renaming device failed", source))
291+}
292+
293+pub(super) async fn delete_devices(
294+ client: &Client,
295+ device_ids: &[OwnedDeviceId],
296+ auth: Option<InteractiveAuth>,
297+) -> Result<(), Error> {
298+ let user_id = client
299+ .user_id()
300+ .ok_or_else(|| Error::new(ErrorKind::Unauthenticated, "the client is not logged in"))?;
301+ let auth = auth
302+ .map(|auth| auth.into_sdk(user_id))
303+ .transpose()
304+ .map_err(|message| {
305+ Error::new(
306+ ErrorKind::Invalid,
307+ format!("interactive authentication data is invalid, {message}"),
308+ )
309+ })?;
310+ match client.delete_devices(device_ids, auth).await {
311+ Ok(_) => Ok(()),
312+ Err(source) => match source.as_uiaa_response() {
313+ Some(info) => Err(Error::interactive_auth(InteractiveAuthChallenge::from(
314+ info,
315+ ))),
316+ None => Err(Error::from_http("deleting devices failed", source)),
317+ },
318+ }
319+}
Added file: src/engine/media.rs+470 additions
@@ -0,0 +1,572 @@
1+use std::{
2+ borrow::Cow,
3+ collections::{HashMap, VecDeque},
4+ io::Read as _,
5+ panic::AssertUnwindSafe,
6+ sync::Arc,
7+};
8+
9+use futures_util::{
10+ FutureExt, StreamExt,
11+ future::{BoxFuture, Shared, WeakShared},
12+};
13+use matrix_sdk::{
14+ Client, Room,
15+ media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
16+ room::reply::{EnforceThread, Reply as SdkReply},
17+ ruma::{
18+ api::{
19+ Metadata, OutgoingRequest,
20+ auth_scheme::SendAccessToken,
21+ client::{authenticated_media, media},
22+ },
23+ events::room::{
24+ MediaSource,
25+ message::{AddMentions, ReplyWithinThread},
26+ },
27+ },
28+};
29+use matrix_sdk_crypto::{AttachmentDecryptor, MediaEncryptionInfo};
30+use ruma::MxcUri;
31+use tokio::sync::Mutex;
32+
33+use crate::{
34+ AttachmentUpload, Error, ErrorKind, Media, MediaId, MediaKind, MediaSize, TimelineId,
35+ normalise::{MediaRegistration, MediaScope},
36+ upload::prepare_upload,
37+};
38+
39+/// the uploader's thumbnail is used when there is one, this is only what we ask the server for
40+const TIMELINE_THUMBNAIL: u32 = 800;
41+
42+type FlightFuture = BoxFuture<'static, Result<Media, Error>>;
43+type Flight = Shared<FlightFuture>;
44+
45+/// owns everything needed to turn a media id or an mxc uri into bytes, so the
46+/// handle can answer media queries without a trip through the engine
47+pub(crate) struct MediaService {
48+ client: Client,
49+ registry: Mutex<MediaRegistry>,
50+ flights: Mutex<HashMap<String, WeakShared<FlightFuture>>>,
51+ max_bytes: usize,
52+}
53+
54+impl MediaService {
55+ pub(crate) fn new(client: Client, max_bytes: usize, max_sources: usize) -> Self {
56+ Self {
57+ client,
58+ registry: Mutex::new(MediaRegistry::new(max_sources)),
59+ flights: Mutex::new(HashMap::new()),
60+ max_bytes,
61+ }
62+ }
63+
64+ pub(crate) async fn register(&self, media: Vec<MediaRegistration>) {
65+ if !media.is_empty() {
66+ self.registry.lock().await.insert_all(media);
67+ }
68+ }
69+
70+ pub(crate) async fn release(&self, scope: &MediaScope) {
71+ self.registry.lock().await.release(scope);
72+ }
73+
74+ pub(crate) async fn clear(&self) {
75+ self.registry.lock().await.clear();
76+ }
77+
78+ pub(crate) async fn timeline_media(
79+ &self,
80+ media_id: &MediaId,
81+ size: MediaSize,
82+ ) -> Result<Media, Error> {
83+ let media = self
84+ .registry
85+ .lock()
86+ .await
87+ .get(media_id)
88+ .cloned()
89+ .ok_or_else(|| {
90+ Error::new(
91+ ErrorKind::MediaUnknown,
92+ "media is not registered in a retained timeline",
93+ )
94+ })?;
95+ let (source, format, mimetype, declared_size) = match size {
96+ MediaSize::Full => (
97+ media.full.source,
98+ MediaFormat::File,
99+ media.full.mimetype,
100+ media.full.declared_size,
101+ ),
102+ MediaSize::Thumbnail { .. } => match media.thumbnail {
103+ Some(thumbnail) => (
104+ thumbnail.source,
105+ MediaFormat::File,
106+ thumbnail.mimetype,
107+ thumbnail.declared_size,
108+ ),
109+ // the server can only resize what it can read
110+ None if matches!(media.kind, MediaKind::Image | MediaKind::Video)
111+ && matches!(media.full.source, MediaSource::Plain(_)) =>
112+ {
113+ (
114+ media.full.source,
115+ thumbnail_format(TIMELINE_THUMBNAIL, TIMELINE_THUMBNAIL),
116+ media.full.mimetype,
117+ None,
118+ )
119+ }
120+ None => {
121+ return Err(Error::new(
122+ ErrorKind::MediaUnavailable,
123+ "the requested media size is unavailable",
124+ ));
125+ }
126+ },
127+ };
128+ if declared_size.is_some_and(|size| size > self.max_bytes as u64) {
129+ return Err(too_large("media exceeds the configured byte limit"));
130+ }
131+ self.fetch(MediaRequestParameters { source, format }, mimetype)
132+ .await
133+ }
134+
135+ pub(crate) async fn mxc_media(&self, uri: &MxcUri, size: MediaSize) -> Result<Media, Error> {
136+ if !uri.is_valid() {
137+ return Err(Error::new(ErrorKind::Invalid, "mxc uri is malformed"));
138+ }
139+ let format = match size {
140+ MediaSize::Full => MediaFormat::File,
141+ MediaSize::Thumbnail { width, height } => thumbnail_format(width, height),
142+ };
143+ self.fetch(
144+ MediaRequestParameters {
145+ source: MediaSource::Plain(uri.to_owned()),
146+ format,
147+ },
148+ None,
149+ )
150+ .await
151+ }
152+
153+ async fn fetch(
154+ &self,
155+ request: MediaRequestParameters,
156+ mimetype: Option<String>,
157+ ) -> Result<Media, Error> {
158+ let key = flight_key(&request);
159+ let flight = {
160+ let mut flights = self.flights.lock().await;
161+ flights.retain(|_, flight| flight.upgrade().is_some());
162+ match flights.get(&key).and_then(WeakShared::upgrade) {
163+ Some(flight) => flight,
164+ None => {
165+ let flight: Flight = AssertUnwindSafe(fetch(
166+ self.client.clone(),
167+ request,
168+ mimetype,
169+ self.max_bytes,
170+ ))
171+ .catch_unwind()
172+ .map(|result| {
173+ result.unwrap_or_else(|_| {
174+ Err(Error::new(ErrorKind::Other, "media fetch panicked"))
175+ })
176+ })
177+ .boxed()
178+ .shared();
179+ if let Some(weak) = flight.downgrade() {
180+ flights.insert(key, weak);
181+ }
182+ flight
183+ }
184+ }
185+ };
186+ flight.await
187+ }
188+}
189+
190+fn thumbnail_format(width: u32, height: u32) -> MediaFormat {
191+ MediaFormat::Thumbnail(MediaThumbnailSettings::new(width.into(), height.into()))
192+}
193+
194+fn flight_key(request: &MediaRequestParameters) -> String {
195+ let uri = match &request.source {
196+ MediaSource::Plain(uri) => uri.as_str(),
197+ MediaSource::Encrypted(file) => file.url.as_str(),
198+ };
199+ match &request.format {
200+ MediaFormat::File => uri.to_owned(),
201+ MediaFormat::Thumbnail(settings) => format!(
202+ "{uri}|{}|{}x{}|{}",
203+ settings.method, settings.width, settings.height, settings.animated
204+ ),
205+ }
206+}
207+
208+fn too_large(message: &'static str) -> Error {
209+ Error::new(ErrorKind::MediaTooLarge, message)
210+}
211+
212+async fn fetch(
213+ client: Client,
214+ request: MediaRequestParameters,
215+ mimetype: Option<String>,
216+ max_bytes: usize,
217+) -> Result<Media, Error> {
218+ if let Some(cached) = cached_media(&client, &request).await? {
219+ if cached.len() > max_bytes {
220+ if let Ok(store) = client.media_store().lock().await {
221+ let _ = store.remove_media_content(&request).await;
222+ }
223+ return Err(too_large("cached media exceeds the configured byte limit"));
224+ }
225+ return Ok(media(cached, mimetype));
226+ }
227+ let (bytes, served_mimetype) = download_bounded(&client, &request, max_bytes).await?;
228+ // the sdk caches plaintext under the encrypted key, so we do too or nobody can read anything
229+ let bytes = maybe_decrypt(bytes, &request, max_bytes)?;
230+ if let Ok(store) = client.media_store().lock().await {
231+ let _ = store
232+ .add_media_content(
233+ &request,
234+ bytes.clone(),
235+ matrix_sdk::media::store::IgnoreMediaRetentionPolicy::No,
236+ )
237+ .await;
238+ }
239+ Ok(media(bytes, mimetype.or(served_mimetype)))
240+}
241+
242+fn media(bytes: Vec<u8>, mimetype: Option<String>) -> Media {
243+ let mimetype = mimetype
244+ .or_else(|| sniff_image(&bytes).map(str::to_owned))
245+ .unwrap_or_else(|| "application/octet-stream".to_owned());
246+ Media {
247+ bytes: Arc::from(bytes),
248+ mimetype,
249+ }
250+}
251+
252+/// the cache forgets content types, and avatars never declare one. images are the common case
253+fn sniff_image(bytes: &[u8]) -> Option<&'static str> {
254+ match bytes {
255+ [0x89, b'P', b'N', b'G', ..] => Some("image/png"),
256+ [0xff, 0xd8, 0xff, ..] => Some("image/jpeg"),
257+ [b'G', b'I', b'F', b'8', ..] => Some("image/gif"),
258+ [
259+ b'R',
260+ b'I',
261+ b'F',
262+ b'F',
263+ _,
264+ _,
265+ _,
266+ _,
267+ b'W',
268+ b'E',
269+ b'B',
270+ b'P',
271+ ..,
272+ ] => Some("image/webp"),
273+ _ => None,
274+ }
275+}
276+
277+async fn cached_media(
278+ client: &Client,
279+ request: &MediaRequestParameters,
280+) -> Result<Option<Vec<u8>>, Error> {
281+ fn cache_failed(source: impl std::error::Error + Send + Sync + 'static) -> Error {
282+ Error::with_source(ErrorKind::Store, "reading media cache failed", source)
283+ }
284+ client
285+ .media_store()
286+ .lock()
287+ .await
288+ .map_err(cache_failed)?
289+ .get_media_content(request)
290+ .await
291+ .map_err(cache_failed)
292+}
293+
294+fn maybe_decrypt(
295+ bytes: Vec<u8>,
296+ request: &MediaRequestParameters,
297+ max_bytes: usize,
298+) -> Result<Vec<u8>, Error> {
299+ let MediaSource::Encrypted(file) = &request.source else {
300+ return Ok(bytes);
301+ };
302+ fn decrypt_failed(source: impl std::error::Error + Send + Sync + 'static) -> Error {
303+ Error::with_source(
304+ ErrorKind::MediaUnavailable,
305+ "media decryption failed",
306+ source,
307+ )
308+ }
309+ let info: MediaEncryptionInfo = (**file).clone().into();
310+ let mut cursor = std::io::Cursor::new(bytes);
311+ let mut decryptor = AttachmentDecryptor::new(&mut cursor, info).map_err(decrypt_failed)?;
312+ let mut decrypted = Vec::new();
313+ let mut chunk = [0_u8; 8192];
314+ loop {
315+ let read = decryptor.read(&mut chunk).map_err(decrypt_failed)?;
316+ if read == 0 {
317+ return Ok(decrypted);
318+ }
319+ if read > max_bytes.saturating_sub(decrypted.len()) {
320+ return Err(too_large(
321+ "decrypted media exceeds the configured byte limit",
322+ ));
323+ }
324+ decrypted.extend_from_slice(&chunk[..read]);
325+ }
326+}
327+
328+/// the sdk's own download has no size cap and no timeout, so we do it by hand
329+async fn download_bounded(
330+ client: &Client,
331+ request: &MediaRequestParameters,
332+ max_bytes: usize,
333+) -> Result<(Vec<u8>, Option<String>), Error> {
334+ let uri = match &request.source {
335+ MediaSource::Plain(uri) => uri,
336+ MediaSource::Encrypted(file) => &file.url,
337+ };
338+ if uri
339+ .server_name()
340+ .is_ok_and(|server| server.as_str() == "send-queue.localhost")
341+ {
342+ return Err(Error::new(
343+ ErrorKind::MediaUnavailable,
344+ "local media is not in the media cache",
345+ ));
346+ }
347+ let network_failed =
348+ |source| Error::with_source(ErrorKind::Network, "media download failed", source);
349+ let mut refreshed = false;
350+ let response = loop {
351+ let http_request = media_http_request(client, request).await?;
352+ let (parts, _) = http_request.into_parts();
353+ let mut builder = client.http_client().get(parts.uri.to_string());
354+ for (name, value) in &parts.headers {
355+ builder = builder.header(name.as_str(), value.as_bytes());
356+ }
357+ let response = builder.send().await.map_err(network_failed)?;
358+ if response.status() != matrix_sdk::reqwest::StatusCode::UNAUTHORIZED {
359+ break response;
360+ }
361+ if refreshed
362+ || client
363+ .session_tokens()
364+ .is_none_or(|tokens| tokens.refresh_token.is_none())
365+ {
366+ return Err(Error::new(
367+ ErrorKind::Unauthenticated,
368+ "media download was not authorized",
369+ ));
370+ }
371+ client.refresh_access_token().await.map_err(|source| {
372+ Error::from_refresh_token("refreshing media access token failed", source)
373+ })?;
374+ refreshed = true;
375+ };
376+ let status = response.status();
377+ if !status.is_success() {
378+ return Err(Error::new(
379+ ErrorKind::MediaUnavailable,
380+ format!("media download failed with status {status}"),
381+ ));
382+ }
383+ if response
384+ .content_length()
385+ .is_some_and(|length| length > max_bytes as u64)
386+ {
387+ return Err(too_large("media exceeds the configured byte limit"));
388+ }
389+ let mimetype = response
390+ .headers()
391+ .get(http::header::CONTENT_TYPE)
392+ .and_then(|value| value.to_str().ok())
393+ .map(str::to_owned);
394+ let mut stream = response.bytes_stream();
395+ let mut bytes = Vec::new();
396+ while let Some(chunk) = stream.next().await {
397+ let chunk = chunk.map_err(network_failed)?;
398+ if chunk.len() > max_bytes.saturating_sub(bytes.len()) {
399+ return Err(too_large(
400+ "downloaded media exceeds the configured byte limit",
401+ ));
402+ }
403+ bytes.extend_from_slice(&chunk);
404+ }
405+ Ok((bytes, mimetype))
406+}
407+
408+async fn media_http_request(
409+ client: &Client,
410+ request: &MediaRequestParameters,
411+) -> Result<http::Request<Vec<u8>>, Error> {
412+ let versions = client.supported_versions().await.map_err(|source| {
413+ Error::with_source(
414+ ErrorKind::Network,
415+ "reading supported server versions failed",
416+ source,
417+ )
418+ })?;
419+ let authenticated =
420+ authenticated_media::get_content::v1::Request::PATH_BUILDER.is_supported(&versions);
421+ let base = client.homeserver().to_string();
422+ let token = client.access_token();
423+ let invalid_uri =
424+ |source| Error::with_source(ErrorKind::Invalid, "media URI is invalid", source);
425+ let (uri, thumbnail) = match (&request.source, &request.format) {
426+ (MediaSource::Encrypted(file), _) => (&file.url, None),
427+ (MediaSource::Plain(uri), MediaFormat::File) => (uri, None),
428+ (MediaSource::Plain(uri), MediaFormat::Thumbnail(settings)) => (uri, Some(settings)),
429+ };
430+ // each endpoint has its own auth input type, so no generic helper can take them all
431+ macro_rules! into_http {
432+ ($endpoint:expr, $token:expr) => {
433+ $endpoint
434+ .try_into_http_request::<Vec<u8>>(&base, $token, Cow::Borrowed(&versions))
435+ .map_err(|source| {
436+ Error::with_source(ErrorKind::Other, "media request is invalid", source)
437+ })?
438+ };
439+ }
440+ let authed = SendAccessToken::IfRequired(token.as_deref().unwrap_or(""));
441+ #[allow(deprecated)]
442+ let mut http_request = match (thumbnail, authenticated) {
443+ (None, true) => into_http!(
444+ authenticated_media::get_content::v1::Request::from_uri(uri).map_err(invalid_uri)?,
445+ authed
446+ ),
447+ (None, false) => into_http!(
448+ media::get_content::v3::Request::from_url(uri).map_err(invalid_uri)?,
449+ SendAccessToken::None
450+ ),
451+ (Some(settings), true) => {
452+ let mut endpoint = authenticated_media::get_content_thumbnail::v1::Request::from_uri(
453+ uri,
454+ settings.width,
455+ settings.height,
456+ )
457+ .map_err(invalid_uri)?;
458+ endpoint.method = Some(settings.method.clone());
459+ endpoint.animated = Some(settings.animated);
460+ into_http!(endpoint, authed)
461+ }
462+ (Some(settings), false) => {
463+ let mut endpoint = media::get_content_thumbnail::v3::Request::from_url(
464+ uri,
465+ settings.width,
466+ settings.height,
467+ )
468+ .map_err(invalid_uri)?;
469+ endpoint.method = Some(settings.method.clone());
470+ en

show full diff (262144 bytes truncated)