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+572 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+ endpoint.animated = Some(settings.animated);
471+ into_http!(endpoint, SendAccessToken::None)
472+ }
473+ };
474+ // legacy endpoints don't ask for a token but plenty of servers want one anyway
475+ if let Some(token) = token.as_deref() {
476+ let value = format!("Bearer {token}").parse().map_err(|_| {
477+ Error::new(ErrorKind::Unauthenticated, "stored access token is invalid")
478+ })?;
479+ http_request
480+ .headers_mut()
481+ .insert(http::header::AUTHORIZATION, value);
482+ }
483+ Ok(http_request)
484+}
485+
486+pub(crate) async fn queue_upload(
487+ room: Room,
488+ upload: AttachmentUpload,
489+ max_bytes: usize,
490+) -> Result<(), Error> {
491+ let (upload, mime, mut config) = prepare_upload(upload, max_bytes)?;
492+ if let TimelineId::Thread { root_event_id, .. } = &upload.timeline_id {
493+ config.reply = Some(SdkReply {
494+ event_id: root_event_id.clone(),
495+ enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
496+ add_mentions: AddMentions::No,
497+ });
498+ }
499+ room.send_queue()
500+ .send_attachment(upload.filename, mime, upload.bytes, config)
501+ .await
502+ .map(drop)
503+ .map_err(|source| {
504+ Error::with_source(
505+ ErrorKind::MediaUnavailable,
506+ "media upload could not be queued",
507+ source,
508+ )
509+ })
510+}
511+
512+/// least recently seen goes first. stale order slots are skipped by stamp rather than hunted down
513+struct MediaRegistry {
514+ entries: HashMap<MediaId, (MediaRegistration, u64)>,
515+ order: VecDeque<(MediaId, u64)>,
516+ next_stamp: u64,
517+ limit: usize,
518+}
519+
520+impl MediaRegistry {
521+ fn new(limit: usize) -> Self {
522+ Self {
523+ entries: HashMap::new(),
524+ order: VecDeque::new(),
525+ next_stamp: 0,
526+ limit,
527+ }
528+ }
529+
530+ fn get(&self, id: &MediaId) -> Option<&MediaRegistration> {
531+ self.entries.get(id).map(|(media, _)| media)
532+ }
533+
534+ fn insert_all(&mut self, media: Vec<MediaRegistration>) {
535+ for media in media {
536+ let stamp = self.next_stamp;
537+ self.next_stamp += 1;
538+ self.order.push_back((media.id.clone(), stamp));
539+ self.entries.insert(media.id.clone(), (media, stamp));
540+ }
541+ while self.entries.len() > self.limit {
542+ let Some((id, stamp)) = self.order.pop_front() else {
543+ break;
544+ };
545+ if self.is_current(&id, stamp) {
546+ self.entries.remove(&id);
547+ }
548+ }
549+ if self.order.len() > self.limit.saturating_mul(4) {
550+ let entries = &self.entries;
551+ self.order
552+ .retain(|(id, stamp)| entries.get(id).is_some_and(|(_, current)| current == stamp));
553+ }
554+ }
555+
556+ fn is_current(&self, id: &MediaId, stamp: u64) -> bool {
557+ self.entries
558+ .get(id)
559+ .is_some_and(|(_, current)| *current == stamp)
560+ }
561+
562+ fn release(&mut self, scope: &MediaScope) {
563+ self.entries.retain(|id, _| !scope.owns(id));
564+ let entries = &self.entries;
565+ self.order.retain(|(id, _)| entries.contains_key(id));
566+ }
567+
568+ fn clear(&mut self) {
569+ self.entries.clear();
570+ self.order.clear();
571+ }
572+}
Added file: src/engine/mod.rs+776 additions
@@ -0,0 +1,776 @@
1+mod commands;
2+mod media;
3+mod mutations;
4+mod room;
5+mod timeline;
6+mod watchers;
7+
8+use std::{
9+ collections::{HashMap, VecDeque},
10+ future::Future,
11+ sync::Arc,
12+};
13+
14+use futures_util::future::BoxFuture;
15+use matrix_sdk::{Client, SessionChange};
16+use matrix_sdk_ui::sync_service::SyncService;
17+use ruma::{OwnedRoomId, RoomId};
18+use thiserror::Error as ThisError;
19+use tokio::{
20+ sync::{Notify, broadcast, mpsc, watch},
21+ task::{AbortHandle, Id as TaskId, JoinError, JoinSet},
22+};
23+
24+pub(crate) use media::MediaService;
25+
26+use self::{
27+ commands::Job,
28+ mutations::{Mutation, MutationJob},
29+ room::{ActorRequest, Exited, RoomActor},
30+ timeline::{SessionContext, Supervisor},
31+};
32+use crate::{
33+ AccountCommand, ClientHandle, Command, ConnectionState, EncryptionCommand, EngineErrorKind,
34+ EngineFailure, Error, ErrorKind, Event, EventStream, LifecycleCommand, MediaCommand,
35+ NotificationCommand, RoomCommand, TimelineCommand,
36+ transport::{DriverPort, Request, Responder, transport_pair},
37+};
38+
39+#[derive(Clone, Debug, Eq, PartialEq)]
40+#[non_exhaustive]
41+pub struct EngineConfig {
42+ pub command_capacity: usize,
43+ pub event_capacity: usize,
44+ pub initial_history_limit: u16,
45+ pub max_media_bytes: usize,
46+ pub max_media_sources: usize,
47+}
48+
49+impl EngineConfig {
50+ #[must_use]
51+ pub const fn with_command_capacity(mut self, capacity: usize) -> Self {
52+ self.command_capacity = capacity;
53+ self
54+ }
55+
56+ #[must_use]
57+ pub const fn with_event_capacity(mut self, capacity: usize) -> Self {
58+ self.event_capacity = capacity;
59+ self
60+ }
61+
62+ #[must_use]
63+ pub const fn with_initial_history_limit(mut self, limit: u16) -> Self {
64+ self.initial_history_limit = limit;
65+ self
66+ }
67+
68+ #[must_use]
69+ pub const fn with_max_media_bytes(mut self, bytes: usize) -> Self {
70+ self.max_media_bytes = bytes;
71+ self
72+ }
73+
74+ #[must_use]
75+ pub const fn with_max_media_sources(mut self, sources: usize) -> Self {
76+ self.max_media_sources = sources;
77+ self
78+ }
79+}
80+
81+impl Default for EngineConfig {
82+ fn default() -> Self {
83+ Self {
84+ command_capacity: 64,
85+ event_capacity: 128,
86+ initial_history_limit: 30,
87+ max_media_bytes: 20 * 1024 * 1024,
88+ max_media_sources: 2_048,
89+ }
90+ }
91+}
92+
93+#[derive(Debug, ThisError)]
94+pub enum EngineError {
95+ #[error("the Matrix client is not authenticated")]
96+ Unauthenticated,
97+ #[error("command, event, and media source capacities must be nonzero")]
98+ ZeroCapacity,
99+ #[error("could not build the sync service")]
100+ SyncService(#[source] matrix_sdk_ui::sync_service::Error),
101+ #[error("the event receiver has closed")]
102+ EventReceiverClosed,
103+ #[error("engine task {task} panicked")]
104+ TaskPanicked { task: String },
105+}
106+
107+impl EngineError {
108+ fn failure(&self) -> EngineFailure {
109+ EngineFailure {
110+ kind: match self {
111+ Self::EventReceiverClosed => EngineErrorKind::EventReceiverClosed,
112+ Self::TaskPanicked { .. } => EngineErrorKind::Watcher,
113+ Self::Unauthenticated | Self::ZeroCapacity | Self::SyncService(_) => {
114+ EngineErrorKind::Other
115+ }
116+ },
117+ message: self.to_string(),
118+ }
119+ }
120+}
121+
122+#[derive(Clone)]
123+enum TaskContext {
124+ /// a panic here costs the caller an `Interrupted` answer and nothing else
125+ Command(&'static str),
126+ /// a panic here takes the engine with it
127+ Watcher(&'static str),
128+ RoomActor {
129+ room_id: OwnedRoomId,
130+ generation: u64,
131+ },
132+}
133+
134+impl TaskContext {
135+ fn label(&self) -> String {
136+ match self {
137+ Self::Command(name) | Self::Watcher(name) => (*name).to_owned(),
138+ Self::RoomActor {
139+ room_id,
140+ generation,
141+ } => format!("room actor {room_id} generation {generation}"),
142+ }
143+ }
144+}
145+
146+struct TaskRegistry {
147+ tasks: JoinSet<()>,
148+ contexts: HashMap<TaskId, TaskContext>,
149+}
150+
151+impl TaskRegistry {
152+ fn spawn(
153+ &mut self,
154+ context: TaskContext,
155+ task: impl Future<Output = ()> + Send + 'static,
156+ ) -> AbortHandle {
157+ let handle = self.tasks.spawn(task);
158+ self.contexts.insert(handle.id(), context);
159+ handle
160+ }
161+
162+ async fn join_next(&mut self) -> Option<(TaskContext, Result<(), JoinError>)> {
163+ let result = self.tasks.join_next_with_id().await?;
164+ let id = match &result {
165+ Ok((id, ())) => *id,
166+ Err(error) => error.id(),
167+ };
168+ let context = self.contexts.remove(&id)?;
169+ Some((context, result.map(drop)))
170+ }
171+
172+ async fn shutdown(&mut self) {
173+ self.tasks.abort_all();
174+ while self.tasks.join_next().await.is_some() {}
175+ self.contexts.clear();
176+ }
177+}
178+
179+enum RoomSlot {
180+ Active {
181+ generation: u64,
182+ sender: mpsc::Sender<ActorRequest>,
183+ },
184+ /// opens and closes that arrive while the actor winds down, replayed in order once it's gone
185+ Closing {
186+ generation: u64,
187+ pending: VecDeque<Deferred>,
188+ },
189+}
190+
191+impl RoomSlot {
192+ const fn generation(&self) -> u64 {
193+ match self {
194+ Self::Active { generation, .. } | Self::Closing { generation, .. } => *generation,
195+ }
196+ }
197+}
198+
199+enum Deferred {
200+ Open(Responder),
201+ Close(Responder),
202+}
203+
204+struct Queues {
205+ mutations: mpsc::Receiver<MutationJob>,
206+ notifications: mpsc::Receiver<Job<NotificationCommand>>,
207+ encryption: mpsc::Receiver<Job<EncryptionCommand>>,
208+ session_changes: broadcast::Receiver<SessionChange>,
209+}
210+
211+pub struct Engine {
212+ client: Client,
213+ sync: Arc<SyncService>,
214+ port: DriverPort,
215+ config: EngineConfig,
216+ media: Arc<MediaService>,
217+ tasks: TaskRegistry,
218+ rooms: HashMap<OwnedRoomId, RoomSlot>,
219+ next_generation: u64,
220+ exits: mpsc::UnboundedReceiver<Exited>,
221+ exit_sender: mpsc::UnboundedSender<Exited>,
222+ mutations: mpsc::Sender<MutationJob>,
223+ notifications: mpsc::Sender<Job<NotificationCommand>>,
224+ encryption: mpsc::Sender<Job<EncryptionCommand>>,
225+ queues: Option<Queues>,
226+ /// one progress forwarder per room that has uploaded, and whether it's subscribed yet
227+ uploads: HashMap<OwnedRoomId, (AbortHandle, watch::Receiver<bool>)>,
228+ stop: Arc<Notify>,
229+}
230+
231+impl Engine {
232+ /// the escape hatch for a client you built yourself. `Session` is the usual way in
233+ pub async fn from_client(
234+ client: Client,
235+ config: EngineConfig,
236+ ) -> Result<(Self, ClientHandle, EventStream), EngineError> {
237+ if client.user_id().is_none() {
238+ return Err(EngineError::Unauthenticated);
239+ }
240+ if config.command_capacity == 0
241+ || config.event_capacity == 0
242+ || config.max_media_sources == 0
243+ {
244+ return Err(EngineError::ZeroCapacity);
245+ }
246+ let session_changes = client.subscribe_to_session_changes();
247+ let sync = Arc::new(
248+ SyncService::builder(client.clone())
249+ .with_offline_mode()
250+ .build()
251+ .await
252+ .map_err(EngineError::SyncService)?,
253+ );
254+ let media = Arc::new(MediaService::new(
255+ client.clone(),
256+ config.max_media_bytes,
257+ config.max_media_sources,
258+ ));
259+ let (handle, port, events) = transport_pair(
260+ client.clone(),
261+ sync.clone(),
262+ media.clone(),
263+ config.command_capacity,
264+ config.event_capacity,
265+ );
266+ client.send_queue().enable_upload_progress(true);
267+ let (exit_sender, exits) = mpsc::unbounded_channel();
268+ let (mutations, mutation_queue) = mpsc::channel(config.command_capacity);
269+ let (notifications, notification_queue) = mpsc::channel(config.command_capacity);
270+ let (encryption, encryption_queue) = mpsc::channel(config.command_capacity);
271+ let engine = Self {
272+ client,
273+ sync,
274+ port,
275+ config,
276+ media,
277+ tasks: TaskRegistry {
278+ tasks: JoinSet::new(),
279+ contexts: HashMap::new(),
280+ },
281+ rooms: HashMap::new(),
282+ next_generation: 0,
283+ exits,
284+ exit_sender,
285+ mutations,
286+ notifications,
287+ encryption,
288+ queues: Some(Queues {
289+ mutations: mutation_queue,
290+ notifications: notification_queue,
291+ encryption: encryption_queue,
292+ session_changes,
293+ }),
294+ uploads: HashMap::new(),
295+ stop: Arc::new(Notify::new()),
296+ };
297+ Ok((engine, handle, events))
298+ }
299+
300+ pub async fn run(mut self) -> Result<(), EngineError> {
301+ let events = self.port.event_sender();
302+ let user_id = self
303+ .client
304+ .user_id()
305+ .expect("authentication was checked during construction")
306+ .to_owned();
307+ self.port
308+ .connection_sender()
309+ .send_replace(ConnectionState::Connecting);
310+ for event in [
311+ Event::Authenticated { user_id },
312+ Event::ConnectionChanged(ConnectionState::Connecting),
313+ ] {
314+ if events.send(event).await.is_err() {
315+ self.shutdown().await;
316+ return Err(EngineError::EventReceiverClosed);
317+ }
318+ }
319+ self.start_tasks();
320+ self.sync.start().await;
321+
322+ let mut failure = None;
323+ let mut shutdown_reply = None;
324+ loop {
325+ tokio::select! {
326+ biased;
327+ () = events.closed() => {
328+ failure = Some(EngineError::EventReceiverClosed);
329+ break;
330+ }
331+ () = self.stop.notified() => break,
332+ request = self.port.recv() => {
333+ let Some(request) = request else { break };
334+ if let Some(reply) = self.dispatch(request).await {
335+ shutdown_reply = Some(reply);
336+ break;
337+ }
338+ }
339+ Some((context, result)) = self.tasks.join_next(), if !self.tasks.tasks.is_empty() => {
340+ let Err(error) = result else { continue };
341+ if !error.is_panic() {
342+ continue;
343+ }
344+ if let TaskContext::Command(label) = context {
345+ tracing::error!(label, %error, "command task panicked");
346+ continue;
347+ }
348+ failure = Some(EngineError::TaskPanicked { task: context.label() });
349+ break;
350+ }
351+ Some(exited) = self.exits.recv() => self.room_exited(exited).await,
352+ }
353+ }
354+
355+ if let Some(error) = &failure {
356+ let _ = events.send(Event::Failed(error.failure())).await;
357+ }
358+ self.shutdown().await;
359+ let _ = events.send(Event::Stopped).await;
360+ if let Some(reply) = shutdown_reply {
361+ reply.done(Ok(()));
362+ }
363+ failure.map_or(Ok(()), Err)
364+ }
365+
366+ fn start_tasks(&mut self) {
367+ let queues = self.queues.take().expect("engine tasks start once");
368+ let client = &self.client;
369+ let events = || self.port.event_sender();
370+ let watchers: [(&'static str, BoxFuture<'static, ()>); 9] = [
371+ (
372+ "session change watcher",
373+ Box::pin(watchers::session(
374+ queues.session_changes,
375+ events(),
376+ self.stop.clone(),
377+ )),
378+ ),
379+ (
380+ "connection state watcher",
381+ Box::pin(watchers::connection(
382+ self.sync.state(),
383+ self.sync.clone(),
384+ client.clone(),
385+ events(),
386+ self.port.connection_sender(),
387+ )),
388+ ),
389+ (
390+ "send queue watcher",
391+ Box::pin(watchers::send_queue_errors(
392+ client.clone(),
393+ client.send_queue().subscribe_errors(),
394+ self.port.connection_sender().subscribe(),
395+ )),
396+ ),
397+ (
398+ "encryption state watcher",
399+ Box::pin(watchers::encryption(client.clone(), events())),
400+ ),
401+ (
402+ "room list watcher",
403+ Box::pin(watchers::rooms(client.clone(), events())),
404+ ),
405+ (
406+ "notification settings watcher",
407+ Box::pin(watchers::notification_settings(client.clone(), events())),
408+ ),
409+ (
410+ "room command worker",
411+ Box::pin(mutations::run_scheduler(
412+ client.clone(),
413+ queues.mutations,
414+ self.config.command_capacity,
415+ )),
416+ ),
417+ (
418+ "notification command worker",
419+ Box::pin(commands::run_notifications(
420+ client.clone(),
421+ queues.notifications,
422+ )),
423+ ),
424+ (
425+ "encryption command worker",
426+ Box::pin(commands::run_encryption(
427+ client.clone(),
428+ events(),
429+ queues.encryption,
430+ )),
431+ ),
432+ ];
433+ for (label, task) in watchers {
434+ self.tasks.spawn(TaskContext::Watcher(label), task);
435+ }
436+ }
437+
438+ /// hands back the shutdown request's responder, which is answered once we've stopped
439+ async fn dispatch(&mut self, Request { command, reply }: Request) -> Option<Responder> {
440+ let client = self.client.clone();
441+ match command {
442+ Command::Lifecycle(LifecycleCommand::Shutdown) => return Some(reply),
443+ Command::Account(AccountCommand::Logout) => {
444+ let events = self.port.event_sender();
445+ let stop = self.stop.clone();
446+ self.spawn_command("logout", reply, async move {
447+ client
448+ .logout()
449+ .await
450+ .map_err(|source| Error::from_sdk("logout failed", source))?;
451+ let _ = events.send(Event::LoggedOut).await;
452+ stop.notify_one();
453+ Ok(())
454+ });
455+ }
456+ Command::Account(AccountCommand::SetAccountData {
457+ event_type,
458+ content,
459+ }) => enqueue(
460+ &self.mutations,
461+ Job {
462+ command: Mutation::GlobalAccountData {
463+ event_type,
464+ content,
465+ },
466+ reply,
467+ },
468+ ),
469+ Command::Account(AccountCommand::SetDisplayName(name)) => {
470+ self.spawn_command("set display name", reply, async move {
471+ client
472+ .account()
473+ .set_display_name(name.as_deref())
474+ .await
475+ .map_err(|source| Error::from_sdk("setting display name failed", source))
476+ });
477+ }
478+ Command::Account(AccountCommand::SetAvatar(avatar)) => {
479+ self.spawn_command("set avatar", reply, async move {
480+ client
481+ .account()
482+ .set_avatar_url(avatar.as_deref())
483+ .await
484+ .map_err(|source| Error::from_sdk("setting avatar failed", source))
485+ });
486+ }
487+ Command::Account(AccountCommand::UploadAvatar { mimetype, bytes }) => {
488+ let max_bytes = self.config.max_media_bytes;
489+ self.spawn_command("upload avatar", reply, async move {
490+ commands::upload_avatar(&client, &mimetype, bytes, max_bytes).await
491+ });
492+ }
493+ Command::Account(AccountCommand::RenameDevice {
494+ device_id,
495+ display_name,
496+ }) => self.spawn_command("rename device", reply, async move {
497+ commands::rename_device(&client, &device_id, &display_name).await
498+ }),
499+ Command::Account(AccountCommand::DeleteDevices { device_ids, auth }) => {
500+ self.spawn_command("delete devices", reply, async move {
501+ commands::delete_devices(&client, &device_ids, auth).await
502+ });
503+ }
504+ Command::Room(RoomCommand::Open(room_id)) => self.open_room(room_id, reply).await,
505+ Command::Room(RoomCommand::Close(room_id)) => self.close_room(room_id, reply).await,
506+ Command::Room(RoomCommand::SetTyping { room_id, typing }) => {
507+ self.spawn_command("typing", reply, async move {
508+ commands::set_typing(&client, &room_id, typing).await
509+ });
510+ }
511+ Command::Room(RoomCommand::CancelSend {
512+ room_id,
513+ transaction_id,
514+ }) => self.spawn_command("cancel send", reply, async move {
515+ commands::cancel_send(&client, &room_id, &transaction_id).await
516+ }),
517+ Command::Room(RoomCommand::RetrySend {
518+ room_id,
519+ transaction_id,
520+ }) => self.spawn_command("retry send", reply, async move {
521+ commands::retry_send(&client, &room_id, &transaction_id).await
522+ }),
523+ Command::Room(command) => enqueue(
524+ &self.mutations,
525+ Job {
526+ command: Mutation::Room(command),
527+ reply,
528+ },
529+ ),
530+ Command::Timeline(command) => self.route_timeline(command, reply),
531+ Command::Media(MediaCommand::Upload(upload)) => self.upload(upload, reply),
532+ Command::Encryption(command) => enqueue(&self.encryption, Job { command, reply }),
533+ Command::Notification(command) => {
534+ enqueue(&self.notifications, Job { command, reply });
535+ }
536+ }
537+ None
538+ }
539+
540+ fn spawn_command(
541+ &mut self,
542+ label: &'static str,
543+ reply: Responder,
544+ work: impl Future<Output = Result<(), Error>> + Send + 'static,
545+ ) {
546+ self.tasks.spawn(TaskContext::Command(label), async move {
547+ reply.done(work.await);
548+ });
549+ }
550+
551+ fn upload(&mut self, upload: crate::AttachmentUpload, reply: Responder) {
552+ let Some(room) = self.client.get_room(upload.timeline_id.room_id()) else {
553+ return reply.done(Err(Error::room_unknown()));
554+ };
555+ let room_id = room.room_id().to_owned();
556+ self.uploads
557+ .retain(|_, (forwarder, _)| !forwarder.is_finished());
558+ let mut ready = match self.uploads.get(&room_id) {
559+ Some((_, ready)) => ready.clone(),
560+ None => {
561+ let (subscribed, ready) = watch::channel(false);
562+ let forwarder = self.tasks.spawn(
563+ TaskContext::Command("upload watcher"),
564+ watchers::room_uploads(room.clone(), self.port.event_sender(), subscribed),
565+ );
566+ self.uploads.insert(room_id, (forwarder, ready.clone()));
567+ ready
568+ }
569+ };
570+ let max_bytes = self.config.max_media_bytes;
571+ // queueing before the forwarder listens would lose the first progress ticks
572+ self.spawn_command("upload", reply, async move {
573+ ready
574+ .wait_for(|subscribed| *subscribed)
575+ .await
576+ .map_err(|_| Error::new(ErrorKind::Store, "reading the media send queue failed"))?;
577+ media::queue_upload(room, upload, max_bytes).await
578+ });
579+ }
580+
581+ fn route_timeline(&mut self, command: TimelineCommand, reply: Responder) {
582+ let sender = match self.rooms.get(command.room_id()) {
583+ Some(RoomSlot::Active { sender, .. }) => Some(sender.clone()),
584+ _ => None,
585+ };
586+ match (sender, command) {
587+ (Some(sender), command) => send_actor(&sender, ActorRequest::Execute(command, reply)),
588+ (
589+ None,
590+ TimelineCommand::MarkRead {
591+ timeline_id,
592+ event_id,
593+ },
594+ ) => {
595+ let client = self.client.clone();
596+ self.spawn_command("mark read", reply, async move {
597+ commands::mark_read(&client, &timeline_id, event_id).await
598+ });
599+ }
600+ (None, _) => reply.done(Err(Error::new(ErrorKind::RoomNotOpen, "room is not open"))),
601+ }
602+ }
603+
604+ async fn open_room(&mut self, room_id: OwnedRoomId, reply: Responder) {
605+ match self.rooms.get_mut(&room_id) {
606+ Some(RoomSlot::Active { sender, .. }) => send_actor(sender, ActorRequest::Open(reply)),
607+ Some(RoomSlot::Closing { pending, .. }) => {
608+ defer(pending, Deferred::Open(reply), self.config.command_capacity);
609+ }
610+ None => self.start_actor(room_id, reply).await,
611+ }
612+ }
613+
614+ async fn close_room(&mut self, room_id: OwnedRoomId, reply: Responder) {
615+ let (generation, sender) = match self.rooms.get_mut(&room_id) {
616+ None => return reply.done(Ok(())),
617+ Some(RoomSlot::Closing { pending, .. }) => {
618+ return defer(
619+ pending,
620+ Deferred::Close(reply),
621+ self.config.command_capacity,
622+ );
623+ }
624+ Some(RoomSlot::Active { generation, sender }) => (*generation, sender.clone()),
625+ };
626+ match sender.try_send(ActorRequest::Close(reply)) {
627+ // a closed channel means the actor already left on its own, which is closed enough
628+ Ok(()) | Err(mpsc::error::TrySendError::Closed(_)) => {}
629+ Err(mpsc::error::TrySendError::Full(request)) => {
630+ return request.into_reply().done(Err(Error::new(
631+ ErrorKind::RoomBusy,
632+ "room command queue is full",
633+ )));
634+ }
635+ }
636+ self.rooms.insert(
637+ room_id,
638+ RoomSlot::Closing {
639+ generation,
640+ pending: VecDeque::new(),
641+ },
642+ );
643+ self.update_room_subscriptions().await;
644+ }
645+
646+ async fn start_actor(&mut self, room_id: OwnedRoomId, reply: Responder) {
647+ let Some(room) = self.client.get_room(&room_id) else {
648+ return reply.done(Err(Error::room_unknown()));
649+ };
650+ self.next_generation = self.next_generation.wrapping_add(1);
651+ let generation = self.next_generation;
652+ let (sender, requests) = mpsc::channel(self.config.command_capacity);
653+ let (supervisor, panics) = Supervisor::new();
654+ let actor = RoomActor {
655+ room_id: room_id.clone(),
656+ generation,
657+ room,
658+ requests,
659+ exits: self.exit_sender.clone(),
660+ context: SessionContext {
661+ events: self.port.event_sender(),
662+ media: self.media.clone(),
663+ current_user_id: self.client.user_id().map(ToOwned::to_owned),
664+ initial_history_limit: self.config.initial_history_limit,
665+ supervisor,
666+ },
667+ panics,
668+ opening: vec![reply],
669+ };
670+ self.rooms
671+ .insert(room_id.clone(), RoomSlot::Active { generation, sender });
672+ self.tasks.spawn(
673+ TaskContext::RoomActor {
674+ room_id,
675+ generation,
676+ },
677+ actor.run(),
678+ );
679+ self.update_room_subscriptions().await;
680+ }
681+
682+ async fn room_exited(
683+ &mut self,
684+ Exited {
685+ room_id,
686+ generation,
687+ }: Exited,
688+ ) {
689+ if self
690+ .rooms
691+ .get(&room_id)
692+ .is_none_or(|slot| slot.generation() != generation)
693+ {
694+ return;
695+ }
696+ if let Some(RoomSlot::Closing { pending, .. }) = self.rooms.remove(&room_id) {
697+ for deferred in pending {
698+ match deferred {
699+ Deferred::Open(reply) => self.open_room(room_id.clone(), reply).await,
700+ Deferred::Close(reply) => self.close_room(room_id.clone(), reply).await,
701+ }
702+ }
703+ }
704+ self.update_room_subscriptions().await;
705+ }
706+
707+ async fn update_room_subscriptions(&self) {
708+ let room_ids = self
709+ .rooms
710+ .iter()
711+ .filter(|(_, slot)| matches!(slot, RoomSlot::Active { .. }))
712+ .map(|(room_id, _)| room_id.as_ref())
713+ .collect::<Vec<&RoomId>>();
714+ self.sync
715+ .room_list_service()
716+ .subscribe_to_rooms(&room_ids)
717+ .await;
718+ }
719+
720+ async fn shutdown(&mut self) {
721+ self.port.close();
722+ self.tasks.shutdown().await;
723+ self.rooms.clear();
724+ self.uploads.clear();
725+ self.media.clear().await;
726+ self.sync.stop().await;
727+ self.port
728+ .connection_sender()
729+ .send_replace(ConnectionState::Stopped);
730+ let _ = self
731+ .port
732+ .event_sender()
733+ .send(Event::ConnectionChanged(ConnectionState::Stopped))
734+ .await;
735+ }
736+}
737+
738+fn send_actor(sender: &mpsc::Sender<ActorRequest>, request: ActorRequest) {
739+ if let Err(error) = sender.try_send(request) {
740+ let (request, error) = match error {
741+ mpsc::error::TrySendError::Full(request) => (
742+ request,
743+ Error::new(ErrorKind::RoomBusy, "room command queue is full"),
744+ ),
745+ mpsc::error::TrySendError::Closed(request) => (
746+ request,
747+ Error::new(ErrorKind::RoomNotOpen, "room is not open"),
748+ ),
749+ };
750+ request.into_reply().done(Err(error));
751+ }
752+}
753+
754+fn defer(pending: &mut VecDeque<Deferred>, deferred: Deferred, capacity: usize) {
755+ if pending.len() < capacity {
756+ return pending.push_back(deferred);
757+ }
758+ let (Deferred::Open(reply) | Deferred::Close(reply)) = deferred;
759+ reply.done(Err(Error::new(
760+ ErrorKind::RoomBusy,
761+ "room lifecycle queue is full",
762+ )));
763+}
764+
765+fn enqueue<T>(sender: &mpsc::Sender<Job<T>>, job: Job<T>) {
766+ if let Err(error) = sender.try_send(job) {
767+ let (job, error) = match error {
768+ mpsc::error::TrySendError::Full(job) => (
769+ job,
770+ Error::new(ErrorKind::QueueFull, "command queue is full"),
771+ ),
772+ mpsc::error::TrySendError::Closed(job) => (job, Error::interrupted()),
773+ };
774+ job.reply.done(Err(error));
775+ }
776+}
Added file: src/engine/mutations.rs+337 additions
@@ -0,0 +1,337 @@
1+use std::collections::{HashMap, HashSet, VecDeque};
2+
3+use matrix_sdk::{
4+ Client, Room,
5+ ruma::{
6+ Int,
7+ api::client::room::{
8+ Visibility as SdkVisibility,
9+ create_room::v3::{Request as CreateRoomRequest, RoomPreset as SdkPreset},
10+ },
11+ events::{
12+ AnyGlobalAccountDataEventContent, GlobalAccountDataEventType, MessageLikeEventType,
13+ StateEventType, room::power_levels::RoomPowerLevelsEventContent,
14+ },
15+ serde::Raw,
16+ },
17+};
18+use ruma::{OwnedRoomId, OwnedUserId};
19+use tokio::{sync::mpsc, task::JoinSet};
20+
21+use super::commands::{Job, guarded};
22+use crate::{
23+ CommandOutput, CreateRoomOptions, Error, ErrorKind, PowerLevelChanges, RoomCommand, RoomPreset,
24+ RoomVisibility,
25+};
26+
27+/// state changes that must not race each other. same room, same alias, same dm
28+/// partner or same account data type run in order, everything else runs at once
29+pub(super) enum Mutation {
30+ Room(RoomCommand),
31+ GlobalAccountData {
32+ event_type: GlobalAccountDataEventType,
33+ content: Raw<AnyGlobalAccountDataEventContent>,
34+ },
35+}
36+
37+pub(super) type MutationJob = Job<Mutation>;
38+
39+#[derive(Clone, Debug, Eq, Hash, PartialEq)]
40+enum Key {
41+ Room(OwnedRoomId),
42+ Alias(String),
43+ Direct(OwnedUserId),
44+ AccountData(String),
45+ Independent(u64),
46+}
47+
48+pub(super) async fn run_scheduler(
49+ client: Client,
50+ mut jobs: mpsc::Receiver<MutationJob>,
51+ queue_limit: usize,
52+) {
53+ let mut waiting = HashMap::<Key, VecDeque<MutationJob>>::new();
54+ let mut running = HashSet::<Key>::new();
55+ let mut tasks = JoinSet::<Key>::new();
56+ let mut queued = 0_usize;
57+ let mut independent = 0_u64;
58+ let mut receiving = true;
59+ while receiving || !tasks.is_empty() {
60+ tokio::select! {
61+ biased;
62+ Some(finished) = tasks.join_next(), if !tasks.is_empty() => {
63+ // a job's own panic is caught inside it, so this is only ever a key
64+ let Ok(key) = finished else { continue };
65+ let next = waiting.get_mut(&key).and_then(VecDeque::pop_front);
66+ if waiting.get(&key).is_some_and(VecDeque::is_empty) {
67+ waiting.remove(&key);
68+ }
69+ match next {
70+ Some(job) => {
71+ queued -= 1;
72+ spawn(&mut tasks, &client, key, job);
73+ }
74+ None => {
75+ running.remove(&key);
76+ }
77+ }
78+ }
79+ job = jobs.recv(), if receiving => {
80+ let Some(job) = job else {
81+ receiving = false;
82+ continue;
83+ };
84+ let key = key(&job.command).unwrap_or_else(|| {
85+ independent += 1;
86+ Key::Independent(independent)
87+ });
88+ if running.insert(key.clone()) {
89+ spawn(&mut tasks, &client, key, job);
90+ } else if queued < queue_limit {
91+ waiting.entry(key).or_default().push_back(job);
92+ queued += 1;
93+ } else {
94+ job.reply.done(Err(Error::new(
95+ ErrorKind::QueueFull,
96+ "room command queue is full",
97+ )));
98+ }
99+ }
100+ }
101+ }
102+}
103+
104+fn key(mutation: &Mutation) -> Option<Key> {
105+ let room_id = match mutation {
106+ Mutation::GlobalAccountData { event_type, .. } => {
107+ return Some(Key::AccountData(event_type.to_string()));
108+ }
109+ Mutation::Room(RoomCommand::Join { room, .. }) => {
110+ return Some(match OwnedRoomId::try_from(room.clone()) {
111+ Ok(room_id) => Key::Room(room_id),
112+ Err(alias) => Key::Alias(alias.to_string()),
113+ });
114+ }
115+ Mutation::Room(RoomCommand::CreateDirect(user_id)) => {
116+ return Some(Key::Direct(user_id.clone()));
117+ }
118+ Mutation::Room(
119+ RoomCommand::Leave(room_id)
120+ | RoomCommand::Forget(room_id)
121+ | RoomCommand::AcceptInvite(room_id)
122+ | RoomCommand::RejectInvite(room_id)
123+ | RoomCommand::InviteUser { room_id, .. }
124+ | RoomCommand::KickUser { room_id, .. }
125+ | RoomCommand::BanUser { room_id, .. }
126+ | RoomCommand::UnbanUser { room_id, .. }
127+ | RoomCommand::SetPowerLevels { room_id, .. }
128+ | RoomCommand::SetUserPowerLevel { room_id, .. }
129+ | RoomCommand::SetAccountData { room_id, .. },
130+ ) => room_id,
131+ Mutation::Room(_) => return None,
132+ };
133+ Some(Key::Room(room_id.clone()))
134+}
135+
136+fn spawn(tasks: &mut JoinSet<Key>, client: &Client, key: Key, job: MutationJob) {
137+ let client = client.clone();
138+ tasks.spawn(async move {
139+ let Job { command, reply } = job;
140+ reply.send(guarded("room command", execute(&client, command)).await);
141+ key
142+ });
143+}
144+
145+async fn execute(client: &Client, mutation: Mutation) -> Result<CommandOutput, Error> {
146+ let command = match mutation {
147+ Mutation::GlobalAccountData {
148+ event_type,
149+ content,
150+ } => {
151+ client
152+ .account()
153+ .set_account_data_raw(event_type, content)
154+ .await
155+ .map_err(|source| Error::from_sdk("writing account data failed", source))?;
156+ return Ok(CommandOutput::Done);
157+ }
158+ Mutation::Room(command) => command,
159+ };
160+ let room = |room_id: &OwnedRoomId| client.get_room(room_id).ok_or_else(Error::room_unknown);
161+ let joined = |room: Room| CommandOutput::Room(room.room_id().to_owned());
162+ let sdk = |message: &'static str| move |source| Error::from_sdk(message, source);
163+ match command {
164+ RoomCommand::Join { room, via } => client
165+ .join_room_by_id_or_alias(&room, &via)
166+ .await
167+ .map(joined)
168+ .map_err(sdk("joining room failed")),
169+ RoomCommand::Leave(room_id) => room(&room_id)?
170+ .leave()
171+ .await
172+ .map(done)
173+ .map_err(sdk("leaving room failed")),
174+ RoomCommand::Forget(room_id) => room(&room_id)?
175+ .forget()
176+ .await
177+ .map(done)
178+ .map_err(sdk("forgetting room failed")),
179+ RoomCommand::AcceptInvite(room_id) => room(&room_id)?
180+ .join()
181+ .await
182+ .map(done)
183+ .map_err(sdk("accepting invite failed")),
184+ RoomCommand::RejectInvite(room_id) => room(&room_id)?
185+ .leave()
186+ .await
187+ .map(done)
188+ .map_err(sdk("rejecting invite failed")),
189+ RoomCommand::InviteUser { room_id, user_id } => room(&room_id)?
190+ .invite_user_by_id(&user_id)
191+ .await
192+ .map(done)
193+ .map_err(sdk("inviting user failed")),
194+ RoomCommand::KickUser {
195+ room_id,
196+ user_id,
197+ reason,
198+ } => room(&room_id)?
199+ .kick_user(&user_id, reason.as_deref())
200+ .await
201+ .map(done)
202+ .map_err(sdk("kicking user failed")),
203+ RoomCommand::BanUser {
204+ room_id,
205+ user_id,
206+ reason,
207+ } => room(&room_id)?
208+ .ban_user(&user_id, reason.as_deref())
209+ .await
210+ .map(done)
211+ .map_err(sdk("banning user failed")),
212+ RoomCommand::UnbanUser {
213+ room_id,
214+ user_id,
215+ reason,
216+ } => room(&room_id)?
217+ .unban_user(&user_id, reason.as_deref())
218+ .await
219+ .map(done)
220+ .map_err(sdk("unbanning user failed")),
221+ RoomCommand::SetPowerLevels { room_id, changes } => {
222+ apply_power_level_changes(&room(&room_id)?, changes)
223+ .await
224+ .map(done)
225+ }
226+ RoomCommand::SetUserPowerLevel {
227+ room_id,
228+ user_id,
229+ level,
230+ } => room(&room_id)?
231+ .update_power_levels(vec![(&user_id, power_level(level)?)])
232+ .await
233+ .map(done)
234+ .map_err(sdk("updating user power level failed")),
235+ RoomCommand::SetAccountData {
236+ room_id,
237+ event_type,
238+ content,
239+ } => room(&room_id)?
240+ .set_account_data_raw(event_type, content)
241+ .await
242+ .map(done)
243+ .map_err(sdk("writing room account data failed")),
244+ RoomCommand::Create(options) => client
245+ .create_room(create_room_request(options))
246+ .await
247+ .map(joined)
248+ .map_err(sdk("creating room failed")),
249+ RoomCommand::CreateDirect(user_id) => match client.get_dm_room(&user_id) {
250+ Some(existing) => Ok(joined(existing)),
251+ None => client
252+ .create_dm(&user_id)
253+ .await
254+ .map(joined)
255+ .map_err(sdk("creating direct room failed")),
256+ },
257+ _ => Err(Error::new(ErrorKind::Other, "room command was misrouted")),
258+ }
259+}
260+
261+/// one state event carrying every requested change, since the sdk's own patcher is private
262+async fn apply_power_level_changes(room: &Room, changes: PowerLevelChanges) -> Result<(), Error> {
263+ let mut levels = room
264+ .power_levels()
265+ .await
266+ .map_err(|source| Error::from_sdk("reading power levels failed", source.into()))?;
267+ for (target, value) in [
268+ (&mut levels.ban, changes.ban),
269+ (&mut levels.invite, changes.invite),
270+ (&mut levels.kick, changes.kick),
271+ (&mut levels.redact, changes.redact),
272+ (&mut levels.events_default, changes.events_default),
273+ (&mut levels.state_default, changes.state_default),
274+ (&mut levels.users_default, changes.users_default),
275+ (&mut levels.notifications.room, changes.room_notification),
276+ ] {
277+ if let Some(value) = value {
278+ *target = power_level(value)?;
279+ }
280+ }
281+ for (event_type, value) in [
282+ (StateEventType::RoomName.into(), changes.room_name),
283+ (StateEventType::RoomAvatar.into(), changes.room_avatar),
284+ (StateEventType::RoomTopic.into(), changes.room_topic),
285+ (StateEventType::SpaceChild.into(), changes.space_child),
286+ (MessageLikeEventType::Beacon.into(), changes.beacon),
287+ (StateEventType::BeaconInfo.into(), changes.beacon_info),
288+ ] {
289+ if let Some(value) = value {
290+ levels.events.insert(event_type, power_level(value)?);
291+ }
292+ }
293+ let content = RoomPowerLevelsEventContent::try_from(levels).map_err(|source| {
294+ Error::with_source(
295+ ErrorKind::Invalid,
296+ "normalising power levels failed",
297+ source,
298+ )
299+ })?;
300+ room.send_state_event(content)
301+ .await
302+ .map(drop)
303+ .map_err(|source| Error::from_sdk("updating power levels failed", source))
304+}
305+
306+fn done<T>(_: T) -> CommandOutput {
307+ CommandOutput::Done
308+}
309+
310+fn power_level(value: i64) -> Result<Int, Error> {
311+ value.try_into().map_err(|source| {
312+ Error::with_source(
313+ ErrorKind::Invalid,
314+ "power level is outside the Matrix integer range",
315+ source,
316+ )
317+ })
318+}
319+
320+fn create_room_request(options: CreateRoomOptions) -> CreateRoomRequest {
321+ let mut request = CreateRoomRequest::new();
322+ request.name = options.name;
323+ request.topic = options.topic;
324+ request.room_alias_name = options.alias_localpart;
325+ request.invite = options.invite;
326+ request.is_direct = options.direct;
327+ request.visibility = match options.visibility {
328+ RoomVisibility::Private => SdkVisibility::Private,
329+ RoomVisibility::Public => SdkVisibility::Public,
330+ };
331+ request.preset = options.preset.map(|preset| match preset {
332+ RoomPreset::PrivateChat => SdkPreset::PrivateChat,
333+ RoomPreset::TrustedPrivateChat => SdkPreset::TrustedPrivateChat,
334+ RoomPreset::PublicChat => SdkPreset::PublicChat,
335+ });
336+ request
337+}
Added file: src/engine/room.rs+414 additions
@@ -0,0 +1,414 @@
1+use std::collections::{HashMap, VecDeque};
2+
3+use matrix_sdk::{Room, RoomState};
4+use matrix_sdk_ui::timeline::{RoomExt, Timeline, TimelineFocus, TimelineReadReceiptTracking};
5+use ruma::{OwnedEventId, OwnedRoomId};
6+use tokio::{
7+ sync::mpsc,
8+ task::{AbortHandle, JoinSet},
9+};
10+
11+use super::timeline::{SessionContext, TimelineSession};
12+use crate::{
13+ Error, ErrorKind, Event, TimelineCommand, TimelineId, TypingState, transport::Responder,
14+};
15+
16+pub(super) enum ActorRequest {
17+ /// opening an open room answers with a fresh reset
18+ Open(Responder),
19+ Execute(TimelineCommand, Responder),
20+ Close(Responder),
21+}
22+
23+impl ActorRequest {
24+ pub(super) fn into_reply(self) -> Responder {
25+ match self {
26+ Self::Open(reply) | Self::Execute(_, reply) | Self::Close(reply) => reply,
27+ }
28+ }
29+}
30+
31+pub(super) struct Exited {
32+ pub(super) room_id: OwnedRoomId,
33+ pub(super) generation: u64,
34+}
35+
36+pub(super) struct RoomActor {
37+ pub(super) room_id: OwnedRoomId,
38+ pub(super) generation: u64,
39+ pub(super) room: Room,
40+ pub(super) requests: mpsc::Receiver<ActorRequest>,
41+ pub(super) exits: mpsc::UnboundedSender<Exited>,
42+ pub(super) context: SessionContext,
43+ pub(super) panics: mpsc::UnboundedReceiver<&'static str>,
44+ pub(super) opening: Vec<Responder>,
45+}
46+
47+impl RoomActor {
48+ pub(super) async fn run(mut self) {
49+ let exits = self.exits.clone();
50+ let exited = Exited {
51+ room_id: self.room_id.clone(),
52+ generation: self.generation,
53+ };
54+ self.serve().await;
55+ let _ = exits.send(exited);
56+ }
57+
58+ async fn serve(&mut self) {
59+ let timeline = match build_timeline(
60+ &self.room,
61+ TimelineFocus::Live {
62+ hide_threaded_events: true,
63+ },
64+ )
65+ .await
66+ {
67+ Ok(timeline) => timeline,
68+ Err(error) => return self.refuse_everything(error).await,
69+ };
70+ let Ok(room_timeline) = TimelineSession::start(
71+ TimelineId::Room(self.room_id.clone()),
72+ timeline,
73+ self.context.clone(),
74+ )
75+ .await
76+ else {
77+ return;
78+ };
79+ for reply in self.opening.drain(..) {
80+ reply.done(Ok(()));
81+ }
82+ let (typing_guard, typing) = self.room.subscribe_to_typing_notifications();
83+ let typing_watcher = self.context.supervisor.spawn(
84+ "typing watcher",
85+ watch_typing(self.room_id.clone(), typing, self.context.events.clone()),
86+ );
87+ let mut timelines = Timelines {
88+ room: room_timeline,
89+ threads: Threads::new(self.requests.max_capacity()),
90+ work: JoinSet::new(),
91+ };
92+ let close = self.serve_open(&mut timelines).await;
93+
94+ // let redactions and receipts that already left finish rather than lie about them
95+ while timelines.work.join_next().await.is_some() {}
96+ drop(typing_guard);
97+ typing_watcher.abort();
98+ timelines.threads.close().await;
99+ timelines.room.close().await;
100+ if let Some(reply) = close {
101+ reply.done(Ok(()));
102+ }
103+ }
104+
105+ /// returns the close request's responder, if a close is why we stopped
106+ async fn serve_open(&mut self, timelines: &mut Timelines) -> Option<Responder> {
107+ let mut room_info = self.room.subscribe_info();
108+ loop {
109+ tokio::select! {
110+ Some(info) = room_info.next() => {
111+ if matches!(info.state(), RoomState::Left | RoomState::Banned) {
112+ tracing::debug!(room_id = %self.room_id, "room actor stopped because we left the room");
113+ return None;
114+ }
115+ }
116+ request = self.requests.recv() => match request {
117+ None => return None,
118+ Some(ActorRequest::Close(reply)) => return Some(reply),
119+ Some(ActorRequest::Open(reply)) => reply.done(timelines.room.resync().await),
120+ Some(ActorRequest::Execute(command, reply)) => {
121+ timelines.execute(command, reply, &self.room).await;
122+ }
123+ },
124+ Some(built) = timelines.threads.builds.join_next(), if !timelines.threads.builds.is_empty() => {
125+ timelines.thread_built(built, &self.room_id, &self.context).await;
126+ }
127+ Some(label) = self.panics.recv() => panic!("{label} panicked in room {}", self.room_id),
128+ Some(done) = timelines.work.join_next(), if !timelines.work.is_empty() => {
129+ if let Err(error) = done && error.is_panic() {
130+ tracing::warn!(room_id = %self.room_id, %error, "timeline operation panicked");
131+ }
132+ }
133+ }
134+ }
135+ }
136+
137+ async fn refuse_everything(&mut self, error: matrix_sdk_ui::timeline::Error) {
138+ tracing::warn!(%error, room_id = %self.room_id, "room actor could not build its timeline");
139+ let error = Error::with_source(ErrorKind::Timeline, "room timeline build failed", error);
140+ for reply in self.opening.drain(..) {
141+ reply.done(Err(error.clone()));
142+ }
143+ self.requests.close();
144+ while let Some(request) = self.requests.recv().await {
145+ request.into_reply().done(Err(error.clone()));
146+ }
147+ }
148+}
149+
150+async fn build_timeline(
151+ room: &Room,
152+ focus: TimelineFocus,
153+) -> Result<Timeline, matrix_sdk_ui::timeline::Error> {
154+ room.timeline_builder()
155+ .with_focus(focus)
156+ .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)
157+ .build()
158+ .await
159+}
160+
161+async fn watch_typing(
162+ room_id: OwnedRoomId,
163+ mut typing: tokio::sync::broadcast::Receiver<Vec<ruma::OwnedUserId>>,
164+ events: crate::transport::EventSender,
165+) {
166+ loop {
167+ match typing.recv().await {
168+ Ok(users) => {
169+ let state = TypingState {
170+ room_id: room_id.clone(),
171+ users,
172+ };
173+ if events.send(Event::TypingChanged(state)).await.is_err() {
174+ return;
175+ }
176+ }
177+ Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
178+ tracing::warn!(%room_id, skipped, "typing watcher lagged");
179+ }
180+ Err(tokio::sync::broadcast::error::RecvError::Closed) => return,
181+ }
182+ }
183+}
184+
185+struct Timelines {
186+ room: TimelineSession,
187+ threads: Threads,
188+ work: JoinSet<()>,
189+}
190+
191+impl Timelines {
192+ async fn execute(&mut self, command: TimelineCommand, reply: Responder, room: &Room) {
193+ match command {
194+ TimelineCommand::OpenThread { root_event_id, .. } => {
195+ self.threads.open(root_event_id, reply, room);
196+ }
197+ TimelineCommand::CloseThread { root_event_id, .. } => {
198+ reply.done(self.threads.close_one(&root_event_id).await);
199+ }
200+ TimelineCommand::Resync {
201+ timeline_id: TimelineId::Room(_),
202+ } => reply.done(self.room.resync().await),
203+ command => {
204+ let root = command
205+ .timeline_id()
206+ .and_then(TimelineId::thread_root)
207+ .cloned();
208+ let Some(root) = root else {
209+ return self.room.execute(command, reply, &mut self.work).await;
210+ };
211+ match self.threads.route(root, command, reply) {
212+ Routed::Open(session, TimelineCommand::Resync { .. }, reply) => {
213+ reply.done(session.resync().await);
214+ }
215+ Routed::Open(session, command, reply) => {
216+ session.execute(command, reply, &mut self.work).await;
217+ }
218+ Routed::Queued => {}
219+ }
220+ }
221+ }
222+ }
223+
224+ async fn thread_built(
225+ &mut self,
226+ built: Result<Built, tokio::task::JoinError>,
227+ room_id: &OwnedRoomId,
228+ context: &SessionContext,
229+ ) {
230+ let Some((root, queued, timeline)) = self.threads.built(built) else {
231+ return;
232+ };
233+ let timeline = match timeline {
234+ Ok(timeline) => timeline,
235+ Err(error) => {
236+ tracing::warn!(%error, %room_id, %root, "thread timeline build failed");
237+ let error =
238+ Error::with_source(ErrorKind::Timeline, "thread timeline build failed", error);
239+ for (_, reply) in queued {
240+ reply.done(Err(error.clone()));
241+ }
242+ return;
243+ }
244+ };
245+ let id = TimelineId::thread(room_id.clone(), root.clone());
246+ let Ok(session) = TimelineSession::start(id, timeline, context.clone()).await else {
247+ return;
248+ };
249+ let session = self
250+ .threads
251+ .open
252+ .entry(root)
253+ .insert_entry(session)
254+ .into_mut();
255+ for (command, reply) in queued {
256+ match command {
257+ None => reply.done(Ok(())),
258+ Some(TimelineCommand::Resync { .. }) => reply.done(session.resync().await),
259+ Some(command) => session.execute(command, reply, &mut self.work).await,
260+ }
261+ }
262+ }
263+}
264+
265+type Built = (
266+ OwnedEventId,
267+ u64,
268+ Result<Timeline, matrix_sdk_ui::timeline::Error>,
269+);
270+
271+/// `None` is the open itself, everything else waits behind it
272+type Queued = VecDeque<(Option<TimelineCommand>, Responder)>;
273+
274+enum Routed<'a> {
275+ Open(&'a mut TimelineSession, TimelineCommand, Responder),
276+ Queued,
277+}
278+
279+struct OpeningThread {
280+ generation: u64,
281+ build: AbortHandle,
282+ queued: Queued,
283+}
284+
285+struct Threads {
286+ open: HashMap<OwnedEventId, TimelineSession>,
287+ opening: HashMap<OwnedEventId, OpeningThread>,
288+ builds: JoinSet<Built>,
289+ next_generation: u64,
290+ queued: usize,
291+ capacity: usize,
292+}
293+
294+impl Threads {
295+ fn new(capacity: usize) -> Self {
296+ Self {
297+ open: HashMap::new(),
298+ opening: HashMap::new(),
299+ builds: JoinSet::new(),
300+ next_generation: 0,
301+ queued: 0,
302+ capacity,
303+ }
304+ }
305+
306+ fn enqueue(&mut self, root: &OwnedEventId, command: Option<TimelineCommand>, reply: Responder) {
307+ if self.queued >= self.capacity {
308+ return reply.done(Err(Error::new(
309+ ErrorKind::QueueFull,
310+ "thread command queue is full",
311+ )));
312+ }
313+ let Some(opening) = self.opening.get_mut(root) else {
314+ return reply.done(Err(Error::new(
315+ ErrorKind::TimelineNotOpen,
316+ "thread timeline is not open",
317+ )));
318+ };
319+ opening.queued.push_back((command, reply));
320+ self.queued += 1;
321+ }
322+
323+ fn open(&mut self, root: OwnedEventId, reply: Responder, room: &Room) {
324+ if self.open.contains_key(&root) {
325+ return reply.done(Ok(()));
326+ }
327+ if !self.opening.contains_key(&root) {
328+ let generation = self.next_generation;
329+ self.next_generation = self.next_generation.wrapping_add(1);
330+ let room = room.clone();
331+ let build_root = root.clone();
332+ let build = self.builds.spawn(async move {
333+ let focus = TimelineFocus::Thread {
334+ root_event_id: build_root.clone(),
335+ };
336+ (build_root, generation, build_timeline(&room, focus).await)
337+ });
338+ self.opening.insert(
339+ root.clone(),
340+ OpeningThread {
341+ generation,
342+ build,
343+ queued: VecDeque::new(),
344+ },
345+ );
346+ }
347+ self.enqueue(&root, None, reply);
348+ }
349+
350+ fn route(
351+ &mut self,
352+ root: OwnedEventId,
353+ command: TimelineCommand,
354+ reply: Responder,
355+ ) -> Routed<'_> {
356+ if !self.open.contains_key(&root) {
357+ self.enqueue(&root, Some(command), reply);
358+ return Routed::Queued;
359+ }
360+ let session = self.open.get_mut(&root).expect("thread was just checked");
361+ Routed::Open(session, command, reply)
362+ }
363+
364+ /// `None` when the build belongs to a thread that was closed while it ran
365+ fn built(
366+ &mut self,
367+ built: Result<Built, tokio::task::JoinError>,
368+ ) -> Option<(
369+ OwnedEventId,
370+ Queued,
371+ Result<Timeline, matrix_sdk_ui::timeline::Error>,
372+ )> {
373+ let (root, generation, timeline) = match built {
374+ Ok(built) => built,
375+ Err(error) if error.is_cancelled() => return None,
376+ Err(error) => std::panic::resume_unwind(error.into_panic()),
377+ };
378+ if self
379+ .opening
380+ .get(&root)
381+ .is_none_or(|opening| opening.generation != generation)
382+ {
383+ return None;
384+ }
385+ let opening = self.opening.remove(&root)?;
386+ self.queued -= opening.queued.len();
387+ Some((root, opening.queued, timeline))
388+ }
389+
390+ async fn close_one(&mut self, root: &OwnedEventId) -> Result<(), Error> {
391+ if let Some(session) = self.open.remove(root) {
392+ session.close().await;
393+ return Ok(());
394+ }
395+ let Some(opening) = self.opening.remove(root) else {
396+ return Err(Error::new(
397+ ErrorKind::TimelineNotOpen,
398+ "thread timeline is not open",
399+ ));
400+ };
401+ opening.build.abort();
402+ self.queued -= opening.queued.len();
403+ Ok(())
404+ }
405+
406+ async fn close(&mut self) {
407+ self.builds.abort_all();
408+ while self.builds.join_next().await.is_some() {}
409+ self.opening.clear();
410+ for (_, session) in self.open.drain() {
411+ session.close().await;
412+ }
413+ }
414+}
Added file: src/engine/timeline.rs+406 additions
@@ -0,0 +1,406 @@
1+use std::{future::Future, panic::AssertUnwindSafe, pin::Pin, sync::Arc};
2+
3+use futures_util::{FutureExt, StreamExt};
4+use matrix_sdk::{
5+ room::edit::EditedContent,
6+ room::reply::{EnforceThread, Reply as SdkReply},
7+ ruma::{
8+ api::client::receipt::create_receipt::v3::ReceiptType,
9+ events::{
10+ Mentions,
11+ room::message::{
12+ AddMentions, Relation, ReplyWithinThread, RoomMessageEventContentWithoutRelation,
13+ },
14+ },
15+ },
16+};
17+use matrix_sdk_ui::timeline::{Timeline, TimelineEventItemId};
18+use ruma::{OwnedEventId, OwnedUserId};
19+use tokio::{
20+ sync::mpsc,
21+ task::{JoinHandle, JoinSet},
22+};
23+
24+use crate::{
25+ Error, ErrorKind, Event, HistoryState, MessageContent, MessageEditContent, MessageMentions,
26+ RichText, TimelineCommand, TimelineId,
27+ engine::MediaService,
28+ normalise::{MediaScope, normalise_timeline, normalise_timeline_diffs},
29+ transport::{EventSender, Responder, SendEventError},
30+};
31+
32+const PAGINATION_QUEUE: usize = 32;
33+
34+/// long-lived tasks inside a room actor report panics here so the actor can die loudly
35+#[derive(Clone)]
36+pub(super) struct Supervisor(mpsc::UnboundedSender<&'static str>);
37+
38+impl Supervisor {
39+ pub(super) fn new() -> (Self, mpsc::UnboundedReceiver<&'static str>) {
40+ let (sender, receiver) = mpsc::unbounded_channel();
41+ (Self(sender), receiver)
42+ }
43+
44+ pub(super) fn spawn(
45+ &self,
46+ label: &'static str,
47+ task: impl Future<Output = ()> + Send + 'static,
48+ ) -> JoinHandle<()> {
49+ let panics = self.0.clone();
50+ tokio::spawn(async move {
51+ if AssertUnwindSafe(task).catch_unwind().await.is_err() {
52+ let _ = panics.send(label);
53+ }
54+ })
55+ }
56+}
57+
58+/// what a timeline session needs from the room that owns it
59+#[derive(Clone)]
60+pub(super) struct SessionContext {
61+ pub(super) events: EventSender,
62+ pub(super) media: Arc<MediaService>,
63+ pub(super) current_user_id: Option<OwnedUserId>,
64+ pub(super) initial_history_limit: u16,
65+ pub(super) supervisor: Supervisor,
66+}
67+
68+pub(super) struct TimelineSession {
69+ pub(super) id: TimelineId,
70+ timeline: Arc<Timeline>,
71+ context: SessionContext,
72+ scope: MediaScope,
73+ watcher: JoinHandle<()>,
74+ pagination: mpsc::Sender<PaginationRequest>,
75+ pagination_worker: JoinHandle<()>,
76+}
77+
78+struct PaginationRequest {
79+ limit: u16,
80+ reply: Option<Responder>,
81+}
82+
83+impl TimelineSession {
84+ pub(super) async fn start(
85+ id: TimelineId,
86+ timeline: Timeline,
87+ context: SessionContext,
88+ ) -> Result<Self, SendEventError> {
89+ let timeline = Arc::new(timeline);
90+ let scope = MediaScope::new();
91+ let watcher = start_watcher(&id, &timeline, &context, &scope).await?;
92+ let (pagination, requests) = mpsc::channel(PAGINATION_QUEUE);
93+ let pagination_worker = context.supervisor.spawn(
94+ "pagination worker",
95+ run_pagination(
96+ requests,
97+ timeline.clone(),
98+ context.events.clone(),
99+ id.clone(),
100+ ),
101+ );
102+ let session = Self {
103+ id,
104+ timeline,
105+ scope,
106+ watcher,
107+ pagination,
108+ pagination_worker,
109+ context,
110+ };
111+ if session.context.initial_history_limit > 0 {
112+ session.paginate(session.context.initial_history_limit, None);
113+ }
114+ Ok(session)
115+ }
116+
117+ /// swaps in a fresh watcher. the old one is dead before the reset goes out,
118+ /// so no stale diff can land after it
119+ pub(super) async fn resync(&mut self) -> Result<(), Error> {
120+ self.watcher.abort();
121+ let _ = (&mut self.watcher).await;
122+ self.watcher = start_watcher(&self.id, &self.timeline, &self.context, &self.scope)
123+ .await
124+ .map_err(|_| Error::new(ErrorKind::Timeline, "timeline reset could not be sent"))?;
125+ Ok(())
126+ }
127+
128+ pub(super) fn paginate(&self, limit: u16, reply: Option<Responder>) {
129+ if let Err(error) = self.pagination.try_send(PaginationRequest { limit, reply }) {
130+ let (request, error) = match error {
131+ mpsc::error::TrySendError::Full(request) => (
132+ request,
133+ Error::new(ErrorKind::QueueFull, "pagination queue is full"),
134+ ),
135+ mpsc::error::TrySendError::Closed(request) => (
136+ request,
137+ Error::new(ErrorKind::TimelineNotOpen, "timeline is closing"),
138+ ),
139+ };
140+ if let Some(reply) = request.reply {
141+ reply.done(Err(error));
142+ }
143+ }
144+ }
145+
146+ /// pending paginations answer `Interrupted` as their responders drop
147+ pub(super) async fn close(mut self) {
148+ self.context.media.release(&self.scope).await;
149+ self.pagination_worker.abort();
150+ let _ = (&mut self.pagination_worker).await;
151+ self.watcher.abort();
152+ let _ = (&mut self.watcher).await;
153+ }
154+
155+ pub(super) async fn execute(
156+ &self,
157+ command: TimelineCommand,
158+ reply: Responder,
159+ work: &mut JoinSet<()>,
160+ ) {
161+ let timeline = self.timeline.clone();
162+ let thread_root = self.id.thread_root().cloned();
163+ let background: Pin<Box<dyn Future<Output = Result<(), Error>> + Send>> = match command {
164+ TimelineCommand::Paginate { limit, .. } => return self.paginate(limit, Some(reply)),
165+ // network-bound, so it runs beside the actor instead of queueing the room behind it
166+ TimelineCommand::Redact {
167+ event_id, reason, ..
168+ } => Box::pin(async move {
169+ timeline
170+ .redact(&TimelineEventItemId::EventId(event_id), reason.as_deref())
171+ .await
172+ .map_err(timeline_error)
173+ }),
174+ TimelineCommand::ToggleReaction { event_id, key, .. } => Box::pin(async move {
175+ timeline
176+ .toggle_reaction(&TimelineEventItemId::EventId(event_id), &key)
177+ .await
178+ .map(drop)
179+ .map_err(timeline_error)
180+ }),
181+ TimelineCommand::MarkRead { event_id, .. } => Box::pin(async move {
182+ let receipt_failed =
183+ |source| Error::from_sdk("sending read receipt failed", source);
184+ timeline
185+ .send_single_receipt(ReceiptType::Read, event_id.clone())
186+ .await
187+ .map_err(receipt_failed)?;
188+ if thread_root.is_none() {
189+ timeline
190+ .send_single_receipt(ReceiptType::FullyRead, event_id)
191+ .await
192+ .map_err(receipt_failed)?;
193+ }
194+ Ok(())
195+ }),
196+ command => return reply.done(self.execute_serial(command).await),
197+ };
198+ work.spawn(async move { reply.done(background.await) });
199+ }
200+
201+ /// sends stay on the actor so they hit the send queue in the order they were asked for
202+ async fn execute_serial(&self, command: TimelineCommand) -> Result<(), Error> {
203+ let timeline = &self.timeline;
204+ match command {
205+ TimelineCommand::Send { content, .. } => {
206+ let body = match &content {
207+ MessageContent::Text { content, .. }
208+ | MessageContent::Emote { content, .. }
209+ | MessageContent::Reply { content, .. } => &content.body,
210+ };
211+ if body.trim().is_empty() {
212+ return Err(Error::new(ErrorKind::EmptyBody, "message body is empty"));
213+ }
214+ let content = match content {
215+ MessageContent::Text { content, mentions } => {
216+ message_content(content, mentions, false).with_relation(None)
217+ }
218+ MessageContent::Emote { content, mentions } => {
219+ message_content(content, mentions, true).with_relation(None)
220+ }
221+ MessageContent::Reply {
222+ event_id,
223+ content,
224+ mentions,
225+ notify_replied_to,
226+ } => {
227+ self.reply_content(event_id, content, mentions, notify_replied_to)
228+ .await?
229+ }
230+ };
231+ timeline
232+ .send(content.into())
233+ .await
234+ .map(drop)
235+ .map_err(timeline_error)
236+ }
237+ TimelineCommand::Edit {
238+ event_id, content, ..
239+ } => {
240+ let (content, mentions, emote) = match content {
241+ MessageEditContent::Text { content, mentions } => (content, mentions, false),
242+ MessageEditContent::Emote { content, mentions } => (content, mentions, true),
243+ };
244+ if content.body.trim().is_empty() {
245+ return Err(Error::new(ErrorKind::EmptyBody, "message body is empty"));
246+ }
247+ timeline
248+ .edit(
249+ &TimelineEventItemId::EventId(event_id),
250+ EditedContent::RoomMessage(message_content(content, mentions, emote)),
251+ )
252+ .await
253+ .map_err(timeline_error)
254+ }
255+ _ => Err(Error::new(
256+ ErrorKind::Other,
257+ "timeline command was misrouted",
258+ )),
259+ }
260+ }
261+
262+ async fn reply_content(
263+ &self,
264+ event_id: OwnedEventId,
265+ content: RichText,
266+ mentions: MessageMentions,
267+ notify_replied_to: bool,
268+ ) -> Result<matrix_sdk::ruma::events::room::message::RoomMessageEventContent, Error> {
269+ let thread_root = self.id.thread_root();
270+ let content = self
271+ .timeline
272+ .room()
273+ .make_reply_event(
274+ message_content(content, mentions, false),
275+ SdkReply {
276+ event_id,
277+ enforce_thread: if thread_root.is_some() {
278+ EnforceThread::Threaded(ReplyWithinThread::Yes)
279+ } else {
280+ EnforceThread::MaybeThreaded
281+ },
282+ add_mentions: if notify_replied_to {
283+ AddMentions::Yes
284+ } else {
285+ AddMentions::No
286+ },
287+ },
288+ )
289+ .await
290+ .map_err(|source| {
291+ Error::with_source(ErrorKind::Timeline, "reply operation failed", source)
292+ })?;
293+ if let Some(thread_root) = thread_root
294+ && !matches!(
295+ content.relates_to.as_ref(),
296+ Some(Relation::Thread(thread)) if &thread.event_id == thread_root
297+ )
298+ {
299+ return Err(Error::new(
300+ ErrorKind::Invalid,
301+ "reply target does not belong to the selected thread",
302+ ));
303+ }
304+ Ok(content)
305+ }
306+}
307+
308+impl Drop for TimelineSession {
309+ fn drop(&mut self) {
310+ self.watcher.abort();
311+ self.pagination_worker.abort();
312+ }
313+}
314+
315+async fn start_watcher(
316+ id: &TimelineId,
317+ timeline: &Arc<Timeline>,
318+ context: &SessionContext,
319+ scope: &MediaScope,
320+) -> Result<JoinHandle<()>, SendEventError> {
321+ let (items, mut stream) = timeline.subscribe().await;
322+ let current_user_id = context.current_user_id.clone();
323+ let normalised = normalise_timeline(items.iter(), current_user_id.as_deref(), scope);
324+ context.media.register(normalised.media).await;
325+ context
326+ .events
327+ .send(Event::TimelineReset {
328+ timeline_id: id.clone(),
329+ items: normalised.items,
330+ })
331+ .await?;
332+ let id = id.clone();
333+ let events = context.events.clone();
334+ let media = context.media.clone();
335+ let scope = scope.clone();
336+ Ok(context.supervisor.spawn("timeline watcher", async move {
337+ while let Some(diffs) = stream.next().await {
338+ let normalised = normalise_timeline_diffs(diffs, current_user_id.as_deref(), &scope);
339+ media.register(normalised.media).await;
340+ let event = Event::TimelineChanged {
341+ timeline_id: id.clone(),
342+ diffs: normalised.diffs,
343+ };
344+ if events.send(event).await.is_err() {
345+ return;
346+ }
347+ }
348+ }))
349+}
350+
351+async fn run_pagination(
352+ mut requests: mpsc::Receiver<PaginationRequest>,
353+ timeline: Arc<Timeline>,
354+ events: EventSender,
355+ timeline_id: TimelineId,
356+) {
357+ while let Some(PaginationRequest { limit, reply }) = requests.recv().await {
358+ let history = |state| Event::HistoryChanged {
359+ timeline_id: timeline_id.clone(),
360+ state,
361+ };
362+ if events.send(history(HistoryState::Loading)).await.is_err() {
363+ return;
364+ }
365+ let result = timeline
366+ .paginate_backwards(limit.max(1))
367+ .await
368+ .map_err(timeline_error);
369+ let state = match &result {
370+ Ok(true) => HistoryState::StartReached,
371+ Ok(false) => HistoryState::Idle,
372+ Err(error) => HistoryState::Failed(error.to_string()),
373+ };
374+ let _ = events.send(history(state)).await;
375+ if let Some(reply) = reply {
376+ reply.done(result.map(drop));
377+ }
378+ }
379+}
380+
381+fn message_content(
382+ content: RichText,
383+ mentions: MessageMentions,
384+ emote: bool,
385+) -> RoomMessageEventContentWithoutRelation {
386+ let content = match content.formatted_body {
387+ Some(formatted_body) if emote => {
388+ RoomMessageEventContentWithoutRelation::emote_html(content.body, formatted_body)
389+ }
390+ Some(formatted_body) => {
391+ RoomMessageEventContentWithoutRelation::text_html(content.body, formatted_body)
392+ }
393+ None if emote => RoomMessageEventContentWithoutRelation::emote_plain(content.body),
394+ None => RoomMessageEventContentWithoutRelation::text_plain(content.body),
395+ };
396+ let content = content.add_mentions(Mentions::with_user_ids(mentions.user_ids));
397+ if mentions.room {
398+ content.add_mentions(Mentions::with_room_mention())
399+ } else {
400+ content
401+ }
402+}
403+
404+pub(super) fn timeline_error(source: matrix_sdk_ui::timeline::Error) -> Error {
405+ Error::with_source(ErrorKind::Timeline, "timeline operation failed", source)
406+}
Added file: src/engine/watchers.rs+268 additions
@@ -0,0 +1,268 @@
1+use std::{sync::Arc, time::Duration};
2+
3+use futures_util::StreamExt;
4+use matrix_sdk::{
5+ Client, Room, SessionChange,
6+ ruma::events::{AnyMessageLikeEventContent, room::message::MessageType},
7+ send_queue::{LocalEchoContent, RoomSendQueueUpdate},
8+};
9+use matrix_sdk_ui::sync_service::{State as SyncState, SyncService};
10+use tokio::{
11+ sync::{Notify, broadcast::error::RecvError, watch},
12+ task::JoinSet,
13+};
14+
15+use crate::{
16+ ConnectionState, Event, encryption::encryption_state, error::classify_sdk_error,
17+ transport::EventSender,
18+};
19+
20+const SYNC_RESTART_MIN: Duration = Duration::from_secs(1);
21+const SYNC_RESTART_MAX: Duration = Duration::from_secs(60);
22+const SEND_QUEUE_RETRY: Duration = Duration::from_secs(5);
23+
24+pub(super) async fn session(
25+ mut changes: tokio::sync::broadcast::Receiver<SessionChange>,
26+ events: EventSender,
27+ stop: Arc<Notify>,
28+) {
29+ loop {
30+ let event = match changes.recv().await {
31+ Ok(SessionChange::TokensRefreshed) => Event::SessionTokensChanged,
32+ Ok(SessionChange::UnknownToken(data)) => {
33+ let _ = events
34+ .send(Event::SessionAccessRejected {
35+ soft_logout: data.soft_logout,
36+ })
37+ .await;
38+ stop.notify_one();
39+ return;
40+ }
41+ // a missed refresh costs one needless save, a missed rejection comes round again
42+ Err(RecvError::Lagged(_)) => Event::SessionTokensChanged,
43+ Err(RecvError::Closed) => return,
44+ };
45+ if events.send(event).await.is_err() {
46+ return;
47+ }
48+ }
49+}
50+
51+/// `states` must be taken before sync starts, a late subscriber never hears the transition it missed
52+pub(super) async fn connection(
53+ mut states: impl futures_util::Stream<Item = SyncState> + Unpin,
54+ sync: Arc<SyncService>,
55+ client: Client,
56+ events: EventSender,
57+ connection: watch::Sender<ConnectionState>,
58+) {
59+ let mut backoff = SYNC_RESTART_MIN;
60+ while let Some(state) = states.next().await {
61+ let (state, restart) = match state {
62+ SyncState::Idle => (ConnectionState::Idle, false),
63+ SyncState::Running => {
64+ backoff = SYNC_RESTART_MIN;
65+ // the sdk parks a room queue on any send error and never unparks it
66+ client.send_queue().set_enabled(true).await;
67+ (ConnectionState::Online, false)
68+ }
69+ SyncState::Offline => (ConnectionState::Offline, false),
70+ SyncState::Terminated => (ConnectionState::Offline, true),
71+ SyncState::Error(error) => {
72+ tracing::warn!(%error, "sync service reported an error");
73+ (ConnectionState::Offline, true)
74+ }
75+ };
76+ if *connection.borrow() != state {
77+ connection.send_replace(state);
78+ if events.send(Event::ConnectionChanged(state)).await.is_err() {
79+ return;
80+ }
81+ }
82+ if restart {
83+ tokio::time::sleep(backoff).await;
84+ backoff = (backoff * 2).min(SYNC_RESTART_MAX);
85+ sync.start().await;
86+ }
87+ }
88+}
89+
90+/// a recoverable failure while we're online is a hiccup, so give the room another go
91+pub(super) async fn send_queue_errors(
92+ client: Client,
93+ mut errors: tokio::sync::broadcast::Receiver<matrix_sdk::send_queue::SendQueueRoomError>,
94+ online: watch::Receiver<ConnectionState>,
95+) {
96+ let mut retries = JoinSet::new();
97+ loop {
98+ let error = tokio::select! {
99+ error = errors.recv() => error,
100+ Some(_) = retries.join_next(), if !retries.is_empty() => continue,
101+ };
102+ match error {
103+ Ok(error) if error.is_recoverable => {
104+ let client = client.clone();
105+ let online = online.clone();
106+ retries.spawn(async move {
107+ tokio::time::sleep(SEND_QUEUE_RETRY).await;
108+ if *online.borrow() == ConnectionState::Online
109+ && let Some(room) = client.get_room(&error.room_id)
110+ {
111+ room.send_queue().set_enabled(true);
112+ }
113+ });
114+ }
115+ Ok(_) | Err(RecvError::Lagged(_)) => {}
116+ Err(RecvError::Closed) => return,
117+ }
118+ }
119+}
120+
121+/// upload progress only goes out on the room's own channel, never the client-wide one
122+pub(super) async fn room_uploads(room: Room, events: EventSender, ready: watch::Sender<bool>) {
123+ let room_id = room.room_id().to_owned();
124+ let mut updates = match room.send_queue().subscribe().await {
125+ Ok((_, updates)) => updates,
126+ Err(error) => {
127+ tracing::warn!(%room_id, %error, "reading the media send queue failed");
128+ return;
129+ }
130+ };
131+ ready.send_replace(true);
132+ loop {
133+ let update = match updates.recv().await {
134+ Ok(update) => update,
135+ Err(RecvError::Lagged(skipped)) => {
136+ tracing::warn!(%room_id, skipped, "upload watcher lagged");
137+ continue;
138+ }
139+ Err(RecvError::Closed) => return,
140+ };
141+ let event = match update {
142+ RoomSendQueueUpdate::MediaUpload {
143+ related_to,
144+ file,
145+ progress,
146+ ..
147+ } => Event::MediaUploadProgress {
148+ room_id: room_id.clone(),
149+ transaction_id: related_to,
150+ uploaded_bytes: progress.current as u64,
151+ total_bytes: progress.total as u64,
152+ complete: file.is_some() && progress.current == progress.total,
153+ },
154+ RoomSendQueueUpdate::SendError {
155+ transaction_id,
156+ error,
157+ is_recoverable,
158+ } => {
159+ let is_attachment = room
160+ .send_queue()
161+ .subscribe()
162+ .await
163+ .is_ok_and(|(echoes, _)| {
164+ echoes.iter().any(|echo| {
165+ echo.transaction_id == transaction_id
166+ && is_attachment_echo(&echo.content)
167+ })
168+ });
169+ if !is_attachment {
170+ continue;
171+ }
172+ Event::MediaUploadFailed {
173+ room_id: room_id.clone(),
174+ transaction_id,
175+ kind: classify_sdk_error(&error),
176+ recoverable: is_recoverable,
177+ }
178+ }
179+ _ => continue,
180+ };
181+ if events.send(event).await.is_err() {
182+ return;
183+ }
184+ }
185+}
186+
187+fn is_attachment_echo(content: &LocalEchoContent) -> bool {
188+ let LocalEchoContent::Event {
189+ serialized_event, ..
190+ } = content
191+ else {
192+ return false;
193+ };
194+ matches!(
195+ serialized_event.deserialize(),
196+ Ok(AnyMessageLikeEventContent::RoomMessage(content))
197+ if matches!(
198+ content.msgtype,
199+ MessageType::Audio(_) | MessageType::File(_) | MessageType::Image(_) | MessageType::Video(_)
200+ )
201+ )
202+}
203+
204+pub(super) async fn encryption(client: Client, events: EventSender) {
205+ let Some(device_id) = client.device_id().map(ToOwned::to_owned) else {
206+ tracing::warn!("encryption watcher stopped because the client has no device id");
207+ return;
208+ };
209+ let recovery = client.encryption().recovery();
210+ let backups = client.encryption().backups();
211+ let mut recovery_stream = recovery.state_stream();
212+ let mut backup_stream = backups.state_stream();
213+ let mut verification_stream = client.encryption().verification_state();
214+ let mut recovery_state = recovery.state();
215+ let mut backup_state = backups.state();
216+ let mut verification_state = verification_stream.get();
217+ loop {
218+ let state = encryption_state(
219+ device_id.clone(),
220+ recovery_state,
221+ verification_state,
222+ backup_state,
223+ );
224+ if events.send(Event::EncryptionChanged(state)).await.is_err() {
225+ return;
226+ }
227+ tokio::select! {
228+ Some(update) = recovery_stream.next() => recovery_state = update,
229+ Some(update) = verification_stream.next() => verification_state = update,
230+ Some(update) = backup_stream.next() => {
231+ backup_state = update.unwrap_or_else(|_| backups.state());
232+ }
233+ else => return,
234+ }
235+ }
236+}
237+
238+pub(super) async fn rooms(client: Client, events: EventSender) {
239+ let (_, mut rooms) = client.rooms_stream();
240+ let mut info = client.room_info_notable_update_receiver();
241+ loop {
242+ if events.send(Event::RoomsChanged).await.is_err() {
243+ return;
244+ }
245+ tokio::select! {
246+ update = rooms.next() => if update.is_none() { return },
247+ update = info.recv() => if matches!(update, Err(RecvError::Closed)) { return },
248+ }
249+ // a sync touching fifty rooms is one invalidation, not fifty
250+ while let Ok(_) | Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) =
251+ info.try_recv()
252+ {}
253+ }
254+}
255+
256+pub(super) async fn notification_settings(client: Client, events: EventSender) {
257+ let settings = client.notification_settings().await;
258+ let mut changes = settings.subscribe_to_changes();
259+ while let Ok(()) | Err(RecvError::Lagged(_)) = changes.recv().await {
260+ if events
261+ .send(Event::NotificationSettingsChanged)
262+ .await
263+ .is_err()
264+ {
265+ return;
266+ }
267+ }
268+}
Modified file: src/error.rs+113 additions−44 deletions
@@ -1,4 +1,4 @@
1-use thiserror::Error;
1+use std::sync::Arc;
22
33 use matrix_sdk::{
44 HttpError, RefreshTokenError,
@@ -12,36 +12,98 @@ use matrix_sdk::{
1212 secret_storage::{ImportError, SecretStorageError},
1313 },
1414 };
15+#[cfg(feature = "serde")]
16+use serde::{Deserialize, Serialize};
1517
16-use crate::CommandErrorKind;
18+use crate::InteractiveAuthChallenge;
1719
18-#[derive(Debug, Error)]
20+/// the one thing that goes wrong, for commands and queries alike. match on `kind`, show `message`
21+#[derive(Clone, Debug, thiserror::Error)]
22+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1923 #[error("{message}")]
20-pub(crate) struct CommandError {
21- pub(crate) kind: CommandErrorKind,
22- message: String,
24+#[non_exhaustive]
25+pub struct Error {
26+ pub kind: ErrorKind,
27+ pub message: String,
28+ /// set when the server wants interactive auth before it'll do the thing
29+ pub challenge: Option<Box<InteractiveAuthChallenge>>,
2330 #[source]
24- source: Option<Box<dyn std::error::Error + Send + Sync>>,
31+ #[cfg_attr(feature = "serde", serde(skip))]
32+ source: Option<Arc<dyn std::error::Error + Send + Sync>>,
2533 }
2634
27-impl CommandError {
28- pub(crate) fn new(kind: CommandErrorKind, message: impl Into<String>) -> Self {
35+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
37+#[non_exhaustive]
38+pub enum ErrorKind {
39+ /// the engine stopped, or the task doing the work died, before answering
40+ Interrupted,
41+ Unauthenticated,
42+ InteractiveAuthRequired,
43+ Invalid,
44+ RoomUnknown,
45+ RoomNotOpen,
46+ TimelineNotOpen,
47+ RoomBusy,
48+ QueueFull,
49+ EmptyBody,
50+ MediaUnknown,
51+ MediaTooLarge,
52+ MediaUnavailable,
53+ VerificationInactive,
54+ VerificationUnsupported,
55+ NoCrossSigningIdentity,
56+ RecoveryRejected,
57+ Network,
58+ Server,
59+ Store,
60+ Timeline,
61+ Encryption,
62+ Other,
63+}
64+
65+impl Error {
66+ pub(crate) fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
2967 Self {
3068 kind,
3169 message: message.into(),
70+ challenge: None,
3271 source: None,
3372 }
3473 }
3574
3675 pub(crate) fn with_source(
37- kind: CommandErrorKind,
76+ kind: ErrorKind,
3877 message: impl Into<String>,
3978 source: impl std::error::Error + Send + Sync + 'static,
4079 ) -> Self {
4180 Self {
42- kind,
43- message: message.into(),
44- source: Some(Box::new(source)),
81+ source: Some(Arc::new(source)),
82+ ..Self::new(kind, message)
83+ }
84+ }
85+
86+ pub(crate) fn interrupted() -> Self {
87+ Self::new(
88+ ErrorKind::Interrupted,
89+ "the engine stopped before the operation finished",
90+ )
91+ }
92+
93+ pub(crate) fn room_unknown() -> Self {
94+ Self::new(
95+ ErrorKind::RoomUnknown,
96+ "room is not available in the local store",
97+ )
98+ }
99+
100+ pub(crate) fn interactive_auth(challenge: InteractiveAuthChallenge) -> Self {
101+ Self {
102+ challenge: Some(Box::new(challenge)),
103+ ..Self::new(
104+ ErrorKind::InteractiveAuthRequired,
105+ "interactive authentication is required",
106+ )
45107 }
46108 }
47109
@@ -52,6 +114,7 @@ impl CommandError {
52114 pub(crate) fn from_http(message: impl Into<String>, source: HttpError) -> Self {
53115 Self::with_source(classify_http_error(&source), message, source)
54116 }
117+
55118 pub(crate) fn from_refresh_token(
56119 message: impl Into<String>,
57120 source: RefreshTokenError,
@@ -68,21 +131,21 @@ impl CommandError {
68131 RecoveryError::SecretStorage(SecretStorageError::ImportError { error, .. }) => {
69132 match error {
70133 ImportError::Sdk(source) => classify_sdk_error(source),
71- _ => CommandErrorKind::RecoveryRejected,
134+ _ => ErrorKind::RecoveryRejected,
72135 }
73136 }
74137 RecoveryError::SecretStorage(SecretStorageError::Verification(source)) => {
75138 match source {
76139 ManualVerifyError::Http(source) => classify_http_error(source),
77- _ => CommandErrorKind::RecoveryRejected,
140+ _ => ErrorKind::RecoveryRejected,
78141 }
79142 }
80143 RecoveryError::SecretStorage(SecretStorageError::SecretStorageKey(_)) => {
81- CommandErrorKind::RecoveryRejected
144+ ErrorKind::RecoveryRejected
82145 }
83- RecoveryError::SecretStorage(SecretStorageError::Storage(_)) => CommandErrorKind::Store,
146+ RecoveryError::SecretStorage(SecretStorageError::Storage(_)) => ErrorKind::Store,
84147 RecoveryError::BackupExistsOnServer | RecoveryError::SecretStorage(_) => {
85- CommandErrorKind::RecoveryRejected
148+ ErrorKind::RecoveryRejected
86149 }
87150 };
88151 Self::with_source(kind, message, source)
@@ -94,77 +157,83 @@ impl CommandError {
94157 ) -> Self {
95158 let kind = match &source {
96159 RequestVerificationError::Sdk(source) => classify_sdk_error(source),
97- RequestVerificationError::RoomCreation(_) => CommandErrorKind::Encryption,
160+ RequestVerificationError::RoomCreation(_) => ErrorKind::Encryption,
98161 };
99162 Self::with_source(kind, message, source)
100163 }
101164 }
102165
103-pub(crate) fn classify_sdk_error(source: &matrix_sdk::Error) -> CommandErrorKind {
166+impl PartialEq for Error {
167+ fn eq(&self, other: &Self) -> bool {
168+ self.kind == other.kind
169+ && self.message == other.message
170+ && self.challenge == other.challenge
171+ }
172+}
173+
174+pub(crate) fn classify_sdk_error(source: &matrix_sdk::Error) -> ErrorKind {
104175 match source {
105176 matrix_sdk::Error::Http(source) => classify_http_error(source),
106177 matrix_sdk::Error::OAuth(source) => classify_oauth_error(source),
107- matrix_sdk::Error::AuthenticationRequired => CommandErrorKind::Unauthenticated,
178+ matrix_sdk::Error::AuthenticationRequired => ErrorKind::Unauthenticated,
108179 matrix_sdk::Error::BadCryptoStoreState
109180 | matrix_sdk::Error::CryptoStoreError(_)
110181 | matrix_sdk::Error::CrossProcessLockError(_)
111182 | matrix_sdk::Error::StateStore(_)
112183 | matrix_sdk::Error::EventCacheStore(_)
113- | matrix_sdk::Error::MediaStore(_) => CommandErrorKind::Store,
184+ | matrix_sdk::Error::MediaStore(_) => ErrorKind::Store,
114185 matrix_sdk::Error::NoOlmMachine
115186 | matrix_sdk::Error::OlmError(_)
116187 | matrix_sdk::Error::MegolmError(_)
117- | matrix_sdk::Error::DecryptorError(_) => CommandErrorKind::Encryption,
118- matrix_sdk::Error::Media(_) => CommandErrorKind::MediaUnavailable,
119- matrix_sdk::Error::Timeout => CommandErrorKind::Network,
120- _ => CommandErrorKind::Other,
188+ | matrix_sdk::Error::DecryptorError(_) => ErrorKind::Encryption,
189+ matrix_sdk::Error::Media(_) => ErrorKind::MediaUnavailable,
190+ matrix_sdk::Error::Timeout => ErrorKind::Network,
191+ _ => ErrorKind::Other,
121192 }
122193 }
123194
124-fn classify_http_error(source: &HttpError) -> CommandErrorKind {
195+fn classify_http_error(source: &HttpError) -> ErrorKind {
125196 match source {
126- HttpError::Reqwest(_) => CommandErrorKind::Network,
127- HttpError::Api(_) => CommandErrorKind::Server,
197+ HttpError::Reqwest(_) => ErrorKind::Network,
198+ HttpError::Api(_) => ErrorKind::Server,
128199 HttpError::RefreshToken(source) => classify_refresh_token_error(source),
129200 HttpError::Cached(source) => classify_http_error(source),
130- _ => CommandErrorKind::Other,
201+ _ => ErrorKind::Other,
131202 }
132203 }
133204
134-fn classify_refresh_token_error(source: &RefreshTokenError) -> CommandErrorKind {
205+fn classify_refresh_token_error(source: &RefreshTokenError) -> ErrorKind {
135206 match source {
136- RefreshTokenError::RefreshTokenRequired => CommandErrorKind::Unauthenticated,
207+ RefreshTokenError::RefreshTokenRequired => ErrorKind::Unauthenticated,
137208 RefreshTokenError::MatrixAuth(source) => classify_http_error(source),
138209 RefreshTokenError::OAuth(source) => classify_oauth_error(source),
139210 }
140211 }
141212
142-fn classify_oauth_error(source: &OAuthError) -> CommandErrorKind {
213+fn classify_oauth_error(source: &OAuthError) -> ErrorKind {
143214 let classify_token_request_error = |source: &_| match source {
144- RequestTokenError::Request(_) => CommandErrorKind::Network,
145- RequestTokenError::ServerResponse(_) => CommandErrorKind::Unauthenticated,
146- _ => CommandErrorKind::Other,
215+ RequestTokenError::Request(_) => ErrorKind::Network,
216+ RequestTokenError::ServerResponse(_) => ErrorKind::Unauthenticated,
217+ _ => ErrorKind::Other,
147218 };
148219 match source {
149220 OAuthError::Discovery(source) => match source {
150221 OAuthDiscoveryError::Http(source) => classify_http_error(source),
151222 OAuthDiscoveryError::Oidc(source) => classify_token_request_error(source),
152- _ => CommandErrorKind::Unauthenticated,
223+ _ => ErrorKind::Unauthenticated,
153224 },
154225 OAuthError::RefreshToken(source) => classify_token_request_error(source),
155226 OAuthError::AuthorizationCode(OAuthAuthorizationCodeError::RequestToken(source)) => {
156227 classify_token_request_error(source)
157228 }
158- OAuthError::AuthorizationCode(_) => CommandErrorKind::Unauthenticated,
229+ OAuthError::AuthorizationCode(_) => ErrorKind::Unauthenticated,
159230 OAuthError::Logout(source) => match source {
160- OAuthTokenRevocationError::Revoke(RequestTokenError::Request(_)) => {
161- CommandErrorKind::Network
162- }
231+ OAuthTokenRevocationError::Revoke(RequestTokenError::Request(_)) => ErrorKind::Network,
163232 OAuthTokenRevocationError::Revoke(RequestTokenError::ServerResponse(_)) => {
164- CommandErrorKind::Unauthenticated
233+ ErrorKind::Unauthenticated
165234 }
166- _ => CommandErrorKind::Other,
235+ _ => ErrorKind::Other,
167236 },
168- _ => CommandErrorKind::Unauthenticated,
237+ _ => ErrorKind::Unauthenticated,
169238 }
170239 }
Modified file: src/lib.rs+12 additions−13 deletions
@@ -19,23 +19,22 @@ pub use account::{
1919 };
2020
2121 pub use engine::{Engine, EngineConfig, EngineError};
22+pub use error::{Error, ErrorKind};
2223 pub use model::{
23- AccountCommand, AttachmentUpload, Command, CommandErrorKind, CommandFailure, CommandId,
24- CommandKind, ConnectionState, CreateRoomOptions, EncryptionCommand, EncryptionState,
25- EngineErrorKind, EngineFailure, Event, HistoryState, KeyBackupState, LifecycleCommand,
26- MediaCommand, MediaDescriptor, MediaId, MediaKind, MediaSize, MemberFilter, MemberMembership,
27- MemberSummary, MessageContent, MessageEditContent, MessageMentions, NotificationCommand,
28- OwnDevice, PowerLevelChanges, PushNotification, PushNotificationEvent, PushNotificationRoom,
29- PushNotificationSender, PushNotificationStatus, Reaction, RecoveryState, ReplyReference,
30- RichText, RoomCategory, RoomCommand, RoomListFilter, RoomMembership, RoomNotificationMode,
24+ AccountCommand, AttachmentUpload, Command, CommandOutput, ConnectionState, CreateRoomOptions,
25+ EncryptionCommand, EncryptionState, EngineErrorKind, EngineFailure, Event, HistoryState,
26+ KeyBackupState, LatestEvent, LifecycleCommand, Media, MediaCommand, MediaDescriptor, MediaId,
27+ MediaKind, MediaSize, MemberFilter, MemberMembership, MemberSummary, MessageContent,
28+ MessageEditContent, MessageMentions, NotificationCommand, OwnDevice, PowerLevelChanges,
29+ Profile, PushNotification, PushNotificationEvent, PushNotificationRoom, PushNotificationSender,
30+ PushNotificationStatus, Reaction, RecoveryState, ReplyReference, RichText, RoomCategory,
31+ RoomCommand, RoomHero, RoomListFilter, RoomMatcher, RoomMembership, RoomNotificationMode,
3132 RoomNotificationSettings, RoomPowerLevels, RoomPreset, RoomSummary, RoomVisibility, SendState,
32- ThreadSummary, TimelineCommand, TimelineDiff, TimelineDiffError, TimelineId, TimelineItem,
33- TimelineItemId, TimelineMessage, TypingState, UploadDetails, UploadThumbnail,
33+ SpaceChild, ThreadSummary, TimelineCommand, TimelineDiff, TimelineDiffError, TimelineId,
34+ TimelineItem, TimelineItemId, TimelineMessage, TypingState, UploadDetails, UploadThumbnail,
3435 VerificationAction, VerificationFailure, VerificationState, VerificationTarget,
3536 };
36-pub use transport::{
37- ClientHandle, ClientQueryError, CommandResultError, EventStream, SendCommandError,
38-};
37+pub use transport::{ClientHandle, EventStream, Pending, SpaceHierarchy};
3938 pub use uiaa::{
4039 InteractiveAuth, InteractiveAuthChallenge, InteractiveAuthFailure, InteractiveAuthFlow,
4140 InteractiveAuthOutcome,
Modified file: src/model.rs+209 additions−127 deletions
@@ -3,13 +3,19 @@ use std::{fmt, sync::Arc};
33 use ruma::{
44 OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedRoomOrAliasId,
55 OwnedServerName, OwnedTransactionId, OwnedUserId,
6- events::{AnySyncTimelineEvent, room::member::StrippedRoomMemberEvent},
6+ events::{
7+ AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent, AnySyncTimelineEvent,
8+ GlobalAccountDataEventType, RoomAccountDataEventType,
9+ room::member::StrippedRoomMemberEvent,
10+ },
711 serde::Raw,
812 };
913 #[cfg(feature = "serde")]
1014 use serde::{Deserialize, Serialize};
1115
12-#[derive(Debug, Eq, PartialEq)]
16+/// every command answers exactly once through `ClientHandle::send` or `execute`.
17+/// the event stream only ever carries state
18+#[derive(Debug)]
1319 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1420 #[non_exhaustive]
1521 pub enum Command {
@@ -22,11 +28,22 @@ pub enum Command {
2228 Lifecycle(LifecycleCommand),
2329 }
2430
25-#[derive(Debug, Eq, PartialEq)]
31+#[derive(Debug)]
2632 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
2733 #[non_exhaustive]
2834 pub enum AccountCommand {
2935 Logout,
36+ SetAccountData {
37+ event_type: GlobalAccountDataEventType,
38+ content: Raw<AnyGlobalAccountDataEventContent>,
39+ },
40+ SetDisplayName(Option<String>),
41+ SetAvatar(Option<OwnedMxcUri>),
42+ /// uploads the image and points the profile at it, same byte cap as any upload
43+ UploadAvatar {
44+ mimetype: String,
45+ bytes: Vec<u8>,
46+ },
3047 RenameDevice {
3148 device_id: OwnedDeviceId,
3249 display_name: String,
@@ -37,10 +54,11 @@ pub enum AccountCommand {
3754 },
3855 }
3956
40-#[derive(Clone, Debug, Eq, PartialEq)]
57+#[derive(Clone, Debug)]
4158 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
4259 #[non_exhaustive]
4360 pub enum RoomCommand {
61+ /// the snapshot turns up as `Event::TimelineReset`. opening an open room sends a fresh one
4462 Open(OwnedRoomId),
4563 Close(OwnedRoomId),
4664 Join {
@@ -94,10 +112,16 @@ pub enum RoomCommand {
94112 room_id: OwnedRoomId,
95113 typing: bool,
96114 },
115+ SetAccountData {
116+ room_id: OwnedRoomId,
117+ event_type: RoomAccountDataEventType,
118+ content: Raw<AnyRoomAccountDataEventContent>,
119+ },
97120 }
98121
99122 #[derive(Clone, Debug, Default, Eq, PartialEq)]
100123 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
124+#[non_exhaustive]
101125 pub struct CreateRoomOptions {
102126 pub name: Option<String>,
103127 pub topic: Option<String>,
@@ -108,6 +132,50 @@ pub struct CreateRoomOptions {
108132 pub preset: Option<RoomPreset>,
109133 }
110134
135+impl CreateRoomOptions {
136+ #[must_use]
137+ pub fn with_name(mut self, name: impl Into<String>) -> Self {
138+ self.name = Some(name.into());
139+ self
140+ }
141+
142+ #[must_use]
143+ pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
144+ self.topic = Some(topic.into());
145+ self
146+ }
147+
148+ #[must_use]
149+ pub fn with_alias_localpart(mut self, localpart: impl Into<String>) -> Self {
150+ self.alias_localpart = Some(localpart.into());
151+ self
152+ }
153+
154+ #[must_use]
155+ pub fn with_invite(mut self, invite: impl IntoIterator<Item = OwnedUserId>) -> Self {
156+ self.invite = invite.into_iter().collect();
157+ self
158+ }
159+
160+ #[must_use]
161+ pub const fn with_direct(mut self, direct: bool) -> Self {
162+ self.direct = direct;
163+ self
164+ }
165+
166+ #[must_use]
167+ pub const fn with_visibility(mut self, visibility: RoomVisibility) -> Self {
168+ self.visibility = visibility;
169+ self
170+ }
171+
172+ #[must_use]
173+ pub const fn with_preset(mut self, preset: RoomPreset) -> Self {
174+ self.preset = Some(preset);
175+ self
176+ }
177+}
178+
111179 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
112180 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
113181 pub enum RoomVisibility {
@@ -247,18 +315,8 @@ impl From<OwnedRoomId> for TimelineId {
247315 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
248316 #[non_exhaustive]
249317 pub enum MediaCommand {
250- Fetch {
251- id: MediaId,
252- size: MediaSize,
253- },
254- Upload {
255- timeline_id: TimelineId,
256- filename: String,
257- mimetype: String,
258- bytes: Vec<u8>,
259- caption: Option<String>,
260- },
261- UploadAttachment(AttachmentUpload),
318+ /// succeeds once the send queue has it. progress and failure arrive as events
319+ Upload(AttachmentUpload),
262320 }
263321
264322 #[derive(Clone, Debug, Eq, PartialEq)]
@@ -320,6 +378,7 @@ pub enum UploadDetails {
320378
321379 #[derive(Clone, Debug, Eq, PartialEq)]
322380 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
381+#[non_exhaustive]
323382 pub struct UploadThumbnail {
324383 pub bytes: Vec<u8>,
325384 pub mimetype: String,
@@ -327,6 +386,18 @@ pub struct UploadThumbnail {
327386 pub height: u32,
328387 }
329388
389+impl UploadThumbnail {
390+ #[must_use]
391+ pub fn new(bytes: Vec<u8>, mimetype: impl Into<String>, width: u32, height: u32) -> Self {
392+ Self {
393+ bytes,
394+ mimetype: mimetype.into(),
395+ width,
396+ height,
397+ }
398+ }
399+}
400+
330401 #[derive(Eq, PartialEq)]
331402 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
332403 #[non_exhaustive]
@@ -388,31 +459,12 @@ pub enum LifecycleCommand {
388459 Shutdown,
389460 }
390461
391-impl Command {
392- #[must_use]
393- pub const fn kind(&self) -> CommandKind {
394- match self {
395- Self::Account(_) => CommandKind::Account,
396- Self::Room(_) => CommandKind::Room,
397- Self::Timeline(_) => CommandKind::Timeline,
398- Self::Media(_) => CommandKind::Media,
399- Self::Encryption(_) => CommandKind::Encryption,
400- Self::Notification(_) => CommandKind::Notification,
401- Self::Lifecycle(_) => CommandKind::Lifecycle,
402- }
403- }
404-}
405-
406-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
462+#[derive(Clone, Debug, Eq, PartialEq)]
407463 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
408-pub enum CommandKind {
409- Account,
410- Room,
411- Timeline,
412- Media,
413- Encryption,
414- Notification,
415- Lifecycle,
464+#[non_exhaustive]
465+pub enum CommandOutput {
466+ Done,
467+ Room(OwnedRoomId),
416468 }
417469
418470 #[derive(Clone, Debug, Eq, PartialEq)]
@@ -590,14 +642,8 @@ pub enum Event {
590642 },
591643 NotificationSettingsChanged,
592644 VerificationTargetChanged(VerificationTarget),
593- InteractiveAuthRequired {
594- id: CommandId,
595- command: CommandKind,
596- challenge: crate::InteractiveAuthChallenge,
597- },
598645 RoomsChanged,
599- /// Authoritative snapshot on open or resync. A resync reset precedes the
600- /// corresponding terminal command event on this stream.
646+ /// replaces whatever you had for this timeline. every diff after it applies to it
601647 TimelineReset {
602648 timeline_id: TimelineId,
603649 items: Vec<TimelineItem>,
@@ -611,12 +657,6 @@ pub enum Event {
611657 state: HistoryState,
612658 },
613659 TypingChanged(TypingState),
614- MediaReady {
615- media_id: MediaId,
616- size: MediaSize,
617- bytes: Arc<[u8]>,
618- mimetype: String,
619- },
620660 MediaUploadProgress {
621661 room_id: OwnedRoomId,
622662 transaction_id: OwnedTransactionId,
@@ -627,24 +667,11 @@ pub enum Event {
627667 MediaUploadFailed {
628668 room_id: OwnedRoomId,
629669 transaction_id: OwnedTransactionId,
630- kind: CommandErrorKind,
670+ kind: crate::ErrorKind,
631671 recoverable: bool,
632672 },
633673 EncryptionChanged(EncryptionState),
634674 VerificationChanged(VerificationState),
635- /// Terminal command success. For sends, this means the SDK send queue
636- /// accepted the item; network delivery is reflected by its send state.
637- CommandSucceeded {
638- id: CommandId,
639- command: CommandKind,
640- },
641- CommandSucceededWithRoom {
642- id: CommandId,
643- command: CommandKind,
644- room_id: OwnedRoomId,
645- },
646- /// Terminal failure correlated by `CommandFailure::id`.
647- CommandFailed(CommandFailure),
648675 /// logout went through and the engine stops. delete the record and the store
649676 LoggedOut,
650677 Failed(EngineFailure),
@@ -653,23 +680,9 @@ pub enum Event {
653680 Stopped,
654681 }
655682
656-#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
657-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
658-pub struct CommandId(u64);
659-
660-impl CommandId {
661- pub(crate) const fn new(value: u64) -> Self {
662- Self(value)
663- }
664-
665- #[must_use]
666- pub const fn get(self) -> u64 {
667- self.0
668- }
669-}
670-
671683 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
672684 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
685+#[non_exhaustive]
673686 pub enum ConnectionState {
674687 Idle,
675688 Connecting,
@@ -694,12 +707,76 @@ pub struct RoomSummary {
694707 pub low_priority: bool,
695708 pub avatar: Option<OwnedMxcUri>,
696709 pub latest_event_timestamp_ms: Option<u64>,
710+ pub latest_event: Option<LatestEvent>,
697711 pub notification_mode: Option<RoomNotificationMode>,
698712 pub notification_mode_custom: bool,
713+ pub topic: Option<String>,
714+ pub canonical_alias: Option<OwnedRoomAliasId>,
715+ pub joined_members: u64,
716+ pub invited_members: u64,
717+ /// who a nameless room gets named after
718+ pub heroes: Vec<RoomHero>,
719+ /// set once the room has been upgraded, this is where everyone went
720+ pub successor: Option<OwnedRoomId>,
721+ pub inviter: Option<OwnedUserId>,
722+ /// joined spaces that list this room as a child
723+ pub parent_spaces: Vec<OwnedRoomId>,
724+ /// for a space, the children it lists, joined or not
725+ pub children: Vec<OwnedRoomId>,
726+}
727+
728+#[derive(Clone, Debug, Eq, PartialEq)]
729+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
730+#[non_exhaustive]
731+pub struct RoomHero {
732+ pub user_id: OwnedUserId,
733+ pub display_name: Option<String>,
734+ pub avatar: Option<OwnedMxcUri>,
735+}
736+
737+/// enough for a room list preview line. `body` is empty for anything that isn't a plain message
738+#[derive(Clone, Debug, Eq, PartialEq)]
739+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
740+#[non_exhaustive]
741+pub struct LatestEvent {
742+ pub sender: Option<OwnedUserId>,
743+ pub body: Option<String>,
744+ pub timestamp_ms: Option<u64>,
745+ pub send_state: SendState,
746+}
747+
748+#[derive(Clone, Debug, Eq, PartialEq)]
749+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
750+#[non_exhaustive]
751+pub struct Profile {
752+ pub user_id: OwnedUserId,
753+ pub display_name: Option<String>,
754+ pub avatar: Option<OwnedMxcUri>,
755+}
756+
757+/// one room in a space's hierarchy, as the server describes it, which may be a room you're not in
758+#[derive(Clone, Debug, Eq, PartialEq)]
759+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
760+#[non_exhaustive]
761+pub struct SpaceChild {
762+ pub room_id: OwnedRoomId,
763+ pub name: String,
764+ pub topic: Option<String>,
765+ pub avatar: Option<OwnedMxcUri>,
766+ pub canonical_alias: Option<OwnedRoomAliasId>,
767+ pub joined_members: u64,
768+ pub space: bool,
769+ pub children_count: u64,
770+ pub membership: Option<RoomMembership>,
771+ pub via: Vec<OwnedServerName>,
772+ pub suggested: bool,
773+ pub world_readable: Option<bool>,
774+ pub guest_can_join: bool,
699775 }
700776
701777 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
702778 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
779+#[non_exhaustive]
703780 pub enum RoomNotificationMode {
704781 AllMessages,
705782 MentionsAndKeywordsOnly,
@@ -810,6 +887,7 @@ pub struct PowerLevelChanges {
810887
811888 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
812889 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
890+#[non_exhaustive]
813891 pub enum RoomMembership {
814892 Joined,
815893 Invited,
@@ -862,21 +940,44 @@ impl RoomListFilter {
862940
863941 #[must_use]
864942 pub fn matches(&self, room: &RoomSummary) -> bool {
865- self.membership
943+ self.matcher().matches(room)
944+ }
945+
946+ /// lowercases the search once, for filtering a whole list
947+ #[must_use]
948+ pub fn matcher(&self) -> RoomMatcher<'_> {
949+ RoomMatcher {
950+ filter: self,
951+ search: self.search.as_deref().map(str::to_lowercase),
952+ }
953+ }
954+}
955+
956+#[derive(Debug)]
957+pub struct RoomMatcher<'a> {
958+ filter: &'a RoomListFilter,
959+ search: Option<String>,
960+}
961+
962+impl RoomMatcher<'_> {
963+ #[must_use]
964+ pub fn matches(&self, room: &RoomSummary) -> bool {
965+ let filter = self.filter;
966+ filter
967+ .membership
866968 .is_none_or(|membership| room.membership == membership)
867- && self.category.is_none_or(|category| match category {
969+ && filter.category.is_none_or(|category| match category {
868970 RoomCategory::People => room.direct && !room.space,
869971 RoomCategory::Rooms => !room.direct && !room.space,
870972 RoomCategory::Spaces => room.space,
871973 })
872- && self
974+ && filter
873975 .favourite
874976 .is_none_or(|favourite| room.favourite == favourite)
875- && (!self.unread_only || room.unread_messages > 0 || room.unread_mentions > 0)
977+ && (!filter.unread_only || room.unread_messages > 0 || room.unread_mentions > 0)
876978 && self.search.as_ref().is_none_or(|search| {
877- let search = search.to_lowercase();
878- room.name.to_lowercase().contains(&search)
879- || room.room_id.as_str().to_lowercase().contains(&search)
979+ room.name.to_lowercase().contains(search)
980+ || room.room_id.as_str().to_lowercase().contains(search)
880981 })
881982 }
882983 }
@@ -938,6 +1039,7 @@ pub struct OwnDevice {
9381039
9391040 #[derive(Clone, Debug, Eq, PartialEq)]
9401041 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1042+#[non_exhaustive]
9411043 pub enum TimelineItem {
9421044 Message(Box<TimelineMessage>),
9431045 DayDivider { timestamp_ms: u64 },
@@ -1161,6 +1263,7 @@ pub enum SendState {
11611263
11621264 #[derive(Clone, Debug, Eq, PartialEq)]
11631265 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1266+#[non_exhaustive]
11641267 pub struct ReplyReference {
11651268 pub event_id: OwnedEventId,
11661269 pub sender_id: Option<OwnedUserId>,
@@ -1215,8 +1318,19 @@ impl std::fmt::Display for MediaId {
12151318 #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
12161319 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12171320 pub enum MediaSize {
1218- Thumbnail,
12191321 Full,
1322+ /// a hint. an uploader's own thumbnail wins over the server resizing anything
1323+ Thumbnail {
1324+ width: u32,
1325+ height: u32,
1326+ },
1327+}
1328+
1329+#[derive(Clone, Debug, Eq, PartialEq)]
1330+#[non_exhaustive]
1331+pub struct Media {
1332+ pub bytes: Arc<[u8]>,
1333+ pub mimetype: String,
12201334 }
12211335
12221336 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -1232,6 +1346,7 @@ pub enum MediaKind {
12321346
12331347 #[derive(Clone, Debug, Eq, PartialEq)]
12341348 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1349+#[non_exhaustive]
12351350 pub struct TypingState {
12361351 pub room_id: OwnedRoomId,
12371352 pub users: Vec<OwnedUserId>,
@@ -1239,6 +1354,7 @@ pub struct TypingState {
12391354
12401355 #[derive(Clone, Debug, Eq, PartialEq)]
12411356 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1357+#[non_exhaustive]
12421358 pub enum HistoryState {
12431359 Idle,
12441360 Loading,
@@ -1258,6 +1374,7 @@ pub struct EncryptionState {
12581374
12591375 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
12601376 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1377+#[non_exhaustive]
12611378 pub enum KeyBackupState {
12621379 Unknown,
12631380 Creating,
@@ -1270,6 +1387,7 @@ pub enum KeyBackupState {
12701387
12711388 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
12721389 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1390+#[non_exhaustive]
12731391 pub enum RecoveryState {
12741392 Unknown,
12751393 Disabled,
@@ -1299,6 +1417,7 @@ pub enum VerificationTarget {
12991417
13001418 #[derive(Clone, Debug, Eq, PartialEq)]
13011419 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1420+#[non_exhaustive]
13021421 pub enum VerificationState {
13031422 Waiting,
13041423 Comparison {
@@ -1325,43 +1444,6 @@ pub enum VerificationFailure {
13251444 #[derive(Clone, Debug, Eq, PartialEq)]
13261445 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13271446 #[non_exhaustive]
1328-pub struct CommandFailure {
1329- pub id: CommandId,
1330- pub command: CommandKind,
1331- pub kind: CommandErrorKind,
1332- pub message: String,
1333-}
1334-
1335-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1336-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1337-#[non_exhaustive]
1338-pub enum CommandErrorKind {
1339- Unauthenticated,
1340- RoomUnknown,
1341- RoomNotOpen,
1342- TimelineNotOpen,
1343- RoomBusy,
1344- QueueFull,
1345- InteractiveAuthRequired,
1346- EmptyBody,
1347- MediaUnknown,
1348- MediaTooLarge,
1349- MediaUnavailable,
1350- VerificationInactive,
1351- VerificationUnsupported,
1352- NoCrossSigningIdentity,
1353- RecoveryRejected,
1354- EventReceiverClosed,
1355- Network,
1356- Server,
1357- Store,
1358- Timeline,
1359- Encryption,
1360- Other,
1361-}
1362-
1363-#[derive(Clone, Debug, Eq, PartialEq)]
1364-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13651447 pub struct EngineFailure {
13661448 pub kind: EngineErrorKind,
13671449 pub message: String,
Modified file: src/normalise/mod.rs+1 additions−1 deletions
@@ -6,5 +6,5 @@ mod timeline;
66 pub(crate) use media::{MediaRegistration, MediaScope};
77 pub(crate) use member::member_summary;
88 pub(crate) use room::notification_mode;
9-pub(crate) use room::room_summaries;
9+pub(crate) use room::{room_summaries, space_child};
1010 pub(crate) use timeline::{normalise_timeline, normalise_timeline_diffs};
Modified file: src/normalise/room.rs+157 additions−12 deletions
@@ -1,11 +1,23 @@
11 use futures_util::{StreamExt, stream};
2-use matrix_sdk::{Client, Room, RoomState};
2+use matrix_sdk::{
3+ Client, Room, RoomState,
4+ deserialized_responses::SyncOrStrippedState,
5+ latest_events::LatestEventValue,
6+ ruma::events::{
7+ AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncStateEvent,
8+ space::child::SpaceChildEventContent,
9+ },
10+};
11+use matrix_sdk_ui::spaces::{SpaceRoom, SpaceService};
12+use ruma::OwnedRoomId;
313
4-use crate::{RoomMembership, RoomNotificationMode, RoomSummary};
14+use crate::{
15+ LatestEvent, RoomHero, RoomMembership, RoomNotificationMode, RoomSummary, SendState, SpaceChild,
16+};
517
6-pub(crate) async fn room_summaries(client: &Client) -> Vec<RoomSummary> {
18+pub(crate) async fn room_summaries(client: &Client, spaces: &SpaceService) -> Vec<RoomSummary> {
719 let mut summaries = stream::iter(client.rooms())
8- .map(|room| async move { room_summary(&room).await })
20+ .map(|room| async move { room_summary(&room, spaces).await })
921 .buffer_unordered(16)
1022 .collect::<Vec<_>>()
1123 .await;
@@ -19,7 +31,7 @@ pub(crate) async fn room_summaries(client: &Client) -> Vec<RoomSummary> {
1931 summaries
2032 }
2133
22-async fn room_summary(room: &Room) -> RoomSummary {
34+async fn room_summary(room: &Room, spaces: &SpaceService) -> RoomSummary {
2335 let name = room
2436 .display_name()
2537 .await
@@ -30,16 +42,23 @@ async fn room_summary(room: &Room) -> RoomSummary {
3042 Some(mode) => Some(mode),
3143 None => room.notification_mode().await,
3244 };
45+ let inviter = match room.state() {
46+ RoomState::Invited => room
47+ .invite_details()
48+ .await
49+ .ok()
50+ .map(|invite| invite.inviter_id),
51+ _ => None,
52+ };
53+ let children = if room.is_space() {
54+ space_children(room).await
55+ } else {
56+ Vec::new()
57+ };
3358 RoomSummary {
3459 room_id: room.room_id().to_owned(),
3560 name,
36- membership: match room.state() {
37- RoomState::Joined => RoomMembership::Joined,
38- RoomState::Invited => RoomMembership::Invited,
39- RoomState::Left => RoomMembership::Left,
40- RoomState::Knocked => RoomMembership::Knocked,
41- RoomState::Banned => RoomMembership::Banned,
42- },
61+ membership: membership(room.state()),
4362 unread_messages: room.num_unread_messages(),
4463 unread_mentions: room.num_unread_mentions(),
4564 encrypted: room.encryption_state().is_encrypted(),
@@ -51,8 +70,134 @@ async fn room_summary(room: &Room) -> RoomSummary {
5170 latest_event_timestamp_ms: room
5271 .latest_event_timestamp()
5372 .map(|value| value.get().into()),
73+ latest_event: latest_event(room),
5474 notification_mode: effective_notification_mode.map(notification_mode),
5575 notification_mode_custom: custom_notification_mode.is_some(),
76+ topic: room.topic(),
77+ canonical_alias: room.canonical_alias(),
78+ joined_members: room.joined_members_count(),
79+ invited_members: room.invited_members_count(),
80+ heroes: room
81+ .heroes()
82+ .into_iter()
83+ .map(|hero| RoomHero {
84+ user_id: hero.user_id,
85+ display_name: hero.display_name,
86+ avatar: hero.avatar_url,
87+ })
88+ .collect(),
89+ successor: room.successor_room().map(|successor| successor.room_id),
90+ inviter,
91+ parent_spaces: spaces
92+ .joined_parents_of_child(room.room_id())
93+ .await
94+ .into_iter()
95+ .map(|parent| parent.room_id)
96+ .collect(),
97+ children,
98+ }
99+}
100+
101+const fn membership(state: RoomState) -> RoomMembership {
102+ match state {
103+ RoomState::Joined => RoomMembership::Joined,
104+ RoomState::Invited => RoomMembership::Invited,
105+ RoomState::Left => RoomMembership::Left,
106+ RoomState::Knocked => RoomMembership::Knocked,
107+ RoomState::Banned => RoomMembership::Banned,
108+ }
109+}
110+
111+/// what the space itself lists, straight from local state. redacted children don't count
112+async fn space_children(room: &Room) -> Vec<OwnedRoomId> {
113+ let Ok(events) = room
114+ .get_state_events_static::<SpaceChildEventContent>()
115+ .await
116+ else {
117+ return Vec::new();
118+ };
119+ events
120+ .into_iter()
121+ .filter_map(|event| match event.deserialize().ok()? {
122+ SyncOrStrippedState::Sync(SyncStateEvent::Original(event)) => Some(event.state_key),
123+ SyncOrStrippedState::Stripped(event) => Some(event.state_key),
124+ SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_)) => None,
125+ })
126+ .collect()
127+}
128+
129+fn latest_event(room: &Room) -> Option<LatestEvent> {
130+ let (sender, body, timestamp, send_state) = match room.latest_event() {
131+ LatestEventValue::None => return None,
132+ LatestEventValue::Remote(event) => {
133+ let body = match event.raw().deserialize() {
134+ Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
135+ message,
136+ ))) => message
137+ .as_original()
138+ .map(|message| message.content.body().to_owned()),
139+ _ => None,
140+ };
141+ (event.sender(), body, event.timestamp(), SendState::Sent)
142+ }
143+ LatestEventValue::RemoteInvite {
144+ timestamp, inviter, ..
145+ } => (inviter, None, Some(timestamp), SendState::Sent),
146+ LatestEventValue::LocalIsSending(local) => local_event(room, &local, SendState::Sending),
147+ LatestEventValue::LocalHasBeenSent { value, .. } => {
148+ local_event(room, &value, SendState::Sent)
149+ }
150+ LatestEventValue::LocalCannotBeSent(local) => {
151+ local_event(room, &local, SendState::Failed { retriable: true })
152+ }
153+ };
154+ Some(LatestEvent {
155+ sender,
156+ body,
157+ timestamp_ms: timestamp.map(|timestamp| timestamp.get().into()),
158+ send_state,
159+ })
160+}
161+
162+type LatestParts = (
163+ Option<ruma::OwnedUserId>,
164+ Option<String>,
165+ Option<ruma::MilliSecondsSinceUnixEpoch>,
166+ SendState,
167+);
168+
169+fn local_event(
170+ room: &Room,
171+ local: &matrix_sdk::latest_events::LocalLatestEventValue,
172+ send_state: SendState,
173+) -> LatestParts {
174+ let body = match local.content.deserialize() {
175+ Ok(AnyMessageLikeEventContent::RoomMessage(content)) => Some(content.body().to_owned()),
176+ _ => None,
177+ };
178+ (
179+ room.own_user_id().to_owned().into(),
180+ body,
181+ Some(local.timestamp),
182+ send_state,
183+ )
184+}
185+
186+pub(crate) fn space_child(room: SpaceRoom) -> SpaceChild {
187+ SpaceChild {
188+ name: room.display_name,
189+ room_id: room.room_id,
190+ topic: room.topic,
191+ avatar: room.avatar_url,
192+ canonical_alias: room.canonical_alias,
193+ joined_members: room.num_joined_members,
194+ space: room.room_type == Some(ruma::room::RoomType::Space),
195+ children_count: room.children_count,
196+ membership: room.state.map(membership),
197+ via: room.via,
198+ suggested: room.suggested,
199+ world_readable: room.world_readable,
200+ guest_can_join: room.guest_can_join,
56201 }
57202 }
58203
Modified file: src/transport.rs+519 additions−512 deletions
@@ -1,10 +1,8 @@
1-use std::sync::{
2- Arc,
3- atomic::{AtomicU64, Ordering},
4-};
51 use std::{
62 fmt,
3+ future::Future,
74 pin::Pin,
5+ sync::Arc,
86 task::{Context, Poll},
97 };
108
@@ -13,37 +11,44 @@ use matrix_sdk_ui::{
1311 notification_client::{
1412 NotificationClient, NotificationProcessSetup, NotificationStatus, RawNotificationEvent,
1513 },
14+ spaces::{SpaceRoomList, SpaceService, room_list::SpaceRoomListPaginationState},
1615 sync_service::SyncService,
1716 };
1817 use ruma::{
19- EventId, RoomId,
18+ EventId, MxcUri, OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedRoomOrAliasId,
19+ OwnedServerName, OwnedTransactionId, OwnedUserId, RoomId, UserId,
2020 events::{
21- AnyGlobalAccountDataEventContent, AnyRoomAccountDataEvent, GlobalAccountDataEventType,
22- RoomAccountDataEventType,
21+ AnyGlobalAccountDataEventContent, AnyRoomAccountDataEvent, AnyRoomAccountDataEventContent,
22+ GlobalAccountDataEventType, RoomAccountDataEventType,
2323 },
2424 serde::Raw,
2525 };
26-use thiserror::Error;
2726 use tokio::sync::{OnceCell, mpsc, oneshot, watch};
2827
2928 use crate::{
30- AccountCommand, Command, CommandId, ConnectionState, EncryptionState, Event, InteractiveAuth,
31- InteractiveAuthOutcome, MemberFilter, MemberSummary, NotificationCommand, OwnDevice,
32- PowerLevelChanges, PushNotification, PushNotificationEvent, PushNotificationStatus,
33- RoomListFilter, RoomNotificationMode, RoomNotificationSettings, RoomPowerLevels, RoomSummary,
29+ AccountCommand, AttachmentUpload, Command, CommandOutput, ConnectionState, CreateRoomOptions,
30+ EncryptionCommand, EncryptionState, Error, ErrorKind, Event, InteractiveAuth,
31+ InteractiveAuthOutcome, LifecycleCommand, Media, MediaCommand, MediaId, MediaSize,
32+ MemberFilter, MemberSummary, MessageContent, MessageEditContent, NotificationCommand,
33+ OwnDevice, PowerLevelChanges, Profile, PushNotification, PushNotificationEvent,
34+ PushNotificationStatus, RoomCommand, RoomListFilter, RoomNotificationMode,
35+ RoomNotificationSettings, RoomPowerLevels, RoomSummary, SpaceChild, TimelineCommand,
36+ TimelineId, VerificationAction, VerificationTarget,
3437 encryption::encryption_state,
38+ engine::MediaService,
3539 model::{PushNotificationRoom, PushNotificationSender},
36- normalise::{member_summary, room_summaries},
40+ normalise::{member_summary, room_summaries, space_child},
3741 };
3842
3943 #[derive(Clone)]
4044 pub struct ClientHandle {
41- commands: mpsc::Sender<DriverRequest>,
42- next_id: Arc<AtomicU64>,
45+ requests: mpsc::Sender<Request>,
4346 client: Client,
4447 connection: watch::Receiver<ConnectionState>,
4548 notification_client: Arc<OnceCell<NotificationClient>>,
4649 sync_service: Arc<SyncService>,
50+ media: Arc<MediaService>,
51+ spaces: Arc<OnceCell<SpaceService>>,
4752 }
4853
4954 impl fmt::Debug for ClientHandle {
@@ -54,73 +59,154 @@ impl fmt::Debug for ClientHandle {
5459 }
5560 }
5661
62+/// a command's answer. drop it if you don't care, the command still runs
63+#[derive(Debug)]
64+#[must_use = "dropping it is fine but then you'll never know"]
65+pub struct Pending(oneshot::Receiver<Result<CommandOutput, Error>>);
66+
67+impl Future for Pending {
68+ type Output = Result<CommandOutput, Error>;
69+
70+ fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
71+ Pin::new(&mut self.0)
72+ .poll(context)
73+ .map(|result| result.unwrap_or_else(|_| Err(Error::interrupted())))
74+ }
75+}
76+
5777 impl ClientHandle {
58- /// Current credentials after a refresh. Persist this snapshot atomically
59- /// when `Event::SessionTokensChanged` arrives; it contains secrets.
78+ /// current credentials. persist this atomically on `Event::SessionTokensChanged`, it holds secrets
6079 #[cfg(feature = "sqlite")]
6180 pub fn session_record(&self) -> Result<crate::SessionRecord, crate::AccountError> {
6281 crate::account::session_record(&self.client)
6382 }
6483
65- /// Queue a command and return its correlation ID, not its result. Drain
66- /// `EventStream` concurrently and match one terminal result event by ID.
67- /// A successful timeline send only means the SDK queue accepted it.
68- pub async fn send(&self, command: Command) -> Result<CommandId, SendCommandError> {
69- let id = self.next_command_id();
70- self.commands
71- .send(DriverRequest::Command(CommandEnvelope { id, command }))
84+ /// queue a command, waiting for room in the queue if there isn't any
85+ pub async fn send(&self, command: Command) -> Result<Pending, Error> {
86+ let (request, pending) = Request::new(command);
87+ self.requests
88+ .send(request)
7289 .await
73- .map_err(|_| SendCommandError::Closed)?;
74- Ok(id)
90+ .map_err(|_| Error::interrupted())?;
91+ Ok(pending)
92+ }
93+
94+ /// queue a command or fail with `QueueFull` right now
95+ pub fn try_send(&self, command: Command) -> Result<Pending, Error> {
96+ let (request, pending) = Request::new(command);
97+ self.requests
98+ .try_send(request)
99+ .map_err(|error| match error {
100+ mpsc::error::TrySendError::Full(_) => {
101+ Error::new(ErrorKind::QueueFull, "the command queue is full")
102+ }
103+ mpsc::error::TrySendError::Closed(_) => Error::interrupted(),
104+ })?;
105+ Ok(pending)
106+ }
107+
108+ /// queue a command and wait for its answer. timeline sends answer once the
109+ /// sdk send queue has them, delivery shows up later as the item's send state
110+ pub async fn execute(&self, command: Command) -> Result<CommandOutput, Error> {
111+ self.send(command).await?.await
112+ }
113+
114+ async fn run(&self, command: Command) -> Result<(), Error> {
115+ self.execute(command).await.map(drop)
116+ }
117+
118+ async fn run_for_room(&self, command: RoomCommand) -> Result<OwnedRoomId, Error> {
119+ match self.execute(Command::Room(command)).await? {
120+ CommandOutput::Room(room_id) => Ok(room_id),
121+ output => Err(Error::new(
122+ ErrorKind::Other,
123+ format!("expected a room but got {output:?}"),
124+ )),
125+ }
126+ }
127+
128+ pub async fn shutdown(&self) -> Result<(), Error> {
129+ self.run(Command::Lifecycle(LifecycleCommand::Shutdown))
130+ .await
131+ }
132+
133+ pub async fn logout(&self) -> Result<(), Error> {
134+ self.run(Command::Account(AccountCommand::Logout)).await
75135 }
76136
77137 pub async fn delete_devices(
78138 &self,
79- device_ids: Vec<ruma::OwnedDeviceId>,
139+ device_ids: Vec<OwnedDeviceId>,
80140 auth: Option<InteractiveAuth>,
81- ) -> Result<InteractiveAuthOutcome, CommandResultError> {
82- let (reply, result) = oneshot::channel();
83- self.commands
84- .send(DriverRequest::DeleteDevices {
85- id: self.next_command_id(),
141+ ) -> Result<InteractiveAuthOutcome, Error> {
142+ match self
143+ .run(Command::Account(AccountCommand::DeleteDevices {
86144 device_ids,
87145 auth,
88- reply,
89- })
146+ }))
90147 .await
91- .map_err(|_| SendCommandError::Closed)?;
92- result.await.map_err(|_| CommandResultError::Closed)?
148+ {
149+ Ok(()) => Ok(InteractiveAuthOutcome::Completed),
150+ Err(Error {
151+ challenge: Some(challenge),
152+ ..
153+ }) => Ok(InteractiveAuthOutcome::Challenge(*challenge)),
154+ Err(error) => Err(error),
155+ }
93156 }
94157
95158 pub async fn rename_device(
96159 &self,
97- device_id: ruma::OwnedDeviceId,
160+ device_id: OwnedDeviceId,
98161 display_name: impl Into<String>,
99- ) -> Result<CommandId, SendCommandError> {
100- self.send(Command::Account(AccountCommand::RenameDevice {
162+ ) -> Result<(), Error> {
163+ self.run(Command::Account(AccountCommand::RenameDevice {
101164 device_id,
102165 display_name: display_name.into(),
103166 }))
104167 .await
105168 }
106169
170+ pub async fn set_global_account_data(
171+ &self,
172+ event_type: GlobalAccountDataEventType,
173+ content: Raw<AnyGlobalAccountDataEventContent>,
174+ ) -> Result<(), Error> {
175+ self.run(Command::Account(AccountCommand::SetAccountData {
176+ event_type,
177+ content,
178+ }))
179+ .await
180+ }
181+
182+ pub async fn set_room_account_data(
183+ &self,
184+ room_id: OwnedRoomId,
185+ event_type: RoomAccountDataEventType,
186+ content: Raw<AnyRoomAccountDataEventContent>,
187+ ) -> Result<(), Error> {
188+ self.run(Command::Room(RoomCommand::SetAccountData {
189+ room_id,
190+ event_type,
191+ content,
192+ }))
193+ .await
194+ }
195+
107196 pub async fn set_room_notification_mode(
108197 &self,
109- room_id: ruma::OwnedRoomId,
198+ room_id: OwnedRoomId,
110199 mode: RoomNotificationMode,
111- ) -> Result<CommandId, SendCommandError> {
112- self.send(Command::Notification(NotificationCommand::SetRoomMode {
200+ ) -> Result<(), Error> {
201+ self.run(Command::Notification(NotificationCommand::SetRoomMode {
113202 room_id,
114203 mode,
115204 }))
116205 .await
117206 }
118207
119- pub async fn reset_room_notification_mode(
120- &self,
121- room_id: ruma::OwnedRoomId,
122- ) -> Result<CommandId, SendCommandError> {
123- self.send(Command::Notification(NotificationCommand::ResetRoomMode(
208+ pub async fn reset_room_notification_mode(&self, room_id: OwnedRoomId) -> Result<(), Error> {
209+ self.run(Command::Notification(NotificationCommand::ResetRoomMode(
124210 room_id,
125211 )))
126212 .await
@@ -131,8 +217,8 @@ impl ClientHandle {
131217 encrypted: bool,
132218 one_to_one: bool,
133219 mode: RoomNotificationMode,
134- ) -> Result<CommandId, SendCommandError> {
135- self.send(Command::Notification(NotificationCommand::SetDefaultMode {
220+ ) -> Result<(), Error> {
221+ self.run(Command::Notification(NotificationCommand::SetDefaultMode {
136222 encrypted,
137223 one_to_one,
138224 mode,
@@ -140,11 +226,8 @@ impl ClientHandle {
140226 .await
141227 }
142228
143- pub async fn add_notification_keyword(
144- &self,
145- keyword: impl Into<String>,
146- ) -> Result<CommandId, SendCommandError> {
147- self.send(Command::Notification(NotificationCommand::AddKeyword(
229+ pub async fn add_notification_keyword(&self, keyword: impl Into<String>) -> Result<(), Error> {
230+ self.run(Command::Notification(NotificationCommand::AddKeyword(
148231 keyword.into(),
149232 )))
150233 .await
@@ -153,35 +236,27 @@ impl ClientHandle {
153236 pub async fn remove_notification_keyword(
154237 &self,
155238 keyword: impl Into<String>,
156- ) -> Result<CommandId, SendCommandError> {
157- self.send(Command::Notification(NotificationCommand::RemoveKeyword(
239+ ) -> Result<(), Error> {
240+ self.run(Command::Notification(NotificationCommand::RemoveKeyword(
158241 keyword.into(),
159242 )))
160243 .await
161244 }
162245
163- pub async fn open_room(
164- &self,
165- room_id: ruma::OwnedRoomId,
166- ) -> Result<CommandId, SendCommandError> {
167- self.send(Command::Room(crate::RoomCommand::Open(room_id)))
168- .await
246+ pub async fn open_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {
247+ self.run(Command::Room(RoomCommand::Open(room_id))).await
169248 }
170249
171- pub async fn close_room(
172- &self,
173- room_id: ruma::OwnedRoomId,
174- ) -> Result<CommandId, SendCommandError> {
175- self.send(Command::Room(crate::RoomCommand::Close(room_id)))
176- .await
250+ pub async fn close_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {
251+ self.run(Command::Room(RoomCommand::Close(room_id))).await
177252 }
178253
179254 pub async fn open_thread(
180255 &self,
181- room_id: ruma::OwnedRoomId,
182- root_event_id: ruma::OwnedEventId,
183- ) -> Result<CommandId, SendCommandError> {
184- self.send(Command::Timeline(crate::TimelineCommand::OpenThread {
256+ room_id: OwnedRoomId,
257+ root_event_id: OwnedEventId,
258+ ) -> Result<(), Error> {
259+ self.run(Command::Timeline(TimelineCommand::OpenThread {
185260 room_id,
186261 root_event_id,
187262 }))
@@ -190,76 +265,142 @@ impl ClientHandle {
190265
191266 pub async fn close_thread(
192267 &self,
193- room_id: ruma::OwnedRoomId,
194- root_event_id: ruma::OwnedEventId,
195- ) -> Result<CommandId, SendCommandError> {
196- self.send(Command::Timeline(crate::TimelineCommand::CloseThread {
268+ room_id: OwnedRoomId,
269+ root_event_id: OwnedEventId,
270+ ) -> Result<(), Error> {
271+ self.run(Command::Timeline(TimelineCommand::CloseThread {
197272 room_id,
198273 root_event_id,
199274 }))
200275 .await
201276 }
202277
203- pub async fn join_room(
204- &self,
205- room: ruma::OwnedRoomOrAliasId,
206- via: Vec<ruma::OwnedServerName>,
207- ) -> Result<ruma::OwnedRoomId, CommandResultError> {
208- self.room_result(crate::RoomCommand::Join { room, via })
278+ pub async fn resync(&self, timeline_id: TimelineId) -> Result<(), Error> {
279+ self.run(Command::Timeline(TimelineCommand::Resync { timeline_id }))
209280 .await
210281 }
211282
212- pub async fn leave_room(
283+ pub async fn paginate(&self, timeline_id: TimelineId, limit: u16) -> Result<(), Error> {
284+ self.run(Command::Timeline(TimelineCommand::Paginate {
285+ timeline_id,
286+ limit,
287+ }))
288+ .await
289+ }
290+
291+ pub async fn send_message(
213292 &self,
214- room_id: ruma::OwnedRoomId,
215- ) -> Result<CommandId, SendCommandError> {
216- self.send(Command::Room(crate::RoomCommand::Leave(room_id)))
217- .await
293+ timeline_id: TimelineId,
294+ content: MessageContent,
295+ ) -> Result<(), Error> {
296+ self.run(Command::Timeline(TimelineCommand::Send {
297+ timeline_id,
298+ content,
299+ }))
300+ .await
218301 }
219302
220- pub async fn forget_room(
303+ pub async fn edit_message(
221304 &self,
222- room_id: ruma::OwnedRoomId,
223- ) -> Result<CommandId, SendCommandError> {
224- self.send(Command::Room(crate::RoomCommand::Forget(room_id)))
225- .await
305+ timeline_id: TimelineId,
306+ event_id: OwnedEventId,
307+ content: MessageEditContent,
308+ ) -> Result<(), Error> {
309+ self.run(Command::Timeline(TimelineCommand::Edit {
310+ timeline_id,
311+ event_id,
312+ content,
313+ }))
314+ .await
226315 }
227316
228- pub async fn accept_invite(
317+ pub async fn redact(
229318 &self,
230- room_id: ruma::OwnedRoomId,
231- ) -> Result<CommandId, SendCommandError> {
232- self.send(Command::Room(crate::RoomCommand::AcceptInvite(room_id)))
319+ timeline_id: TimelineId,
320+ event_id: OwnedEventId,
321+ reason: Option<String>,
322+ ) -> Result<(), Error> {
323+ self.run(Command::Timeline(TimelineCommand::Redact {
324+ timeline_id,
325+ event_id,
326+ reason,
327+ }))
328+ .await
329+ }
330+
331+ pub async fn toggle_reaction(
332+ &self,
333+ timeline_id: TimelineId,
334+ event_id: OwnedEventId,
335+ key: impl Into<String>,
336+ ) -> Result<(), Error> {
337+ self.run(Command::Timeline(TimelineCommand::ToggleReaction {
338+ timeline_id,
339+ event_id,
340+ key: key.into(),
341+ }))
342+ .await
343+ }
344+
345+ pub async fn mark_read(
346+ &self,
347+ timeline_id: TimelineId,
348+ event_id: OwnedEventId,
349+ ) -> Result<(), Error> {
350+ self.run(Command::Timeline(TimelineCommand::MarkRead {
351+ timeline_id,
352+ event_id,
353+ }))
354+ .await
355+ }
356+
357+ pub async fn set_typing(&self, room_id: OwnedRoomId, typing: bool) -> Result<(), Error> {
358+ self.run(Command::Room(RoomCommand::SetTyping { room_id, typing }))
233359 .await
234360 }
235361
236- pub async fn reject_invite(
362+ pub async fn join_room(
237363 &self,
238- room_id: ruma::OwnedRoomId,
239- ) -> Result<CommandId, SendCommandError> {
240- self.send(Command::Room(crate::RoomCommand::RejectInvite(room_id)))
364+ room: OwnedRoomOrAliasId,
365+ via: Vec<OwnedServerName>,
366+ ) -> Result<OwnedRoomId, Error> {
367+ self.run_for_room(RoomCommand::Join { room, via }).await
368+ }
369+
370+ pub async fn leave_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {
371+ self.run(Command::Room(RoomCommand::Leave(room_id))).await
372+ }
373+
374+ pub async fn forget_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {
375+ self.run(Command::Room(RoomCommand::Forget(room_id))).await
376+ }
377+
378+ pub async fn accept_invite(&self, room_id: OwnedRoomId) -> Result<(), Error> {
379+ self.run(Command::Room(RoomCommand::AcceptInvite(room_id)))
380+ .await
381+ }
382+
383+ pub async fn reject_invite(&self, room_id: OwnedRoomId) -> Result<(), Error> {
384+ self.run(Command::Room(RoomCommand::RejectInvite(room_id)))
241385 .await
242386 }
243387
244388 pub async fn invite_user(
245389 &self,
246- room_id: ruma::OwnedRoomId,
247- user_id: ruma::OwnedUserId,
248- ) -> Result<CommandId, SendCommandError> {
249- self.send(Command::Room(crate::RoomCommand::InviteUser {
250- room_id,
251- user_id,
252- }))
253- .await
390+ room_id: OwnedRoomId,
391+ user_id: OwnedUserId,
392+ ) -> Result<(), Error> {
393+ self.run(Command::Room(RoomCommand::InviteUser { room_id, user_id }))
394+ .await
254395 }
255396
256397 pub async fn kick_user(
257398 &self,
258- room_id: ruma::OwnedRoomId,
259- user_id: ruma::OwnedUserId,
399+ room_id: OwnedRoomId,
400+ user_id: OwnedUserId,
260401 reason: Option<String>,
261- ) -> Result<CommandId, SendCommandError> {
262- self.send(Command::Room(crate::RoomCommand::KickUser {
402+ ) -> Result<(), Error> {
403+ self.run(Command::Room(RoomCommand::KickUser {
263404 room_id,
264405 user_id,
265406 reason,
@@ -269,11 +410,11 @@ impl ClientHandle {
269410
270411 pub async fn ban_user(
271412 &self,
272- room_id: ruma::OwnedRoomId,
273- user_id: ruma::OwnedUserId,
413+ room_id: OwnedRoomId,
414+ user_id: OwnedUserId,
274415 reason: Option<String>,
275- ) -> Result<CommandId, SendCommandError> {
276- self.send(Command::Room(crate::RoomCommand::BanUser {
416+ ) -> Result<(), Error> {
417+ self.run(Command::Room(RoomCommand::BanUser {
277418 room_id,
278419 user_id,
279420 reason,
@@ -283,11 +424,11 @@ impl ClientHandle {
283424
284425 pub async fn unban_user(
285426 &self,
286- room_id: ruma::OwnedRoomId,
287- user_id: ruma::OwnedUserId,
427+ room_id: OwnedRoomId,
428+ user_id: OwnedUserId,
288429 reason: Option<String>,
289- ) -> Result<CommandId, SendCommandError> {
290- self.send(Command::Room(crate::RoomCommand::UnbanUser {
430+ ) -> Result<(), Error> {
431+ self.run(Command::Room(RoomCommand::UnbanUser {
291432 room_id,
292433 user_id,
293434 reason,
@@ -297,10 +438,10 @@ impl ClientHandle {
297438
298439 pub async fn set_power_levels(
299440 &self,
300- room_id: ruma::OwnedRoomId,
441+ room_id: OwnedRoomId,
301442 changes: PowerLevelChanges,
302- ) -> Result<CommandId, SendCommandError> {
303- self.send(Command::Room(crate::RoomCommand::SetPowerLevels {
443+ ) -> Result<(), Error> {
444+ self.run(Command::Room(RoomCommand::SetPowerLevels {
304445 room_id,
305446 changes,
306447 }))
@@ -309,11 +450,11 @@ impl ClientHandle {
309450
310451 pub async fn set_user_power_level(
311452 &self,
312- room_id: ruma::OwnedRoomId,
313- user_id: ruma::OwnedUserId,
453+ room_id: OwnedRoomId,
454+ user_id: OwnedUserId,
314455 level: i64,
315- ) -> Result<CommandId, SendCommandError> {
316- self.send(Command::Room(crate::RoomCommand::SetUserPowerLevel {
456+ ) -> Result<(), Error> {
457+ self.run(Command::Room(RoomCommand::SetUserPowerLevel {
317458 room_id,
318459 user_id,
319460 level,
@@ -321,43 +462,21 @@ impl ClientHandle {
321462 .await
322463 }
323464
324- pub async fn create_room(
325- &self,
326- options: crate::CreateRoomOptions,
327- ) -> Result<ruma::OwnedRoomId, CommandResultError> {
328- self.room_result(crate::RoomCommand::Create(options)).await
329- }
330-
331- pub async fn create_direct(
332- &self,
333- user_id: ruma::OwnedUserId,
334- ) -> Result<ruma::OwnedRoomId, CommandResultError> {
335- self.room_result(crate::RoomCommand::CreateDirect(user_id))
336- .await
465+ pub async fn create_room(&self, options: CreateRoomOptions) -> Result<OwnedRoomId, Error> {
466+ self.run_for_room(RoomCommand::Create(options)).await
337467 }
338468
339- async fn room_result(
340- &self,
341- command: crate::RoomCommand,
342- ) -> Result<ruma::OwnedRoomId, CommandResultError> {
343- let (reply, result) = oneshot::channel();
344- self.commands
345- .send(DriverRequest::RoomResult {
346- id: self.next_command_id(),
347- command,
348- reply,
349- })
350- .await
351- .map_err(|_| SendCommandError::Closed)?;
352- result.await.map_err(|_| CommandResultError::Closed)?
469+ /// hands back the existing dm with them if there is one
470+ pub async fn create_direct(&self, user_id: OwnedUserId) -> Result<OwnedRoomId, Error> {
471+ self.run_for_room(RoomCommand::CreateDirect(user_id)).await
353472 }
354473
355474 pub async fn cancel_send(
356475 &self,
357- room_id: ruma::OwnedRoomId,
358- transaction_id: ruma::OwnedTransactionId,
359- ) -> Result<CommandId, SendCommandError> {
360- self.send(Command::Room(crate::RoomCommand::CancelSend {
476+ room_id: OwnedRoomId,
477+ transaction_id: OwnedTransactionId,
478+ ) -> Result<(), Error> {
479+ self.run(Command::Room(RoomCommand::CancelSend {
361480 room_id,
362481 transaction_id,
363482 }))
@@ -366,134 +485,152 @@ impl ClientHandle {
366485
367486 pub async fn retry_send(
368487 &self,
369- room_id: ruma::OwnedRoomId,
370- transaction_id: ruma::OwnedTransactionId,
371- ) -> Result<CommandId, SendCommandError> {
372- self.send(Command::Room(crate::RoomCommand::RetrySend {
488+ room_id: OwnedRoomId,
489+ transaction_id: OwnedTransactionId,
490+ ) -> Result<(), Error> {
491+ self.run(Command::Room(RoomCommand::RetrySend {
373492 room_id,
374493 transaction_id,
375494 }))
376495 .await
377496 }
378497
379- pub async fn upload_media(
380- &self,
381- timeline_id: crate::TimelineId,
382- filename: String,
383- mimetype: String,
384- bytes: Vec<u8>,
385- caption: Option<String>,
386- ) -> Result<CommandId, SendCommandError> {
387- self.send(Command::Media(crate::MediaCommand::Upload {
388- timeline_id,
389- filename,
390- mimetype,
391- bytes,
392- caption,
393- }))
394- .await
498+ pub async fn upload(&self, upload: AttachmentUpload) -> Result<(), Error> {
499+ self.run(Command::Media(MediaCommand::Upload(upload))).await
395500 }
396501
397- pub async fn upload_attachment(
398- &self,
399- upload: crate::AttachmentUpload,
400- ) -> Result<CommandId, SendCommandError> {
401- self.send(Command::Media(crate::MediaCommand::UploadAttachment(
402- upload,
502+ pub async fn recover(&self, recovery_key: impl Into<String>) -> Result<(), Error> {
503+ self.run(Command::Encryption(EncryptionCommand::Recover(
504+ recovery_key.into(),
403505 )))
404506 .await
405507 }
406508
407- pub async fn create_key_backup(&self) -> Result<CommandId, SendCommandError> {
408- self.send(Command::Encryption(
409- crate::EncryptionCommand::CreateKeyBackup,
410- ))
411- .await
509+ pub async fn create_key_backup(&self) -> Result<(), Error> {
510+ self.run(Command::Encryption(EncryptionCommand::CreateKeyBackup))
511+ .await
412512 }
413513
414- pub async fn enable_key_backup(&self) -> Result<CommandId, SendCommandError> {
415- self.send(Command::Encryption(
416- crate::EncryptionCommand::EnableKeyBackup,
417- ))
418- .await
514+ pub async fn enable_key_backup(&self) -> Result<(), Error> {
515+ self.run(Command::Encryption(EncryptionCommand::EnableKeyBackup))
516+ .await
419517 }
420518
421- pub async fn disable_key_backup(
422- &self,
423- delete_from_server: bool,
424- ) -> Result<CommandId, SendCommandError> {
425- self.send(Command::Encryption(
426- crate::EncryptionCommand::DisableKeyBackup { delete_from_server },
427- ))
519+ pub async fn disable_key_backup(&self, delete_from_server: bool) -> Result<(), Error> {
520+ self.run(Command::Encryption(EncryptionCommand::DisableKeyBackup {
521+ delete_from_server,
522+ }))
428523 .await
429524 }
430525
431- pub async fn recover_key_backup(
432- &self,
433- recovery_key: impl Into<String>,
434- ) -> Result<CommandId, SendCommandError> {
435- self.send(Command::Encryption(
436- crate::EncryptionCommand::RecoverKeyBackup(recovery_key.into()),
437- ))
526+ pub async fn recover_key_backup(&self, recovery_key: impl Into<String>) -> Result<(), Error> {
527+ self.run(Command::Encryption(EncryptionCommand::RecoverKeyBackup(
528+ recovery_key.into(),
529+ )))
438530 .await
439531 }
440532
441- pub async fn start_verification(
442- &self,
443- target: crate::VerificationTarget,
444- ) -> Result<CommandId, SendCommandError> {
445- self.send(Command::Encryption(
446- crate::EncryptionCommand::StartVerification(target),
447- ))
533+ pub async fn start_verification(&self, target: VerificationTarget) -> Result<(), Error> {
534+ self.run(Command::Encryption(EncryptionCommand::StartVerification(
535+ target,
536+ )))
448537 .await
449538 }
450539
451- pub fn try_send(&self, command: Command) -> Result<CommandId, SendCommandError> {
452- let id = self.next_command_id();
453- self.commands
454- .try_send(DriverRequest::Command(CommandEnvelope { id, command }))
455- .map_err(|error| match error {
456- mpsc::error::TrySendError::Full(_) => SendCommandError::Full,
457- mpsc::error::TrySendError::Closed(_) => SendCommandError::Closed,
458- })?;
459- Ok(id)
540+ pub async fn verify(&self, action: VerificationAction) -> Result<(), Error> {
541+ self.run(Command::Encryption(EncryptionCommand::Verify(action)))
542+ .await
460543 }
461544
462- fn next_command_id(&self) -> CommandId {
463- CommandId::new(self.next_id.fetch_add(1, Ordering::Relaxed))
545+ /// bytes for media a retained timeline showed you. concurrent asks share one download
546+ pub async fn media(&self, media_id: &MediaId, size: MediaSize) -> Result<Media, Error> {
547+ self.media.timeline_media(media_id, size).await
548+ }
549+
550+ /// bytes for any plain mxc uri, avatars mostly. same byte cap as everything else
551+ pub async fn mxc_media(&self, uri: &MxcUri, size: MediaSize) -> Result<Media, Error> {
552+ self.media.mxc_media(uri, size).await
553+ }
554+
555+ /// the space graph builds on first use and then keeps itself current
556+ async fn spaces(&self) -> &SpaceService {
557+ self.spaces
558+ .get_or_init(|| SpaceService::new(self.client.clone()))
559+ .await
464560 }
465561
466562 pub async fn rooms(&self) -> Vec<RoomSummary> {
467- room_summaries(&self.client).await
563+ room_summaries(&self.client, self.spaces().await).await
468564 }
469565
470- pub async fn rooms_filtered(&self, filter: &RoomListFilter) -> Vec<RoomSummary> {
471- let mut room_filter = filter.clone();
472- let search = room_filter
473- .search
474- .take()
475- .map(|search| search.to_lowercase());
566+ /// browse a space the way the server sees it, rooms you haven't joined included
567+ pub async fn space_hierarchy(&self, space_id: OwnedRoomId) -> SpaceHierarchy {
568+ SpaceHierarchy(SpaceRoomList::new(self.client.clone(), space_id).await)
569+ }
570+
571+ pub async fn own_profile(&self) -> Result<Profile, Error> {
572+ let user_id = self
573+ .client
574+ .user_id()
575+ .ok_or_else(|| Error::new(ErrorKind::Unauthenticated, "the client is not logged in"))?
576+ .to_owned();
577+ let response = self
578+ .client
579+ .account()
580+ .fetch_user_profile()
581+ .await
582+ .map_err(|source| Error::from_sdk("fetching the profile failed", source))?;
583+ profile(user_id, &response)
584+ }
585+
586+ pub async fn user_profile(&self, user_id: &UserId) -> Result<Profile, Error> {
587+ let response = self
588+ .client
589+ .account()
590+ .fetch_user_profile_of(user_id)
591+ .await
592+ .map_err(|source| Error::from_sdk("fetching the profile failed", source))?;
593+ profile(user_id.to_owned(), &response)
594+ }
595+
596+ pub async fn set_display_name(&self, name: Option<String>) -> Result<(), Error> {
597+ self.run(Command::Account(AccountCommand::SetDisplayName(name)))
598+ .await
599+ }
600+
601+ pub async fn set_avatar(&self, avatar: Option<OwnedMxcUri>) -> Result<(), Error> {
602+ self.run(Command::Account(AccountCommand::SetAvatar(avatar)))
603+ .await
604+ }
605+
606+ pub async fn upload_avatar(
607+ &self,
608+ mimetype: impl Into<String>,
609+ bytes: Vec<u8>,
610+ ) -> Result<(), Error> {
611+ self.run(Command::Account(AccountCommand::UploadAvatar {
612+ mimetype: mimetype.into(),
613+ bytes,
614+ }))
615+ .await
616+ }
476617
618+ pub async fn rooms_filtered(&self, filter: &RoomListFilter) -> Vec<RoomSummary> {
619+ let matcher = filter.matcher();
477620 self.rooms()
478621 .await
479622 .into_iter()
480- .filter(|room| {
481- room_filter.matches(room)
482- && search.as_ref().is_none_or(|search| {
483- room.name.to_lowercase().contains(search)
484- || room.room_id.as_str().to_lowercase().contains(search)
485- })
486- })
623+ .filter(|room| matcher.matches(room))
487624 .collect()
488625 }
489626
490- pub async fn devices(&self) -> Result<Vec<OwnDevice>, ClientQueryError> {
627+ pub async fn devices(&self) -> Result<Vec<OwnDevice>, Error> {
491628 let current_device_id = self.client.device_id();
492629 Ok(self
493630 .client
494631 .devices()
495632 .await
496- .map_err(|source| ClientQueryError::Sdk(source.into()))?
633+ .map_err(|source| Error::from_http("listing devices failed", source))?
497634 .devices
498635 .into_iter()
499636 .map(|device| OwnDevice {
@@ -506,14 +643,17 @@ impl ClientHandle {
506643 .collect())
507644 }
508645
646+ fn room(&self, room_id: &RoomId) -> Result<matrix_sdk::Room, Error> {
647+ self.client
648+ .get_room(room_id)
649+ .ok_or_else(Error::room_unknown)
650+ }
651+
509652 pub async fn room_notification_settings(
510653 &self,
511654 room_id: &RoomId,
512- ) -> Result<Option<RoomNotificationSettings>, ClientQueryError> {
513- let room = self
514- .client
515- .get_room(room_id)
516- .ok_or_else(|| ClientQueryError::RoomUnknown(room_id.to_owned()))?;
655+ ) -> Result<Option<RoomNotificationSettings>, Error> {
656+ let room = self.room(room_id)?;
517657 let custom = room.user_defined_notification_mode().await;
518658 let mode = match custom {
519659 Some(mode) => mode,
@@ -542,7 +682,14 @@ impl ClientHandle {
542682 &self,
543683 room_id: &RoomId,
544684 event_id: &EventId,
545- ) -> Result<PushNotificationStatus, ClientQueryError> {
685+ ) -> Result<PushNotificationStatus, Error> {
686+ let notification_failed = |source| {
687+ Error::with_source(
688+ ErrorKind::Other,
689+ "resolving the push notification failed",
690+ source,
691+ )
692+ };
546693 let client = self
547694 .notification_client
548695 .get_or_try_init(|| async {
@@ -555,11 +702,11 @@ impl ClientHandle {
555702 .await
556703 })
557704 .await
558- .map_err(ClientQueryError::Notification)?;
705+ .map_err(notification_failed)?;
559706 let status = client
560707 .get_notification(room_id, event_id)
561708 .await
562- .map_err(ClientQueryError::Notification)?;
709+ .map_err(notification_failed)?;
563710 Ok(normalise_push_notification(room_id, event_id, status))
564711 }
565712
@@ -567,11 +714,7 @@ impl ClientHandle {
567714 &self,
568715 room_id: &RoomId,
569716 filter: MemberFilter,
570- ) -> Result<Vec<MemberSummary>, ClientQueryError> {
571- let room = self
572- .client
573- .get_room(room_id)
574- .ok_or_else(|| ClientQueryError::RoomUnknown(room_id.to_owned()))?;
717+ ) -> Result<Vec<MemberSummary>, Error> {
575718 let memberships = match filter {
576719 MemberFilter::Active => RoomMemberships::ACTIVE,
577720 MemberFilter::Joined => RoomMemberships::JOIN,
@@ -580,27 +723,22 @@ impl ClientHandle {
580723 // membership, including custom states that `all()` would exclude.
581724 MemberFilter::All => RoomMemberships::empty(),
582725 };
583- Ok(room
726+ Ok(self
727+ .room(room_id)?
584728 .members(memberships)
585729 .await
586- .map_err(ClientQueryError::Sdk)?
730+ .map_err(|source| Error::from_sdk("listing members failed", source))?
587731 .into_iter()
588732 .map(member_summary)
589733 .collect())
590734 }
591735
592- pub async fn room_power_levels(
593- &self,
594- room_id: &RoomId,
595- ) -> Result<RoomPowerLevels, ClientQueryError> {
596- let room = self
597- .client
598- .get_room(room_id)
599- .ok_or_else(|| ClientQueryError::RoomUnknown(room_id.to_owned()))?;
600- let levels = room
736+ pub async fn room_power_levels(&self, room_id: &RoomId) -> Result<RoomPowerLevels, Error> {
737+ let levels = self
738+ .room(room_id)?
601739 .power_levels()
602740 .await
603- .map_err(|source| ClientQueryError::Sdk(source.into()))?;
741+ .map_err(|source| Error::from_sdk("reading power levels failed", source.into()))?;
604742 Ok(RoomPowerLevels {
605743 ban: levels.ban.into(),
606744 invite: levels.invite.into(),
@@ -626,16 +764,15 @@ impl ClientHandle {
626764 pub async fn pending_send_ids(
627765 &self,
628766 room_id: &RoomId,
629- ) -> Result<Vec<ruma::OwnedTransactionId>, ClientQueryError> {
630- let room = self
631- .client
632- .get_room(room_id)
633- .ok_or_else(|| ClientQueryError::RoomUnknown(room_id.to_owned()))?;
634- let (echoes, _) = room
767+ ) -> Result<Vec<OwnedTransactionId>, Error> {
768+ let (echoes, _) = self
769+ .room(room_id)?
635770 .send_queue()
636771 .subscribe()
637772 .await
638- .map_err(ClientQueryError::SendQueue)?;
773+ .map_err(|source| {
774+ Error::with_source(ErrorKind::Store, "reading the send queue failed", source)
775+ })?;
639776 Ok(echoes
640777 .into_iter()
641778 .filter(|echo| {
@@ -651,62 +788,23 @@ impl ClientHandle {
651788 pub async fn global_account_data(
652789 &self,
653790 event_type: GlobalAccountDataEventType,
654- ) -> Result<Option<Raw<AnyGlobalAccountDataEventContent>>, ClientQueryError> {
791+ ) -> Result<Option<Raw<AnyGlobalAccountDataEventContent>>, Error> {
655792 self.client
656793 .account()
657794 .account_data_raw(event_type)
658795 .await
659- .map_err(ClientQueryError::Sdk)
660- }
661-
662- pub async fn set_global_account_data(
663- &self,
664- event_type: GlobalAccountDataEventType,
665- content: Raw<AnyGlobalAccountDataEventContent>,
666- ) -> Result<(), ClientQueryError> {
667- let (reply, result) = oneshot::channel();
668- self.commands
669- .send(DriverRequest::SetGlobalAccountData {
670- event_type,
671- content,
672- reply,
673- })
674- .await
675- .map_err(|_| ClientQueryError::EngineStopped)?;
676- result.await.map_err(|_| ClientQueryError::EngineStopped)?
796+ .map_err(|source| Error::from_sdk("reading account data failed", source))
677797 }
678798
679799 pub async fn room_account_data(
680800 &self,
681801 room_id: &RoomId,
682802 event_type: RoomAccountDataEventType,
683- ) -> Result<Option<Raw<AnyRoomAccountDataEvent>>, ClientQueryError> {
684- let room = self
685- .client
686- .get_room(room_id)
687- .ok_or_else(|| ClientQueryError::RoomUnknown(room_id.to_owned()))?;
688- room.account_data(event_type)
689- .await
690- .map_err(ClientQueryError::Sdk)
691- }
692-
693- pub async fn set_room_account_data(
694- &self,
695- room_id: &RoomId,
696- event_type: RoomAccountDataEventType,
697- content: Raw<ruma::events::AnyRoomAccountDataEventContent>,
698- ) -> Result<(), ClientQueryError> {
699- let (reply, result) = oneshot::channel();
700- self.commands
701- .send(DriverRequest::SetRoomAccountData {
702- room_id: room_id.to_owned(),
703- event_type,
704- content,
705- reply,
706- })
803+ ) -> Result<Option<Raw<AnyRoomAccountDataEvent>>, Error> {
804+ self.room(room_id)?
805+ .account_data(event_type)
707806 .await
708- .map_err(|_| ClientQueryError::EngineStopped)?;
709- result.await.map_err(|_| ClientQueryError::EngineStopped)?
807+ .map_err(|source| Error::from_sdk("reading room account data failed", source))
710808 }
711809
712810 #[must_use]
@@ -727,37 +825,96 @@ impl ClientHandle {
727825 }
728826 }
729827
730-#[derive(Debug, Eq, PartialEq)]
731-pub(crate) struct CommandEnvelope {
732- pub(crate) id: CommandId,
828+/// one space's hierarchy, a page at a time. hold on to it while you browse, it remembers where it got to
829+pub struct SpaceHierarchy(SpaceRoomList);
830+
831+impl fmt::Debug for SpaceHierarchy {
832+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
833+ formatter
834+ .debug_struct("SpaceHierarchy")
835+ .finish_non_exhaustive()
836+ }
837+}
838+
839+impl SpaceHierarchy {
840+ /// fetches the next page. a no-op once the end is reached or while a page is in flight
841+ pub async fn paginate(&self) -> Result<(), Error> {
842+ self.0.paginate().await.map_err(|source| {
843+ Error::with_source(
844+ ErrorKind::Server,
845+ "fetching the space hierarchy failed",
846+ source,
847+ )
848+ })
849+ }
850+
851+ /// everything fetched so far, the space itself excluded
852+ pub async fn rooms(&self) -> Vec<SpaceChild> {
853+ self.0.rooms().await.into_iter().map(space_child).collect()
854+ }
855+
856+ #[must_use]
857+ pub fn end_reached(&self) -> bool {
858+ matches!(
859+ self.0.pagination_state(),
860+ SpaceRoomListPaginationState::Idle { end_reached: true }
861+ )
862+ }
863+}
864+
865+fn profile(
866+ user_id: OwnedUserId,
867+ response: &ruma::api::client::profile::get_profile::v3::Response,
868+) -> Result<Profile, Error> {
869+ use ruma::api::client::profile::{AvatarUrl, DisplayName};
870+ let malformed = |source| {
871+ Error::with_source(
872+ ErrorKind::Server,
873+ "the server sent a malformed profile",
874+ source,
875+ )
876+ };
877+ Ok(Profile {
878+ user_id,
879+ display_name: response.get_static::<DisplayName>().map_err(malformed)?,
880+ avatar: response.get_static::<AvatarUrl>().map_err(malformed)?,
881+ })
882+}
883+
884+#[derive(Debug)]
885+pub(crate) struct Request {
733886 pub(crate) command: Command,
887+ pub(crate) reply: Responder,
888+}
889+
890+impl Request {
891+ fn new(command: Command) -> (Self, Pending) {
892+ let (reply, pending) = oneshot::channel();
893+ (
894+ Self {
895+ command,
896+ reply: Responder(reply),
897+ },
898+ Pending(pending),
899+ )
900+ }
734901 }
735902
903+/// whoever holds this owes the caller an answer. dropping it answers `Interrupted`
736904 #[derive(Debug)]
737-pub(crate) enum DriverRequest {
738- Command(CommandEnvelope),
739- DeleteDevices {
740- id: CommandId,
741- device_ids: Vec<ruma::OwnedDeviceId>,
742- auth: Option<InteractiveAuth>,
743- reply: oneshot::Sender<Result<InteractiveAuthOutcome, CommandResultError>>,
744- },
745- RoomResult {
746- id: CommandId,
747- command: crate::RoomCommand,
748- reply: oneshot::Sender<Result<ruma::OwnedRoomId, CommandResultError>>,
749- },
750- SetGlobalAccountData {
751- event_type: GlobalAccountDataEventType,
752- content: Raw<AnyGlobalAccountDataEventContent>,
753- reply: oneshot::Sender<Result<(), ClientQueryError>>,
754- },
755- SetRoomAccountData {
756- room_id: ruma::OwnedRoomId,
757- event_type: RoomAccountDataEventType,
758- content: Raw<ruma::events::AnyRoomAccountDataEventContent>,
759- reply: oneshot::Sender<Result<(), ClientQueryError>>,
760- },
905+pub(crate) struct Responder(oneshot::Sender<Result<CommandOutput, Error>>);
906+
907+impl Responder {
908+ pub(crate) fn send(self, result: Result<CommandOutput, Error>) {
909+ if let Err(error) = &result {
910+ tracing::debug!(%error, kind = ?error.kind, "command failed");
911+ }
912+ let _ = self.0.send(result);
913+ }
914+
915+ pub(crate) fn done(self, result: Result<(), Error>) {
916+ self.send(result.map(|()| CommandOutput::Done));
917+ }
761918 }
762919
763920 #[derive(Debug)]
@@ -785,7 +942,7 @@ impl futures_util::Stream for EventStream {
785942
786943 #[derive(Debug)]
787944 pub(crate) struct DriverPort {
788- commands: mpsc::Receiver<DriverRequest>,
945+ requests: mpsc::Receiver<Request>,
789946 events: mpsc::Sender<Event>,
790947 connection: watch::Sender<ConnectionState>,
791948 }
@@ -795,11 +952,10 @@ pub(crate) struct EventSender(mpsc::Sender<Event>);
795952
796953 impl EventSender {
797954 pub(crate) async fn send(&self, event: Event) -> Result<(), SendEventError> {
798- self.0.send(event).await.map_err(|_| SendEventError::Closed)
955+ self.0.send(event).await.map_err(|_| SendEventError)
799956 }
800957
801- /// Resolves once every [`EventStream`] receiver has been dropped, so the
802- /// engine can notice a lost event consumer inside a `select!`.
958+ /// resolves once every `EventStream` is gone
803959 pub(crate) async fn closed(&self) {
804960 self.0.closed().await;
805961 }
@@ -814,74 +970,42 @@ impl DriverPort {
814970 self.connection.clone()
815971 }
816972
817- pub async fn recv(&mut self) -> Option<DriverRequest> {
818- self.commands.recv().await
973+ pub(crate) async fn recv(&mut self) -> Option<Request> {
974+ self.requests.recv().await
819975 }
820976
821- pub async fn send(&self, event: Event) -> Result<(), SendEventError> {
822- self.event_sender().send(event).await
977+ pub(crate) fn close(&mut self) {
978+ self.requests.close();
823979 }
824980 }
825981
826-#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
827-pub enum SendCommandError {
828- #[error("the command channel is full")]
829- Full,
830- #[error("the engine has stopped")]
831- Closed,
832-}
833-
834-#[derive(Debug, Error)]
835-pub enum CommandResultError {
836- #[error(transparent)]
837- Send(#[from] SendCommandError),
838- #[error("the engine stopped before the operation completed")]
839- Closed,
840- #[error("{}", .0.message)]
841- Failed(crate::CommandFailure),
842-}
843-
844-#[derive(Debug, Error)]
845-pub enum ClientQueryError {
846- #[error("room {0} is not available in the local store")]
847- RoomUnknown(ruma::OwnedRoomId),
848- #[error("Matrix query failed")]
849- Sdk(#[source] matrix_sdk::Error),
850- #[error("reading the send queue failed")]
851- SendQueue(#[source] matrix_sdk::send_queue::RoomSendQueueError),
852- #[error("resolving the push notification failed")]
853- Notification(#[source] matrix_sdk_ui::notification_client::Error),
854- #[error("the engine stopped before the operation completed")]
855- EngineStopped,
856-}
857-
858-#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
859-pub(crate) enum SendEventError {
860- #[error("the event receiver has closed")]
861- Closed,
862-}
982+#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
983+#[error("the event receiver has closed")]
984+pub(crate) struct SendEventError;
863985
864986 #[must_use]
865987 pub(crate) fn transport_pair(
866988 client: Client,
867989 sync_service: Arc<SyncService>,
990+ media: Arc<MediaService>,
868991 command_capacity: usize,
869992 event_capacity: usize,
870993 ) -> (ClientHandle, DriverPort, EventStream) {
871- let (command_tx, command_rx) = mpsc::channel(command_capacity);
994+ let (request_tx, request_rx) = mpsc::channel(command_capacity);
872995 let (event_tx, event_rx) = mpsc::channel(event_capacity);
873996 let (connection_tx, connection_rx) = watch::channel(ConnectionState::Idle);
874997 (
875998 ClientHandle {
876- commands: command_tx,
877- next_id: Arc::new(AtomicU64::new(1)),
999+ requests: request_tx,
8781000 client,
8791001 connection: connection_rx,
8801002 notification_client: Arc::new(OnceCell::new()),
8811003 sync_service,
1004+ media,
1005+ spaces: Arc::new(OnceCell::new()),
8821006 },
8831007 DriverPort {
884- commands: command_rx,
1008+ requests: request_rx,
8851009 events: event_tx,
8861010 connection: connection_tx,
8871011 },
@@ -941,120 +1065,3 @@ fn normalise_push_notification(
9411065 NotificationStatus::EventRedacted => PushNotificationStatus::Redacted,
9421066 }
9431067 }
944-
945-#[cfg(test)]
946-mod tests {
947- use matrix_sdk::{Client, SessionMeta, SessionTokens, authentication::matrix::MatrixSession};
948-
949- use crate::{ConnectionState, LifecycleCommand};
950-
951- use super::*;
952-
953- async fn sync_service(client: Client) -> Arc<SyncService> {
954- Arc::new(SyncService::builder(client).build().await.unwrap())
955- }
956-
957- async fn test_client() -> Client {
958- let client = Client::builder()
959- .homeserver_url("http://localhost")
960- .build()
961- .await
962- .unwrap();
963- client
964- .restore_session(MatrixSession {
965- meta: SessionMeta {
966- user_id: "@test:localhost".parse().unwrap(),
967- device_id: "DEVICE".into(),
968- },
969- tokens: SessionTokens {
970- access_token: "access_token".to_owned(),
971- refresh_token: None,
972- },
973- })
974- .await
975- .unwrap();
976- client
977- }
978-
979- #[tokio::test]
980- async fn transports_are_isolated() {
981- let first_client = test_client().await;
982- let second_client = test_client().await;
983- let (first_handle, mut first_driver, mut first_events) =
984- transport_pair(first_client.clone(), sync_service(first_client).await, 2, 2);
985- let (_second_handle, mut second_driver, mut second_events) = transport_pair(
986- second_client.clone(),
987- sync_service(second_client).await,
988- 2,
989- 2,
990- );
991-
992- let id = first_handle
993- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
994- .await
995- .unwrap();
996- let request = first_driver.recv().await.unwrap();
997- assert!(matches!(
998- request,
999- DriverRequest::Command(CommandEnvelope {
1000- id: received_id,
1001- command: Command::Lifecycle(LifecycleCommand::Shutdown),
1002- }) if received_id == id
1003- ));
1004- assert!(second_driver.commands.try_recv().is_err());
1005-
1006- first_driver
1007- .send(Event::ConnectionChanged(ConnectionState::Online))
1008- .await
1009- .unwrap();
1010- assert_eq!(
1011- first_events.recv().await,
1012- Some(Event::ConnectionChanged(ConnectionState::Online))
1013- );
1014- assert!(second_events.try_recv().is_err());
1015- }
1016-
1017- #[tokio::test]
1018- async fn bounded_command_channel_reports_pressure() {
1019- let client = test_client().await;
1020- let (handle, _driver, _events) =
1021- transport_pair(client.clone(), sync_service(client).await, 1, 1);
1022-
1023- handle
1024- .try_send(Command::Lifecycle(LifecycleCommand::Shutdown))
1025- .unwrap();
1026- assert_eq!(
1027- handle.try_send(Command::Lifecycle(LifecycleCommand::Shutdown)),
1028- Err(SendCommandError::Full)
1029- );
1030- }
1031-
1032- #[tokio::test]
1033- async fn cloned_handles_share_command_id_sequence() {
1034- let client = test_client().await;
1035- let (handle, mut driver, _events) =
1036- transport_pair(client.clone(), sync_service(client).await, 1, 1);
1037- let cloned = handle.clone();
1038- let first = handle
1039- .try_send(Command::Lifecycle(LifecycleCommand::Shutdown))
1040- .unwrap();
1041- assert_eq!(first.get(), 1);
1042- assert_eq!(
1043- cloned.try_send(Command::Lifecycle(LifecycleCommand::Shutdown)),
1044- Err(SendCommandError::Full)
1045- );
1046- driver.recv().await.unwrap();
1047-
1048- let third = cloned
1049- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
1050- .await
1051- .unwrap();
1052- assert_eq!(third.get(), 3);
1053- driver.recv().await.unwrap();
1054-
1055- let fourth = handle
1056- .try_send(Command::Lifecycle(LifecycleCommand::Shutdown))
1057- .unwrap();
1058- assert_eq!(fourth.get(), 4);
1059- }
1060-}
Modified file: src/upload.rs+23 additions−22 deletions
@@ -8,26 +8,26 @@ use matrix_sdk::{
88 ruma::events::room::message::TextMessageEventContent,
99 };
1010
11-use crate::{AttachmentUpload, CommandErrorKind, UploadDetails, error::CommandError};
11+use crate::{AttachmentUpload, Error, ErrorKind, UploadDetails};
1212
1313 pub(crate) fn prepare_upload(
1414 mut upload: AttachmentUpload,
1515 max_media_bytes: usize,
16-) -> Result<(AttachmentUpload, mime::Mime, AttachmentConfig), CommandError> {
16+) -> Result<(AttachmentUpload, mime::Mime, AttachmentConfig), Error> {
1717 if upload.bytes.len() > max_media_bytes {
18- return Err(CommandError::new(
19- CommandErrorKind::MediaTooLarge,
18+ return Err(Error::new(
19+ ErrorKind::MediaTooLarge,
2020 "attachment exceeds the configured byte limit",
2121 ));
2222 }
2323 let mimetype: mime::Mime = upload
2424 .mimetype
2525 .parse()
26- .map_err(|_| CommandError::new(CommandErrorKind::Other, "media MIME type is invalid"))?;
26+ .map_err(|_| Error::new(ErrorKind::Invalid, "media MIME type is invalid"))?;
2727
2828 let size = ruma::UInt::try_from(upload.bytes.len()).map_err(|_| {
29- CommandError::new(
30- CommandErrorKind::MediaTooLarge,
29+ Error::new(
30+ ErrorKind::MediaTooLarge,
3131 "attachment size is not representable",
3232 )
3333 })?;
@@ -67,8 +67,8 @@ pub(crate) fn prepare_upload(
6767 waveform,
6868 }) if mimetype.type_() == mime::AUDIO => {
6969 if waveform.iter().any(|sample| *sample > 1024) {
70- return Err(CommandError::new(
71- CommandErrorKind::Other,
70+ return Err(Error::new(
71+ ErrorKind::Invalid,
7272 "voice waveform samples must be at most 1024",
7373 ));
7474 }
@@ -106,15 +106,15 @@ pub(crate) fn prepare_upload(
106106 _ => AttachmentInfo::File(BaseFileInfo { size: Some(size) }),
107107 },
108108 _ => {
109- return Err(CommandError::new(
110- CommandErrorKind::Other,
109+ return Err(Error::new(
110+ ErrorKind::Invalid,
111111 "attachment metadata does not match its MIME type",
112112 ));
113113 }
114114 };
115115 if upload.thumbnail.is_some() && mimetype.type_() == mime::AUDIO {
116- return Err(CommandError::new(
117- CommandErrorKind::MediaUnavailable,
116+ return Err(Error::new(
117+ ErrorKind::Invalid,
118118 "audio attachments do not support thumbnails",
119119 ));
120120 }
@@ -126,23 +126,24 @@ pub(crate) fn prepare_upload(
126126 || thumbnail.width == 0
127127 || thumbnail.height == 0
128128 {
129- return Err(CommandError::new(
130- CommandErrorKind::MediaTooLarge,
129+ return Err(Error::new(
130+ ErrorKind::MediaTooLarge,
131131 "thumbnail size or dimensions are invalid",
132132 ));
133133 }
134- let content_type: mime::Mime = thumbnail.mimetype.parse().map_err(|_| {
135- CommandError::new(CommandErrorKind::Other, "thumbnail MIME type is invalid")
136- })?;
134+ let content_type: mime::Mime = thumbnail
135+ .mimetype
136+ .parse()
137+ .map_err(|_| Error::new(ErrorKind::Invalid, "thumbnail MIME type is invalid"))?;
137138 if content_type.type_() != mime::IMAGE {
138- return Err(CommandError::new(
139- CommandErrorKind::MediaUnavailable,
139+ return Err(Error::new(
140+ ErrorKind::Invalid,
140141 "thumbnail MIME type must be an image",
141142 ));
142143 }
143144 let size = ruma::UInt::try_from(thumbnail.bytes.len()).map_err(|_| {
144- CommandError::new(
145- CommandErrorKind::MediaTooLarge,
145+ Error::new(
146+ ErrorKind::MediaTooLarge,
146147 "thumbnail size is not representable",
147148 )
148149 })?;
Modified file: tests/account.rs+16 additions−37 deletions
@@ -1,10 +1,9 @@
11 #![cfg(feature = "sqlite")]
2-use std::time::{Duration, SystemTime};
2+mod support;
33
4-use macltok::{
5- Command, EngineConfig, Event, LifecycleCommand, PasswordLogin, Session, SessionRecord,
6- StoreConfig,
7-};
4+use std::time::SystemTime;
5+
6+use macltok::{EngineConfig, Event, PasswordLogin, Session, SessionRecord, StoreConfig};
87 use matrix_sdk::{ruma::api::client::sync::sync_events::v5, test_utils::mocks::MatrixMockServer};
98
109 #[tokio::test]
@@ -26,13 +25,8 @@ async fn password_session_round_trips_through_an_encrypted_store() {
2625 std::fs::create_dir_all(&store_path).unwrap();
2726 let config = EngineConfig::default().with_initial_history_limit(0);
2827 let session = Session::login_password(
29- PasswordLogin {
30- homeserver: server.uri(),
31- username: "example".into(),
32- password: "wordpass".into(),
33- device_display_name: Some("macltok test".into()),
34- device_id: None,
35- },
28+ PasswordLogin::new(server.uri(), "example", "wordpass")
29+ .with_device_display_name("macltok test"),
3630 StoreConfig::new(&store_path, "test store passphrase"),
3731 )
3832 .await
@@ -46,12 +40,11 @@ async fn password_session_round_trips_through_an_encrypted_store() {
4640
4741 let (engine, handle, mut events) = session.build(config.clone()).await.unwrap();
4842 let running = tokio::spawn(engine.run());
49- receive_authenticated(&mut events).await;
50- handle
51- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
52- .await
53- .unwrap();
54- running.await.unwrap().unwrap();
43+ support::receive_until(&mut events, |event| {
44+ matches!(event, Event::Authenticated { .. })
45+ })
46+ .await;
47+ support::shut_down(&handle, &mut events, running).await;
5548
5649 let session = Session::restore(
5750 record,
@@ -61,29 +54,15 @@ async fn password_session_round_trips_through_an_encrypted_store() {
6154 .unwrap();
6255 let (engine, handle, mut events) = session.build(config).await.unwrap();
6356 let running = tokio::spawn(engine.run());
64- receive_authenticated(&mut events).await;
65- handle
66- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
67- .await
68- .unwrap();
69- running.await.unwrap().unwrap();
57+ support::receive_until(&mut events, |event| {
58+ matches!(event, Event::Authenticated { .. })
59+ })
60+ .await;
61+ support::shut_down(&handle, &mut events, running).await;
7062
7163 std::fs::remove_dir_all(store_path).unwrap();
7264 }
7365
74-async fn receive_authenticated(events: &mut macltok::EventStream) {
75- tokio::time::timeout(Duration::from_secs(2), async {
76- while let Some(event) = events.recv().await {
77- if matches!(event, Event::Authenticated { .. }) {
78- return;
79- }
80- }
81- panic!("event stream closed before authentication");
82- })
83- .await
84- .expect("authentication event did not arrive");
85-}
86-
8766 fn temporary_store_path() -> std::path::PathBuf {
8867 let nonce = SystemTime::now()
8968 .duration_since(SystemTime::UNIX_EPOCH)
Modified file: tests/commands.rs+68 additions−113 deletions
@@ -1,18 +1,20 @@
1+mod support;
2+
13 use std::time::Duration;
24
35 use macltok::{
4- ClientHandle, Command, CommandErrorKind, CommandId, CommandKind, Engine, EngineConfig,
5- EngineError, Event, EventStream, LifecycleCommand, MessageContent, MessageEditContent,
6- RichText, RoomCommand, TimelineCommand,
6+ ClientHandle, Command, Engine, EngineConfig, EngineError, ErrorKind, Event, EventStream,
7+ MessageContent, MessageEditContent, RichText, RoomCommand, TimelineCommand,
78 };
89 use matrix_sdk::{
910 ruma::{RoomId, api::client::sync::sync_events::v5, event_id, room_id},
1011 test_utils::mocks::MatrixMockServer,
1112 };
13+use support::shut_down;
1214 use tokio::task::JoinHandle;
1315
1416 #[tokio::test]
15-async fn empty_message_bodies_report_their_command_ids() {
17+async fn empty_message_bodies_fail_with_empty_body() {
1618 let room_id = room_id!("!empty-bodies:localhost");
1719 let (server, handle, mut events, running) = start_engine(room_id).await;
1820 open_room(&handle, &mut events, room_id).await;
@@ -42,14 +44,14 @@ async fn empty_message_bodies_report_their_command_ids() {
4244 ];
4345
4446 for command in commands {
45- let id = handle.send(Command::Timeline(command)).await.unwrap();
46- let failure = receive_command_failure(&mut events, id).await;
47- assert_eq!(failure.id, id);
48- assert_eq!(failure.command, CommandKind::Timeline);
49- assert_eq!(failure.kind, CommandErrorKind::EmptyBody);
47+ let error = handle
48+ .execute(Command::Timeline(command))
49+ .await
50+ .expect_err("empty body command unexpectedly succeeded");
51+ assert_eq!(error.kind, ErrorKind::EmptyBody);
5052 }
5153
52- stop_engine(handle, &mut events, running).await;
54+ shut_down(&handle, &mut events, running).await;
5355 drop(server);
5456 }
5557
@@ -60,7 +62,7 @@ async fn open_resets_the_timeline_before_succeeding() {
6062
6163 open_room(&handle, &mut events, room_id).await;
6264
63- stop_engine(handle, &mut events, running).await;
65+ shut_down(&handle, &mut events, running).await;
6466 drop(server);
6567 }
6668
@@ -69,28 +71,56 @@ async fn same_room_sends_complete_in_submission_order() {
6971 let room_id = room_id!("!send-order:localhost");
7072 let (server, handle, mut events, running) = start_engine(room_id).await;
7173 open_room(&handle, &mut events, room_id).await;
74+ server
75+ .mock_room_send()
76+ .ok(event_id!("$sent:localhost"))
77+ .expect(2)
78+ .mount()
79+ .await;
7280
73- let first_id = handle
74- .send(Command::Timeline(TimelineCommand::Send {
81+ let first = handle
82+ .try_send(Command::Timeline(TimelineCommand::Send {
7583 timeline_id: room_id.to_owned().into(),
7684 content: MessageContent::text("first"),
7785 }))
78- .await
7986 .unwrap();
80- let second_id = handle
81- .send(Command::Timeline(TimelineCommand::Send {
87+ let second = handle
88+ .try_send(Command::Timeline(TimelineCommand::Send {
8289 timeline_id: room_id.to_owned().into(),
8390 content: MessageContent::text("second"),
8491 }))
85- .await
8692 .unwrap();
8793
88- assert_eq!(
89- receive_timeline_completion(&mut events, &[first_id, second_id]).await,
90- vec![first_id, second_id]
91- );
94+ // both answers land almost together, so racing them can't show which was
95+ // processed first, only the order the sends actually reached the homeserver can
96+ let (first_result, second_result) = tokio::join!(first, second);
97+ first_result.expect("first send failed");
98+ second_result.expect("second send failed");
9299
93- stop_engine(handle, &mut events, running).await;
100+ tokio::time::timeout(Duration::from_secs(2), async {
101+ loop {
102+ let bodies: Vec<String> = server
103+ .received_requests()
104+ .await
105+ .unwrap()
106+ .iter()
107+ .filter(|request| request.url.path().contains("/send/m.room.message/"))
108+ .filter_map(|request| {
109+ serde_json::from_slice::<serde_json::Value>(&request.body).ok()
110+ })
111+ .filter_map(|body| body["body"].as_str().map(str::to_owned))
112+ .collect();
113+ if bodies.len() >= 2 {
114+ assert_eq!(bodies, ["first", "second"]);
115+ break;
116+ }
117+ tokio::task::yield_now().await;
118+ }
119+ })
120+ .await
121+ .expect("both sends did not reach the homeserver in submission order");
122+
123+ shut_down(&handle, &mut events, running).await;
94124 drop(server);
95125 }
96126
@@ -117,108 +147,33 @@ async fn start_engine(
117147 (server, handle, events, running)
118148 }
119149
120-async fn open_room(handle: &ClientHandle, events: &mut EventStream, room_id: &RoomId) -> CommandId {
121- let id = handle
150+/// opens `room_id` and confirms its `TimelineReset` was already on the
151+/// stream by the time the open command answered
152+async fn open_room(handle: &ClientHandle, events: &mut EventStream, room_id: &RoomId) {
153+ let mut pending = handle
122154 .send(Command::Room(RoomCommand::Open(room_id.to_owned())))
123155 .await
124156 .unwrap();
125157 let mut reset_received = false;
126-
127158 tokio::time::timeout(Duration::from_secs(2), async {
128159 loop {
129- match events.recv().await.expect("event stream closed") {
130- Event::TimelineReset {
131- timeline_id: reset_timeline,
132- ..
133- } if reset_timeline.room_id() == room_id => reset_received = true,
134- Event::CommandSucceeded {
135- id: succeeded_id,
136- command: CommandKind::Room,
137- } if succeeded_id == id => {
138- assert!(reset_received);
139- break;
160+ tokio::select! {
161+ biased;
162+ event = events.recv() => {
163+ if let Event::TimelineReset { timeline_id, .. } = event.expect("event stream closed")
164+ && timeline_id.room_id() == room_id
165+ {
166+ reset_received = true;
167+ }
140168 }
141- Event::CommandFailed(failure) if failure.id == id => {
142- panic!("opening room failed {failure:?}")
143- }
144- _ => {}
145- }
146- }
147- })
148- .await
149- .expect("room did not open");
150- id
151-}
152-
153-async fn receive_command_failure(
154- events: &mut EventStream,
155- expected_id: CommandId,
156-) -> macltok::CommandFailure {
157- tokio::time::timeout(Duration::from_secs(2), async {
158- loop {
159- match events.recv().await.expect("event stream closed") {
160- Event::CommandFailed(failure) if failure.id == expected_id => return failure,
161- Event::CommandSucceeded { id, .. } if id == expected_id => {
162- panic!("command unexpectedly succeeded")
163- }
164- _ => {}
165- }
166- }
167- })
168- .await
169- .expect("command failure did not arrive")
170-}
171-
172-async fn receive_timeline_completion(
173- events: &mut EventStream,
174- expected_ids: &[CommandId],
175-) -> Vec<CommandId> {
176- tokio::time::timeout(Duration::from_secs(2), async {
177- let mut completed = Vec::new();
178- while completed.len() < expected_ids.len() {
179- match events.recv().await.expect("event stream closed") {
180- Event::CommandSucceeded {
181- id,
182- command: CommandKind::Timeline,
183- } if expected_ids.contains(&id) => completed.push(id),
184- Event::CommandFailed(failure) if expected_ids.contains(&failure.id) => {
185- panic!("timeline command failed {failure:?}")
186- }
187- _ => {}
188- }
189- }
190- completed
191- })
192- .await
193- .expect("timeline commands did not complete")
194-}
195-
196-async fn stop_engine(
197- handle: ClientHandle,
198- events: &mut EventStream,
199- running: JoinHandle<Result<(), EngineError>>,
200-) {
201- let id = handle
202- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
203- .await
204- .unwrap();
205- tokio::time::timeout(Duration::from_secs(2), async {
206- let mut succeeded = false;
207- while let Some(event) = events.recv().await {
208- match event {
209- Event::CommandSucceeded {
210- id: succeeded_id,
211- command: CommandKind::Lifecycle,
212- } if succeeded_id == id => succeeded = true,
213- Event::Stopped => {
214- assert!(succeeded);
169+ result = &mut pending => {
170+ result.expect("opening room failed");
215171 break;
216172 }
217- _ => {}
218173 }
219174 }
220175 })
221176 .await
222- .expect("engine did not stop");
223- running.await.unwrap().unwrap();
177+ .expect("room did not open");
178+ assert!(reset_received, "open completed before its reset");
224179 }
Modified file: tests/engine.rs+162 additions−267 deletions
@@ -1,15 +1,17 @@
1+mod support;
2+
13 use std::time::Duration;
24
35 use macltok::{
4- Command, CommandErrorKind, Engine, EngineConfig, Event, HistoryState, LifecycleCommand,
5- MediaCommand, MessageContent, RoomCommand, TimelineCommand, TimelineDiff, TimelineId,
6- TimelineItem,
6+ ClientHandle, Command, Engine, EngineConfig, ErrorKind, Event, EventStream, HistoryState,
7+ MediaSize, MessageContent, RoomCommand, TimelineCommand, TimelineId, TimelineItem,
78 };
89 use matrix_sdk::{
910 ruma::{api::client::sync::sync_events::v5, event_id, mxc_uri, room_id},
1011 test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate},
1112 };
1213 use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};
14+use support::{event_messages, receive_until, shut_down};
1315
1416 #[tokio::test]
1517 async fn opens_a_room_and_sends_messages() {
@@ -36,10 +38,7 @@ async fn opens_a_room_and_sends_messages() {
3638 let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
3739 let running = tokio::spawn(engine.run());
3840
39- handle
40- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))
41- .await
42- .unwrap();
41+ handle.open_room(room_id.to_owned()).await.unwrap();
4342 receive_until(
4443 &mut events,
4544 |event| matches!(event, Event::TimelineReset { timeline_id: reset, .. } if reset.room_id() == room_id),
@@ -47,10 +46,10 @@ async fn opens_a_room_and_sends_messages() {
4746 .await;
4847
4948 handle
50- .send(Command::Timeline(TimelineCommand::Send {
51- timeline_id: room_id.to_owned().into(),
52- content: MessageContent::text("hello from macltok"),
53- }))
49+ .send_message(
50+ room_id.to_owned().into(),
51+ MessageContent::text("hello from macltok"),
52+ )
5453 .await
5554 .unwrap();
5655 tokio::time::timeout(Duration::from_secs(2), async {
@@ -90,31 +89,19 @@ async fn opens_a_room_and_sends_messages() {
9089 )
9190 .await;
9291 let media_event = receive_until(&mut events, |event| {
93- event_messages(event).any(|message| message.media.is_some())
92+ event_messages(event)
93+ .into_iter()
94+ .any(|message| message.media.is_some())
9495 })
9596 .await;
9697 let media_id = event_messages(&media_event)
98+ .into_iter()
9799 .find_map(|message| message.media.as_ref().map(|media| media.id.clone()))
98100 .unwrap();
99- handle
100- .send(Command::Media(MediaCommand::Fetch {
101- id: media_id.clone(),
102- size: macltok::MediaSize::Full,
103- }))
104- .await
105- .unwrap();
106- let media = receive_until(&mut events, |event| {
107- matches!(event, Event::MediaReady { media_id: received, .. } if received == &media_id)
108- })
109- .await;
110- assert!(matches!(media, Event::MediaReady { bytes, .. } if !bytes.is_empty()));
101+ let media = handle.media(&media_id, MediaSize::Full).await.unwrap();
102+ assert!(!media.bytes.is_empty());
111103
112- handle
113- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
114- .await
115- .unwrap();
116- receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;
117- running.await.unwrap().unwrap();
104+ shut_down(&handle, &mut events, running).await;
118105 }
119106
120107 #[tokio::test]
@@ -136,14 +123,12 @@ async fn keeps_open_room_timelines_independent() {
136123 let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
137124 let running = tokio::spawn(engine.run());
138125
139- handle
140- .send(Command::Room(RoomCommand::Open(first_id.to_owned())))
141- .await
142- .unwrap();
143- handle
144- .send(Command::Room(RoomCommand::Open(second_id.to_owned())))
145- .await
146- .unwrap();
126+ let (first_open, second_open) = tokio::join!(
127+ handle.open_room(first_id.to_owned()),
128+ handle.open_room(second_id.to_owned())
129+ );
130+ first_open.unwrap();
131+ second_open.unwrap();
147132 receive_until(
148133 &mut events,
149134 |event| matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == first_id),
@@ -179,19 +164,20 @@ async fn keeps_open_room_timelines_independent() {
179164 let event = events.recv().await.expect("event stream closed");
180165 if let Event::TimelineChanged { timeline_id, .. } = &event {
181166 first_updated |= timeline_id.room_id() == first_id
182- && event_messages(&event).any(|message| message.body == "first update");
167+ && event_messages(&event)
168+ .into_iter()
169+ .any(|message| message.body == "first update");
183170 second_updated |= timeline_id.room_id() == second_id
184- && event_messages(&event).any(|message| message.body == "second update");
171+ && event_messages(&event)
172+ .into_iter()
173+ .any(|message| message.body == "second update");
185174 }
186175 }
187176 })
188177 .await
189178 .expect("both retained rooms did not update");
190179
191- handle
192- .send(Command::Room(RoomCommand::Close(first_id.to_owned())))
193- .await
194- .unwrap();
180+ handle.close_room(first_id.to_owned()).await.unwrap();
195181 server
196182 .sync_room(
197183 &sync_client,
@@ -201,16 +187,13 @@ async fn keeps_open_room_timelines_independent() {
201187 .await;
202188 receive_until(&mut events, |event| {
203189 matches!(event, Event::TimelineChanged { timeline_id, .. } if timeline_id.room_id() == second_id)
204- && event_messages(event).any(|message| message.body == "still live")
190+ && event_messages(event)
191+ .into_iter()
192+ .any(|message| message.body == "still live")
205193 })
206194 .await;
207195
208- handle
209- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
210- .await
211- .unwrap();
212- receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;
213- running.await.unwrap().unwrap();
196+ shut_down(&handle, &mut events, running).await;
214197 }
215198
216199 #[tokio::test]
@@ -230,10 +213,7 @@ async fn resync_replaces_timeline_before_ack_and_preserves_following_diffs() {
230213 let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
231214 let running = tokio::spawn(engine.run());
232215
233- handle
234- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))
235- .await
236- .unwrap();
216+ handle.open_room(room_id.to_owned()).await.unwrap();
237217 receive_until(&mut events, |event| {
238218 matches!(event,
239219 Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id
@@ -248,37 +228,14 @@ async fn resync_replaces_timeline_before_ack_and_preserves_following_diffs() {
248228 )
249229 .await;
250230 receive_until(&mut events, |event| {
251- event_messages(event).any(|message| message.body == "before reset")
231+ event_messages(event)
232+ .into_iter()
233+ .any(|message| message.body == "before reset")
252234 })
253235 .await;
254236
255- let id = handle
256- .send(Command::Timeline(TimelineCommand::Resync {
257- timeline_id: room_id.to_owned().into(),
258- }))
259- .await
260- .unwrap();
261- let mut reset = None;
262- tokio::time::timeout(Duration::from_secs(2), async {
263- loop {
264- match events.recv().await.expect("event stream closed") {
265- Event::TimelineReset { timeline_id, items } if timeline_id.room_id() == room_id => {
266- reset = Some(items);
267- }
268- Event::CommandSucceeded { id: completed, .. } if completed == id => {
269- assert!(reset.is_some(), "resync completed before its reset");
270- break;
271- }
272- Event::CommandFailed(failure) if failure.id == id => {
273- panic!("resync failed: {failure:?}")
274- }
275- _ => {}
276- }
277- }
278- })
279- .await
280- .expect("resync did not complete");
281- let mut items = reset.unwrap();
237+ let mut items =
238+ resync_and_confirm_reset_first(&handle, &mut events, room_id.to_owned().into()).await;
282239 assert!(items.iter().any(|item| matches!(item,
283240 TimelineItem::Message(message) if message.body == "before reset"
284241 )));
@@ -313,12 +270,7 @@ async fn resync_replaces_timeline_before_ack_and_preserves_following_diffs() {
313270 .await
314271 .expect("resynced timeline did not update");
315272
316- handle
317- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
318- .await
319- .unwrap();
320- receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;
321- running.await.unwrap().unwrap();
273+ shut_down(&handle, &mut events, running).await;
322274 }
323275
324276 #[tokio::test]
@@ -337,76 +289,76 @@ async fn closes_and_reopens_a_room_in_command_order() {
337289 let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
338290 let running = tokio::spawn(engine.run());
339291
340- let initial_open = handle
341- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))
342- .await
343- .unwrap();
292+ handle.open_room(room_id.to_owned()).await.unwrap();
344293 receive_until(
345294 &mut events,
346295 |event| matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id),
347296 )
348297 .await;
349- receive_until(
350- &mut events,
351- |event| matches!(event, Event::CommandSucceeded { id, .. } if id == &initial_open),
352- )
353- .await;
354298
355- let close = handle
356- .send(Command::Room(RoomCommand::Close(room_id.to_owned())))
357- .await
299+ let mut close = handle
300+ .try_send(Command::Room(RoomCommand::Close(room_id.to_owned())))
358301 .unwrap();
359- let replacement_open = handle
360- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))
361- .await
302+ let mut replacement_open = handle
303+ .try_send(Command::Room(RoomCommand::Open(room_id.to_owned())))
362304 .unwrap();
363- let second_close = handle
364- .send(Command::Room(RoomCommand::Close(room_id.to_owned())))
365- .await
305+ let mut second_close = handle
306+ .try_send(Command::Room(RoomCommand::Close(room_id.to_owned())))
366307 .unwrap();
367- let final_open = handle
368- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))
369- .await
308+ let mut final_open = handle
309+ .try_send(Command::Room(RoomCommand::Open(room_id.to_owned())))
370310 .unwrap();
371- let expected = [close, replacement_open, second_close, final_open];
372- let mut completed = 0;
311+
312+ // biased select drains every buffered reset before it ever reports a ready reply,
313+ // so the engine can race ahead and land both resets before either open is observed
373314 let mut resets = 0;
315+ let mut order = Vec::new();
316+ let (mut close_done, mut replacement_open_done, mut second_close_done, mut final_open_done) =
317+ (false, false, false, false);
374318 tokio::time::timeout(Duration::from_secs(3), async {
375- while completed < expected.len() {
376- match events.recv().await.expect("event stream closed") {
377- Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id => {
378- resets += 1;
379- assert!(resets <= 2, "a room generation reset more than once");
380- }
381- Event::CommandSucceeded { id, .. } if expected.contains(&id) => {
382- assert_eq!(
383- id, expected[completed],
384- "room lifecycle commands completed out of order"
385- );
386- if completed == 1 || completed == 3 {
387- assert_eq!(
388- resets,
389- completed / 2 + 1,
390- "room open completed before timeline reset"
391- );
319+ while order.len() < 4 {
320+ tokio::select! {
321+ biased;
322+ event = events.recv() => {
323+ if let Event::TimelineReset { timeline_id, .. } = event.expect("event stream closed")
324+ && timeline_id.room_id() == room_id
325+ {
326+ resets += 1;
327+ assert!(resets <= 2, "a room generation reset more than once");
392328 }
393- completed += 1;
394329 }
395- Event::CommandFailed(failure) if expected.contains(&failure.id) => {
396- panic!("room lifecycle command failed: {failure:?}");
330+ result = &mut close, if !close_done => {
331+ result.expect("close failed");
332+ close_done = true;
333+ order.push("close");
334+ }
335+ result = &mut replacement_open, if !replacement_open_done => {
336+ result.expect("replacement open failed");
337+ replacement_open_done = true;
338+ order.push("replacement_open");
339+ }
340+ result = &mut second_close, if !second_close_done => {
341+ result.expect("second close failed");
342+ second_close_done = true;
343+ order.push("second_close");
344+ }
345+ result = &mut final_open, if !final_open_done => {
346+ result.expect("final open failed");
347+ final_open_done = true;
348+ order.push("final_open");
397349 }
398- _ => {}
399350 }
400351 }
401352 })
402353 .await
403354 .expect("room lifecycle commands did not complete");
404- handle
405- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
406- .await
407- .unwrap();
408- receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;
409- running.await.unwrap().unwrap();
355+ assert_eq!(
356+ order,
357+ ["close", "replacement_open", "second_close", "final_open"]
358+ );
359+ assert_eq!(resets, 2, "each open should have reset the timeline once");
360+
361+ shut_down(&handle, &mut events, running).await;
410362 }
411363
412364 #[tokio::test]
@@ -429,83 +381,50 @@ async fn thread_command_waits_for_opening_timeline() {
429381 .await
430382 .unwrap();
431383 let running = tokio::spawn(engine.run());
432- let room_open = handle
433- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))
434- .await
435- .unwrap();
436- receive_until(
437- &mut events,
438- |event| matches!(event, Event::CommandSucceeded { id, .. } if *id == room_open),
439- )
440- .await;
384+ handle.open_room(room_id.to_owned()).await.unwrap();
441385
442- let open = handle
443- .send(Command::Timeline(TimelineCommand::OpenThread {
386+ let mut open = handle
387+ .try_send(Command::Timeline(TimelineCommand::OpenThread {
444388 room_id: room_id.to_owned(),
445389 root_event_id: root.to_owned(),
446390 }))
447- .await
448391 .unwrap();
449- let send = handle
450- .send(Command::Timeline(TimelineCommand::Send {
392+ let mut send = handle
393+ .try_send(Command::Timeline(TimelineCommand::Send {
451394 timeline_id: TimelineId::thread(room_id.to_owned(), root.to_owned()),
452395 content: MessageContent::text(""),
453396 }))
454- .await
455- .unwrap();
456- let resync = handle
457- .send(Command::Timeline(TimelineCommand::Resync {
458- timeline_id: TimelineId::thread(room_id.to_owned(), root.to_owned()),
459- }))
460- .await
461397 .unwrap();
462398 let mut open_succeeded = false;
399+ let mut send_done = false;
463400 tokio::time::timeout(Duration::from_secs(3), async {
464- loop {
465- match events.recv().await.expect("event stream closed") {
466- Event::CommandSucceeded { id, .. } if id == open => open_succeeded = true,
467- Event::CommandFailed(failure) if failure.id == send => {
468- assert!(open_succeeded, "thread command completed before opening");
469- assert_eq!(failure.kind, CommandErrorKind::EmptyBody);
470- break;
401+ while !send_done {
402+ tokio::select! {
403+ biased;
404+ result = &mut open, if !open_succeeded => {
405+ result.expect("thread open failed");
406+ open_succeeded = true;
471407 }
472- Event::CommandFailed(failure) if failure.id == open => {
473- panic!("thread open failed: {failure:?}");
408+ result = &mut send, if !send_done => {
409+ assert!(open_succeeded, "thread command completed before opening");
410+ let error = result.expect_err("empty body send unexpectedly succeeded");
411+ assert_eq!(error.kind, ErrorKind::EmptyBody);
412+ send_done = true;
474413 }
475- _ => {}
476414 }
477415 }
478416 })
479417 .await
480418 .expect("thread commands did not complete");
481- let mut reset_seen = false;
482- tokio::time::timeout(Duration::from_secs(3), async {
483- loop {
484- match events.recv().await.expect("event stream closed") {
485- Event::TimelineReset { timeline_id, .. }
486- if timeline_id.thread_root().is_some_and(|event| event == root) =>
487- {
488- reset_seen = true
489- }
490- Event::CommandSucceeded { id, .. } if id == resync => {
491- assert!(reset_seen, "thread resync completed before its reset");
492- break;
493- }
494- Event::CommandFailed(failure) if failure.id == resync => {
495- panic!("thread resync failed: {failure:?}")
496- }
497- _ => {}
498- }
499- }
500- })
501- .await
502- .expect("thread resync did not complete");
503- handle
504- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
505- .await
506- .unwrap();
507- receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;
508- running.await.unwrap().unwrap();
419+
420+ resync_and_confirm_reset_first(
421+ &handle,
422+ &mut events,
423+ TimelineId::thread(room_id.to_owned(), root.to_owned()),
424+ )
425+ .await;
426+
427+ shut_down(&handle, &mut events, running).await;
509428 }
510429
511430 #[tokio::test]
@@ -525,10 +444,7 @@ async fn pagination_reports_and_remembers_start_of_history() {
525444 let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
526445 let running = tokio::spawn(engine.run());
527446
528- handle
529- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))
530- .await
531- .unwrap();
447+ handle.open_room(room_id.to_owned()).await.unwrap();
532448 receive_until(
533449 &mut events,
534450 |event| matches!(event, Event::TimelineReset { timeline_id: reset, .. } if reset.room_id() == room_id),
@@ -546,7 +462,9 @@ async fn pagination_reports_and_remembers_start_of_history() {
546462 .await;
547463 receive_until(&mut events, |event| {
548464 matches!(event, Event::TimelineChanged { timeline_id: changed, .. } if changed.room_id() == room_id)
549- && event_messages(event).any(|message| message.body == "latest")
465+ && event_messages(event)
466+ .into_iter()
467+ .any(|message| message.body == "latest")
550468 })
551469 .await;
552470
@@ -560,10 +478,7 @@ async fn pagination_reports_and_remembers_start_of_history() {
560478 let mut reached_start = false;
561479 for _ in 0..3 {
562480 handle
563- .send(Command::Timeline(TimelineCommand::Paginate {
564- timeline_id: room_id.to_owned().into(),
565- limit: 10,
566- }))
481+ .paginate(room_id.to_owned().into(), 10)
567482 .await
568483 .unwrap();
569484 receive_history(&mut events, room_id, HistoryState::Loading).await;
@@ -582,24 +497,54 @@ async fn pagination_reports_and_remembers_start_of_history() {
582497 );
583498
584499 handle
585- .send(Command::Timeline(TimelineCommand::Paginate {
586- timeline_id: room_id.to_owned().into(),
587- limit: 10,
588- }))
500+ .paginate(room_id.to_owned().into(), 10)
589501 .await
590502 .unwrap();
591503 receive_history(&mut events, room_id, HistoryState::StartReached).await;
592504
593- handle
594- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
505+ shut_down(&handle, &mut events, running).await;
506+}
507+
508+/// resyncs `timeline_id` and confirms its `TimelineReset` was already on the
509+/// stream by the time the resync command answered, racing the two so we
510+/// don't have to drain the whole stream first
511+async fn resync_and_confirm_reset_first(
512+ handle: &ClientHandle,
513+ events: &mut EventStream,
514+ timeline_id: TimelineId,
515+) -> Vec<TimelineItem> {
516+ let mut pending = handle
517+ .send(Command::Timeline(TimelineCommand::Resync {
518+ timeline_id: timeline_id.clone(),
519+ }))
595520 .await
596521 .unwrap();
597- receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;
598- running.await.unwrap().unwrap();
522+ let mut reset_items = None;
523+ tokio::time::timeout(Duration::from_secs(3), async {
524+ loop {
525+ tokio::select! {
526+ biased;
527+ event = events.recv() => {
528+ if let Event::TimelineReset { timeline_id: reset_id, items } = event.expect("event stream closed")
529+ && reset_id == timeline_id
530+ {
531+ reset_items = Some(items);
532+ }
533+ }
534+ result = &mut pending => {
535+ result.expect("resync failed");
536+ break;
537+ }
538+ }
539+ }
540+ })
541+ .await
542+ .expect("resync did not complete");
543+ reset_items.expect("resync completed before its reset")
599544 }
600545
601546 async fn receive_history(
602- events: &mut macltok::EventStream,
547+ events: &mut EventStream,
603548 room_id: &matrix_sdk::ruma::RoomId,
604549 expected: HistoryState,
605550 ) {
@@ -609,42 +554,8 @@ async fn receive_history(
609554 .await;
610555 }
611556
612-fn event_messages(event: &Event) -> std::vec::IntoIter<&macltok::TimelineMessage> {
613- let items = match event {
614- Event::TimelineReset { items, .. } => items.iter().collect::<Vec<_>>(),
615- Event::TimelineChanged { diffs, .. } => {
616- diffs.iter().flat_map(diff_items).collect::<Vec<_>>()
617- }
618- _ => Vec::new(),
619- };
620- items
621- .into_iter()
622- .filter_map(|item| match item {
623- TimelineItem::Message(message) => Some(message.as_ref()),
624- _ => None,
625- })
626- .collect::<Vec<_>>()
627- .into_iter()
628-}
629-
630-fn diff_items(diff: &TimelineDiff) -> Vec<&TimelineItem> {
631- match diff {
632- TimelineDiff::Append { values } | TimelineDiff::Reset { values } => values.iter().collect(),
633- TimelineDiff::PushFront { value }
634- | TimelineDiff::PushBack { value }
635- | TimelineDiff::Insert { value, .. }
636- | TimelineDiff::Set { value, .. } => vec![value],
637- TimelineDiff::Clear
638- | TimelineDiff::PopFront
639- | TimelineDiff::PopBack
640- | TimelineDiff::Remove { .. }
641- | TimelineDiff::Truncate { .. } => Vec::new(),
642- _ => Vec::new(),
643- }
644-}
645-
646557 async fn receive_history_terminal(
647- events: &mut macltok::EventStream,
558+ events: &mut EventStream,
648559 room_id: &matrix_sdk::ruma::RoomId,
649560 ) -> HistoryState {
650561 match receive_until(events, |event| {
@@ -690,19 +601,3 @@ async fn wait_for_room_subscriptions(
690601 .await
691602 .expect("Sliding Sync did not retain every open room subscription");
692603 }
693-
694-async fn receive_until(
695- events: &mut macltok::EventStream,
696- predicate: impl Fn(&Event) -> bool,
697-) -> Event {
698- tokio::time::timeout(Duration::from_secs(2), async {
699- while let Some(event) = events.recv().await {
700- if predicate(&event) {
701- return event;
702- }
703- }
704- panic!("event stream closed before the expected event");
705- })
706- .await
707- .expect("expected event did not arrive")
708-}
Modified file: tests/media.rs+114 additions−294 deletions
@@ -1,9 +1,10 @@
1+mod support;
2+
13 use std::{sync::Arc, time::Duration};
24
35 use macltok::{
4- AttachmentUpload, ClientHandle, Command, CommandErrorKind, Engine, EngineConfig, EngineError,
5- Event, EventStream, LifecycleCommand, MediaCommand, MediaId, MediaSize, RoomCommand,
6- TimelineDiff, TimelineItem, UploadDetails, UploadThumbnail,
6+ AttachmentUpload, ClientHandle, Engine, EngineConfig, EngineError, ErrorKind, Event,
7+ EventStream, MediaId, MediaSize, UploadDetails, UploadThumbnail,
78 };
89 use matrix_sdk::{
910 media::{MediaFormat, MediaRequestParameters},
@@ -43,7 +44,7 @@ async fn full_size_uses_full_source() {
4344 let mut harness = start(file_with_thumbnail()).await;
4445 mount_downloads(&harness.server).await;
4546
46- let bytes = fetch_ready(&mut harness, MediaSize::Full).await;
47+ let bytes = fetch(&harness, MediaSize::Full).await;
4748 assert_eq!(&*bytes, b"binaryjpegfullimagedata");
4849 assert!(
4950 media_paths(&harness.server)
@@ -52,7 +53,7 @@ async fn full_size_uses_full_source() {
5253 .any(|path| path.ends_with("/localhost/full"))
5354 );
5455
55- stop(harness).await;
56+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
5657 }
5758
5859 #[tokio::test]
@@ -94,13 +95,10 @@ async fn encrypted_media_is_cached_as_plaintext_like_the_sdk_does() {
9495 .await
9596 .unwrap();
9697 assert_eq!(from_sdk, plaintext);
97- assert_eq!(
98- &*fetch_ready(&mut harness, MediaSize::Full).await,
99- &plaintext[..]
100- );
98+ assert_eq!(&*fetch(&harness, MediaSize::Full).await, &plaintext[..]);
10199 assert_eq!(media_paths(&harness.server).await.len(), 1);
102100
103- stop(harness).await;
101+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
104102 }
105103
106104 #[tokio::test]
@@ -116,42 +114,17 @@ async fn simultaneous_fetches_share_one_download() {
116114 .mount(harness.server.server())
117115 .await;
118116
119- let mut pending = Vec::new();
120- for _ in 0..2 {
121- pending.push(
122- harness
123- .handle
124- .send(Command::Media(MediaCommand::Fetch {
125- id: harness.media_id.clone(),
126- size: MediaSize::Full,
127- }))
128- .await
129- .unwrap(),
130- );
131- }
132- let mut ready = false;
133- tokio::time::timeout(Duration::from_secs(5), async {
134- loop {
135- match harness.events.recv().await.expect("event stream closed") {
136- Event::MediaReady {
137- media_id, bytes, ..
138- } if media_id == harness.media_id => {
139- assert_eq!(&*bytes, b"shared image bytes");
140- ready = true;
141- }
142- Event::CommandSucceeded { id, .. } => pending.retain(|waiting| *waiting != id),
143- Event::CommandFailed(failure) if pending.contains(&failure.id) => {
144- panic!("fetch failed: {failure:?}");
145- }
146- _ => {}
147- }
148- if ready && pending.is_empty() {
149- break;
150- }
151- }
117+ let (first, second) = tokio::time::timeout(Duration::from_secs(5), async {
118+ tokio::join!(
119+ harness.handle.media(&harness.media_id, MediaSize::Full),
120+ harness.handle.media(&harness.media_id, MediaSize::Full),
121+ )
152122 })
153123 .await
154124 .expect("both fetches did not complete");
125+ assert_eq!(&*first.unwrap().bytes, b"shared image bytes");
126+ assert_eq!(&*second.unwrap().bytes, b"shared image bytes");
127+
155128 let downloads = harness
156129 .server
157130 .received_requests()
@@ -161,7 +134,7 @@ async fn simultaneous_fetches_share_one_download() {
161134 .filter(|request| request.url.path().ends_with("/download/localhost/image"))
162135 .count();
163136 assert_eq!(downloads, 1, "duplicate in-flight media download");
164- stop(harness).await;
137+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
165138 }
166139
167140 #[tokio::test]
@@ -178,19 +151,15 @@ async fn downloaded_media_over_limit_is_rejected() {
178151 .respond_with(ResponseTemplate::new(200).set_body_bytes(b"too large".to_vec()))
179152 .mount(harness.server.server())
180153 .await;
181- let id = harness
154+
155+ let error = harness
182156 .handle
183- .send(Command::Media(MediaCommand::Fetch {
184- id: harness.media_id.clone(),
185- size: MediaSize::Full,
186- }))
157+ .media(&harness.media_id, MediaSize::Full)
187158 .await
188- .unwrap();
189- receive_until(&mut harness.events, |event| {
190- matches!(event, Event::CommandFailed(failure) if failure.id == id && failure.kind == CommandErrorKind::MediaTooLarge)
191- })
192- .await;
193- stop(harness).await;
159+ .unwrap_err();
160+ assert_eq!(error.kind, ErrorKind::MediaTooLarge);
161+
162+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
194163 }
195164
196165 #[tokio::test]
@@ -198,7 +167,7 @@ async fn thumbnail_size_prefers_explicit_thumbnail() {
198167 let mut harness = start(file_with_thumbnail()).await;
199168 mount_downloads(&harness.server).await;
200169
201- let bytes = fetch_ready(&mut harness, MediaSize::Thumbnail).await;
170+ let bytes = fetch(&harness, thumbnail_size()).await;
202171 assert_eq!(&*bytes, b"binaryjpegfullimagedata");
203172 let paths = media_paths(&harness.server).await;
204173 assert!(
@@ -208,7 +177,7 @@ async fn thumbnail_size_prefers_explicit_thumbnail() {
208177 );
209178 assert!(!paths.iter().any(|path| path.ends_with("/localhost/full")));
210179
211- stop(harness).await;
180+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
212181 }
213182
214183 #[tokio::test]
@@ -227,7 +196,7 @@ async fn plain_image_thumbnail_uses_thumbnail_endpoint() {
227196 .mount()
228197 .await;
229198
230- let bytes = fetch_ready(&mut harness, MediaSize::Thumbnail).await;
199+ let bytes = fetch(&harness, thumbnail_size()).await;
231200 assert_eq!(&*bytes, b"binaryjpegthumbnaildata");
232201 assert!(
233202 media_paths(&harness.server)
@@ -236,36 +205,21 @@ async fn plain_image_thumbnail_uses_thumbnail_endpoint() {
236205 .any(|path| path.contains("/thumbnail/localhost/image"))
237206 );
238207
239- stop(harness).await;
208+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
240209 }
241210
242211 #[tokio::test]
243-async fn unavailable_thumbnail_reports_returned_command_id() {
212+async fn audio_thumbnail_fetch_fails_with_media_unavailable() {
244213 let mut harness = start(plain_audio()).await;
245- let command_id = harness
214+
215+ let error = harness
246216 .handle
247- .send(Command::Media(MediaCommand::Fetch {
248- id: harness.media_id.clone(),
249- size: MediaSize::Thumbnail,
250- }))
217+ .media(&harness.media_id, thumbnail_size())
251218 .await
252- .unwrap();
219+ .unwrap_err();
220+ assert_eq!(error.kind, ErrorKind::MediaUnavailable);
253221
254- let event = receive_until(&mut harness.events, |event| {
255- matches!(
256- event,
257- Event::CommandFailed(failure)
258- if failure.id == command_id && failure.kind == CommandErrorKind::MediaUnavailable
259- )
260- })
261- .await;
262- assert!(matches!(
263- event,
264- Event::CommandFailed(failure)
265- if failure.id == command_id && failure.kind == CommandErrorKind::MediaUnavailable
266- ));
267-
268- stop(harness).await;
222+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
269223 }
270224
271225 #[tokio::test]
@@ -293,22 +247,14 @@ async fn media_upload_is_queued_and_sent() {
293247 .mount()
294248 .await;
295249
296- let id = harness
297- .handle
298- .upload_media(
299- room_id!("!media:localhost").to_owned().into(),
300- "photo.jpg".to_owned(),
301- "image/jpeg".to_owned(),
302- b"photo data".to_vec(),
303- Some("holiday".to_owned()),
304- )
305- .await
306- .unwrap();
307- receive_until(
308- &mut harness.events,
309- |event| matches!(event, Event::CommandSucceeded { id: completed, .. } if *completed == id),
310- )
311- .await;
250+ let mut upload = AttachmentUpload::new(
251+ room_id!("!media:localhost").to_owned(),
252+ "photo.jpg",
253+ "image/jpeg",
254+ b"photo data".to_vec(),
255+ );
256+ upload.caption = Some("holiday".to_owned());
257+ harness.handle.upload(upload).await.unwrap();
312258
313259 tokio::time::timeout(Duration::from_secs(5), async {
314260 loop {
@@ -325,7 +271,7 @@ async fn media_upload_is_queued_and_sent() {
325271 .await
326272 .expect("queued media was not sent");
327273
328- stop(harness).await;
274+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
329275 }
330276
331277 #[tokio::test]
@@ -344,9 +290,9 @@ async fn queued_offline_attachment_exposes_fetchable_local_media() {
344290 height: Some(16),
345291 blurhash: None,
346292 });
347- harness.handle.upload_attachment(upload).await.unwrap();
348- let event = receive_until(&mut harness.events, |event| {
349- event_messages(event).any(|message| {
293+ harness.handle.upload(upload).await.unwrap();
294+ let event = support::receive_until(&mut harness.events, |event| {
295+ support::event_messages(event).into_iter().any(|message| {
350296 message.own
351297 && message.transaction_id.is_some()
352298 && message
@@ -356,7 +302,8 @@ async fn queued_offline_attachment_exposes_fetchable_local_media() {
356302 })
357303 })
358304 .await;
359- let media = event_messages(&event)
305+ let media = support::event_messages(&event)
306+ .into_iter()
360307 .find(|message| message.own && message.media.is_some())
361308 .and_then(|message| message.media.as_ref())
362309 .unwrap();
@@ -364,26 +311,14 @@ async fn queued_offline_attachment_exposes_fetchable_local_media() {
364311 assert_eq!(media.size_bytes, Some(original.len() as u64));
365312 let media_id = media.id.clone();
366313
367- let fetch_id = harness
314+ let fetched = harness
368315 .handle
369- .send(Command::Media(MediaCommand::Fetch {
370- id: media_id.clone(),
371- size: MediaSize::Full,
372- }))
316+ .media(&media_id, MediaSize::Full)
373317 .await
374318 .unwrap();
375- let ready = receive_until(
376- &mut harness.events,
377- |event| matches!(event, Event::MediaReady { media_id: id, .. } if id == &media_id),
378- )
379- .await;
380- assert!(matches!(ready, Event::MediaReady { bytes, .. } if *bytes == original));
381- receive_until(
382- &mut harness.events,
383- |event| matches!(event, Event::CommandSucceeded { id, .. } if *id == fetch_id),
384- )
385- .await;
386- stop(harness).await;
319+ assert_eq!(&*fetched.bytes, &original[..]);
320+
321+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
387322 }
388323
389324 #[tokio::test]
@@ -431,18 +366,13 @@ async fn image_upload_uses_caller_supplied_metadata_and_thumbnail() {
431366 height: Some(450),
432367 blurhash: None,
433368 });
434- upload.thumbnail = Some(UploadThumbnail {
435- bytes: thumbnail_bytes.clone(),
436- mimetype: "image/png".to_owned(),
437- width: 800,
438- height: 400,
439- });
440- let id = harness.handle.upload_attachment(upload).await.unwrap();
441- receive_until(
442- &mut harness.events,
443- |event| matches!(event, Event::CommandSucceeded { id: completed, .. } if *completed == id),
444- )
445- .await;
369+ upload.thumbnail = Some(UploadThumbnail::new(
370+ thumbnail_bytes.clone(),
371+ "image/png",
372+ 800,
373+ 400,
374+ ));
375+ harness.handle.upload(upload).await.unwrap();
446376
447377 let content = uploaded_content(&harness.server).await;
448378 assert_eq!(content["msgtype"], "m.image");
@@ -475,7 +405,7 @@ async fn image_upload_uses_caller_supplied_metadata_and_thumbnail() {
475405 .any(|request| request.body == thumbnail_bytes)
476406 );
477407
478- stop(harness).await;
408+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
479409 }
480410
481411 #[tokio::test]
@@ -523,18 +453,13 @@ async fn video_upload_preserves_metadata_and_supplied_thumbnail() {
523453 duration_ms: Some(3500),
524454 blurhash: Some("blurhash".to_owned()),
525455 });
526- upload.thumbnail = Some(UploadThumbnail {
527- bytes: b"poster bytes".to_vec(),
528- mimetype: "image/png".to_owned(),
529- width: 320,
530- height: 180,
531- });
532- let id = harness.handle.upload_attachment(upload).await.unwrap();
533- receive_until(
534- &mut harness.events,
535- |event| matches!(event, Event::CommandSucceeded { id: completed, .. } if *completed == id),
536- )
537- .await;
456+ upload.thumbnail = Some(UploadThumbnail::new(
457+ b"poster bytes".to_vec(),
458+ "image/png",
459+ 320,
460+ 180,
461+ ));
462+ harness.handle.upload(upload).await.unwrap();
538463
539464 let content = uploaded_content(&harness.server).await;
540465 assert_eq!(content["msgtype"], "m.video");
@@ -556,7 +481,7 @@ async fn video_upload_preserves_metadata_and_supplied_thumbnail() {
556481 request.url.path().ends_with("/_matrix/media/v3/upload") && request.body == b"poster bytes"
557482 }));
558483
559- stop(harness).await;
484+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
560485 }
561486
562487 #[tokio::test]
@@ -599,12 +524,7 @@ async fn voice_upload_emits_voice_details_and_correlated_progress() {
599524 duration_ms: 4200,
600525 waveform: vec![0, 512, 1024],
601526 });
602- let id = harness.handle.upload_attachment(upload).await.unwrap();
603- receive_until(
604- &mut harness.events,
605- |event| matches!(event, Event::CommandSucceeded { id: completed, .. } if *completed == id),
606- )
607- .await;
527+ harness.handle.upload(upload).await.unwrap();
608528
609529 let pending = harness
610530 .handle
@@ -613,7 +533,7 @@ async fn voice_upload_emits_voice_details_and_correlated_progress() {
613533 .unwrap();
614534 assert_eq!(pending.len(), 1);
615535 let transaction_id = pending[0].clone();
616- let progress = receive_until(&mut harness.events, |event| {
536+ let progress = support::receive_until(&mut harness.events, |event| {
617537 matches!(event, Event::MediaUploadProgress { transaction_id: seen, complete: true, .. } if seen == &transaction_id)
618538 })
619539 .await;
@@ -636,7 +556,7 @@ async fn voice_upload_emits_voice_details_and_correlated_progress() {
636556 );
637557 assert_eq!(content["org.matrix.msc3245.voice"], json!({}));
638558
639- stop(harness).await;
559+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
640560 }
641561
642562 #[tokio::test]
@@ -653,15 +573,8 @@ async fn mismatched_upload_metadata_and_invalid_waveform_fail_without_uploading(
653573 height: Some(32),
654574 blurhash: None,
655575 });
656- let id = harness.handle.upload_attachment(mismatched).await.unwrap();
657- let failure = receive_until(
658- &mut harness.events,
659- |event| matches!(event, Event::CommandFailed(failure) if failure.id == id),
660- )
661- .await;
662- assert!(
663- matches!(failure, Event::CommandFailed(failure) if failure.kind == CommandErrorKind::Other)
664- );
576+ let error = harness.handle.upload(mismatched).await.unwrap_err();
577+ assert_eq!(error.kind, ErrorKind::Invalid);
665578
666579 let mut voice = AttachmentUpload::new(
667580 room_id!("!media:localhost").to_owned(),
@@ -673,15 +586,8 @@ async fn mismatched_upload_metadata_and_invalid_waveform_fail_without_uploading(
673586 duration_ms: 1000,
674587 waveform: vec![1025],
675588 });
676- let id = harness.handle.upload_attachment(voice).await.unwrap();
677- let failure = receive_until(
678- &mut harness.events,
679- |event| matches!(event, Event::CommandFailed(failure) if failure.id == id),
680- )
681- .await;
682- assert!(
683- matches!(failure, Event::CommandFailed(failure) if failure.kind == CommandErrorKind::Other)
684- );
589+ let error = harness.handle.upload(voice).await.unwrap_err();
590+ assert_eq!(error.kind, ErrorKind::Invalid);
685591
686592 assert!(
687593 !harness
@@ -692,7 +598,7 @@ async fn mismatched_upload_metadata_and_invalid_waveform_fail_without_uploading(
692598 .iter()
693599 .any(|request| { request.url.path().ends_with("/_matrix/media/v3/upload") })
694600 );
695- stop(harness).await;
601+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
696602 }
697603
698604 #[tokio::test]
@@ -710,16 +616,8 @@ async fn upload_over_media_limit_fails_without_uploading() {
710616 "application/octet-stream",
711617 b"too large".to_vec(),
712618 );
713- let id = harness.handle.upload_attachment(upload).await.unwrap();
714- let failure = receive_until(
715- &mut harness.events,
716- |event| matches!(event, Event::CommandFailed(failure) if failure.id == id),
717- )
718- .await;
719- assert!(matches!(
720- failure,
721- Event::CommandFailed(failure) if failure.kind == CommandErrorKind::MediaTooLarge
722- ));
619+ let error = harness.handle.upload(upload).await.unwrap_err();
620+ assert_eq!(error.kind, ErrorKind::MediaTooLarge);
723621 assert!(
724622 !harness
725623 .server
@@ -729,7 +627,7 @@ async fn upload_over_media_limit_fails_without_uploading() {
729627 .iter()
730628 .any(|request| { request.url.path().ends_with("/_matrix/media/v3/upload") })
731629 );
732- stop(harness).await;
630+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
733631 }
734632
735633 #[tokio::test]
@@ -741,22 +639,14 @@ async fn audio_thumbnail_fails_without_uploading() {
741639 "audio/ogg",
742640 b"voice bytes".to_vec(),
743641 );
744- upload.thumbnail = Some(UploadThumbnail {
745- bytes: b"thumbnail".to_vec(),
746- mimetype: "image/png".to_owned(),
747- width: 80,
748- height: 80,
749- });
750- let id = harness.handle.upload_attachment(upload).await.unwrap();
751- let failure = receive_until(
752- &mut harness.events,
753- |event| matches!(event, Event::CommandFailed(failure) if failure.id == id),
754- )
755- .await;
756- assert!(matches!(
757- failure,
758- Event::CommandFailed(failure) if failure.kind == CommandErrorKind::MediaUnavailable
642+ upload.thumbnail = Some(UploadThumbnail::new(
643+ b"thumbnail".to_vec(),
644+ "image/png",
645+ 80,
646+ 80,
759647 ));
648+ let error = harness.handle.upload(upload).await.unwrap_err();
649+ assert_eq!(error.kind, ErrorKind::Invalid);
760650 assert!(
761651 !harness
762652 .server
@@ -766,33 +656,22 @@ async fn audio_thumbnail_fails_without_uploading() {
766656 .iter()
767657 .any(|request| { request.url.path().ends_with("/_matrix/media/v3/upload") })
768658 );
769- stop(harness).await;
659+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
770660 }
771661
772662 #[tokio::test]
773-async fn invalid_upload_mime_reports_a_command_failure() {
663+async fn invalid_upload_mime_fails_without_uploading() {
774664 let mut harness = start(plain_image()).await;
775- let id = harness
776- .handle
777- .upload_media(
778- room_id!("!media:localhost").to_owned().into(),
779- "photo.jpg".to_owned(),
780- "not a MIME type".to_owned(),
781- b"photo data".to_vec(),
782- None,
783- )
784- .await
785- .unwrap();
786- let event = receive_until(
787- &mut harness.events,
788- |event| matches!(event, Event::CommandFailed(failure) if failure.id == id),
789- )
790- .await;
791- assert!(
792- matches!(event, Event::CommandFailed(failure) if failure.kind == CommandErrorKind::Other)
665+ let upload = AttachmentUpload::new(
666+ room_id!("!media:localhost").to_owned(),
667+ "photo.jpg",
668+ "not a MIME type",
669+ b"photo data".to_vec(),
793670 );
671+ let error = harness.handle.upload(upload).await.unwrap_err();
672+ assert_eq!(error.kind, ErrorKind::Invalid);
794673
795- stop(harness).await;
674+ support::shut_down(&harness.handle, &mut harness.events, harness.running).await;
796675 }
797676
798677 async fn start(content: RoomMessageEventContent) -> MediaHarness {
@@ -818,11 +697,8 @@ async fn start_with_config(content: RoomMessageEventContent, config: EngineConfi
818697 let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
819698 let running = tokio::spawn(engine.run());
820699
821- handle
822- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))
823- .await
824- .unwrap();
825- receive_until(
700+ handle.open_room(room_id.to_owned()).await.unwrap();
701+ support::receive_until(
826702 &mut events,
827703 |event| matches!(event, Event::TimelineReset { timeline_id: reset, .. } if reset.room_id() == room_id),
828704 )
@@ -835,11 +711,14 @@ async fn start_with_config(content: RoomMessageEventContent, config: EngineConfi
835711 .add_timeline_event(EventFactory::new().event(content).sender(&ALICE)),
836712 )
837713 .await;
838- let event = receive_until(&mut events, |event| {
839- event_messages(event).any(|message| message.media.is_some())
714+ let event = support::receive_until(&mut events, |event| {
715+ support::event_messages(event)
716+ .into_iter()
717+ .any(|message| message.media.is_some())
840718 })
841719 .await;
842- let media_id = event_messages(&event)
720+ let media_id = support::event_messages(&event)
721+ .into_iter()
843722 .find_map(|message| message.media.as_ref().map(|media| media.id.clone()))
844723 .unwrap();
845724
@@ -858,37 +737,20 @@ async fn mount_downloads(server: &MatrixMockServer) {
858737 server.mock_authed_media_download().ok_image().mount().await;
859738 }
860739
861-async fn fetch_ready(harness: &mut MediaHarness, size: MediaSize) -> Arc<[u8]> {
862- harness
863- .handle
864- .send(Command::Media(MediaCommand::Fetch {
865- id: harness.media_id.clone(),
866- size,
867- }))
868- .await
869- .unwrap();
870- match receive_until(&mut harness.events, |event| {
871- matches!(
872- event,
873- Event::MediaReady { media_id, size: received, .. }
874- if media_id == &harness.media_id && received == &size
875- )
876- })
877- .await
878- {
879- Event::MediaReady { bytes, .. } => bytes,
880- _ => unreachable!(),
740+fn thumbnail_size() -> MediaSize {
741+ MediaSize::Thumbnail {
742+ width: 800,
743+ height: 800,
881744 }
882745 }
883746
884-async fn stop(mut harness: MediaHarness) {
747+async fn fetch(harness: &MediaHarness, size: MediaSize) -> Arc<[u8]> {
885748 harness
886749 .handle
887- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
750+ .media(&harness.media_id, size)
888751 .await
889- .unwrap();
890- receive_until(&mut harness.events, |event| matches!(event, Event::Stopped)).await;
891- harness.running.await.unwrap().unwrap();
752+ .unwrap()
753+ .bytes
892754 }
893755
894756 async fn media_paths(server: &MatrixMockServer) -> Vec<String> {
@@ -948,45 +810,3 @@ fn plain_audio() -> RoomMessageEventContent {
948810 mxc_uri!("mxc://localhost/audio").to_owned(),
949811 )))
950812 }
951-
952-fn event_messages(event: &Event) -> std::vec::IntoIter<&macltok::TimelineMessage> {
953- let items = match event {
954- Event::TimelineReset { items, .. } => items.iter().collect::<Vec<_>>(),
955- Event::TimelineChanged { diffs, .. } => {
956- diffs.iter().flat_map(diff_items).collect::<Vec<_>>()
957- }
958- _ => Vec::new(),
959- };
960- items
961- .into_iter()
962- .filter_map(|item| match item {
963- TimelineItem::Message(message) => Some(message.as_ref()),
964- _ => None,
965- })
966- .collect::<Vec<_>>()
967- .into_iter()
968-}
969-
970-fn diff_items(diff: &TimelineDiff) -> Vec<&TimelineItem> {
971- match diff {
972- TimelineDiff::Append { values } | TimelineDiff::Reset { values } => values.iter().collect(),
973- TimelineDiff::PushFront { value }
974- | TimelineDiff::PushBack { value }
975- | TimelineDiff::Insert { value, .. }
976- | TimelineDiff::Set { value, .. } => vec![value],
977- _ => Vec::new(),
978- }
979-}
980-
981-async fn receive_until(events: &mut EventStream, predicate: impl Fn(&Event) -> bool) -> Event {
982- tokio::time::timeout(Duration::from_secs(5), async {
983- while let Some(event) = events.recv().await {
984- if predicate(&event) {
985- return event;
986- }
987- }
988- panic!("event stream closed before the expected event");
989- })
990- .await
991- .expect("expected event did not arrive")
992-}
Modified file: tests/members.rs+9 additions−5 deletions
@@ -1,4 +1,4 @@
1-use macltok::{ClientQueryError, Engine, EngineConfig, MemberFilter, MemberMembership};
1+use macltok::{Engine, EngineConfig, ErrorKind, MemberFilter, MemberMembership};
22 use matrix_sdk::{
33 ruma::{
44 api::client::sync::sync_events::v5, events::room::member::MembershipState, room_id, user_id,
@@ -191,10 +191,14 @@ async fn members_rejects_unknown_room() {
191191 .unwrap();
192192 let room_id = room_id!("!missing:localhost");
193193
194- assert!(matches!(
195- handle.members(room_id, MemberFilter::All).await,
196- Err(ClientQueryError::RoomUnknown(missing)) if missing == room_id
197- ));
194+ assert_eq!(
195+ handle
196+ .members(room_id, MemberFilter::All)
197+ .await
198+ .unwrap_err()
199+ .kind,
200+ ErrorKind::RoomUnknown
201+ );
198202
199203 drop(engine);
200204 }
Modified file: tests/nonblocking.rs+30 additions−110 deletions
@@ -1,15 +1,16 @@
1+mod support;
2+
13 use std::time::Duration;
24
35 use macltok::{
4- Command, CommandErrorKind, CommandKind, Engine, EngineConfig, Event, LifecycleCommand,
5- MediaCommand, MediaSize, MessageContent, RoomCommand, TimelineCommand, TimelineDiff,
6- TimelineItem,
6+ Command, Engine, EngineConfig, ErrorKind, Event, MediaSize, MessageContent, TimelineCommand,
77 };
88 use matrix_sdk::{
99 ruma::{api::client::sync::sync_events::v5, mxc_uri, room_id},
1010 test_utils::mocks::MatrixMockServer,
1111 };
1212 use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};
13+use support::{event_messages, receive_until, shut_down};
1314 use wiremock::ResponseTemplate;
1415
1516 #[tokio::test]
@@ -29,10 +30,7 @@ async fn delayed_media_fetch_does_not_block_timeline_command() {
2930 let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
3031 let running = tokio::spawn(engine.run());
3132
32- handle
33- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))
34- .await
35- .unwrap();
33+ handle.open_room(room_id.to_owned()).await.unwrap();
3634 receive_until(
3735 &mut events,
3836 |event| matches!(event, Event::TimelineReset { timeline_id: reset, .. } if reset.room_id() == room_id),
@@ -53,10 +51,13 @@ async fn delayed_media_fetch_does_not_block_timeline_command() {
5351 )
5452 .await;
5553 let media_event = receive_until(&mut events, |event| {
56- event_messages(event).any(|message| message.media.is_some())
54+ event_messages(event)
55+ .into_iter()
56+ .any(|message| message.media.is_some())
5757 })
5858 .await;
5959 let media_id = event_messages(&media_event)
60+ .into_iter()
6061 .find_map(|message| message.media.as_ref().map(|media| media.id.clone()))
6162 .unwrap();
6263
@@ -76,65 +77,34 @@ async fn delayed_media_fetch_does_not_block_timeline_command() {
7677 .mount()
7778 .await;
7879
79- let media_command_id = handle
80- .send(Command::Media(MediaCommand::Fetch {
81- id: media_id.clone(),
82- size: MediaSize::Full,
83- }))
84- .await
85- .unwrap();
80+ // media no longer travels through the command queue, so driving it on its
81+ // own task is what puts the request in flight concurrently with the send below
82+ let media_task = tokio::spawn({
83+ let handle = handle.clone();
84+ async move { handle.media(&media_id, MediaSize::Full).await }
85+ });
8686 wait_for_media_request(&server).await;
87- let timeline_command_id = handle
88- .send(Command::Timeline(TimelineCommand::Send {
87+
88+ let error = tokio::time::timeout(
89+ Duration::from_millis(300),
90+ handle.execute(Command::Timeline(TimelineCommand::Send {
8991 timeline_id: room_id.to_owned().into(),
9092 content: MessageContent::text(String::new()),
91- }))
92- .await
93- .unwrap();
94-
95- tokio::time::timeout(Duration::from_secs(2), async {
96- loop {
97- match events.recv().await.expect("event stream closed") {
98- Event::CommandFailed(failure) if failure.id == timeline_command_id => {
99- assert_eq!(failure.command, CommandKind::Timeline);
100- assert_eq!(failure.kind, CommandErrorKind::EmptyBody);
101- break;
102- }
103- Event::CommandSucceeded { id, .. } if id == media_command_id => {
104- panic!("media command completed before the timeline command")
105- }
106- Event::CommandFailed(failure) if failure.id == media_command_id => {
107- panic!("media command failed before the timeline command {failure:?}")
108- }
109- Event::MediaReady {
110- media_id: ready_id, ..
111- } if ready_id == media_id => {
112- panic!("media became ready before the timeline command completed")
113- }
114- _ => {}
115- }
116- }
117- })
93+ })),
94+ )
11895 .await
119- .expect("timeline command was blocked by media fetch");
120-
121- receive_until(&mut events, |event| {
122- matches!(
123- event,
124- Event::CommandSucceeded {
125- id,
126- command: CommandKind::Media,
127- } if id == &media_command_id
128- )
129- })
130- .await;
96+ .expect("timeline command was blocked by media fetch")
97+ .expect_err("empty body command unexpectedly succeeded");
98+ assert_eq!(error.kind, ErrorKind::EmptyBody);
13199
132- handle
133- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
100+ let media = tokio::time::timeout(Duration::from_secs(2), media_task)
134101 .await
102+ .expect("media fetch did not complete")
103+ .unwrap()
135104 .unwrap();
136- receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;
137- running.await.unwrap().unwrap();
105+ assert!(!media.bytes.is_empty());
106+
107+ shut_down(&handle, &mut events, running).await;
138108 }
139109
140110 async fn wait_for_media_request(server: &MatrixMockServer) {
@@ -155,53 +125,3 @@ async fn wait_for_media_request(server: &MatrixMockServer) {
155125 .await
156126 .expect("media request did not reach the homeserver");
157127 }
158-
159-fn event_messages(event: &Event) -> std::vec::IntoIter<&macltok::TimelineMessage> {
160- let items = match event {
161- Event::TimelineReset { items, .. } => items.iter().collect::<Vec<_>>(),
162- Event::TimelineChanged { diffs, .. } => {
163- diffs.iter().flat_map(diff_items).collect::<Vec<_>>()
164- }
165- _ => Vec::new(),
166- };
167- items
168- .into_iter()
169- .filter_map(|item| match item {
170- TimelineItem::Message(message) => Some(message.as_ref()),
171- _ => None,
172- })
173- .collect::<Vec<_>>()
174- .into_iter()
175-}
176-
177-fn diff_items(diff: &TimelineDiff) -> Vec<&TimelineItem> {
178- match diff {
179- TimelineDiff::Append { values } | TimelineDiff::Reset { values } => values.iter().collect(),
180- TimelineDiff::PushFront { value }
181- | TimelineDiff::PushBack { value }
182- | TimelineDiff::Insert { value, .. }
183- | TimelineDiff::Set { value, .. } => vec![value],
184- TimelineDiff::Clear
185- | TimelineDiff::PopFront
186- | TimelineDiff::PopBack
187- | TimelineDiff::Remove { .. }
188- | TimelineDiff::Truncate { .. } => Vec::new(),
189- _ => Vec::new(),
190- }
191-}
192-
193-async fn receive_until(
194- events: &mut macltok::EventStream,
195- predicate: impl Fn(&Event) -> bool,
196-) -> Event {
197- tokio::time::timeout(Duration::from_secs(2), async {
198- while let Some(event) = events.recv().await {
199- if predicate(&event) {
200- return event;
201- }
202- }
203- panic!("event stream closed before the expected event")
204- })
205- .await
206- .expect("expected event did not arrive")
207-}
Modified file: tests/send_queue.rs+43 additions−107 deletions
@@ -1,8 +1,9 @@
1+mod support;
2+
13 use std::time::Duration;
24
35 use macltok::{
4- ClientHandle, Command, CommandErrorKind, CommandId, CommandKind, Engine, EngineConfig, Event,
5- EventStream, LifecycleCommand, MessageContent, RoomCommand, TimelineCommand, TimelineItem,
6+ AttachmentUpload, ClientHandle, Engine, EngineConfig, ErrorKind, EventStream, MessageContent,
67 };
78 use matrix_sdk::{
89 Client,
@@ -24,11 +25,10 @@ async fn cancel_send_uses_transaction_id_from_public_timeline() {
2425 .unwrap()
2526 .contains(&transaction_id)
2627 );
27- let id = handle
28+ handle
2829 .cancel_send(room_id.to_owned(), transaction_id.clone())
2930 .await
3031 .unwrap();
31- expect_success(&mut events, id, CommandKind::Room).await;
3232
3333 let (echoes, _) = client
3434 .get_room(room_id)
@@ -43,20 +43,13 @@ async fn cancel_send_uses_transaction_id_from_public_timeline() {
4343 .any(|echo| echo.transaction_id == transaction_id)
4444 );
4545
46- let id = handle
46+ let error = handle
4747 .cancel_send(room_id.to_owned(), transaction_id)
4848 .await
49- .unwrap();
50- let failure = receive_until(
51- &mut events,
52- |event| matches!(event, Event::CommandFailed(failure) if failure.id == id),
53- )
54- .await;
55- assert!(
56- matches!(failure, Event::CommandFailed(failure) if failure.kind == CommandErrorKind::Other)
57- );
49+ .unwrap_err();
50+ assert_eq!(error.kind, ErrorKind::Invalid);
5851
59- stop(handle, &mut events, running).await;
52+ support::shut_down(&handle, &mut events, running).await;
6053 drop(server);
6154 }
6255
@@ -66,28 +59,25 @@ async fn cancel_queued_attachment_by_pending_send_id() {
6659 let room_id = room_id!("!queue-controls:localhost");
6760 client.send_queue().set_enabled(false).await;
6861
69- let id = handle
70- .upload_media(
71- room_id.to_owned().into(),
72- "note.txt".to_owned(),
73- "text/plain".to_owned(),
62+ handle
63+ .upload(AttachmentUpload::new(
64+ room_id.to_owned(),
65+ "note.txt",
66+ "text/plain",
7467 b"attachment".to_vec(),
75- None,
76- )
68+ ))
7769 .await
7870 .unwrap();
79- expect_success(&mut events, id, CommandKind::Media).await;
8071
8172 let pending = handle.pending_send_ids(room_id).await.unwrap();
8273 assert_eq!(pending.len(), 1);
83- let id = handle
74+ handle
8475 .cancel_send(room_id.to_owned(), pending[0].clone())
8576 .await
8677 .unwrap();
87- expect_success(&mut events, id, CommandKind::Room).await;
8878 assert!(handle.pending_send_ids(room_id).await.unwrap().is_empty());
8979
90- stop(handle, &mut events, running).await;
80+ support::shut_down(&handle, &mut events, running).await;
9181 drop(server);
9282 }
9383
@@ -109,11 +99,10 @@ async fn retry_send_wakes_paused_queue() {
10999 .await;
110100
111101 let transaction_id = queue_message(&handle, &mut events, room_id, "retry me").await;
112- let id = handle
102+ handle
113103 .retry_send(room_id.to_owned(), transaction_id)
114104 .await
115105 .unwrap();
116- expect_success(&mut events, id, CommandKind::Room).await;
117106 assert!(client.get_room(room_id).unwrap().send_queue().is_enabled());
118107
119108 tokio::time::timeout(Duration::from_secs(5), async {
@@ -134,7 +123,7 @@ async fn retry_send_wakes_paused_queue() {
134123 .await
135124 .expect("retried message was not sent");
136125
137- stop(handle, &mut events, running).await;
126+ support::shut_down(&handle, &mut events, running).await;
138127 drop(server);
139128 }
140129
@@ -155,101 +144,48 @@ async fn setup() -> (
155144 let room_id = room_id!("!queue-controls:localhost");
156145 server.mock_room_state_encryption().plain().mount().await;
157146 server.sync_joined_room(&client, room_id).await;
158- let (engine, handle, events) = Engine::from_client(
147+ let (engine, handle, mut events) = Engine::from_client(
159148 client.clone(),
160149 EngineConfig::default().with_initial_history_limit(0),
161150 )
162151 .await
163152 .unwrap();
164153 let running = tokio::spawn(engine.run());
165- let mut events = events;
166- let id = handle
167- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))
154+ let (opened, _) = tokio::join!(handle.open_room(room_id.to_owned()), async {
155+ support::receive_until(&mut events, |event| {
156+ matches!(event, macltok::Event::TimelineReset { .. })
157+ })
168158 .await
169- .unwrap();
170- expect_success(&mut events, id, CommandKind::Room).await;
159+ });
160+ opened.unwrap();
171161 (server, client, handle, events, running)
172162 }
173163
164+/// sends a message and waits for its local echo, since only the timeline reveals the transaction id
174165 async fn queue_message(
175166 handle: &ClientHandle,
176167 events: &mut EventStream,
177168 room_id: &RoomId,
178169 body: &str,
179170 ) -> OwnedTransactionId {
180- let id = handle
181- .send(Command::Timeline(TimelineCommand::Send {
182- timeline_id: room_id.to_owned().into(),
183- content: MessageContent::text(body),
184- }))
185- .await
186- .unwrap();
187- let mut succeeded = false;
188- let mut items = Vec::new();
189- tokio::time::timeout(Duration::from_secs(5), async {
190- loop {
191- match events.recv().await.expect("event stream closed") {
192- Event::TimelineChanged { diffs, .. } => {
193- for diff in diffs {
194- diff.try_apply(&mut items).unwrap();
195- }
196- let transaction = items.iter().find_map(|item| match item {
197- TimelineItem::Message(message) if message.body == body => {
198- message.transaction_id.clone()
199- }
200- _ => None,
201- });
202- if let Some(transaction) = transaction {
203- if !succeeded {
204- expect_success(events, id, CommandKind::Timeline).await;
205- }
206- return transaction;
207- }
171+ let send = handle.send_message(room_id.to_owned().into(), MessageContent::text(body));
172+ let wait_for_echo = async {
173+ tokio::time::timeout(Duration::from_secs(5), async {
174+ loop {
175+ let event = events.recv().await.expect("event stream closed");
176+ let transaction = support::event_messages(&event)
177+ .into_iter()
178+ .find(|message| message.body == body)
179+ .and_then(|message| message.transaction_id.clone());
180+ if let Some(transaction) = transaction {
181+ return transaction;
208182 }
209- Event::CommandSucceeded {
210- id: completed,
211- command: CommandKind::Timeline,
212- } if completed == id => succeeded = true,
213- Event::CommandFailed(failure) if failure.id == id => {
214- panic!("send failed {failure:?}")
215- }
216- _ => {}
217- }
218- }
219- })
220- .await
221- .expect("local echo did not appear")
222-}
223-
224-async fn expect_success(events: &mut EventStream, id: CommandId, command: CommandKind) {
225- receive_until(events, |event| {
226- matches!(event, Event::CommandSucceeded { id: completed, command: kind } if *completed == id && *kind == command)
227- })
228- .await;
229-}
230-
231-async fn receive_until(events: &mut EventStream, predicate: impl Fn(&Event) -> bool) -> Event {
232- tokio::time::timeout(Duration::from_secs(5), async {
233- loop {
234- let event = events.recv().await.expect("event stream closed");
235- if predicate(&event) {
236- return event;
237183 }
238- }
239- })
240- .await
241- .expect("expected event did not arrive")
242-}
243-
244-async fn stop(
245- handle: ClientHandle,
246- events: &mut EventStream,
247- running: tokio::task::JoinHandle<Result<(), macltok::EngineError>>,
248-) {
249- let id = handle
250- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
184+ })
251185 .await
252- .unwrap();
253- expect_success(events, id, CommandKind::Lifecycle).await;
254- running.await.unwrap().unwrap();
186+ .expect("local echo did not appear")
187+ };
188+ let (result, transaction_id) = tokio::join!(send, wait_for_echo);
189+ result.unwrap();
190+ transaction_id
255191 }
Modified file: tests/state.rs+11 additions−38 deletions
@@ -1,9 +1,8 @@
1+mod support;
2+
13 use std::time::Duration;
24
3-use macltok::{
4- Command, ConnectionState, Engine, EngineConfig, EngineError, Event, LifecycleCommand,
5- RoomMembership,
6-};
5+use macltok::{ConnectionState, Engine, EngineConfig, EngineError, Event, RoomMembership};
76 use matrix_sdk::{
87 ruma::{api::client::sync::sync_events::v5, room_id},
98 test_utils::mocks::MatrixMockServer,
@@ -26,7 +25,7 @@ async fn rooms_changed_invalidates_room_summaries() {
2625 let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();
2726 let running = tokio::spawn(engine.run());
2827
29- receive_until(&mut events, |event| matches!(event, Event::RoomsChanged)).await;
28+ support::receive_until(&mut events, |event| matches!(event, Event::RoomsChanged)).await;
3029 let rooms = handle.rooms().await;
3130 let summary = rooms
3231 .iter()
@@ -35,12 +34,7 @@ async fn rooms_changed_invalidates_room_summaries() {
3534 assert_eq!(summary.membership, RoomMembership::Joined);
3635 assert!(!summary.name.is_empty());
3736
38- handle
39- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
40- .await
41- .unwrap();
42- receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;
43- running.await.unwrap().unwrap();
37+ support::shut_down(&handle, &mut events, running).await;
4438 }
4539
4640 #[tokio::test]
@@ -60,24 +54,19 @@ async fn connection_watch_tracks_the_engine_lifecycle() {
6054 assert_eq!(*connection_peer.borrow(), ConnectionState::Idle);
6155
6256 let running = tokio::spawn(engine.run());
63- receive_until(&mut events, |event| {
57+ support::receive_until(&mut events, |event| {
6458 matches!(event, Event::ConnectionChanged(ConnectionState::Connecting))
6559 })
6660 .await;
6761 assert_ne!(*connection.borrow(), ConnectionState::Idle);
68- receive_until(&mut events, |event| {
62+ support::receive_until(&mut events, |event| {
6963 matches!(event, Event::ConnectionChanged(ConnectionState::Online))
7064 })
7165 .await;
7266 wait_for_connection(&mut connection, ConnectionState::Online).await;
7367 assert_eq!(*connection_peer.borrow(), ConnectionState::Online);
7468
75- handle
76- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
77- .await
78- .unwrap();
79- receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;
80- running.await.unwrap().unwrap();
69+ support::shut_down(&handle, &mut events, running).await;
8170 wait_for_connection(&mut connection, ConnectionState::Stopped).await;
8271 assert_eq!(*connection_peer.borrow(), ConnectionState::Stopped);
8372 }
@@ -98,11 +87,11 @@ async fn soft_logout_says_so_and_stops_the_engine() {
9887 .await
9988 .unwrap();
10089 let running = tokio::spawn(engine.run());
101- receive_until(&mut events, |event| {
90+ support::receive_until(&mut events, |event| {
10291 matches!(event, Event::SessionAccessRejected { soft_logout: true })
10392 })
10493 .await;
105- receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;
94+ support::receive_until(&mut events, |event| matches!(event, Event::Stopped)).await;
10695 running.await.unwrap().unwrap();
10796 }
10897
@@ -122,7 +111,7 @@ async fn closing_the_event_stream_stops_an_idle_engine() {
122111 .await
123112 .unwrap();
124113 let running = tokio::spawn(engine.run());
125- receive_until(&mut events, |event| {
114+ support::receive_until(&mut events, |event| {
126115 matches!(event, Event::ConnectionChanged(ConnectionState::Online))
127116 })
128117 .await;
@@ -153,19 +142,3 @@ async fn wait_for_connection(
153142 .await
154143 .expect("expected connection state did not arrive");
155144 }
156-
157-async fn receive_until(
158- events: &mut macltok::EventStream,
159- predicate: impl Fn(&Event) -> bool,
160-) -> Event {
161- tokio::time::timeout(Duration::from_secs(2), async {
162- while let Some(event) = events.recv().await {
163- if predicate(&event) {
164- return event;
165- }
166- }
167- panic!("event stream closed before the expected event");
168- })
169- .await
170- .expect("expected event did not arrive")
171-}
Added file: tests/support/mod.rs+57 additions
@@ -0,0 +1,57 @@
1+#![allow(dead_code)]
2+
3+use std::time::Duration;
4+
5+use macltok::{ClientHandle, Event, EventStream, TimelineDiff, TimelineItem, TimelineMessage};
6+
7+pub async fn receive_until(events: &mut EventStream, predicate: impl Fn(&Event) -> bool) -> Event {
8+ tokio::time::timeout(Duration::from_secs(5), async {
9+ while let Some(event) = events.recv().await {
10+ if predicate(&event) {
11+ return event;
12+ }
13+ }
14+ panic!("event stream closed before the expected event");
15+ })
16+ .await
17+ .expect("expected event did not arrive")
18+}
19+
20+/// asks for a shutdown while still draining, since the engine won't answer until its events are out
21+pub async fn shut_down(
22+ handle: &ClientHandle,
23+ events: &mut EventStream,
24+ running: tokio::task::JoinHandle<Result<(), macltok::EngineError>>,
25+) {
26+ let (answer, _) = tokio::join!(handle.shutdown(), async {
27+ receive_until(events, |event| matches!(event, Event::Stopped)).await
28+ });
29+ answer.unwrap();
30+ running.await.unwrap().unwrap();
31+}
32+
33+pub fn event_messages(event: &Event) -> Vec<&TimelineMessage> {
34+ let items = match event {
35+ Event::TimelineReset { items, .. } => items.iter().collect::<Vec<_>>(),
36+ Event::TimelineChanged { diffs, .. } => diffs.iter().flat_map(diff_items).collect(),
37+ _ => Vec::new(),
38+ };
39+ items
40+ .into_iter()
41+ .filter_map(|item| match item {
42+ TimelineItem::Message(message) => Some(message.as_ref()),
43+ _ => None,
44+ })
45+ .collect()
46+}
47+
48+pub fn diff_items(diff: &TimelineDiff) -> Vec<&TimelineItem> {
49+ match diff {
50+ TimelineDiff::Append { values } | TimelineDiff::Reset { values } => values.iter().collect(),
51+ TimelineDiff::PushFront { value }
52+ | TimelineDiff::PushBack { value }
53+ | TimelineDiff::Insert { value, .. }
54+ | TimelineDiff::Set { value, .. } => vec![value],
55+ _ => Vec::new(),
56+ }
57+}
Modified file: tests/tier0.rs+76 additions−126 deletions
@@ -1,9 +1,8 @@
1-use std::time::Duration;
1+mod support;
22
33 use macltok::{
4- ClientHandle, ClientQueryError, Command, CommandErrorKind, CommandId, CommandKind,
5- CreateRoomOptions, Engine, EngineConfig, EngineError, Event, EventStream, LifecycleCommand,
6- RoomCommand, RoomPreset, RoomVisibility,
4+ ClientHandle, CreateRoomOptions, Engine, EngineConfig, EngineError, ErrorKind, EventStream,
5+ RoomPreset, RoomVisibility,
76 };
87 use matrix_sdk::{
98 ruma::{
@@ -43,11 +42,11 @@ async fn joining_by_room_id_forwards_via_servers() {
4342 .await
4443 .unwrap();
4544 assert_eq!(joined_room, room_id);
46- stop_engine(handle, &mut events, running).await;
45+ support::shut_down(&handle, &mut events, running).await;
4746 }
4847
4948 #[tokio::test]
50-async fn leave_and_forget_report_correlated_success() {
49+async fn leaving_then_forgetting_a_room_hits_the_expected_endpoints() {
5150 let room_id = room_id!("!leave:localhost");
5251 let (server, client) = configured_client().await;
5352 server.sync_joined_room(&client, room_id).await;
@@ -55,15 +54,11 @@ async fn leave_and_forget_report_correlated_success() {
5554 server.mock_room_forget().ok().expect(1).mount().await;
5655 let (handle, mut events, running) = run_client(client).await;
5756
58- let leave_id = handle.leave_room(room_id.to_owned()).await.unwrap();
59- receive_room_success(&mut events, leave_id).await;
60- let forget_id = handle
61- .send(Command::Room(RoomCommand::Forget(room_id.to_owned())))
62- .await
63- .unwrap();
64- receive_room_success(&mut events, forget_id).await;
65- assert_ne!(leave_id, forget_id);
66- stop_engine(handle, &mut events, running).await;
57+ handle.leave_room(room_id.to_owned()).await.unwrap();
58+ handle.forget_room(room_id.to_owned()).await.unwrap();
59+
60+ support::shut_down(&handle, &mut events, running).await;
61+ drop(server);
6762 }
6863
6964 #[tokio::test]
@@ -86,11 +81,10 @@ async fn accepting_and_rejecting_invites_use_the_expected_endpoints() {
8681 .await;
8782 let (handle, mut events, running) = run_client(client).await;
8883
89- let accept_id = handle.accept_invite(accepted.to_owned()).await.unwrap();
90- receive_room_success(&mut events, accept_id).await;
91- let reject_id = handle.reject_invite(rejected.to_owned()).await.unwrap();
92- receive_room_success(&mut events, reject_id).await;
93- stop_engine(handle, &mut events, running).await;
84+ handle.accept_invite(accepted.to_owned()).await.unwrap();
85+ handle.reject_invite(rejected.to_owned()).await.unwrap();
86+
87+ support::shut_down(&handle, &mut events, running).await;
9488 }
9589
9690 #[tokio::test]
@@ -114,19 +108,19 @@ async fn creating_a_room_forwards_options() {
114108 .await;
115109
116110 let created_room = handle
117- .create_room(CreateRoomOptions {
118- name: Some("Planning".to_owned()),
119- topic: Some("Next release".to_owned()),
120- alias_localpart: Some("planning".to_owned()),
121- invite: vec![user_id!("@partner:localhost").to_owned()],
122- visibility: RoomVisibility::Public,
123- preset: Some(RoomPreset::PublicChat),
124- ..Default::default()
125- })
111+ .create_room(
112+ CreateRoomOptions::default()
113+ .with_name("Planning")
114+ .with_topic("Next release")
115+ .with_alias_localpart("planning")
116+ .with_invite(vec![user_id!("@partner:localhost").to_owned()])
117+ .with_visibility(RoomVisibility::Public)
118+ .with_preset(RoomPreset::PublicChat),
119+ )
126120 .await
127121 .unwrap();
128122 assert_eq!(created_room, room_id!("!created:localhost").to_owned());
129- stop_engine(handle, &mut events, running).await;
123+ support::shut_down(&handle, &mut events, running).await;
130124 }
131125
132126 #[tokio::test]
@@ -167,23 +161,48 @@ async fn creating_a_direct_room_invites_the_other_user() {
167161 .await
168162 .unwrap();
169163 assert_eq!(direct_room, room_id!("!direct:localhost").to_owned());
170- stop_engine(handle, &mut events, running).await;
164+ support::shut_down(&handle, &mut events, running).await;
171165 }
172166
173167 #[tokio::test]
174-async fn unknown_rooms_fail_membership_commands_with_their_ids() {
168+async fn unknown_rooms_fail_membership_commands_with_room_unknown() {
175169 let (server, handle, mut events, running) = start_engine().await;
176170 let unknown = room_id!("!unknown:localhost");
177- for command in [
178- RoomCommand::Leave(unknown.to_owned()),
179- RoomCommand::Forget(unknown.to_owned()),
180- RoomCommand::AcceptInvite(unknown.to_owned()),
181- RoomCommand::RejectInvite(unknown.to_owned()),
182- ] {
183- let id = handle.send(Command::Room(command)).await.unwrap();
184- receive_room_failure(&mut events, id, CommandErrorKind::RoomUnknown).await;
185- }
186- stop_engine(handle, &mut events, running).await;
171+
172+ assert_eq!(
173+ handle
174+ .leave_room(unknown.to_owned())
175+ .await
176+ .unwrap_err()
177+ .kind,
178+ ErrorKind::RoomUnknown
179+ );
180+ assert_eq!(
181+ handle
182+ .forget_room(unknown.to_owned())
183+ .await
184+ .unwrap_err()
185+ .kind,
186+ ErrorKind::RoomUnknown
187+ );
188+ assert_eq!(
189+ handle
190+ .accept_invite(unknown.to_owned())
191+ .await
192+ .unwrap_err()
193+ .kind,
194+ ErrorKind::RoomUnknown
195+ );
196+ assert_eq!(
197+ handle
198+ .reject_invite(unknown.to_owned())
199+ .await
200+ .unwrap_err()
201+ .kind,
202+ ErrorKind::RoomUnknown
203+ );
204+
205+ support::shut_down(&handle, &mut events, running).await;
187206 drop(server);
188207 }
189208
@@ -235,12 +254,14 @@ async fn account_data_reads_synced_values_and_writes_raw_content() {
235254 room.deserialize_as::<serde_json::Value>().unwrap()["content"]["event_id"],
236255 "$seen:localhost"
237256 );
238- assert!(matches!(
257+ assert_eq!(
239258 handle
240259 .room_account_data(room_id!("!missing:localhost"), room_type.clone())
241- .await,
242- Err(ClientQueryError::RoomUnknown(_))
243- ));
260+ .await
261+ .unwrap_err()
262+ .kind,
263+ ErrorKind::RoomUnknown
264+ );
244265
245266 Mock::given(method("PUT"))
246267 .and(path(
@@ -268,7 +289,7 @@ async fn account_data_reads_synced_values_and_writes_raw_content() {
268289 .unwrap();
269290 handle
270291 .set_room_account_data(
271- room_id,
292+ room_id.to_owned(),
272293 room_type,
273294 Raw::new(&json!({ "event_id": "$later:localhost" }))
274295 .unwrap()
@@ -276,19 +297,21 @@ async fn account_data_reads_synced_values_and_writes_raw_content() {
276297 )
277298 .await
278299 .unwrap();
279- assert!(matches!(
300+ assert_eq!(
280301 handle
281302 .set_room_account_data(
282- room_id!("!missing:localhost"),
303+ room_id!("!missing:localhost").to_owned(),
283304 RoomAccountDataEventType::FullyRead,
284305 Raw::new(&json!({ "event_id": "$later:localhost" }))
285306 .unwrap()
286307 .cast_unchecked()
287308 )
288- .await,
289- Err(ClientQueryError::RoomUnknown(_))
290- ));
291- stop_engine(handle, &mut events, running).await;
309+ .await
310+ .unwrap_err()
311+ .kind,
312+ ErrorKind::RoomUnknown
313+ );
314+ support::shut_down(&handle, &mut events, running).await;
292315 }
293316
294317 async fn configured_client() -> (MatrixMockServer, matrix_sdk::Client) {
@@ -326,76 +349,3 @@ async fn run_client(
326349 let running = tokio::spawn(engine.run());
327350 (handle, events, running)
328351 }
329-
330-async fn receive_room_success(events: &mut EventStream, expected_id: CommandId) {
331- tokio::time::timeout(Duration::from_secs(3), async {
332- loop {
333- match events.recv().await.expect("event stream closed") {
334- Event::CommandSucceeded {
335- id,
336- command: CommandKind::Room,
337- } if id == expected_id => break,
338- Event::CommandFailed(failure) if failure.id == expected_id => {
339- panic!("room command failed {failure:?}")
340- }
341- _ => {}
342- }
343- }
344- })
345- .await
346- .expect("room command did not complete");
347-}
348-
349-async fn receive_room_failure(
350- events: &mut EventStream,
351- expected_id: CommandId,
352- expected_kind: CommandErrorKind,
353-) {
354- tokio::time::timeout(Duration::from_secs(3), async {
355- loop {
356- match events.recv().await.expect("event stream closed") {
357- Event::CommandFailed(failure) if failure.id == expected_id => {
358- assert_eq!(failure.command, CommandKind::Room);
359- assert_eq!(failure.kind, expected_kind);
360- break;
361- }
362- Event::CommandSucceeded { id, .. } if id == expected_id => {
363- panic!("room command unexpectedly succeeded")
364- }
365- _ => {}
366- }
367- }
368- })
369- .await
370- .expect("room command did not fail");
371-}
372-
373-async fn stop_engine(
374- handle: ClientHandle,
375- events: &mut EventStream,
376- running: JoinHandle<Result<(), EngineError>>,
377-) {
378- let id = handle
379- .send(Command::Lifecycle(LifecycleCommand::Shutdown))
380- .await
381- .unwrap();
382- tokio::time::timeout(Duration::from_secs(3), async {
383- let mut succeeded = false;
384- while let Some(event) = events.recv().await {
385- match event {
386- Event::CommandSucceeded {
387- id: completed,
388- command: CommandKind::Lifecycle,
389- } if completed == id => succeeded = true,
390- Event::Stopped => {
391- assert!(succeeded);
392- break;
393- }
394- _ => {}
395- }
396- }
397- })
398- .await
399- .expect("engine did not stop");
400- running.await.unwrap().unwrap();
401-}

show truncated diff