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
+ enshow full diff (262144 bytes truncated)