atagen

macltok

public
Star0
Watch0
cff1b0a4eb30290a6fe32fea825c816b696fe4f4
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/mod.rsRust1283 lines46.2 KB
1
mod commands;
2
pub(crate) mod context;
3
pub(crate) mod executor;
4
mod media;
5
mod mutations;
6
mod packs;
7
mod prepare;
8
mod repair;
9
mod room;
10
mod session;
11
mod thread_list;
12
mod timeline;
13
mod url_preview;
14
mod watchers;
15
16
use std::{
17
    collections::{HashMap, VecDeque},
18
    future::Future,
19
    panic::AssertUnwindSafe,
20
    sync::Arc,
21
};
22
23
use futures_util::FutureExt;
24
use matrix_sdk::{Client, SendOutsideWasm, SessionChange};
25
use matrix_sdk_ui::sync_service::SyncService;
26
use ruma::{OwnedDeviceId, OwnedRoomId};
27
use thiserror::Error as ThisError;
28
use tokio::sync::{broadcast, mpsc, watch};
29
30
pub(crate) use media::MediaService;
31
pub(crate) use session::Persistence;
32
33
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
34
pub(crate) use crate::store_lock::StoreLock;
35
#[cfg(not(any(feature = "sqlite", feature = "indexeddb")))]
36
pub(crate) enum StoreLock {}
37
38
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
39
pub(crate) use crate::mentions::store::MentionsStore;
40
#[cfg(not(any(feature = "sqlite", feature = "indexeddb")))]
41
pub(crate) enum MentionsStore {}
42
43
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
44
pub(crate) use crate::search::store::SearchStore;
45
#[cfg(not(any(feature = "sqlite", feature = "indexeddb")))]
46
pub(crate) enum SearchStore {}
47
48
use self::{
49
    commands::Job,
50
    executor::{EngineBoxFuture, TaskSet},
51
    mutations::{Mutation, MutationJob},
52
    packs::PackEdit,
53
    room::{ActorNote, ActorRequest, RoomActor},
54
    timeline::{SessionContext, Supervisor},
55
};
56
use crate::{
57
    AccountCommand, ClientHandle, Command, CommandOutput, ConnectionState, EncryptionCommand,
58
    EngineErrorKind, EngineFailure, Error, ErrorKind, Event, EventStream, ImagePackSource,
59
    LifecycleCommand, MediaCommand, NotificationCommand, PackCommand, RoomCommand, Termination,
60
    ThreadListCommand, TimelineCommand,
61
    normalise::{ROOM_PACKS, ROOM_PREVIEW_URLS},
62
    transport::{DriverPort, Request, Responder, transport_pair},
63
};
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
    /// what a thread paginates as it opens, and a room if its recent load fails. 0 for neither
71
    pub initial_history_limit: u16,
72
    pub max_media_bytes: usize,
73
    /// media one event context session keeps registered, least recently used dropped first
74
    pub max_context_media: usize,
75
    pub max_mentions_bytes: usize,
76
    pub max_mentions_entries: usize,
77
    pub max_mention_context_events: usize,
78
    pub max_mention_candidates: usize,
79
    /// warm a room up for sending when it's opened or someone starts typing in it
80
    pub prepare_to_send: bool,
81
    /// `/messages` pages each joined room gets per engine run for search backfill. 0 for none
82
    pub search_backfill_pages_per_room: u16,
83
    pub search_backfill_page_size: u16,
84
    /// the least time between two backfill requests, across every room
85
    pub search_backfill_interval_ms: u64,
86
    /// undecryptable messages kept to index once their keys turn up, newest first
87
    pub max_search_undecrypted: usize,
