Commit 31ce2453c82f
tarball32 files changed+5086-6553
@@ -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 }@@ -21,16 +21,16 @@ already have remote event IDs.
2121
2222
## Consuming the engine2323
24
-Run `Engine::run` and drain its single `EventStream` independently of command25
-submission. `ClientHandle::send` returns an enqueue ID; match that ID against26
-`CommandSucceeded`, `CommandSucceededWithRoom`, or `CommandFailed` before27
-reporting completion. A successful message command only enters the SDK send28
-queue; the timeline item's send state reports subsequent network delivery.29
-30
-Start a timeline from `TimelineReset`, then apply `TimelineChanged` diffs with31
-`TimelineDiff::try_apply`. If a diff does not match the current snapshot, stop32
-applying diffs for that timeline and send `TimelineCommand::Resync`. Resume only33
-after its fresh `TimelineReset`; the reset precedes the command completion.24
+spawn `Engine::run`, drain the `EventStream` from its own task, and talk to the25
+engine through `ClientHandle`. every command answers once, awaited26
+(`handle.send_message(..).await?`) or as a `Pending` you can await later or27
+drop. a sent message answers when the sdk send queue has it, delivery shows up28
+as the timeline item's send state. reads like `rooms`, `members` and `media`29
+go straight to the sdk. the stream only carries state30
+31
+start a timeline from `TimelineReset`, then apply `TimelineChanged` diffs with32
+`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 its3636
encrypted SQLite store. On `SessionTokensChanged`, obtain a new record from@@ -1,172 +1,139 @@
11
# macltok design decisions22
3
-This document describes the contract implemented in the working tree.4
-5
-## Engine execution6
-7
-The engine keeps a contextual task registry. Every spawned task is registered8
-with either its command id and kind, a watcher name, or a room id and9
-generation. The run loop continuously joins completed tasks and removes their10
-context. A command-task panic produces its correlated `CommandFailed` event.11
-A watcher or current room-actor panic is terminal to the engine. Shutdown aborts12
-and joins the remaining registered tasks.13
-14
-The engine emits `Authenticated`, then `ConnectionChanged(Connecting)`, before15
-starting the sync service. `ConnectionChanged` also represents the later SDK16
-states and the final `Stopped` transition. `Engine::run` returns an17
-`EngineError` for terminal failure and emits `Failed` before `Stopped` when the18
-event stream remains available.19
-20
-The driver command channel remains lossless and clients must drain21
-`EventStream` independently of command submission. `ClientHandle` also exposes22
-a `watch` receiver for the current connection state. `RoomsChanged` is an23
-invalidation event. Call `ClientHandle::rooms` for current, normalised room24
-summaries.25
-26
-`ClientHandle` sends `Command` values and receives a `CommandId`. Completion is27
-reported as `CommandSucceeded`, `CommandSucceededWithRoom`, or28
-`CommandFailed`. `CommandFailure` contains the id, outer `CommandKind`, typed29
-`CommandErrorKind`, and display message. Direct request and response methods30
-return `CommandResultError`, which is either a delivered `CommandFailure` or a31
-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 id37
-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 while39
-commands are submitted, even when several frontend clients share one engine.40
-41
-## Room lifetime and ordering42
-43
-An open room has an explicit generation and exactly one of three states.44
-45
-- `Opening` owns the generation and actor request sender46
-- `Open` owns the generation and actor request sender47
-- `Closing` owns the generation and a bounded FIFO of deferred open and close commands48
-49
-An `Open` command starts an actor immediately and returns its terminal result50
-only after the actor reports readiness. There is no separate readiness waiter or51
-readiness task. Repeated opens while opening are retained by the actor and each52
-gets one terminal result. A close changes the room to `Closing`, rejects work53
-that needs an open actor, and waits for that generation to exit. Opens and54
-closes during closing are queued in order; after exit, each deferred command is55
-replayed, with a fresh generation started when needed. Events from earlier56
-generations are ignored.57
-58
-Each room actor serialises its timeline work in FIFO order. Different rooms run59
-concurrently. Room mutations which do not require an open timeline use a60
-separate keyed FIFO scheduler. Jobs with the same room, target, or direct-chat61
-key run in submission order while unrelated keys run concurrently. Notification62
-commands and encryption commands each have one dedicated serial worker. The63
-encryption worker owns the verification controller and cancels it before64
-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 its68
-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 the70
-session completion mechanism and is not an engine-wide routing design.71
-72
-`TimelineDiff::try_apply` rejects indices that do not match the consumer's73
-snapshot without modifying that snapshot. A consumer then stops applying diffs74
-for that timeline and sends `TimelineCommand::Resync`. The timeline session75
-aborts and joins its old watcher, atomically subscribes to a fresh SDK snapshot76
-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 a80
-thread by its root event. Thread actors and pagination state belong to the room81
-actor. Stateful timeline commands require the room actor. Standalone room82
-commands such as typing, receipts, send-queue control, membership actions, and83
-moderation use the SDK `Room` directly.84
-85
-## Account construction and secrets86
-87
-`Session::login_password`, `Session::begin_oauth`, and `Session::restore` use88
-one shared account client builder. It configures encrypted SQLite storage,89
-encryption settings, refresh-token handling, and enabled threading without90
-thread subscriptions. `Session::build` passes the authenticated client to the91
-engine.92
-93
-Secret-bearing input types do not derive `Clone`. The store passphrase is94
-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()` after98
-login, then consume `SessionTokensChanged` and atomically replace that saved99
-record with `ClientHandle::session_record()`. A previously captured record may100
-hold an exhausted refresh token; failing to persist a rotation must be surfaced101
-rather than silently treating the stale record as safe to restore. Restore with102
-the same `StoreConfig` path and passphrase. logout and a rejected token both103
-stop the engine. logout and a hard rejection kill the record and the store, a104
-soft logout keeps the store for a same-device login, `Stopped` alone kills105
-nothing. Neither cookies nor application-specific secret storage belong in the106
-toolkit.107
-108
-## Notifications and room data109
-110
-The notification client is constructed lazily in an `OnceCell` and shares the111
-engine sync service. It is created only when notification resolution is first112
-requested. `PushNotification` exposes typed room and sender metadata and a113
-typed `PushNotificationEvent`. The event is `Raw<AnySyncTimelineEvent>` for a114
-timeline notification or `Raw<StrippedRoomMemberEvent>` for an invite. SDK UI115
-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 runs119
-display-name requests with bounded concurrency. Sorting is descending latest120
-event timestamp, then case-insensitive name, then room id as a deterministic tie121
-break.122
-123
-Global and room account data retain typed Matrix event-type parameters and raw124
-event content. The handle offers direct raw reads and engine-backed raw writes.125
-126
-## Commands and errors127
-128
-`Command` is grouped into `Account`, `Room`, `Timeline`, `Media`,129
-`Encryption`, `Notification`, and `Lifecycle` domains. `CommandKind` identifies130
-the outer domain in result events.131
-132
-`RoomCommand::InviteUser` contains only room and user ids. It deliberately does133
-not expose a reason because the SDK invite helper accepts none. Kick, ban, and134
-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 a141
-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, SAS144
-startup and acceptance failures retain their message, while stream termination145
-has dedicated variants. A cancelled verification can be started again.146
-The watcher and action paths claim terminal emission so a racing cancellation147
-cannot replace `Done` with a local `Cancelled` event.148
-149
-`CommandErrorKind` is the stable typed failure category. It includes room and150
-timeline availability, queue pressure, validation, media, verification,151
-authentication, network, server, store, timeline, encryption, and fallback152
-categories. The rendered message preserves operation context while internal153
-errors retain their source chains.154
-155
-## Power levels156
-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 keeps162
-an atomic local mapping, applies all requested changes to it, and sends one163
-state event. It does not patch upstream and does not send separate events for164
-individual fields.165
-166
-## Public model boundaries167
-168
-The public model uses owned ruma identifiers and typed data wherever it has a169
-stable representation. Raw Matrix event payloads remain `Raw` where arbitrary170
-event content must be preserved. Public output models are non-exhaustive where171
-they are expected to grow. SDK clients remain available through the handle for172
-direct state queries that do not need engine coordination.3
+the contract the working tree actually implements. if the code disagrees with4
+this, one of them is lying and it should be fixed5
+6
+## two ways in, one way out7
+8
+state changes are commands. `ClientHandle::send` queues one and hands back a9
+`Pending`, `execute` queues and waits, and there's an awaited convenience method10
+for every command. each command answers exactly once with11
+`Result<CommandOutput, Error>`. drop the `Pending` if you don't care, the12
+command still runs13
+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 here17
+18
+the event stream carries state and only state. timelines, history, typing,19
+rooms, connection, encryption, verification, upload progress, session. no20
+command results ride on it. drain it from its own task, it's bounded and the21
+engine waits for you22
+23
+one error type for everything, `Error { kind, message, challenge }`. match on24
+`kind`. `Interrupted` means the engine stopped, the room closed, or the task25
+doing the work died before it could answer. `challenge` turns up with26
+`InteractiveAuthRequired`27
+28
+## engine29
+30
+the run loop routes and does nothing slow. every task it spawns is registered31
+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, and36
+`ConnectionChanged` shows `Offline` meanwhile. every return to `Online` re-arms37
+the send queues, since the sdk parks a room's queue on any send failure and38
+never unparks it. a recoverable failure while online gets the room another go39
+after a few seconds40
+41
+logout and a rejected token both stop the engine. `Shutdown` answers once42
+`Stopped` has gone out43
+44
+## rooms45
+46
+one actor per open room. the engine holds a slot per room, `Active` with a47
+sender or `Closing` with a fifo of opens and closes that arrived while it wound48
+down. those replay in order once the actor exits. a room you leave, or get49
+kicked or banned from, closes itself50
+51
+opening sends `TimelineReset` on the stream before it answers. opening an52
+already open room sends a fresh reset, so a second frontend gets a snapshot53
+54
+sends, edits and replies run on the actor in order, so they reach the send55
+queue in the order they were asked for. redactions, reactions and read56
+receipts are network calls, so they run beside the actor and a slow one57
+doesn't queue the room behind it. closing lets them finish rather than lie58
+59
+threads belong to their room's actor. commands for a thread that's still60
+opening wait behind the open, up to the actor's queue capacity61
+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 gappy64
+sync can unload it again65
+66
+every task inside an actor is supervised, and a panic in any of them panics the67
+actor68
+69
+## timelines70
+71
+`TimelineDiff::try_apply` refuses a diff whose indices don't fit your snapshot72
+and leaves the snapshot alone. stop applying, `resync`, and carry on from the73
+next `TimelineReset`. resync kills the old watcher before the new reset goes74
+out, so no stale diff can land after it75
+76
+`TimelineId` tells a room from a thread by its root event77
+78
+## mutations79
+80
+membership, moderation, power levels, room creation and account data writes go81
+through one keyed fifo scheduler. same room, same alias, same dm partner or82
+same account data type run in order, everything else runs at once.83
+`create_direct` hands back the existing dm when there is one84
+85
+notifications and encryption each get one serial worker. the encryption worker86
+owns the verification controller outright87
+88
+power level changes build one state event from the local mapping, since the89
+sdk's own patcher is private90
+91
+## media92
+93
+media is a read. `ClientHandle::media` takes a `MediaId` a retained timeline94
+handed you, `mxc_media` takes any plain mxc uri. both share one bounded95
+download path with the byte cap the sdk's own download lacks, and96
+concurrent asks for the same bytes share one flight97
+98
+the cache holds plaintext, keyed like the sdk keys it, so the sdk and macltok99
+can read each other's entries. that's how your own encrypted uploads come back100
+101
+media ids are registered as timelines render and forgotten when their timeline102
+closes, least recently seen first once the cap is hit103
+104
+uploads are commands that answer once the send queue has them. progress and105
+failure arrive as events from one watcher on the sdk's global send queue106
+107
+## sessions and secrets108
+109
+`Session::login_password`, `begin_oauth` and `restore` share one client builder110
+with encrypted sqlite, refresh tokens and threads. secret-bearing inputs don't111
+derive `Clone`112
+113
+the caller owns the saved record. save `Session::record()` after login and114
+replace it atomically with `ClientHandle::session_record()` on every115
+`SessionTokensChanged`. logout and a hard rejection kill the record and the116
+store. a soft logout keeps the store for a same-device login, or the e2ee keys117
+go with it. a bare `Stopped` kills nothing. one store directory per login118
+119
+## notifications and room data120
+121
+the notification client is built lazily and shares the engine's sync service.122
+sdk ui types stay behind the boundary123
+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 sync126
+touching fifty rooms is one event127
+128
+## verification129
+130
+`Confirm`, `Reject` and `Cancel`. reject tells the other side the emoji didn't131
+match. the watcher and the action paths claim the terminal event, so a racing132
+cancel can't turn `Done` into `Cancelled`133
+134
+## public model135
+136
+owned ruma ids and typed data wherever there's a stable shape, `Raw` where137
+arbitrary content has to survive. anything expected to grow is138
+`#[non_exhaustive]`. `Engine::from_client` stays public for people who built139
+their own client@@ -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 by6969
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 q171
+### C5. Media single-flight is poisoned by a leader panic. done, shared-future single flight in the media query7272
7373
In `spawn_media_fetch` (`src/engine.rs:1870-1911`) followers push their id and7474
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` exists133133
for the lookup (`client/mod.rs:1852`).134134
135
-### C11. Unregistered tasks swallow panics, contradicting the design doc. folded into q1 and the actor restructure135
+### C11. Unregistered tasks swallow panics, contradicting the design doc. done, command tasks answer `Interrupted`, actor internals are supervised and panic the actor136136
137137
The design doc says a watcher panic is terminal. But the timeline watcher138138
(`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 is164
+### C14. Smaller correctness issues. done, bar the lagged session watcher which stays as is165165
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 friendliness187187
188
-### A1. Three completion mechanisms, and the useful one is rationed. OPEN188
+### A1. Three completion mechanisms, and the useful one is rationed. done, one awaited path and one error type189189
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 per203203
request, is not offered for most operations.204204
205
-### A2. Pure reads modelled as commands, against the doc's own rule. OPEN205
+### A2. Pure reads modelled as commands, against the doc's own rule. done, `media` and `mxc_media` are handle queries206206
207207
The design doc says operations are split by whether they change state. Media208208
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 to211211
filter by `MediaId`.212212
213
-### A3. Every timeline item is a `Message`, with presentation strings in `body`. OPEN213
+### A3. Every timeline item is a `Message`, with presentation strings in `body`. open, q3214214
215215
`normalise_timeline_item` (`src/normalise/timeline.rs`) flattens every event216216
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` and236236
`Other { event_type }`. It is the largest single rendering gap.237237
238
-### A4. Timeline commands cannot target local echoes. OPEN238
+### A4. Timeline commands cannot target local echoes. open, lands with q3239239
240240
`Edit`, `Redact` and `ToggleReaction` take an `OwnedEventId` and wrap it in241241
`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 model244244
already exposes.245245
246
-### A5. `RoomSummary` is too thin for a room list. OPEN246
+### A5. `RoomSummary` is too thin for a room list. open, lands with the malice queries and q2247247
248248
Missing fields, all cheap SDK reads. Topic, canonical alias, joined and invited249249
member counts, heroes, tombstone and successor, inviter for invites, a latest250250
event preview, and space parents and children (Malice gap 3).251251
252
-### A6. No incoming verification. OPEN252
+### A6. No incoming verification. open253253
254254
Only outgoing `StartVerification` exists. A request started from another255255
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]`. OPEN260
+### A7. Missing `#[non_exhaustive]`. done261261
262262
Adding the attribute later is itself a breaking change, so it belongs now. Output types that will263263
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 are266266
`CreateRoomOptions`, `PasswordLogin`, `OAuthLogin` and `UploadThumbnail`.267267
268
-### A8. Smaller API issues. OPEN268
+### A8. Smaller API issues. mostly done. `HistoryState::Failed(String)`, stringly power level events and fixed encryption defaults remain269269
270270
- `MediaCommand::Upload` duplicates `UploadAttachment`, and the handle exposes271271
both `upload_media` and `upload_attachment`.@@ -284,6 +284,8 @@ grow need a constructor plus `#[non_exhaustive]`. These are
284284
285285
## D. Duplication and antipatterns286286
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 module288
+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,@@ -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) = handle32
+ Event::RoomsChanged if opened.is_none() => {33
+ let Some(room) = handle3734
.rooms()3835
.await3936
.into_iter()4037
.find(|room| room.membership == RoomMembership::Joined)41
- {42
- room_open_id = Some(handle43
- .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 meanwhile43
+ 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
- handle82
- .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
}@@ -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
+ self60
+ }61
+62
+ #[must_use]63
+ pub fn with_device_id(mut self, device_id: OwnedDeviceId) -> Self {64
+ self.device_id = Some(device_id);65
+ self66
+ }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
+ self103
+ }104
+105
+ /// a statically registered client id, skipping dynamic registration106
+ #[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
+ self110
+ }111
+112
+ #[must_use]113
+ pub fn with_device_id(mut self, device_id: OwnedDeviceId) -> Self {114
+ self.device_id = Some(device_id);115
+ self116
+ }117
+}118
+49119
#[derive(Clone, Copy, Debug, Eq, PartialEq)]120
+#[non_exhaustive]50121
pub enum OAuthApplicationType {51122
Native,52123
Web,@@ -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 without8481
// 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
.await187184
.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
client207204
.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
.await233230
.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
.await249246
.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
.await258255
.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
sender266259
.send(Event::VerificationChanged(UiState::Waiting))267260
.await268261
.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 reached290279
// 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 an305294
// 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
.await318
- .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 reached347333
// 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 user361347
// 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
.await370
- .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 as372358
// rejected. A remote cancellation racing the send turns the SDK call373359
// 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 an383369
// 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 after399385
/// 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 a554
- // shutdown under lock cannot hang on network sends. No terminal event555
- // 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 it534
+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 = client566547
.encryption()567548
.request_user_identity(user_id)568549
.await569
- .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
identity577558
.request_verification_with_methods(vec![VerificationMethod::SasV1])578559
.await579
- .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@@ -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
- self80
- }81
-82
- #[must_use]83
- pub const fn with_event_capacity(mut self, capacity: usize) -> Self {84
- self.event_capacity = capacity;85
- self86
- }87
-88
- #[must_use]89
- pub const fn with_initial_history_limit(mut self, limit: u16) -> Self {90
- self.initial_history_limit = limit;91
- self92
- }93
-94
- #[must_use]95
- pub const fn with_max_media_bytes(mut self, bytes: usize) -> Self {96
- self.max_media_bytes = bytes;97
- self98
- }99
-100
- #[must_use]101
- pub const fn with_max_media_sources(mut self, sources: usize) -> Self {102
- self.max_media_sources = sources;103
- self104
- }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::Unauthenticated139
- | Self::ZeroCapacity140
- | 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
- handle270
- }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 = self279
- .contexts280
- .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 down347
-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 self380
- .entries381
- .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.order390
- .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 it433
-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
- .await491
- .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: self565
- .pagination_results566
- .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
- events621
- .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 events631
- .send(Event::TimelineChanged {632
- timeline_id: id.clone(),633
- diffs: normalised.diffs,634
- })635
- .await636
- .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 == 0666
- || config.event_capacity == 0667
- || config.max_media_sources == 0668
- {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
- .await677
- .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 = self728
- .client729
- .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.port737
- .connection_sender()738
- .send_replace(ConnectionState::Connecting);739
- if sender740
- .send(Event::ConnectionChanged(ConnectionState::Connecting))741
- .await742
- .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
- .await783
- .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) = result792
- && error.is_panic()793
- {794
- match context {795
- TaskContext::Command { id, kind } => {796
- if sender797
- .send(Event::CommandFailed(CommandFailure {798
- id,799
- command: kind,800
- kind: CommandErrorKind::Other,801
- message: "command task panicked".to_owned(),802
- }))803
- .await804
- .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 client875
- .logout()876
- .await877
- .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
- None906
- }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
- None912
- }913
- DriverRequest::SetGlobalAccountData {914
- event_type,915
- content,916
- reply,917
- } => {918
- self.spawn_global_account_data_write(event_type, content, reply);919
- None920
- }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
- None929
- }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
- client1021
- .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 _ = events1064
- .send(Event::InteractiveAuthRequired {1065
- id,1066
- command: CommandKind::Account,1067
- challenge: challenge.clone(),1068
- })1069
- .await;1070
- let _ = events1071
- .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 _ = events1118
- .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 _ = events1127
- .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 = self1148
- .global_account_gates1149
- .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 = client1158
- .account()1159
- .set_account_data_raw(event_type, content)1160
- .await1161
- .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 = self1178
- .room_account_gates1179
- .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) => room1189
- .set_account_data_raw(event_type, content)1190
- .await1191
- .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 = self1203
- .notification_command_receiver1204
- .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
- ¬ification_client,1214
- command,1215
- ))1216
- .catch_unwind()1217
- .await1218
- .unwrap_or_else(|_| {1219
- Err(CommandError::new(1220
- CommandErrorKind::Other,1221
- "notification command panicked",1222
- ))1223
- });1224
- let _ = emit_command_result(1225
- ¬ification_events,1226
- id,1227
- CommandKind::Notification,1228
- result,1229
- )1230
- .await;1231
- }1232
- },1233
- );1234
-1235
- let mut encryption_commands = self1236
- .encryption_command_receiver1237
- .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
- .await1261
- .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::CreateKeyBackup1279
- | EncryptionCommand::EnableKeyBackup1280
- | 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
- .await1289
- .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
- .await1305
- .unwrap_or_else(|_| {1306
- Err(CommandError::new(1307
- CommandErrorKind::Other,1308
- "encryption command panicked",1309
- ))1310
- });1311
- drop(verification);1312
- result1313
- };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 = self1328
- .room_command_receiver1329
- .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 = self1346
- .session_changes1347
- .take()1348
- .expect("session watcher has not started");1349
- let session_sender = self.port.event_sender();1350
- let stop = self.stop.clone();1351
- self.tasks1352
- .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_sender1358
- .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::SessionTokensChanged1367
- }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 it1391
- 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_sender1404
- .send(Event::ConnectionChanged(state))1405
- .await1406
- .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.tasks1423
- .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::Online1438
- && 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_sender1468
- .send(Event::EncryptionChanged(encryption_state(1469
- device_id.clone(),1470
- recovery_state,1471
- verification_state,1472
- backup_state,1473
- )))1474
- .await1475
- .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_sender1507
- .send(Event::EncryptionChanged(encryption_state(1508
- device_id.clone(),1509
- recovery_state,1510
- verification_state,1511
- backup_state,1512
- )))1513
- .await1514
- .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().await1568
- {1569
- if notification_sender1570
- .send(Event::NotificationSettingsChanged)1571
- .await1572
- .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_commands1668
- .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_commands1674
- .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
- error1799
- })1800
- }1801
-1802
- fn route_timeline(1803
- &mut self,1804
- id: CommandId,1805
- command: TimelineCommand,1806
- ) -> Result<bool, CommandError> {1807
- let sender = self1808
- .rooms1809
- .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 = self1870
- .rooms1871
- .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.rooms1885
- .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 self1897
- .rooms1898
- .get(&room_id)1899
- .is_none_or(|state| state.generation() != generation)1900
- {1901
- return Ok(());1902
- }1903
- let state = self1904
- .rooms1905
- .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
- .await1929
- .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
- .await1937
- .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 = self1948
- .rooms1949
- .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.sync1955
- .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() == 11977
- };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
- .await2009
- .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 self2031
- .upload_watchers2032
- .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.tasks2047
- .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 events2066
- .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
- .await2075
- .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_room2086
- .send_queue()2087
- .subscribe()2088
- .await2089
- .is_ok_and(|(echoes, _)| {2090
- echoes.iter().any(|echo| {2091
- echo.transaction_id == transaction_id2092
- && is_attachment_echo(&echo.content)2093
- })2094
- });2095
- if is_attachment2096
- && events2097
- .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
- .await2104
- .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_watchers2116
- .insert(watched_room_id, (watcher, ready.clone()));2117
- ready2118
- };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).await2144
- }2145
- .await;2146
- if emit_command_result(&events, command_id, CommandKind::Media, result)2147
- .await2148
- .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.port2168
- .connection_sender()2169
- .send_replace(ConnectionState::Stopped);2170
- let _ = self2171
- .port2172
- .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 = client2183
- .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
- .await2191
- .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
- .await2195
- .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
- .await2206
- .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
- } = content2213
- 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
- echoes2236
- .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
- .await2392
- .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 } => client2474
- .join_room_by_id_or_alias(&room, &via)2475
- .await2476
- .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) => room2480
- .leave()2481
- .await2482
- .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) => room2488
- .forget()2489
- .await2490
- .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) => room2496
- .join()2497
- .await2498
- .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) => room2504
- .leave()2505
- .await2506
- .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) => room2512
- .invite_user_by_id(&user_id)2513
- .await2514
- .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) => room2524
- .kick_user(&user_id, reason.as_deref())2525
- .await2526
- .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) => room2536
- .ban_user(&user_id, reason.as_deref())2537
- .await2538
- .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) => room2548
- .unban_user(&user_id, reason.as_deref())2549
- .await2550
- .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
- .await2557
- .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) => room2567
- .update_power_levels(vec![(&user_id, level)])2568
- .await2569
- .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) => client2581
- .create_room(create_room_request(options))2582
- .await2583
- .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 => client2588
- .create_dm(&user_id)2589
- .await2590
- .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::AllMessages2613
- }2614
- RoomNotificationMode::MentionsAndKeywordsOnly => {2615
- matrix_sdk::notification_settings::RoomNotificationMode::MentionsAndKeywordsOnly2616
- }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
- settings2631
- .set_room_notification_mode(&room_id, sdk_notification_mode(mode))2632
- .await2633
- }2634
- NotificationCommand::ResetRoomMode(room_id) => {2635
- settings.delete_user_defined_room_rules(&room_id).await2636
- }2637
- NotificationCommand::SetDefaultMode {2638
- encrypted,2639
- one_to_one,2640
- mode,2641
- } => {2642
- settings2643
- .set_default_room_notification_mode(2644
- IsEncrypted::from(encrypted),2645
- IsOneToOne::from(one_to_one),2646
- sdk_notification_mode(mode),2647
- )2648
- .await2649
- }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) => client2670
- .encryption()2671
- .recovery()2672
- .recover(&key)2673
- .await2674
- .map_err(|source| CommandError::from_recovery("account recovery failed", source)),2675
- EncryptionCommand::StartVerification(target) => {2676
- verification.start(client, events.clone(), target).await2677
- }2678
- EncryptionCommand::Verify(action) => {2679
- verification.apply(client, events.clone(), action).await2680
- }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 => client2691
- .encryption()2692
- .backups()2693
- .create()2694
- .await2695
- .map_err(|source| CommandError::from_sdk("creating key backup failed", source)),2696
- EncryptionCommand::EnableKeyBackup => client2697
- .encryption()2698
- .recovery()2699
- .enable_backup()2700
- .await2701
- .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().await2706
- } else {2707
- backups.disable().await2708
- }2709
- .map_err(|source| CommandError::from_sdk("disabling key backup failed", source))2710
- }2711
- EncryptionCommand::RecoverKeyBackup(key) => client2712
- .encryption()2713
- .recovery()2714
- .recover_and_fix_backup(&key)2715
- .await2716
- .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 = room2730
- .power_levels()2731
- .await2732
- .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
- .await2778
- .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
- request2815
-}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
- .await2833
- .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 = registry2852
- .lock()2853
- .await2854
- .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::Video2880
- ) && 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
- events2917
- .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
- .await2924
- .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 anything2929
- let bytes = maybe_decrypt_media(bytes, &request, max_media_bytes)?;2930
- cache_media(&client, &request, &bytes).await;2931
- events2932
- .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
- .await2939
- .map_err(send_error)2940
-}2941
-async fn cached_media(2942
- client: &Client,2943
- request: &MediaRequestParameters,2944
-) -> Result<Option<Vec<u8>>, CommandError> {2945
- client2946
- .media_store()2947
- .lock()2948
- .await2949
- .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
- .await2958
- .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 _ = store2976
- .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 = client3043
- .http_client()3044
- .execute(request_from_http(client, http_request)?)3045
- .await3046
- .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 refreshed3057
- || client3058
- .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 request3079
- .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
- endpoint3159
- .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
- endpoint3182
- .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
- endpoint3212
- .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
- endpoint3242
- };3243
- endpoint3244
- .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
- endpoint3264
- .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
- endpoint3287
- .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
- builder3311
- .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).await3324
- }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
- completed3338
- }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).await3353
- }3354
-3355
- async fn run(mut self, current_user_id: Option<ruma::OwnedUserId>) {3356
- let timeline = match self3357
- .room3358
- .timeline_builder()3359
- .with_focus(TimelineFocus::Live {3360
- hide_threaded_events: true,3361
- })3362
- .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)3363
- .build()3364
- .await3365
- {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
- .await3414
- {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
- .await3425
- .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_sender3444
- .send(Event::TypingChanged(state))3445
- .await3446
- .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
- .await3515
- .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
- .await3531
- .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
- .await3549
- .is_err()3550
- {3551
- break 'actor;3552
- }3553
- } else {3554
- opening3555
- .queued3556
- .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
- .await3570
- .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 = room3582
- .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
- timeline3612
- .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
- .await3624
- .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
- .await3657
- .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
- .await3671
- .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
- .await3700
- .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
- .await3731
- .is_err()3732
- {3733
- break 'actor;3734
- }3735
- }3736
- }3737
- }3738
- command => {3739
- let thread_target = match command3740
- .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
- .await3758
- .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
- .await3778
- .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
- .await3797
- .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
- .await3817
- .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_threads3837
- .get(&root_event_id)3838
- .is_none_or(|opening| opening.generation != generation)3839
- {3840
- continue;3841
- }3842
- let opening = opening_threads3843
- .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
- .await3862
- {3863
- Ok(session) => {3864
- thread_timelines3865
- .insert(root_event_id.clone(), session);3866
- let timeline = thread_timelines3867
- .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
- .await3879
- .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
- timeline3893
- .paginate(limit, Some(id))3894
- .await;3895
- }3896
- command => {3897
- let thread_root = timeline3898
- .id3899
- .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
- .await3910
- .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
- .await3940
- .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
- .await3968
- .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_threads4008
- .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
- timeline4031
- .close(4032
- CommandErrorKind::RoomNotOpen,4033
- "room closed while thread pagination was in progress",4034
- )4035
- .await;4036
- }4037
- room_timeline4038
- .close(4039
- CommandErrorKind::RoomNotOpen,4040
- "room closed while pagination was in progress",4041
- )4042
- .await;4043
- if let Some(id) = close_id4044
- && emit_command_result(&self.events, id, CommandKind::Room, Ok(()))4045
- .await4046
- .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
- timeline4064
- .redact(&TimelineEventItemId::EventId(event_id), reason.as_deref())4065
- .await4066
- .map_err(timeline_error)4067
- }),4068
- TimelineCommand::ToggleReaction { event_id, key, .. } => Box::pin(async move {4069
- timeline4070
- .toggle_reaction(&TimelineEventItemId::EventId(event_id), &key)4071
- .await4072
- .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
- timeline4081
- .send_single_receipt(ReceiptType::Read, event_id.clone())4082
- .await4083
- .map_err(receipt_failed)?;4084
- if !thread {4085
- timeline4086
- .send_single_receipt(ReceiptType::FullyRead, event_id)4087
- .await4088
- .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 } => timeline4138
- .send(4139
- message_content(content, mentions, false)4140
- .with_relation(None)4141
- .into(),4142
- )4143
- .await4144
- .map(|_| ())4145
- .map_err(timeline_error),4146
- MessageContent::Emote { content, mentions } => timeline4147
- .send(4148
- message_content(content, mentions, true)4149
- .with_relation(None)4150
- .into(),4151
- )4152
- .await4153
- .map(|_| ())4154
- .map_err(timeline_error),4155
- MessageContent::Reply {4156
- event_id,4157
- content,4158
- mentions,4159
- notify_replied_to,4160
- } => {4161
- let content = timeline4162
- .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::MaybeThreaded4171
- },4172
- add_mentions: if notify_replied_to {4173
- AddMentions::Yes4174
- } else {4175
- AddMentions::No4176
- },4177
- },4178
- )4179
- .await4180
- .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_root4188
- && !matches!(4189
- content.relates_to.as_ref(),4190
- Some(Relation::Thread(thread)) if &thread.event_id == thread_root4191
- )4192
- {4193
- return Err(CommandError::new(4194
- CommandErrorKind::Timeline,4195
- "reply target does not belong to the selected thread",4196
- ));4197
- }4198
- timeline4199
- .send(content.into())4200
- .await4201
- .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
- timeline4220
- .edit(4221
- &TimelineEventItemId::EventId(event_id),4222
- EditedContent::RoomMessage(message_content(content, mentions, emote)),4223
- )4224
- .await4225
- .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
- content4250
- }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 events4263
- .send(Event::HistoryChanged {4264
- timeline_id: timeline_id.clone(),4265
- state: HistoryState::Loading,4266
- })4267
- .await4268
- .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 = timeline4282
- .paginate_backwards(limit.max(1))4283
- .await4284
- .map_err(timeline_error);4285
- match result {4286
- Ok(at_start) => {4287
- let _ = events4288
- .send(Event::HistoryChanged {4289
- timeline_id,4290
- state: if at_start {4291
- HistoryState::StartReached4292
- } else {4293
- HistoryState::Idle4294
- },4295
- })4296
- .await;4297
- if let Some(id) = command_id {4298
- let _ = results.send((id, Ok(())));4299
- }4300
- }4301
- Err(error) => {4302
- let _ = events4303
- .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
- events4321
- .send(Event::CommandSucceededWithRoom {4322
- id,4323
- command: CommandKind::Room,4324
- room_id,4325
- })4326
- .await4327
-}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).await4348
-}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
-}@@ -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 it28
+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
+ .await35
+ .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) => encryption68
+ .recovery()69
+ .recover(&key)70
+ .await71
+ .map_err(|source| Error::from_recovery("account recovery failed", source)),72
+ EncryptionCommand::RecoverKeyBackup(key) => encryption73
+ .recovery()74
+ .recover_and_fix_backup(&key)75
+ .await76
+ .map_err(|source| Error::from_recovery("recovering key backup failed", source)),77
+ EncryptionCommand::CreateKeyBackup => encryption78
+ .backups()79
+ .create()80
+ .await81
+ .map_err(|source| Error::from_sdk("creating key backup failed", source)),82
+ EncryptionCommand::EnableKeyBackup => encryption83
+ .recovery()84
+ .enable_backup()85
+ .await86
+ .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().await91
+ } else {92
+ backups.disable().await93
+ }94
+ .map_err(|source| Error::from_sdk("disabling key backup failed", source))95
+ }96
+ EncryptionCommand::StartVerification(target) => {97
+ verification.start(client, events.clone(), target).await98
+ }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
+ settings108
+ .set_room_notification_mode(&room_id, sdk_mode(mode))109
+ .await110
+ }111
+ NotificationCommand::ResetRoomMode(room_id) => {112
+ settings.delete_user_defined_room_rules(&room_id).await113
+ }114
+ NotificationCommand::SetDefaultMode {115
+ encrypted,116
+ one_to_one,117
+ mode,118
+ } => {119
+ settings120
+ .set_default_room_notification_mode(121
+ IsEncrypted::from(encrypted),122
+ IsOneToOne::from(one_to_one),123
+ sdk_mode(mode),124
+ )125
+ .await126
+ }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 backwards152
+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
+ .await165
+ .map_err(receipt_failed)?;166
+ if timeline_id.thread_root().is_none() {167
+ room.send_single_receipt(ReceiptType::FullyRead, ReceiptThread::Unthreaded, event_id)168
+ .await169
+ .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
+ .await182
+ .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
+ .await194
+ .map_err(|source| {195
+ Error::with_source(ErrorKind::Store, "reading send queue failed", source)196
+ })?;197
+ echoes198
+ .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
+ .await224
+ {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
+ .await247
+ .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 = mimetype265
+ .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
+ client274
+ .account()275
+ .upload_avatar(&mimetype, bytes)276
+ .await277
+ .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
+ client287
+ .rename_device(device_id, display_name)288
+ .await289
+ .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 = client299
+ .user_id()300
+ .ok_or_else(|| Error::new(ErrorKind::Unauthenticated, "the client is not logged in"))?;301
+ let auth = auth302
+ .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
+}@@ -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 for40
+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 the46
+/// handle can answer media queries without a trip through the engine47
+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 = self84
+ .registry85
+ .lock()86
+ .await87
+ .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 read110
+ 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
+ .await133
+ }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
+ .await151
+ }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
+ flight183
+ }184
+ }185
+ };186
+ flight.await187
+ }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.animated204
+ ),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 anything229
+ let bytes = maybe_decrypt(bytes, &request, max_bytes)?;230
+ if let Ok(store) = client.media_store().lock().await {231
+ let _ = store232
+ .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 = mimetype244
+ .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 case253
+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
+ client285
+ .media_store()286
+ .lock()287
+ .await288
+ .map_err(cache_failed)?289
+ .get_media_content(request)290
+ .await291
+ .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 hand329
+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 uri339
+ .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 refreshed362
+ || client363
+ .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 response384
+ .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 = response390
+ .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 all431
+ macro_rules! into_http {432
+ ($endpoint:expr, $token:expr) => {433
+ $endpoint434
+ .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
+ authed446
+ ),447
+ (None, false) => into_http!(448
+ media::get_content::v3::Request::from_url(uri).map_err(invalid_uri)?,449
+ SendAccessToken::None450
+ ),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 anyway475
+ 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_request480
+ .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
+ .await502
+ .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 down513
+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.order552
+ .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.entries558
+ .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
+}@@ -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
+ self54
+ }55
+56
+ #[must_use]57
+ pub const fn with_event_capacity(mut self, capacity: usize) -> Self {58
+ self.event_capacity = capacity;59
+ self60
+ }61
+62
+ #[must_use]63
+ pub const fn with_initial_history_limit(mut self, limit: u16) -> Self {64
+ self.initial_history_limit = limit;65
+ self66
+ }67
+68
+ #[must_use]69
+ pub const fn with_max_media_bytes(mut self, bytes: usize) -> Self {70
+ self.max_media_bytes = bytes;71
+ self72
+ }73
+74
+ #[must_use]75
+ pub const fn with_max_media_sources(mut self, sources: usize) -> Self {76
+ self.max_media_sources = sources;77
+ self78
+ }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::Other115
+ }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 else125
+ Command(&'static str),126
+ /// a panic here takes the engine with it127
+ 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
+ handle160
+ }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 gone185
+ 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 yet227
+ 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 in233
+ 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 == 0241
+ || config.event_capacity == 0242
+ || config.max_media_sources == 0243
+ {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
+ .await252
+ .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 = self303
+ .client304
+ .user_id()305
+ .expect("authentication was checked during construction")306
+ .to_owned();307
+ self.port308
+ .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 stopped439
+ 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
+ client448
+ .logout()449
+ .await450
+ .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
+ client472
+ .account()473
+ .set_display_name(name.as_deref())474
+ .await475
+ .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
+ client481
+ .account()482
+ .set_avatar_url(avatar.as_deref())483
+ .await484
+ .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).await491
+ });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).await498
+ }),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).await502
+ });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).await509
+ });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).await516
+ }),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).await522
+ }),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
+ None538
+ }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.uploads557
+ .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
+ ready568
+ }569
+ };570
+ let max_bytes = self.config.max_media_bytes;571
+ // queueing before the forwarder listens would lose the first progress ticks572
+ self.spawn_command("upload", reply, async move {573
+ ready574
+ .wait_for(|subscribed| *subscribed)575
+ .await576
+ .map_err(|_| Error::new(ErrorKind::Store, "reading the media send queue failed"))?;577
+ media::queue_upload(room, upload, max_bytes).await578
+ });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).await598
+ });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 enough628
+ 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.rooms671
+ .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 self690
+ .rooms691
+ .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 = self709
+ .rooms710
+ .iter()711
+ .filter(|(_, slot)| matches!(slot, RoomSlot::Active { .. }))712
+ .map(|(room_id, _)| room_id.as_ref())713
+ .collect::<Vec<&RoomId>>();714
+ self.sync715
+ .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.port728
+ .connection_sender()729
+ .send_replace(ConnectionState::Stopped);730
+ let _ = self731
+ .port732
+ .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
+}@@ -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 dm28
+/// partner or same account data type run in order, everything else runs at once29
+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 key64
+ 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
+ key142
+ });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
+ client152
+ .account()153
+ .set_account_data_raw(event_type, content)154
+ .await155
+ .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 } => client165
+ .join_room_by_id_or_alias(&room, &via)166
+ .await167
+ .map(joined)168
+ .map_err(sdk("joining room failed")),169
+ RoomCommand::Leave(room_id) => room(&room_id)?170
+ .leave()171
+ .await172
+ .map(done)173
+ .map_err(sdk("leaving room failed")),174
+ RoomCommand::Forget(room_id) => room(&room_id)?175
+ .forget()176
+ .await177
+ .map(done)178
+ .map_err(sdk("forgetting room failed")),179
+ RoomCommand::AcceptInvite(room_id) => room(&room_id)?180
+ .join()181
+ .await182
+ .map(done)183
+ .map_err(sdk("accepting invite failed")),184
+ RoomCommand::RejectInvite(room_id) => room(&room_id)?185
+ .leave()186
+ .await187
+ .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
+ .await192
+ .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
+ .await201
+ .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
+ .await210
+ .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
+ .await219
+ .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
+ .await224
+ .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
+ .await233
+ .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
+ .await242
+ .map(done)243
+ .map_err(sdk("writing room account data failed")),244
+ RoomCommand::Create(options) => client245
+ .create_room(create_room_request(options))246
+ .await247
+ .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 => client252
+ .create_dm(&user_id)253
+ .await254
+ .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 private262
+async fn apply_power_level_changes(room: &Room, changes: PowerLevelChanges) -> Result<(), Error> {263
+ let mut levels = room264
+ .power_levels()265
+ .await266
+ .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
+ .await302
+ .map(drop)303
+ .map_err(|source| Error::from_sdk("updating power levels failed", source))304
+}305
+306
+fn done<T>(_: T) -> CommandOutput {307
+ CommandOutput::Done308
+}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
+ request337
+}@@ -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 reset18
+ 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
+ .await66
+ {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
+ .await76
+ 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 them95
+ 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 stopped106
+ 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
+ .await159
+}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 = command205
+ .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 = self250
+ .threads251
+ .open252
+ .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 it272
+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 ran365
+ 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 self379
+ .opening380
+ .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
+}@@ -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 loudly35
+#[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 it59
+#[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 it119
+ 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
+ .await124
+ .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 drop147
+ 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 it166
+ TimelineCommand::Redact {167
+ event_id, reason, ..168
+ } => Box::pin(async move {169
+ timeline170
+ .redact(&TimelineEventItemId::EventId(event_id), reason.as_deref())171
+ .await172
+ .map_err(timeline_error)173
+ }),174
+ TimelineCommand::ToggleReaction { event_id, key, .. } => Box::pin(async move {175
+ timeline176
+ .toggle_reaction(&TimelineEventItemId::EventId(event_id), &key)177
+ .await178
+ .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
+ timeline185
+ .send_single_receipt(ReceiptType::Read, event_id.clone())186
+ .await187
+ .map_err(receipt_failed)?;188
+ if thread_root.is_none() {189
+ timeline190
+ .send_single_receipt(ReceiptType::FullyRead, event_id)191
+ .await192
+ .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 for202
+ 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
+ timeline232
+ .send(content.into())233
+ .await234
+ .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
+ timeline248
+ .edit(249
+ &TimelineEventItemId::EventId(event_id),250
+ EditedContent::RoomMessage(message_content(content, mentions, emote)),251
+ )252
+ .await253
+ .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 = self271
+ .timeline272
+ .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::MaybeThreaded281
+ },282
+ add_mentions: if notify_replied_to {283
+ AddMentions::Yes284
+ } else {285
+ AddMentions::No286
+ },287
+ },288
+ )289
+ .await290
+ .map_err(|source| {291
+ Error::with_source(ErrorKind::Timeline, "reply operation failed", source)292
+ })?;293
+ if let Some(thread_root) = thread_root294
+ && !matches!(295
+ content.relates_to.as_ref(),296
+ Some(Relation::Thread(thread)) if &thread.event_id == thread_root297
+ )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
+ context326
+ .events327
+ .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 = timeline366
+ .paginate_backwards(limit.max(1))367
+ .await368
+ .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
+ content401
+ }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
+}@@ -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 _ = events34
+ .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 again42
+ 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 missed52
+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 it66
+ 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 go91
+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::Online109
+ && 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 one122
+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 = room160
+ .send_queue()161
+ .subscribe()162
+ .await163
+ .is_ok_and(|(echoes, _)| {164
+ echoes.iter().any(|echo| {165
+ echo.transaction_id == transaction_id166
+ && 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
+ } = content191
+ 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 fifty250
+ 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 events261
+ .send(Event::NotificationSettingsChanged)262
+ .await263
+ .is_err()264
+ {265
+ return;266
+ }267
+ }268
+}@@ -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 thing29
+ 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 answering40
+ 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::RecoveryRejected144
+ ErrorKind::RecoveryRejected82145
}83
- RecoveryError::SecretStorage(SecretStorageError::Storage(_)) => CommandErrorKind::Store,146
+ RecoveryError::SecretStorage(SecretStorageError::Storage(_)) => ErrorKind::Store,84147
RecoveryError::BackupExistsOnServer | RecoveryError::SecretStorage(_) => {85
- CommandErrorKind::RecoveryRejected148
+ ErrorKind::RecoveryRejected86149
}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.kind169
+ && self.message == other.message170
+ && self.challenge == other.challenge171
+ }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::BadCryptoStoreState109180
| 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::NoOlmMachine115186
| 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::Network162
- }231
+ OAuthTokenRevocationError::Revoke(RequestTokenError::Request(_)) => ErrorKind::Network,163232
OAuthTokenRevocationError::Revoke(RequestTokenError::ServerResponse(_)) => {164
- CommandErrorKind::Unauthenticated233
+ ErrorKind::Unauthenticated165234
}166
- _ => CommandErrorKind::Other,235
+ _ => ErrorKind::Other,167236
},168
- _ => CommandErrorKind::Unauthenticated,237
+ _ => ErrorKind::Unauthenticated,169238
}170239
}@@ -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,@@ -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 state18
+#[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 upload43
+ 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 one4462
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
+ self140
+ }141
+142
+ #[must_use]143
+ pub fn with_topic(mut self, topic: impl Into<String>) -> Self {144
+ self.topic = Some(topic.into());145
+ self146
+ }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
+ self152
+ }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
+ self158
+ }159
+160
+ #[must_use]161
+ pub const fn with_direct(mut self, direct: bool) -> Self {162
+ self.direct = direct;163
+ self164
+ }165
+166
+ #[must_use]167
+ pub const fn with_visibility(mut self, visibility: RoomVisibility) -> Self {168
+ self.visibility = visibility;169
+ self170
+ }171
+172
+ #[must_use]173
+ pub const fn with_preset(mut self, preset: RoomPreset) -> Self {174
+ self.preset = Some(preset);175
+ self176
+ }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 events319
+ 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 the600
- /// corresponding terminal command event on this stream.646
+ /// replaces whatever you had for this timeline. every diff after it applies to it601647
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 queue636
- /// 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 store649676
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.0668
- }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 after718
+ pub heroes: Vec<RoomHero>,719
+ /// set once the room has been upgraded, this is where everyone went720
+ pub successor: Option<OwnedRoomId>,721
+ pub inviter: Option<OwnedUserId>,722
+ /// joined spaces that list this room as a child723
+ pub parent_spaces: Vec<OwnedRoomId>,724
+ /// for a space, the children it lists, joined or not725
+ 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 message738
+#[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 in758
+#[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.membership943
+ self.matcher().matches(room)944
+ }945
+946
+ /// lowercases the search once, for filtering a whole list947
+ #[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
+ filter967
+ .membership866968
.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
- && self974
+ && filter873975
.favourite874976
.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 anything1323
+ 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,@@ -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};@@ -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
summaries2032
}2133
22
-async fn room_summary(room: &Room) -> RoomSummary {34
+async fn room_summary(room: &Room, spaces: &SpaceService) -> RoomSummary {2335
let name = room2436
.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 => room47
+ .invite_details()48
+ .await49
+ .ok()50
+ .map(|invite| invite.inviter_id),51
+ _ => None,52
+ };53
+ let children = if room.is_space() {54
+ space_children(room).await55
+ } 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: room5271
.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: room81
+ .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: spaces92
+ .joined_parents_of_child(room.room_id())93
+ .await94
+ .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 count112
+async fn space_children(room: &Room) -> Vec<OwnedRoomId> {113
+ let Ok(events) = room114
+ .get_state_events_static::<SpaceChildEventContent>()115
+ .await116
+ else {117
+ return Vec::new();118
+ };119
+ events120
+ .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
+ ))) => message137
+ .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
@@ -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 runs63
+#[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 atomically59
- /// when `Event::SessionTokensChanged` arrives; it contains secrets.78
+ /// current credentials. persist this atomically on `Event::SessionTokensChanged`, it holds secrets6079
#[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. Drain66
- /// `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.commands71
- .send(DriverRequest::Command(CommandEnvelope { id, command }))84
+ /// queue a command, waiting for room in the queue if there isn't any85
+ pub async fn send(&self, command: Command) -> Result<Pending, Error> {86
+ let (request, pending) = Request::new(command);87
+ self.requests88
+ .send(request)7289
.await73
- .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 now95
+ pub fn try_send(&self, command: Command) -> Result<Pending, Error> {96
+ let (request, pending) = Request::new(command);97
+ self.requests98
+ .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 the109
+ /// sdk send queue has them, delivery shows up later as the item's send state110
+ pub async fn execute(&self, command: Command) -> Result<CommandOutput, Error> {111
+ self.send(command).await?.await112
+ }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
+ .await131
+ }132
+133
+ pub async fn logout(&self) -> Result<(), Error> {134
+ self.run(Command::Account(AccountCommand::Logout)).await75135
}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.commands84
- .send(DriverRequest::DeleteDevices {85
- id: self.next_command_id(),141
+ ) -> Result<InteractiveAuthOutcome, Error> {142
+ match self143
+ .run(Command::Account(AccountCommand::DeleteDevices {86144
device_ids,87145
auth,88
- reply,89
- })146
+ }))90147
.await91
- .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
.await105168
}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
+ .await180
+ }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
+ .await194
+ }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
.await117206
}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
.await141227
}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
.await161244
}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
- .await246
+ pub async fn open_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {247
+ self.run(Command::Room(RoomCommand::Open(room_id))).await169248
}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
- .await250
+ pub async fn close_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {251
+ self.run(Command::Room(RoomCommand::Close(room_id))).await177252
}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
.await201276
}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
.await210281
}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
+ .await289
+ }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
- .await293
+ timeline_id: TimelineId,294
+ content: MessageContent,295
+ ) -> Result<(), Error> {296
+ self.run(Command::Timeline(TimelineCommand::Send {297
+ timeline_id,298
+ content,299
+ }))300
+ .await218301
}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
- .await305
+ 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
+ .await226315
}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
+ .await329
+ }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
+ .await343
+ }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
+ .await355
+ }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
.await234360
}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 }).await368
+ }369
+370
+ pub async fn leave_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {371
+ self.run(Command::Room(RoomCommand::Leave(room_id))).await372
+ }373
+374
+ pub async fn forget_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {375
+ self.run(Command::Room(RoomCommand::Forget(room_id))).await376
+ }377
+378
+ pub async fn accept_invite(&self, room_id: OwnedRoomId) -> Result<(), Error> {379
+ self.run(Command::Room(RoomCommand::AcceptInvite(room_id)))380
+ .await381
+ }382
+383
+ pub async fn reject_invite(&self, room_id: OwnedRoomId) -> Result<(), Error> {384
+ self.run(Command::Room(RoomCommand::RejectInvite(room_id)))241385
.await242386
}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
- .await390
+ room_id: OwnedRoomId,391
+ user_id: OwnedUserId,392
+ ) -> Result<(), Error> {393
+ self.run(Command::Room(RoomCommand::InviteUser { room_id, user_id }))394
+ .await254395
}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
.await322463
}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)).await329
- }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
- .await465
+ pub async fn create_room(&self, options: CreateRoomOptions) -> Result<OwnedRoomId, Error> {466
+ self.run_for_room(RoomCommand::Create(options)).await337467
}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.commands345
- .send(DriverRequest::RoomResult {346
- id: self.next_command_id(),347
- command,348
- reply,349
- })350
- .await351
- .map_err(|_| SendCommandError::Closed)?;352
- result.await.map_err(|_| CommandResultError::Closed)?469
+ /// hands back the existing dm with them if there is one470
+ pub async fn create_direct(&self, user_id: OwnedUserId) -> Result<OwnedRoomId, Error> {471
+ self.run_for_room(RoomCommand::CreateDirect(user_id)).await353472
}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
.await377496
}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
- .await498
+ pub async fn upload(&self, upload: AttachmentUpload) -> Result<(), Error> {499
+ self.run(Command::Media(MediaCommand::Upload(upload))).await395500
}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
.await405507
}406508
407
- pub async fn create_key_backup(&self) -> Result<CommandId, SendCommandError> {408
- self.send(Command::Encryption(409
- crate::EncryptionCommand::CreateKeyBackup,410
- ))411
- .await509
+ pub async fn create_key_backup(&self) -> Result<(), Error> {510
+ self.run(Command::Encryption(EncryptionCommand::CreateKeyBackup))511
+ .await412512
}413513
414
- pub async fn enable_key_backup(&self) -> Result<CommandId, SendCommandError> {415
- self.send(Command::Encryption(416
- crate::EncryptionCommand::EnableKeyBackup,417
- ))418
- .await514
+ pub async fn enable_key_backup(&self) -> Result<(), Error> {515
+ self.run(Command::Encryption(EncryptionCommand::EnableKeyBackup))516
+ .await419517
}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
.await429524
}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
.await439531
}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
.await449538
}450539
451
- pub fn try_send(&self, command: Command) -> Result<CommandId, SendCommandError> {452
- let id = self.next_command_id();453
- self.commands454
- .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
+ .await460543
}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 download546
+ pub async fn media(&self, media_id: &MediaId, size: MediaSize) -> Result<Media, Error> {547
+ self.media.timeline_media(media_id, size).await548
+ }549
+550
+ /// bytes for any plain mxc uri, avatars mostly. same byte cap as everything else551
+ pub async fn mxc_media(&self, uri: &MxcUri, size: MediaSize) -> Result<Media, Error> {552
+ self.media.mxc_media(uri, size).await553
+ }554
+555
+ /// the space graph builds on first use and then keeps itself current556
+ async fn spaces(&self) -> &SpaceService {557
+ self.spaces558
+ .get_or_init(|| SpaceService::new(self.client.clone()))559
+ .await464560
}465561
466562
pub async fn rooms(&self) -> Vec<RoomSummary> {467
- room_summaries(&self.client).await563
+ room_summaries(&self.client, self.spaces().await).await468564
}469565
470
- pub async fn rooms_filtered(&self, filter: &RoomListFilter) -> Vec<RoomSummary> {471
- let mut room_filter = filter.clone();472
- let search = room_filter473
- .search474
- .take()475
- .map(|search| search.to_lowercase());566
+ /// browse a space the way the server sees it, rooms you haven't joined included567
+ 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 = self573
+ .client574
+ .user_id()575
+ .ok_or_else(|| Error::new(ErrorKind::Unauthenticated, "the client is not logged in"))?576
+ .to_owned();577
+ let response = self578
+ .client579
+ .account()580
+ .fetch_user_profile()581
+ .await582
+ .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 = self588
+ .client589
+ .account()590
+ .fetch_user_profile_of(user_id)591
+ .await592
+ .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
+ .await599
+ }600
+601
+ pub async fn set_avatar(&self, avatar: Option<OwnedMxcUri>) -> Result<(), Error> {602
+ self.run(Command::Account(AccountCommand::SetAvatar(avatar)))603
+ .await604
+ }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
+ .await616
+ }476617
618
+ pub async fn rooms_filtered(&self, filter: &RoomListFilter) -> Vec<RoomSummary> {619
+ let matcher = filter.matcher();477620
self.rooms()478621
.await479622
.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(self493630
.client494631
.devices()495632
.await496
- .map_err(|source| ClientQueryError::Sdk(source.into()))?633
+ .map_err(|source| Error::from_http("listing devices failed", source))?497634
.devices498635
.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.client648
+ .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 = self514
- .client515
- .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 = self547694
.notification_client548695
.get_or_try_init(|| async {@@ -555,11 +702,11 @@ impl ClientHandle {
555702
.await556703
})557704
.await558
- .map_err(ClientQueryError::Notification)?;705
+ .map_err(notification_failed)?;559706
let status = client560707
.get_notification(room_id, event_id)561708
.await562
- .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 = self572
- .client573
- .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(room726
+ Ok(self727
+ .room(room_id)?584728
.members(memberships)585729
.await586
- .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 = self597
- .client598
- .get_room(room_id)599
- .ok_or_else(|| ClientQueryError::RoomUnknown(room_id.to_owned()))?;600
- let levels = room736
+ pub async fn room_power_levels(&self, room_id: &RoomId) -> Result<RoomPowerLevels, Error> {737
+ let levels = self738
+ .room(room_id)?601739
.power_levels()602740
.await603
- .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 = self631
- .client632
- .get_room(room_id)633
- .ok_or_else(|| ClientQueryError::RoomUnknown(room_id.to_owned()))?;634
- let (echoes, _) = room767
+ ) -> Result<Vec<OwnedTransactionId>, Error> {768
+ let (echoes, _) = self769
+ .room(room_id)?635770
.send_queue()636771
.subscribe()637772
.await638
- .map_err(ClientQueryError::SendQueue)?;773
+ .map_err(|source| {774
+ Error::with_source(ErrorKind::Store, "reading the send queue failed", source)775
+ })?;639776
Ok(echoes640777
.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.client656793
.account()657794
.account_data_raw(event_type)658795
.await659
- .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.commands669
- .send(DriverRequest::SetGlobalAccountData {670
- event_type,671
- content,672
- reply,673
- })674
- .await675
- .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 = self685
- .client686
- .get_room(room_id)687
- .ok_or_else(|| ClientQueryError::RoomUnknown(room_id.to_owned()))?;688
- room.account_data(event_type)689
- .await690
- .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.commands701
- .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
.await708
- .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 to829
+pub struct SpaceHierarchy(SpaceRoomList);830
+831
+impl fmt::Debug for SpaceHierarchy {832
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {833
+ formatter834
+ .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 flight841
+ 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 excluded852
+ 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 the802
- /// engine can notice a lost event consumer inside a `select!`.958
+ /// resolves once every `EventStream` is gone803959
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().await973
+ pub(crate) async fn recv(&mut self) -> Option<Request> {974
+ self.requests.recv().await819975
}820976
821
- pub async fn send(&self, event: Event) -> Result<(), SendEventError> {822
- self.event_sender().send(event).await977
+ 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
- .await962
- .unwrap();963
- client964
- .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
- .await975
- .unwrap();976
- client977
- }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_handle993
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))994
- .await995
- .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 == id1003
- ));1004
- assert!(second_driver.commands.try_recv().is_err());1005
-1006
- first_driver1007
- .send(Event::ConnectionChanged(ConnectionState::Online))1008
- .await1009
- .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
- handle1024
- .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 = handle1039
- .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 = cloned1049
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))1050
- .await1051
- .unwrap();1052
- assert_eq!(third.get(), 3);1053
- driver.recv().await.unwrap();1054
-1055
- let fourth = handle1056
- .try_send(Command::Lifecycle(LifecycleCommand::Shutdown))1057
- .unwrap();1058
- assert_eq!(fourth.get(), 4);1059
- }1060
-}@@ -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 = upload2424
.mimetype2525
.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 == 0127127
|| thumbnail.height == 0128128
{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 = thumbnail135
+ .mimetype136
+ .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
})?;@@ -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
- handle51
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))52
- .await53
- .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
- handle66
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))67
- .await68
- .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
- .await84
- .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)@@ -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 = handle48
+ .execute(Command::Timeline(command))49
+ .await50
+ .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
+ server75
+ .mock_room_send()76
+ .ok(event_id!("$sent:localhost"))77
+ .expect(2)78
+ .mount()79
+ .await;7280
73
- let first_id = handle74
- .send(Command::Timeline(TimelineCommand::Send {81
+ let first = handle82
+ .try_send(Command::Timeline(TimelineCommand::Send {7583
timeline_id: room_id.to_owned().into(),7684
content: MessageContent::text("first"),7785
}))78
- .await7986
.unwrap();80
- let second_id = handle81
- .send(Command::Timeline(TimelineCommand::Send {87
+ let second = handle88
+ .try_send(Command::Timeline(TimelineCommand::Send {8289
timeline_id: room_id.to_owned().into(),8390
content: MessageContent::text("second"),8491
}))85
- .await8692
.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 was95
+ // processed first, only the order the sends actually reached the homeserver can96
+ 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> = server103
+ .received_requests()104
+ .await105
+ .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
+ .await121
+ .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 = handle150
+/// opens `room_id` and confirms its `TimelineReset` was already on the151
+/// stream by the time the open command answered152
+async fn open_room(handle: &ClientHandle, events: &mut EventStream, room_id: &RoomId) {153
+ let mut pending = handle122154
.send(Command::Room(RoomCommand::Open(room_id.to_owned())))123155
.await124156
.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_id165
+ {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
- .await149
- .expect("room did not open");150
- id151
-}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
- .await169
- .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
- completed191
- })192
- .await193
- .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 = handle202
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))203
- .await204
- .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
.await222
- .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
}@@ -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
- handle40
- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))41
- .await42
- .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
handle50
- .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
.await5554
.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
- handle100
- .send(Command::Media(MediaCommand::Fetch {101
- id: media_id.clone(),102
- size: macltok::MediaSize::Full,103
- }))104
- .await105
- .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
- handle113
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))114
- .await115
- .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
- handle140
- .send(Command::Room(RoomCommand::Open(first_id.to_owned())))141
- .await142
- .unwrap();143
- handle144
- .send(Command::Room(RoomCommand::Open(second_id.to_owned())))145
- .await146
- .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_id182
- && 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_id184
- && 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
.await189178
.expect("both retained rooms did not update");190179
191
- handle192
- .send(Command::Room(RoomCommand::Close(first_id.to_owned())))193
- .await194
- .unwrap();180
+ handle.close_room(first_id.to_owned()).await.unwrap();195181
server196182
.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
- handle209
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))210
- .await211
- .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
- handle234
- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))235
- .await236
- .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 = handle256
- .send(Command::Timeline(TimelineCommand::Resync {257
- timeline_id: room_id.to_owned().into(),258
- }))259
- .await260
- .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
- .await280
- .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
.await314271
.expect("resynced timeline did not update");315272
316
- handle317
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))318
- .await319
- .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 = handle341
- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))342
- .await343
- .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 = handle356
- .send(Command::Room(RoomCommand::Close(room_id.to_owned())))357
- .await299
+ let mut close = handle300
+ .try_send(Command::Room(RoomCommand::Close(room_id.to_owned())))358301
.unwrap();359
- let replacement_open = handle360
- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))361
- .await302
+ let mut replacement_open = handle303
+ .try_send(Command::Room(RoomCommand::Open(room_id.to_owned())))362304
.unwrap();363
- let second_close = handle364
- .send(Command::Room(RoomCommand::Close(room_id.to_owned())))365
- .await305
+ let mut second_close = handle306
+ .try_send(Command::Room(RoomCommand::Close(room_id.to_owned())))366307
.unwrap();367
- let final_open = handle368
- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))369
- .await308
+ let mut final_open = handle309
+ .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 observed373314
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_id325
+ {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
.await403354
.expect("room lifecycle commands did not complete");404
- handle405
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))406
- .await407
- .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
.await430382
.unwrap();431383
let running = tokio::spawn(engine.run());432
- let room_open = handle433
- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))434
- .await435
- .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 = handle443
- .send(Command::Timeline(TimelineCommand::OpenThread {386
+ let mut open = handle387
+ .try_send(Command::Timeline(TimelineCommand::OpenThread {444388
room_id: room_id.to_owned(),445389
root_event_id: root.to_owned(),446390
}))447
- .await448391
.unwrap();449
- let send = handle450
- .send(Command::Timeline(TimelineCommand::Send {392
+ let mut send = handle393
+ .try_send(Command::Timeline(TimelineCommand::Send {451394
timeline_id: TimelineId::thread(room_id.to_owned(), root.to_owned()),452395
content: MessageContent::text(""),453396
}))454
- .await455
- .unwrap();456
- let resync = handle457
- .send(Command::Timeline(TimelineCommand::Resync {458
- timeline_id: TimelineId::thread(room_id.to_owned(), root.to_owned()),459
- }))460
- .await461397
.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
.await480418
.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 = true489
- }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
- .await502
- .expect("thread resync did not complete");503
- handle504
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))505
- .await506
- .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
- handle529
- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))530
- .await531
- .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
handle563
- .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
.await568483
.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
handle585
- .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
.await590502
.unwrap();591503
receive_history(&mut events, room_id, HistoryState::StartReached).await;592504
593
- handle594
- .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 the509
+/// stream by the time the resync command answered, racing the two so we510
+/// don't have to drain the whole stream first511
+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 = handle517
+ .send(Command::Timeline(TimelineCommand::Resync {518
+ timeline_id: timeline_id.clone(),519
+ }))595520
.await596521
.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_id530
+ {531
+ reset_items = Some(items);532
+ }533
+ }534
+ result = &mut pending => {535
+ result.expect("resync failed");536
+ break;537
+ }538
+ }539
+ }540
+ })541
+ .await542
+ .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
- items621
- .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::Clear638
- | TimelineDiff::PopFront639
- | TimelineDiff::PopBack640
- | 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
.await691602
.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
- .await707
- .expect("expected event did not arrive")708
-}@@ -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
.await9596
.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
- harness123
- .handle124
- .send(Command::Media(MediaCommand::Fetch {125
- id: harness.media_id.clone(),126
- size: MediaSize::Full,127
- }))128
- .await129
- .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
.await154124
.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 = harness156129
.server157130
.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 = harness154
+155
+ let error = harness182156
.handle183
- .send(Command::Media(MediaCommand::Fetch {184
- id: harness.media_id.clone(),185
- size: MediaSize::Full,186
- }))157
+ .media(&harness.media_id, MediaSize::Full)187158
.await188
- .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 = harness214
+215
+ let error = harness246216
.handle247
- .send(Command::Media(MediaCommand::Fetch {248
- id: harness.media_id.clone(),249
- size: MediaSize::Thumbnail,250
- }))217
+ .media(&harness.media_id, thumbnail_size())251218
.await252
- .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::MediaUnavailable259
- )260
- })261
- .await;262
- assert!(matches!(263
- event,264
- Event::CommandFailed(failure)265
- if failure.id == command_id && failure.kind == CommandErrorKind::MediaUnavailable266
- ));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 = harness297
- .handle298
- .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
- .await306
- .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
.await326272
.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.own351297
&& 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 = harness314
+ let fetched = harness368315
.handle369
- .send(Command::Media(MediaCommand::Fetch {370
- id: media_id.clone(),371
- size: MediaSize::Full,372
- }))316
+ .media(&media_id, MediaSize::Full)373317
.await374318
.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 = harness610530
.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::MediaTooLarge722
- ));619
+ let error = harness.handle.upload(upload).await.unwrap_err();620
+ assert_eq!(error.kind, ErrorKind::MediaTooLarge);723621
assert!(724622
!harness725623
.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::MediaUnavailable642
+ 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
!harness762652
.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 = harness776
- .handle777
- .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
- .await785
- .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
- handle822
- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))823
- .await824
- .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
- harness863
- .handle864
- .send(Command::Media(MediaCommand::Fetch {865
- id: harness.media_id.clone(),866
- size,867
- }))868
- .await869
- .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 == &size875
- )876
- })877
- .await878
- {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
harness886749
.handle887
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))750
+ .media(&harness.media_id, size)888751
.await889
- .unwrap();890
- receive_until(&mut harness.events, |event| matches!(event, Event::Stopped)).await;891
- harness.running.await.unwrap().unwrap();752
+ .unwrap()753
+ .bytes892754
}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
- items961
- .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
- .await991
- .expect("expected event did not arrive")992
-}@@ -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_id197
- ));194
+ assert_eq!(195
+ handle196
+ .members(room_id, MemberFilter::All)197
+ .await198
+ .unwrap_err()199
+ .kind,200
+ ErrorKind::RoomUnknown201
+ );198202
199203
drop(engine);200204
}@@ -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
- handle33
- .send(Command::Room(RoomCommand::Open(room_id.to_owned())))34
- .await35
- .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 = handle80
- .send(Command::Media(MediaCommand::Fetch {81
- id: media_id.clone(),82
- size: MediaSize::Full,83
- }))84
- .await85
- .unwrap();80
+ // media no longer travels through the command queue, so driving it on its81
+ // own task is what puts the request in flight concurrently with the send below82
+ 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 = handle88
- .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
- .await93
- .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
.await119
- .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_id128
- )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
- handle133
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))100
+ let media = tokio::time::timeout(Duration::from_secs(2), media_task)134101
.await102
+ .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
.await156126
.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
- items168
- .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::Clear185
- | TimelineDiff::PopFront186
- | TimelineDiff::PopBack187
- | 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
- .await206
- .expect("expected event did not arrive")207
-}@@ -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 = handle28
+ handle2829
.cancel_send(room_id.to_owned(), transaction_id.clone())2930
.await3031
.unwrap();31
- expect_success(&mut events, id, CommandKind::Room).await;3232
3333
let (echoes, _) = client3434
.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 = handle46
+ let error = handle4747
.cancel_send(room_id.to_owned(), transaction_id)4848
.await49
- .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 = handle70
- .upload_media(71
- room_id.to_owned().into(),72
- "note.txt".to_owned(),73
- "text/plain".to_owned(),62
+ handle63
+ .upload(AttachmentUpload::new(64
+ room_id.to_owned(),65
+ "note.txt",66
+ "text/plain",7467
b"attachment".to_vec(),75
- None,76
- )68
+ ))7769
.await7870
.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 = handle74
+ handle8475
.cancel_send(room_id.to_owned(), pending[0].clone())8576
.await8677
.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 = handle102
+ handle113103
.retry_send(room_id.to_owned(), transaction_id)114104
.await115105
.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
.await135124
.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
.await163152
.unwrap();164153
let running = tokio::spawn(engine.run());165
- let mut events = events;166
- let id = handle167
- .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
.await169
- .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 id174165
async fn queue_message(175166
handle: &ClientHandle,176167
events: &mut EventStream,177168
room_id: &RoomId,178169
body: &str,179170
) -> OwnedTransactionId {180
- let id = handle181
- .send(Command::Timeline(TimelineCommand::Send {182
- timeline_id: room_id.to_owned().into(),183
- content: MessageContent::text(body),184
- }))185
- .await186
- .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
- .await221
- .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
- .await241
- .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 = handle250
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))184
+ })251185
.await252
- .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_id255191
}@@ -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 = rooms3231
.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
- handle39
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))40
- .await41
- .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
- handle76
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))77
- .await78
- .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
.await9988
.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
.await123112
.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
.await154143
.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
- .await170
- .expect("expected event did not arrive")171
-}@@ -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
+ .await17
+ .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 out21
+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)).await28
+ });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
+ items40
+ .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
+}@@ -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
.await4443
.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 = handle61
- .send(Command::Room(RoomCommand::Forget(room_id.to_owned())))62
- .await63
- .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 = handle117
- .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
.await127121
.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
.await168162
.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
+ handle174
+ .leave_room(unknown.to_owned())175
+ .await176
+ .unwrap_err()177
+ .kind,178
+ ErrorKind::RoomUnknown179
+ );180
+ assert_eq!(181
+ handle182
+ .forget_room(unknown.to_owned())183
+ .await184
+ .unwrap_err()185
+ .kind,186
+ ErrorKind::RoomUnknown187
+ );188
+ assert_eq!(189
+ handle190
+ .accept_invite(unknown.to_owned())191
+ .await192
+ .unwrap_err()193
+ .kind,194
+ ErrorKind::RoomUnknown195
+ );196
+ assert_eq!(197
+ handle198
+ .reject_invite(unknown.to_owned())199
+ .await200
+ .unwrap_err()201
+ .kind,202
+ ErrorKind::RoomUnknown203
+ );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
handle240259
.room_account_data(room_id!("!missing:localhost"), room_type.clone())241
- .await,242
- Err(ClientQueryError::RoomUnknown(_))243
- ));260
+ .await261
+ .unwrap_err()262
+ .kind,263
+ ErrorKind::RoomUnknown264
+ );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
handle270291
.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
.await278299
.unwrap();279
- assert!(matches!(300
+ assert_eq!(280301
handle281302
.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
+ .await310
+ .unwrap_err()311
+ .kind,312
+ ErrorKind::RoomUnknown313
+ );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
- .await346
- .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
- .await370
- .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 = handle379
- .send(Command::Lifecycle(LifecycleCommand::Shutdown))380
- .await381
- .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
- .await399
- .expect("engine did not stop");400
- running.await.unwrap().unwrap();401
-}