88
}
89
90
impl EngineConfig {
91
    #[must_use]
92
    pub const fn with_max_mentions_bytes(mut self, bytes: usize) -> Self {
93
        self.max_mentions_bytes = bytes;
94
        self
95
    }
96
97
    #[must_use]
98
    pub const fn with_max_mentions_entries(mut self, entries: usize) -> Self {
99
        self.max_mentions_entries = entries;
100
        self
101
    }
102
103
    #[must_use]
104
    pub const fn with_max_mention_context_events(mut self, events: usize) -> Self {
105
        self.max_mention_context_events = events;
106
        self
107
    }
108
109
    #[must_use]
110
    pub const fn with_max_mention_candidates(mut self, candidates: usize) -> Self {
111
        self.max_mention_candidates = candidates;
112
        self
113
    }
114
115
    #[must_use]
116
    pub const fn with_command_capacity(mut self, capacity: usize) -> Self {
117
        self.command_capacity = capacity;
118
        self
119
    }
120
121
    #[must_use]
122
    pub const fn with_event_capacity(mut self, capacity: usize) -> Self {
123
        self.event_capacity = capacity;
124
        self
125
    }
126
127
    #[must_use]
128
    pub const fn with_initial_history_limit(mut self, limit: u16) -> Self {
129
        self.initial_history_limit = limit;
130
        self
131
    }
132
133
    #[must_use]
134
    pub const fn with_max_media_bytes(mut self, bytes: usize) -> Self {
135
        self.max_media_bytes = bytes;
136
        self
137
    }
138
139
    #[must_use]
140
    pub const fn with_max_context_media(mut self, media: usize) -> Self {
141
        self.max_context_media = media;
142
        self
143
    }
144
145
    #[must_use]
146
    pub const fn with_prepare_to_send(mut self, prepare: bool) -> Self {
147
        self.prepare_to_send = prepare;
148
        self
149
    }
150
151
    #[must_use]
152
    pub const fn with_search_backfill_pages_per_room(mut self, pages: u16) -> Self {
153
        self.search_backfill_pages_per_room = pages;
154
        self
155
    }
156
157
    #[must_use]
158
    pub const fn with_search_backfill_page_size(mut self, events: u16) -> Self {
159
        self.search_backfill_page_size = events;
160
        self
161
    }
162
163
    #[must_use]
164
    pub const fn with_search_backfill_interval_ms(mut self, interval_ms: u64) -> Self {
165
        self.search_backfill_interval_ms = interval_ms;
166
        self
167
    }
168
169
    #[must_use]
170
    pub const fn with_max_search_undecrypted(mut self, events: usize) -> Self {
171
        self.max_search_undecrypted = events;
172
        self
173
    }
174
}
175
176
impl Default for EngineConfig {
177
    fn default() -> Self {
178
        Self {
179
            command_capacity: 64,
180
            event_capacity: 128,
181
            initial_history_limit: 30,
182
            max_media_bytes: 20 * 1024 * 1024,
183
            max_context_media: 1_024,
184
            max_mentions_bytes: 64 * 1024 * 1024,
185
            max_mentions_entries: 5_000,
186
            max_mention_context_events: 25_000,
187
            max_mention_candidates: 5_000,
188
            prepare_to_send: true,
189
            search_backfill_pages_per_room: 10,
190
            search_backfill_page_size: 100,
191
            search_backfill_interval_ms: 2_000,
192
            max_search_undecrypted: 2_000,
193
        }
194
    }
195
}
196
197
#[derive(Debug, ThisError)]
198
pub enum EngineError {
199
    #[error("the Matrix client is not authenticated")]
200
    Unauthenticated,
201
    #[error("command, event, media source, and mentions capacities must be nonzero")]
202
    ZeroCapacity,
203
    #[error("could not build the sync service")]
204
    SyncService(#[source] matrix_sdk_ui::sync_service::Error),
205
    #[error("the event receiver has closed")]
206
    EventReceiverClosed,
207
    #[error("engine task {task} panicked")]
208
    TaskPanicked { task: String },
209
}
210
211
impl EngineError {
212
    fn failure(&self) -> EngineFailure {
213
        EngineFailure {
214
            kind: match self {
215
                Self::EventReceiverClosed => EngineErrorKind::EventReceiverClosed,
216
                Self::TaskPanicked { .. } => EngineErrorKind::Watcher,
217
                Self::Unauthenticated | Self::ZeroCapacity | Self::SyncService(_) => {
218
                    EngineErrorKind::Other
219
                }
220
            },
221
            message: self.to_string(),
222
        }
223
    }
224
}
225
226
#[derive(Clone)]
227
enum TaskContext {
228
    /// a panic here costs the caller an `Interrupted` answer and nothing else
229
    Command(&'static str),
230
    /// a panic here takes the engine with it
231
    Watcher(&'static str),
232
    RoomActor {
233
        room_id: OwnedRoomId,
234
        generation: u64,
235
    },
236
}
237
238
impl TaskContext {
239
    fn label(&self) -> String {
240
        match self {
241
            Self::Command(name) | Self::Watcher(name) => (*name).to_owned(),
242
            Self::RoomActor {
243
                room_id,
244
                generation,
245
            } => format!("room actor {room_id} generation {generation}"),
246
        }
247
    }
248
}
249
250
/// tasks catch their own panics so each comes back with its context, since joining
251
/// through the sdk's executor gives no task id to look one up by. wasm has nothing to catch
252
fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
253
    payload
254
        .downcast_ref::<&str>()
255
        .map(ToString::to_string)
256
        .or_else(|| payload.downcast_ref::<String>().cloned())
257
        .unwrap_or_else(|| "task panicked with a non-string payload".to_owned())
258
}
259
260
struct TaskRegistry {
261
    tasks: TaskSet<(TaskContext, Option<String>)>,
262
}
263
264
impl TaskRegistry {
265
    fn spawn(
266
        &mut self,
267
        context: TaskContext,
268
        task: impl Future<Output = ()> + SendOutsideWasm + 'static,
269
    ) {
270
        self.tasks.spawn(async move {
271
            let panic = AssertUnwindSafe(task)
272
                .catch_unwind()
273
                .await
274
                .err()
275
                .map(panic_message);
276
            (context, panic)
277
        });
278
    }
279
280
    fn is_empty(&self) -> bool {
281
        self.tasks.is_empty()
282
    }
283
284
    async fn join_next(&mut self) -> Option<(TaskContext, Option<String>)> {
285
        loop {
286
            if let Ok(done) = self.tasks.join_next().await? {
287
                return Some(done);
288
            }
289
        }
290
    }
291
292
    async fn shutdown(&mut self) {
293
        self.tasks.shutdown().await;
294
    }
295
}
296
297
enum RoomSlot {
298
    Active {
299
        generation: u64,
300
        sender: mpsc::Sender<ActorRequest>,
301
    },
302
    /// opens and closes that arrive while the actor winds down, replayed in order once it's gone
303
    Closing {
304
        generation: u64,
305
        pending: VecDeque<Deferred>,
306
    },
307
}
308
309
impl RoomSlot {
310
    const fn generation(&self) -> u64 {
311
        match self {
312
            Self::Active { generation, .. } | Self::Closing { generation, .. } => *generation,
313
        }
314
    }
315
}
316
317
enum Deferred {
318
    Open(Responder),
319
    Close(Responder),
320
}
321
322
/// logouts in flight, and a token rejection held back because one of them caused it
323
#[derive(Default)]
324
pub(super) struct Logouts {
325
    pub(super) in_flight: usize,
326
    pub(super) rejected: Option<Option<OwnedDeviceId>>,
327
}
328
329
struct Queues {
330
    mutations: mpsc::Receiver<MutationJob>,
331
    notifications: mpsc::Receiver<Job<NotificationCommand>>,
332
    encryption: mpsc::Receiver<Job<EncryptionCommand>>,
333
    thread_lists: mpsc::Receiver<Job<ThreadListCommand>>,
334
    session_changes: broadcast::Receiver<SessionChange>,
335
    persistence: Option<Persistence>,
336
}
337
338
/// requests other devices send us, handed from the sdk's event handler to the encryption worker
339
type IncomingVerifications =
340
    mpsc::UnboundedSender<matrix_sdk::encryption::verification::VerificationRequest>;
341
342
pub struct Engine {
343
    client: Client,
344
    sync: Arc<SyncService>,
345
    port: DriverPort,
346
    config: EngineConfig,
347
    media: Arc<MediaService>,
348
    contexts: Arc<context::Sessions>,
349
    tasks: TaskRegistry,
350
    rooms: HashMap<OwnedRoomId, RoomSlot>,
351
    next_generation: u64,
352
    notes: mpsc::UnboundedReceiver<ActorNote>,
353
    note_sender: mpsc::UnboundedSender<ActorNote>,
354
    /// the rooms sliding sync should follow, applied off the run loop since it touches the store
355
    subscriptions: watch::Sender<Vec<OwnedRoomId>>,
356
    mutations: mpsc::Sender<MutationJob>,
357
    notifications: mpsc::Sender<Job<NotificationCommand>>,
358
    encryption: mpsc::Sender<Job<EncryptionCommand>>,
359
    thread_lists: mpsc::Sender<Job<ThreadListCommand>>,
360
    queues: Option<Queues>,
361
    /// the client can outlive us, so the handler has to come off it when we stop
362
    verification_handler: Option<matrix_sdk::event_handler::EventHandlerHandle>,
363
    stop: mpsc::UnboundedSender<Termination>,
364
    stops: mpsc::UnboundedReceiver<Termination>,
365
    logouts: Arc<watch::Sender<Logouts>>,
366
    preparations: prepare::Preparations,
367
    /// shared with every `ClientHandle`, so the store stays locked while the client is reachable
368
    _store_lock: Option<Arc<StoreLock>>,
369
    mentions: crate::mentions::service::Handle,
370
    mentions_service: Option<crate::mentions::service::Service>,
371
    mentions_updates: Option<watch::Receiver<crate::MentionsPage>>,
372
    search: crate::search::Handle,
373
    search_service: Option<crate::search::Service>,
374
}
375
376
impl Engine {
377
    /// the escape hatch for a client you built yourself. `Session` is the usual way in
378
    pub async fn from_client(
379
        client: Client,
380
        config: EngineConfig,
381
    ) -> Result<(Self, ClientHandle, EventStream), EngineError> {
382
        let session_changes = client.subscribe_to_session_changes();
383
        Self::with_persistence(client, config, session_changes, None, None, None, None).await
384
    }
385
386
    /// `session_changes` is taken when the client is built, since the sdk can refresh
387
    /// before an engine exists
388
    pub(crate) async fn with_persistence(
389
        client: Client,
390
        config: EngineConfig,
391
        session_changes: broadcast::Receiver<SessionChange>,
392
        persistence: Option<Persistence>,
393
        store_lock: Option<StoreLock>,
394
        mentions: Option<Result<MentionsStore, crate::mentions::service::Unopened>>,
395
        search: Option<Result<SearchStore, crate::Error>>,
396
    ) -> Result<(Self, ClientHandle, EventStream), EngineError> {
397
        if client.user_id().is_none() {
398
            return Err(EngineError::Unauthenticated);
399
        }
400
        if config.command_capacity == 0
401
            || config.event_capacity == 0
402
            || config.max_context_media == 0
403
            || config.max_mentions_entries == 0
404
            || config.max_mention_context_events == 0
405
            || config.max_mention_candidates == 0
406
        {
407
            return Err(EngineError::ZeroCapacity);
408
        }
409
        let store_lock = store_lock.map(Arc::new);
410
        let media = Arc::new(MediaService::new(
411
            client.clone(),
412
            config.max_media_bytes,
413
            config.max_context_media,
414
        ));
415
        let (mentions, mentions_service, mentions_updates) =
416
            crate::mentions::service::Service::new(
417
                client.clone(),
418
                media.clone(),
419
                mentions,
420
                config.clone(),
421
            )
422
            .await
423
            // a missing user id is all it still refuses, and that was turned away above
424
            .map_err(|_| EngineError::Unauthenticated)?;
425
        let (search, search_service) =
426
            crate::search::Service::new(client.clone(), media.clone(), search, &config);
427
        let sync = Arc::new(
428
            SyncService::builder(client.clone())
429
                .with_offline_mode()
430
                .with_room_list_extra_required_state(vec![
431
                    (ROOM_PACKS.into(), "*".to_owned()),
432
                    (ROOM_PREVIEW_URLS.into(), String::new()),
433
                ])
434
                .with_room_list_response_commit_hook(mentions.hook())
435
                .build()
436
                .await
437
                .map_err(EngineError::SyncService)?,
438
        );
439
        let (handle, port, events) = transport_pair(
440
            client.clone(),
441
            sync.clone(),
442
            media.clone(),
443
            store_lock.clone(),
444
            config.command_capacity,
445
            config.event_capacity,
446
        );
447
        client.send_queue().enable_upload_progress(true);
448
        client.send_queue().enable_global_upload_progress(true);
449
        let (note_sender, notes) = mpsc::unbounded_channel();
450
        let (stop, stops) = mpsc::unbounded_channel();
451
        let (mutations, mutation_queue) = mpsc::channel(config.command_capacity);
452
        let (notifications, notification_queue) = mpsc::channel(config.command_capacity);
453
        let (encryption, encryption_queue) = mpsc::channel(config.command_capacity);
454
        let (thread_lists, thread_list_queue) = mpsc::channel(config.command_capacity);
455
        let engine = Self {
456
            contexts: Arc::new(context::Sessions::new(port.event_sender())),
457
            client,
458
            sync,
459
            port,
460
            config,
461
            media,
462
            tasks: TaskRegistry {
463
                tasks: TaskSet::new(),
464
            },
465
            rooms: HashMap::new(),
466
            next_generation: 0,
467
            notes,
468
            note_sender,
469
            subscriptions: watch::Sender::new(Vec::new()),
470
            mutations,
471
            notifications,
472
            encryption,
473
            thread_lists,
474
            queues: Some(Queues {
475
                mutations: mutation_queue,
476
                notifications: notification_queue,
477
                encryption: encryption_queue,
478
                thread_lists: thread_list_queue,
479
                session_changes,
480
                persistence,
481
            }),
482
            verification_handler: None,
483
            preparations: prepare::Preparations::default(),
484
            stop,
485
            stops,
486
            logouts: Arc::new(watch::Sender::new(Logouts::default())),
487
            _store_lock: store_lock,
488
            mentions,
489
            mentions_service: Some(mentions_service),
490
            mentions_updates: Some(mentions_updates),
491
            search,
492
            search_service: Some(search_service),
493
        };
494
        Ok((engine, handle, events))
495
    }
496
497
    /// boxed because the engine's future is deep enough to blow the compiler's query depth
498
    /// in every crate that spawns it, which isn't something a client should have to fix
499
    pub fn run(self) -> EngineBoxFuture<'static, Result<Termination, EngineError>> {
500
        Box::pin(self.run_inner())
501
    }
502
503
    async fn run_inner(mut self) -> Result<Termination, EngineError> {
504
        let events = self.port.event_sender();
505
        let user_id = self
506
            .client
507
            .user_id()
508
            .expect("authentication was checked during construction")
509
            .to_owned();
510
        self.port
511
            .connection_sender()
512
            .send_replace(ConnectionState::Connecting);
513
        for event in [
514
            Event::Authenticated { user_id },
515
            Event::ConnectionChanged(ConnectionState::Connecting),
516
        ] {
517
            if events.send(event).await.is_err() {
518
                self.shutdown().await;
519
                return Err(EngineError::EventReceiverClosed);
520
            }
521
        }
522
        self.start_tasks();
523
        self.sync.start().await;
524
525
        let mut shutdown_reply = None;
526
        let mut handles_dropped = false;
527
        let mut logouts = self.logouts.subscribe();
528
        let ending = loop {
529
            tokio::select! {
530
                biased;
531
                Some(termination) = self.stops.recv() => break Ok(termination),
532
                () = events.closed() => break Err(EngineError::EventReceiverClosed),
533
                () = settled(&mut logouts), if handles_dropped => {
534
                    break Ok(Termination::HandlesDropped);
535
                }
536
                Some((context, panic)) = self.tasks.join_next(), if !self.tasks.is_empty() => {
537
                    let Some(message) = panic else { continue };
538
                    if let TaskContext::Command(label) = context {
539
                        tracing::error!(label, panic = %message, "command task panicked");
540
                        continue;
541
                    }
542
                    break Err(EngineError::TaskPanicked { task: context.label() });
543
                }
544
                Some(note) = self.notes.recv() => self.actor_note(note).await,
545
                request = self.port.recv(), if !handles_dropped => {
546
                    // nobody's left to ask, but a logout they asked for still deserves to land
547
                    let Some(request) = request else {
548
                        handles_dropped = true;
549
                        continue;
550
                    };
551
                    if let Some(reply) = self.dispatch(request).await {
552
                        shutdown_reply = Some(reply);
553
                        break Ok(Termination::ShutDown);
554
                    }
555
                }
556
            }
557
        };
558
559
        if let Err(error) = &ending {
560
            let _ = events.send(Event::Failed(error.failure())).await;
561
        }
562
        self.shutdown().await;
563
        let _ = events.send(Event::Stopped).await;
564
        if let Some(reply) = shutdown_reply {
565
            reply.done(Ok(()));
566
        }
567
        // everything else still queued answers `Interrupted` as it drops, but a shutdown did happen
568
        while let Some(Request { command, reply }) = self.port.try_recv() {
569
            if matches!(command, Command::Lifecycle(LifecycleCommand::Shutdown)) {
570
                reply.done(Ok(()));
571
            }
572
        }
573
        ending
574
    }
575
576
    fn start_tasks(&mut self) {
577
        let queues = self.queues.take().expect("engine tasks start once");
578
        if let Some(service) = self.mentions_service.take() {
579
            self.tasks
580
                .spawn(TaskContext::Watcher("mentions service"), service.run());
581
        }
582
        if let Some(service) = self.search_service.take() {
583
            self.tasks
584
                .spawn(TaskContext::Watcher("search service"), service.run());
585
        }
586
        if let Some(updates) = self.mentions_updates.take() {
587
            let events = self.port.event_sender();
588
            self.tasks.spawn(
589
                TaskContext::Watcher("mentions updates"),
590
                crate::mentions::service::watch_updates(updates, events),
591
            );
592
        }
593
        let (incoming, incoming_verifications) = mpsc::unbounded_channel();
594
        self.verification_handler = Some(self.verification_handler(incoming));
595
        let client = &self.client;
596
        let events = || self.port.event_sender();
597
        let (notices, notice_queue) = mpsc::unbounded_channel();
598
        let watchers: [(&'static str, EngineBoxFuture<'static, ()>); 15] = [
599
            (
600
                "upload tracker",
601
                Box::pin(watchers::uploads(
602
                    client.clone(),
603
                    client.send_queue().subscribe(),
604
                    events(),
605
                )),
606
            ),
607
            (
608
                "room subscription watcher",
609
                Box::pin(watchers::room_subscriptions(
610
                    self.sync.clone(),
611
                    self.subscriptions.subscribe(),
612
                )),
613
            ),
614
            (
615
                "session change watcher",
616
                Box::pin(session::session(
617
                    client.clone(),
618
                    queues.session_changes,
619
                    queues.persistence,
620
                    notices,
621
                    client
622
                        .device_id()
623
                        .expect("authentication was checked during construction")
624
                        .to_owned(),
625
                    self.logouts.clone(),
626
                )),
627
            ),
628
            (
629
                "session notice watcher",
630
                Box::pin(session::session_notices(
631
                    notice_queue,
632
                    events(),
633
                    self.stop.clone(),
634
                )),
635
            ),
636
            (
637
                "connection state watcher",
638
                Box::pin(watchers::connection(
639
                    self.sync.state(),
640
                    self.sync.clone(),
641
                    client.clone(),
642
                    events(),
643
                    self.port.connection_sender(),
644
                )),
645
            ),
646
            (
647
                "send queue watcher",
648
                Box::pin(watchers::send_queue_errors(
649
                    client.clone(),
650
                    client.send_queue().subscribe_errors(),
651
                    self.port.connection_sender().subscribe(),
652
                )),
653
            ),
654
            (
655
                "encryption state watcher",
656
                Box::pin(watchers::encryption(client.clone(), events())),
657
            ),
658
            (
659
                "room list watcher",
660
                Box::pin(watchers::rooms(client.clone(), events())),
661
            ),
662
            (
663
                "event context watcher",
664
                Box::pin(context::close_on_leave(
665
                    client.clone(),
666
                    self.media.clone(),
667
                    self.contexts.clone(),
668
                )),
669
            ),
670
            (
671
                "image pack watcher",
672
                Box::pin(watchers::image_packs(client.clone(), events())),
673
            ),
674
            (
675
                "notification settings watcher",
676
                Box::pin(watchers::notification_settings(client.clone(), events())),
677
            ),
678
            (
679
                "room command worker",
680
                Box::pin(mutations::run_scheduler(
681
                    client.clone(),
682
                    queues.mutations,
683
                    self.config.command_capacity,
684
                )),
685
            ),
686
            (
687
                "notification command worker",
688
                Box::pin(commands::run_notifications(
689
                    client.clone(),
690
                    queues.notifications,
691
                )),
692
            ),
693
            (
694
                "encryption command worker",
695
                Box::pin(commands::run_encryption(
696
                    client.clone(),
697
                    events(),
698
                    queues.encryption,
699
                    incoming_verifications,
700
                )),
701
            ),
702
            (
703
                "thread list worker",
704
                Box::pin(thread_list::run(
705
                    client.clone(),
706
                    events(),
707
                    self.media.clone(),
708
                    queues.thread_lists,
709
                )),
710
            ),
711
        ];
712
        for (label, task) in watchers {
713
            self.tasks.spawn(TaskContext::Watcher(label), task);
714
        }
715
    }
716
717
    fn verification_handler(
718
        &self,
719
        incoming: IncomingVerifications,
720
    ) -> matrix_sdk::event_handler::EventHandlerHandle {
721
        use matrix_sdk::ruma::events::key::verification::request::ToDeviceKeyVerificationRequestEvent;
722
        self.client.add_event_handler(
723
            move |event: ToDeviceKeyVerificationRequestEvent, client: Client| {
724
                let incoming = incoming.clone();
725
                async move {
726
                    let request = client
727
                        .encryption()
728
                        .get_verification_request(
729
                            &event.sender,
730
                            event.content.transaction_id.as_str(),
731
                        )
732
                        .await;
733
                    if let Some(request) = request {
734
                        let _ = incoming.send(request);
735
                    }
736
                }
737
            },
738
        )
739
    }
740
741
    /// hands back the shutdown request's responder, which is answered once we've stopped
742
    async fn dispatch(&mut self, Request { command, reply }: Request) -> Option<Responder> {
743
        let client = self.client.clone();
744
        match command {
745
            Command::Lifecycle(LifecycleCommand::Shutdown) => return Some(reply),
746
            Command::Account(AccountCommand::Logout) => {
747
                let events = self.port.event_sender();
748
                let stop = self.stop.clone();
749
                let logouts = self.logouts.clone();
750
                logouts.send_modify(|logouts| logouts.in_flight += 1);
751
                self.spawn_command("logout", reply, async move {
752
                    let result = client.logout().await;
753
                    if result.is_ok() {
754
                        let _ = events.send(Event::LoggedOut).await;
755
                        let _ = stop.send(Termination::LoggedOut);
756
                    }
757
                    let mut rejected = None;
758
                    logouts.send_if_modified(|logouts| {
759
                        if result.is_err() && logouts.in_flight == 1 {
760
                            rejected = logouts.rejected.take();
761
                        }
762
                        false
763
                    });
764
                    if let Some(soft_logout) = rejected {
765
                        session::reject_session(&events, &stop, soft_logout).await;
766
                    }
767
                    logouts.send_modify(|logouts| logouts.in_flight -= 1);
768
                    result.map_err(|source| Error::from_sdk("logout failed", source))
769
                });
770
            }
771
            Command::Account(AccountCommand::SetAccountData {
772
                event_type,
773
                content,
774
            }) => enqueue(
775
                &self.mutations,
776
                Job {
777
                    command: Mutation::GlobalAccountData {
778
                        event_type,
779
                        content,
780
                    },
781
                    reply,
782
                },
783
            ),
784
            Command::Account(AccountCommand::SetDisplayName(name)) => {
785
                self.spawn_command("set display name", reply, async move {
786
                    client
787
                        .account()
788
                        .set_display_name(name.as_deref())
789
                        .await
790
                        .map_err(|source| Error::from_sdk("setting display name failed", source))
791
                });
792
            }
793
            Command::Account(AccountCommand::SetAvatar(avatar)) => {
794
                self.spawn_command("set avatar", reply, async move {
795
                    client
796
                        .account()
797
                        .set_avatar_url(avatar.as_deref())
798
                        .await
799
                        .map_err(|source| Error::from_sdk("setting avatar failed", source))
800
                });
801
            }
802
            Command::Account(AccountCommand::UploadAvatar { mimetype, bytes }) => {
803
                let max_bytes = self.config.max_media_bytes;
804
                self.spawn_command("upload avatar", reply, async move {
805
                    commands::upload_avatar(&client, &mimetype, bytes, max_bytes).await
806
                });
807
            }
808
            Command::Account(AccountCommand::UploadMedia { mimetype, bytes }) => {
809
                let max_bytes = self.config.max_media_bytes;
810
                self.tasks
811
                    .spawn(TaskContext::Command("upload media"), async move {
812
                        let uploaded =
813
                            commands::upload_media(&client, &mimetype, bytes, max_bytes).await;
814
                        reply.send(uploaded.map(CommandOutput::Mxc));
815
                    });
816
            }
817
            Command::Account(AccountCommand::IgnoreUser(user_id)) => enqueue(
818
                &self.mutations,
819
                Job {
820
                    command: Mutation::Ignore {
821
                        user_id,
822
                        ignored: true,
823
                    },
824
                    reply,
825
                },
826
            ),
827
            Command::Account(AccountCommand::UnignoreUser(user_id)) => enqueue(
828
                &self.mutations,
829
                Job {
830
                    command: Mutation::Ignore {
831
                        user_id,
832
                        ignored: false,
833
                    },
834
                    reply,
835
                },
836
            ),
837
            Command::Account(AccountCommand::ReorderSpaces(order)) => enqueue(
838
                &self.mutations,
839
                Job {
840
                    command: Mutation::ReorderSpaces(order),
841
                    reply,
842
                },
843
            ),
844
            Command::Account(AccountCommand::RenameDevice {
845
                device_id,
846
                display_name,
847
            }) => self.spawn_command("rename device", reply, async move {
848
                commands::rename_device(&client, &device_id, &display_name).await
849
            }),
850
            Command::Account(AccountCommand::DeleteDevices { device_ids, auth }) => {
851
                self.spawn_command("delete devices", reply, async move {
852
                    commands::delete_devices(&client, &device_ids, auth).await
853
                });
854
            }
855
            Command::Room(RoomCommand::Open(room_id)) => {
856
                if self.config.prepare_to_send {
857
                    self.prepare_quietly(room_id.clone());
858
                }
859
                self.open_room(room_id, reply).await;
860
            }
861
            Command::Room(RoomCommand::Close(room_id)) => self.close_room(room_id, reply).await,
862
            Command::Room(RoomCommand::PrepareToSend(room_id)) => {
863
                let flight = self.preparations.join(&client, room_id);
864
                self.spawn_command("prepare to send", reply, flight);
865
            }
866
            Command::Room(RoomCommand::SetTyping { room_id, typing }) => {
867
                if typing && self.config.prepare_to_send {
868
                    self.prepare_quietly(room_id.clone());
869
                }
870
                self.spawn_command("typing", reply, async move {
871
                    commands::set_typing(&client, &room_id, typing).await
872
                });
873
            }
874
            Command::Room(RoomCommand::CancelSend {
875
                room_id,
876
                transaction_id,
877
            }) => self.spawn_command("cancel send", reply, async move {
878
                commands::cancel_send(&client, &room_id, &transaction_id).await
879
            }),
880
            Command::Room(RoomCommand::RetrySend {
881
                room_id,
882
                transaction_id,
883
            }) => self.spawn_command("retry send", reply, async move {
884
                commands::retry_send(&client, &room_id, &transaction_id).await
885
            }),
886
            Command::Room(command) => enqueue(
887
                &self.mutations,
888
                Job {
889
                    command: Mutation::Room(command),
890
                    reply,
891
                },
892
            ),
893
            Command::Timeline(command) => self.route_timeline(command, reply),
894
            Command::EventContext(command) => {
895
                let (client, media, contexts) =
896
                    (client.clone(), self.media.clone(), self.contexts.clone());
897
                self.tasks
898
                    .spawn(TaskContext::Command("event context"), async move {
899
                        reply.send(context::execute(client, media, contexts, command).await);
900
                    });
901
            }
902
            Command::Mentions(command) => {
903
                let mentions = self.mentions.clone();
904
                self.tasks
905
                    .spawn(TaskContext::Command("mentions command"), async move {
906
                        reply.send(mentions.execute(command).await);
907
                    });
908
            }
909
            Command::Search(command) => {
910
                let search = self.search.clone();
911
                self.tasks
912
                    .spawn(TaskContext::Command("search command"), async move {
913
                        reply.send(search.execute(command).await);
914
                    });
915
            }
916
            Command::HistoryRepair(command) => self.route_repair(command, reply),
917
            Command::Media(MediaCommand::Upload(upload)) => self.upload(upload, reply),
918
            Command::Encryption(command) => enqueue(&self.encryption, Job { command, reply }),
919
            Command::Notification(command) => {
920
                enqueue(&self.notifications, Job { command, reply });
921
            }
922
            Command::Pack(command) => self.pack(command, reply),
923
            Command::ThreadList(command) => enqueue(&self.thread_lists, Job { command, reply }),
924
        }
925
        None
926
    }
927
928
    fn spawn_command(
929
        &mut self,
930
        label: &'static str,
931
        reply: Responder,
932
        work: impl Future<Output = Result<(), Error>> + SendOutsideWasm + 'static,
933
    ) {
934
        self.tasks.spawn(TaskContext::Command(label), async move {
935
            reply.done(work.await);
936
        });
937
    }
938
939
    fn prepare_quietly(&mut self, room_id: OwnedRoomId) {
940
        let flight = self.preparations.join(&self.client, room_id);
941
        self.tasks
942
            .spawn(TaskContext::Command("prepare to send"), async move {
943
                let _ = flight.await;
944
            });
945
    }
946
947
    fn pack(&mut self, command: PackCommand, reply: Responder) {
948
        let edit = |pack, edit| Mutation::Pack { pack, edit };
949
        let mutation = match command {
950
            PackCommand::CreateRoomPack {
951
                room_id,
952
                state_key,
953
                display_name,
954
            } => edit(
955
                ImagePackSource::Room { room_id, state_key },
956
                PackEdit::Create { display_name },
957
            ),
958
            PackCommand::Delete(pack) => edit(pack, PackEdit::Delete),
959
            PackCommand::SetMeta {
960
                pack,
961
                display_name,
962
                avatar,
963
                usage,
964
            } => edit(
965
                pack,
966
                PackEdit::Meta {
967
                    display_name,
968
                    avatar,
969
                    usage,
970
                },
971
            ),
972
            PackCommand::PutImage {
973
                pack,
974
                shortcode,
975
                mxc,
976
                body,
977
                usage,
978
                info,
979
            } => edit(
980
                pack,
981
                PackEdit::Put {
982
                    shortcode,
983
                    mxc,
984
                    body,
985
                    usage,
986
                    info,
987
                },
988
            ),
989
            PackCommand::RenameImage { pack, from, to } => {
990
                edit(pack, PackEdit::Rename { from, to })
991
            }
992
            PackCommand::RemoveImage { pack, shortcode } => {
993
                edit(pack, PackEdit::Remove { shortcode })
994
            }
995
            PackCommand::SetEnabledEverywhere {
996
                room_id,
997
                state_key,
998
                enabled,
999
            } => Mutation::PackEnabledEverywhere {
1000
                room_id,
1001
                state_key,
1002
                enabled,
1003
            },
1004
            command => {
1005
                let client = self.client.clone();
1006
                let mutations = self.mutations.clone();
1007
                let max_bytes = self.config.max_media_bytes;
1008
                self.tasks.spawn(TaskContext::Command("pack"), async move {
1009
                    reply.send(packs::run_detached(client, mutations, max_bytes, command).await);
1010
                });
1011
                return;
1012
            }
1013
        };
1014
        enqueue(
1015
            &self.mutations,
1016
            Job {
1017
                command: mutation,
1018
                reply,
1019
            },
1020
        );
1021
    }
1022
1023
    fn upload(&mut self, upload: crate::AttachmentUpload, reply: Responder) {
1024
        let Some(room) = self.client.get_room(upload.timeline_id.room_id()) else {
1025
            return reply.done(Err(Error::room_unknown()));
1026
        };
1027
        let max_bytes = self.config.max_media_bytes;
1028
        self.spawn_command("upload", reply, async move {
1029
            media::queue_upload(room, upload, max_bytes).await
1030
        });
1031
    }
1032
1033
    fn route_timeline(&mut self, command: TimelineCommand, reply: Responder) {
1034
        let sender = match self.rooms.get(command.room_id()) {
1035
            Some(RoomSlot::Active { sender, .. }) => Some(sender.clone()),
1036
            _ => None,
1037
        };
1038
        match (sender, command) {
1039
            (Some(sender), command) => send_actor(&sender, ActorRequest::Execute(command, reply)),
1040
            (
1041
                None,
1042
                TimelineCommand::MarkRead {
1043
                    timeline_id,
1044
                    event_id,
1045
                },
1046
            ) => {
1047
                let client = self.client.clone();
1048
                self.spawn_command("mark read", reply, async move {
1049
                    commands::mark_read(&client, &timeline_id, event_id).await
1050
                });
1051
            }
1052
            (None, _) => reply.done(Err(Error::new(ErrorKind::RoomNotOpen, "room is not open"))),
1053
        }
1054
    }
1055
1056
    fn route_repair(&mut self, command: crate::HistoryRepairCommand, reply: Responder) {
1057
        use crate::HistoryRepairCommand;
1058
        let room_id = match &command {
1059
            HistoryRepairCommand::Cancel { room_id }
1060
            | HistoryRepairCommand::RetryRecent { room_id }
1061
            | HistoryRepairCommand::FillGap { room_id, .. } => room_id,
1062
        };
1063
        match self.rooms.get(room_id) {
1064
            Some(RoomSlot::Active { sender, .. }) => {
1065
                send_actor(sender, ActorRequest::Repair(command, reply))
1066
            }
1067
            Some(RoomSlot::Closing { .. }) => {
1068
                reply.done(Err(Error::new(ErrorKind::RoomBusy, "room is closing")))
1069
            }
1070
            None => reply.done(Err(Error::new(ErrorKind::RoomNotOpen, "room is not open"))),
1071
        }
1072
    }
1073
1074
    async fn open_room(&mut self, room_id: OwnedRoomId, reply: Responder) {
1075
        match self.rooms.get_mut(&room_id) {
1076
            Some(RoomSlot::Active { sender, .. }) => send_actor(sender, ActorRequest::Open(reply)),
1077
            Some(RoomSlot::Closing { pending, .. }) => {
1078
                defer(pending, Deferred::Open(reply), self.config.command_capacity);
1079
            }
1080
            None => self.start_actor(room_id, reply).await,
1081
        }
1082
    }
1083
1084
    async fn close_room(&mut self, room_id: OwnedRoomId, reply: Responder) {
1085
        let (generation, sender) = match self.rooms.get_mut(&room_id) {
1086
            None => return reply.done(Ok(())),
1087
            Some(RoomSlot::Closing { pending, .. }) => {
1088
                return defer(
1089
                    pending,
1090
                    Deferred::Close(reply),
1091
                    self.config.command_capacity,
1092
                );
1093
            }
1094
            Some(RoomSlot::Active { generation, sender }) => (*generation, sender.clone()),
1095
        };
1096
        match sender.try_send(ActorRequest::Close(reply)) {
1097
            Ok(()) => {}
1098
            // the actor already left on its own, which is closed enough
1099
            Err(mpsc::error::TrySendError::Closed(request)) => request.into_reply().done(Ok(())),
1100
            Err(mpsc::error::TrySendError::Full(request)) => {
1101
                return request.into_reply().done(Err(Error::new(
1102
                    ErrorKind::RoomBusy,
1103
                    "room command queue is full",
1104
                )));
1105
            }
1106
        }
1107
        self.rooms.insert(
1108
            room_id,
1109
            RoomSlot::Closing {
1110
                generation,
1111
                pending: VecDeque::new(),
1112
            },
1113
        );
1114
        self.update_room_subscriptions();
1115
    }
1116
1117
    async fn start_actor(&mut self, room_id: OwnedRoomId, reply: Responder) {
1118
        let Some(room) = self.client.get_room(&room_id) else {
1119
            return reply.done(Err(Error::room_unknown()));
1120
        };
1121
        self.next_generation = self.next_generation.wrapping_add(1);
1122
        let generation = self.next_generation;
1123
        let (sender, requests) = mpsc::channel(self.config.command_capacity);
1124
        let (supervisor, panics) = Supervisor::new();
1125
        let actor = RoomActor {
1126
            room_id: room_id.clone(),
1127
            generation,
1128
            room,
1129
            requests,
1130
            notes: self.note_sender.clone(),
1131
            context: SessionContext {
1132
                events: self.port.event_sender(),
1133
                media: self.media.clone(),
1134
                current_user_id: self.client.user_id().map(ToOwned::to_owned),
1135
                initial_history_limit: self.config.initial_history_limit,
1136
                supervisor,
1137
            },
1138
            panics,
1139
            opening: vec![reply],
1140
        };
1141
        self.rooms
1142
            .insert(room_id.clone(), RoomSlot::Active { generation, sender });
1143
        self.tasks.spawn(
1144
            TaskContext::RoomActor {
1145
                room_id,
1146
                generation,
1147
            },
1148
            actor.run(),
1149
        );
1150
        self.update_room_subscriptions();
1151
    }
1152
1153
    async fn actor_note(&mut self, note: ActorNote) {
1154
        let (room_id, generation) = match &note {
1155
            ActorNote::Leaving {
1156
                room_id,
1157
                generation,
1158
            }
1159
            | ActorNote::Exited {
1160
                room_id,
1161
                generation,
1162
                ..
1163
            } => (room_id.clone(), *generation),
1164
        };
1165
        if self
1166
            .rooms
1167
            .get(&room_id)
1168
            .is_none_or(|slot| slot.generation() != generation)
1169
        {
1170
            return;
1171
        }
1172
        let ActorNote::Exited { leftovers, .. } = note else {
1173
            if let Some(slot @ RoomSlot::Active { .. }) = self.rooms.get_mut(&room_id) {
1174
                *slot = RoomSlot::Closing {
1175
                    generation,
1176
                    pending: VecDeque::new(),
1177
                };
1178
                self.update_room_subscriptions();
1179
            }
1180
            return;
1181
        };
1182
        let pending = match self.rooms.remove(&room_id) {
1183
            Some(RoomSlot::Closing { pending, .. }) => pending,
1184
            _ => VecDeque::new(),
1185
        };
1186
        // leftovers were sent before anything deferred, so they go first
1187
        for request in leftovers {
1188
            match request {
1189
                ActorRequest::Open(reply) => self.open_room(room_id.clone(), reply).await,
1190
                ActorRequest::Close(reply) => reply.done(Ok(())),
1191
                ActorRequest::Execute(command, reply) => self.route_timeline(command, reply),
1192
                ActorRequest::Repair(command, reply) => self.route_repair(command, reply),
1193
            }
1194
        }
1195
        for deferred in pending {
1196
            match deferred {
1197
                Deferred::Open(reply) => self.open_room(room_id.clone(), reply).await,
1198
                Deferred::Close(reply) => self.close_room(room_id.clone(), reply).await,
1199
            }
1200
        }
1201
        self.update_room_subscriptions();
1202
    }
1203
1204
    fn update_room_subscriptions(&self) {
1205
        let room_ids = self
1206
            .rooms
1207
            .iter()
1208
            .filter(|(_, slot)| matches!(slot, RoomSlot::Active { .. }))
1209
            .map(|(room_id, _)| room_id.clone())
1210
            .collect();
1211
        self.subscriptions.send_replace(room_ids);
1212
    }
1213
1214
    async fn shutdown(&mut self) {
1215
        if let Some(handler) = self.verification_handler.take() {
1216
            self.client.remove_event_handler(handler);
1217
        }
1218
        self.port.close();
1219
        self.sync.stop().await;
1220
        if self.mentions_service.is_none() {
1221
            self.mentions.stop().await;
1222
        }
1223
        if self.search_service.is_none() {
1224
            self.search.stop().await;
1225
        }
1226
        self.tasks.shutdown().await;
1227
        self.rooms.clear();
1228
        self.contexts.clear().await;
1229
        self.media.clear().await;
1230
        self.port
1231
            .connection_sender()
1232
            .send_replace(ConnectionState::Stopped);
1233
        let _ = self
1234
            .port
1235
            .event_sender()
1236
            .send(Event::ConnectionChanged(ConnectionState::Stopped))
1237
            .await;
1238
    }
1239
}
1240
1241
fn send_actor(sender: &mpsc::Sender<ActorRequest>, request: ActorRequest) {
1242
    if let Err(error) = sender.try_send(request) {
1243
        let (request, error) = match error {
1244
            mpsc::error::TrySendError::Full(request) => (
1245
                request,
1246
                Error::new(ErrorKind::RoomBusy, "room command queue is full"),
1247
            ),
1248
            mpsc::error::TrySendError::Closed(request) => (
1249
                request,
1250
                Error::new(ErrorKind::RoomNotOpen, "room is not open"),
1251
            ),
1252
        };
1253
        request.into_reply().done(Err(error));
1254
    }
1255
}
1256
1257
fn defer(pending: &mut VecDeque<Deferred>, deferred: Deferred, capacity: usize) {
1258
    if pending.len() < capacity {
1259
        return pending.push_back(deferred);
1260
    }
1261
    let (Deferred::Open(reply) | Deferred::Close(reply)) = deferred;
1262
    reply.done(Err(Error::new(
1263
        ErrorKind::RoomBusy,
1264
        "room lifecycle queue is full",
1265
    )));
1266
}
1267
1268
async fn settled(logouts: &mut watch::Receiver<Logouts>) {
1269
    let _ = logouts.wait_for(|logouts| logouts.in_flight == 0).await;
1270
}
1271
1272
fn enqueue<T>(sender: &mpsc::Sender<Job<T>>, job: Job<T>) {
1273
    if let Err(error) = sender.try_send(job) {
1274
        let (job, error) = match error {
1275
            mpsc::error::TrySendError::Full(job) => (
1276
                job,
1277
                Error::new(ErrorKind::QueueFull, "command queue is full"),
1278
            ),
1279
            mpsc::error::TrySendError::Closed(job) => (job, Error::interrupted()),
1280
        };
1281
        job.reply.done(Err(error));
1282
    }
1283
}