atagen

macltok

public
Star0
Watch0
2819830aef320361c7ccba85f4c334d884680adf
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/mod.rsRust1300 lines46.9 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 sync = Arc::new(
426
            SyncService::builder(client.clone())
427
                .with_offline_mode()
428
                .with_room_list_extra_required_state(vec![
429
                    (ROOM_PACKS.into(), "*".to_owned()),
430
                    (ROOM_PREVIEW_URLS.into(), String::new()),
431
                ])
432
                .with_room_list_response_commit_hook(mentions.hook())
433
                .build()
434
                .await
435
                .map_err(EngineError::SyncService)?,
436
        );
437
        let (handle, port, events) = transport_pair(
438
            client.clone(),
439
            sync.clone(),
440
            media.clone(),
441
            store_lock.clone(),
442
            config.command_capacity,
443
            config.event_capacity,
444
        );
445
        let (search, search_service) = crate::search::Service::new(
446
            client.clone(),
447
            media.clone(),
448
            search,
449
            port.event_sender(),
450
            &config,
451
        );
452
        client.send_queue().enable_upload_progress(true);
453
        client.send_queue().enable_global_upload_progress(true);
454
        let (note_sender, notes) = mpsc::unbounded_channel();
455
        let (stop, stops) = mpsc::unbounded_channel();
456
        let (mutations, mutation_queue) = mpsc::channel(config.command_capacity);
457
        let (notifications, notification_queue) = mpsc::channel(config.command_capacity);
458
        let (encryption, encryption_queue) = mpsc::channel(config.command_capacity);
459
        let (thread_lists, thread_list_queue) = mpsc::channel(config.command_capacity);
460
        let engine = Self {
461
            contexts: Arc::new(context::Sessions::new(port.event_sender())),
462
            client,
463
            sync,
464
            port,
465
            config,
466
            media,
467
            tasks: TaskRegistry {
468
                tasks: TaskSet::new(),
469
            },
470
            rooms: HashMap::new(),
471
            next_generation: 0,
472
            notes,
473
            note_sender,
474
            subscriptions: watch::Sender::new(Vec::new()),
475
            mutations,
476
            notifications,
477
            encryption,
478
            thread_lists,
479
            queues: Some(Queues {
480
                mutations: mutation_queue,
481
                notifications: notification_queue,
482
                encryption: encryption_queue,
483
                thread_lists: thread_list_queue,
484
                session_changes,
485
                persistence,
486
            }),
487
            verification_handler: None,
488
            preparations: prepare::Preparations::default(),
489
            stop,
490
            stops,
491
            logouts: Arc::new(watch::Sender::new(Logouts::default())),
492
            _store_lock: store_lock,
493
            mentions,
494
            mentions_service: Some(mentions_service),
495
            mentions_updates: Some(mentions_updates),
496
            search,
497
            search_service: Some(search_service),
498
        };
499
        Ok((engine, handle, events))
500
    }
501
502
    /// boxed because the engine's future is deep enough to blow the compiler's query depth
503
    /// in every crate that spawns it, which isn't something a client should have to fix
504
    pub fn run(self) -> EngineBoxFuture<'static, Result<Termination, EngineError>> {
505
        Box::pin(self.run_inner())
506
    }
507
508
    async fn run_inner(mut self) -> Result<Termination, EngineError> {
509
        let events = self.port.event_sender();
510
        let user_id = self
511
            .client
512
            .user_id()
513
            .expect("authentication was checked during construction")
514
            .to_owned();
515
        self.port
516
            .connection_sender()
517
            .send_replace(ConnectionState::Connecting);
518
        for event in [
519
            Event::Authenticated { user_id },
520
            Event::ConnectionChanged(ConnectionState::Connecting),
521
        ] {
522
            if events.send(event).await.is_err() {
523
                self.shutdown().await;
524
                return Err(EngineError::EventReceiverClosed);
525
            }
526
        }
527
        self.start_tasks();
528
        self.sync.start().await;
529
530
        let mut shutdown_reply = None;
531
        let mut handles_dropped = false;
532
        let mut logouts = self.logouts.subscribe();
533
        let ending = loop {
534
            tokio::select! {
535
                biased;
536
                Some(termination) = self.stops.recv() => break Ok(termination),
537
                () = events.closed() => break Err(EngineError::EventReceiverClosed),
538
                () = settled(&mut logouts), if handles_dropped => {
539
                    break Ok(Termination::HandlesDropped);
540
                }
541
                Some((context, panic)) = self.tasks.join_next(), if !self.tasks.is_empty() => {
542
                    let Some(message) = panic else { continue };
543
                    if let TaskContext::Command(label) = context {
544
                        tracing::error!(label, panic = %message, "command task panicked");
545
                        continue;
546
                    }
547
                    break Err(EngineError::TaskPanicked { task: context.label() });
548
                }
549
                Some(note) = self.notes.recv() => self.actor_note(note).await,
550
                request = self.port.recv(), if !handles_dropped => {
551
                    // nobody's left to ask, but a logout they asked for still deserves to land
552
                    let Some(request) = request else {
553
                        handles_dropped = true;
554
                        continue;
555
                    };
556
                    if let Some(reply) = self.dispatch(request).await {
557
                        shutdown_reply = Some(reply);
558
                        break Ok(Termination::ShutDown);
559
                    }
560
                }
561
            }
562
        };
563
564
        if let Err(error) = &ending {
565
            let _ = events.send(Event::Failed(error.failure())).await;
566
        }
567
        self.shutdown().await;
568
        let _ = events.send(Event::Stopped).await;
569
        if let Some(reply) = shutdown_reply {
570
            reply.done(Ok(()));
571
        }
572
        // everything else still queued answers `Interrupted` as it drops, but a shutdown did happen
573
        while let Some(Request { command, reply }) = self.port.try_recv() {
574
            if matches!(command, Command::Lifecycle(LifecycleCommand::Shutdown)) {
575
                reply.done(Ok(()));
576
            }
577
        }
578
        ending
579
    }
580
581
    fn start_tasks(&mut self) {
582
        let queues = self.queues.take().expect("engine tasks start once");
583
        if let Some(service) = self.mentions_service.take() {
584
            self.tasks
585
                .spawn(TaskContext::Watcher("mentions service"), service.run());
586
        }
587
        if let Some(service) = self.search_service.take() {
588
            self.tasks
589
                .spawn(TaskContext::Watcher("search service"), service.run());
590
        }
591
        if let Some(updates) = self.mentions_updates.take() {
592
            let events = self.port.event_sender();
593
            self.tasks.spawn(
594
                TaskContext::Watcher("mentions updates"),
595
                crate::mentions::service::watch_updates(updates, events),
596
            );
597
        }
598
        let (incoming, incoming_verifications) = mpsc::unbounded_channel();
599
        self.verification_handler = Some(self.verification_handler(incoming));
600
        let client = &self.client;
601
        let events = || self.port.event_sender();
602
        let (notices, notice_queue) = mpsc::unbounded_channel();
603
        let (enabling_recovery, recovery_enabling) = watch::channel(false);
604
        let watchers: [(&'static str, EngineBoxFuture<'static, ()>); 15] = [
605
            (
606
                "upload tracker",
607
                Box::pin(watchers::uploads(
608
                    client.clone(),
609
                    client.send_queue().subscribe(),
610
                    events(),
611
                )),
612
            ),
613
            (
614
                "room subscription watcher",
615
                Box::pin(watchers::room_subscriptions(
616
                    self.sync.clone(),
617
                    self.subscriptions.subscribe(),
618
                )),
619
            ),
620
            (
621
                "session change watcher",
622
                Box::pin(session::session(
623
                    client.clone(),
624
                    queues.session_changes,
625
                    queues.persistence,
626
                    notices,
627
                    client
628
                        .device_id()
629
                        .expect("authentication was checked during construction")
630
                        .to_owned(),
631
                    self.logouts.clone(),
632
                )),
633
            ),
634
            (
635
                "session notice watcher",
636
                Box::pin(session::session_notices(
637
                    notice_queue,
638
                    events(),
639
                    self.stop.clone(),
640
                )),
641
            ),
642
            (
643
                "connection state watcher",
644
                Box::pin(watchers::connection(
645
                    self.sync.state(),
646
                    self.sync.clone(),
647
                    client.clone(),
648
                    events(),
649
                    self.port.connection_sender(),
650
                )),
651
            ),
652
            (
653
                "send queue watcher",
654
                Box::pin(watchers::send_queue_errors(
655
                    client.clone(),
656
                    client.send_queue().subscribe_errors(),
657
                    self.port.connection_sender().subscribe(),
658
                )),
659
            ),
660
            (
661
                "encryption state watcher",
662
                Box::pin(watchers::encryption(
663
                    client.clone(),
664
                    events(),
665
                    recovery_enabling,
666
                )),
667
            ),
668
            (
669
                "room list watcher",
670
                Box::pin(watchers::rooms(client.clone(), events())),
671
            ),
672
            (
673
                "event context watcher",
674
                Box::pin(context::close_on_leave(
675
                    client.clone(),
676
                    self.media.clone(),
677
                    self.contexts.clone(),
678
                )),
679
            ),
680
            (
681
                "image pack watcher",
682
                Box::pin(watchers::image_packs(client.clone(), events())),
683
            ),
684
            (
685
                "notification settings watcher",
686
                Box::pin(watchers::notification_settings(client.clone(), events())),
687
            ),
688
            (
689
                "room command worker",
690
                Box::pin(mutations::run_scheduler(
691
                    client.clone(),
692
                    queues.mutations,
693
                    self.config.command_capacity,
694
                )),
695
            ),
696
            (
697
                "notification command worker",
698
                Box::pin(commands::run_notifications(
699
                    client.clone(),
700
                    queues.notifications,
701
                )),
702
            ),
703
            (
704
                "encryption command worker",
705
                Box::pin(commands::run_encryption(
706
                    client.clone(),
707
                    events(),
708
                    queues.encryption,
709
                    incoming_verifications,
710
                    enabling_recovery,
711
                )),
712
            ),
713
            (
714
                "thread list worker",
715
                Box::pin(thread_list::run(
716
                    client.clone(),
717
                    events(),
718
                    self.media.clone(),
719
                    queues.thread_lists,
720
                )),
721
            ),
722
        ];
723
        for (label, task) in watchers {
724
            self.tasks.spawn(TaskContext::Watcher(label), task);
725
        }
726
    }
727
728
    fn verification_handler(
729
        &self,
730
        incoming: IncomingVerifications,
731
    ) -> matrix_sdk::event_handler::EventHandlerHandle {
732
        use matrix_sdk::ruma::events::key::verification::request::ToDeviceKeyVerificationRequestEvent;
733
        self.client.add_event_handler(
734
            move |event: ToDeviceKeyVerificationRequestEvent, client: Client| {
735
                let incoming = incoming.clone();
736
                async move {
737
                    let request = client
738
                        .encryption()
739
                        .get_verification_request(
740
                            &event.sender,
741
                            event.content.transaction_id.as_str(),
742
                        )
743
                        .await;
744
                    if let Some(request) = request {
745
                        let _ = incoming.send(request);
746
                    }
747
                }
748
            },
749
        )
750
    }
751
752
    /// hands back the shutdown request's responder, which is answered once we've stopped
753
    async fn dispatch(&mut self, Request { command, reply }: Request) -> Option<Responder> {
754
        tracing::debug!(command = %executor::label(&command), "engine command");
755
        let client = self.client.clone();
756
        match command {
757
            Command::Lifecycle(LifecycleCommand::Shutdown) => return Some(reply),
758
            Command::Account(AccountCommand::Logout { remove_pushers }) => {
759
                let events = self.port.event_sender();
760
                let stop = self.stop.clone();
761
                let logouts = self.logouts.clone();
762
                logouts.send_modify(|logouts| logouts.in_flight += 1);
763
                self.spawn_command("logout", reply, async move {
764
                    for pusher in remove_pushers {
765
                        if let Err(error) = crate::transport::remove_pusher(&client, pusher).await {
766
                            tracing::debug!(%error, "removing a pusher before logout failed");
767
                        }
768
                    }
769
                    let result = client.logout().await;
770
                    if result.is_ok() {
771
                        let _ = events.send(Event::LoggedOut).await;
772
                        let _ = stop.send(Termination::LoggedOut);
773
                    }
774
                    let mut rejected = None;
775
                    logouts.send_if_modified(|logouts| {
776
                        if result.is_err() && logouts.in_flight == 1 {
777
                            rejected = logouts.rejected.take();
778
                        }
779
                        false
780
                    });
781
                    if let Some(soft_logout) = rejected {
782
                        session::reject_session(&events, &stop, soft_logout).await;
783
                    }
784
                    logouts.send_modify(|logouts| logouts.in_flight -= 1);
785
                    result.map_err(|source| Error::from_sdk("logout failed", source))
786
                });
787
            }
788
            Command::Account(AccountCommand::SetAccountData {
789
                event_type,
790
                content,
791
            }) => enqueue(
792
                &self.mutations,
793
                Job {
794
                    command: Mutation::GlobalAccountData {
795
                        event_type,
796
                        content,
797
                    },
798
                    reply,
799
                },
800
            ),
801
            Command::Account(AccountCommand::SetDisplayName(name)) => {
802
                self.spawn_command("set display name", reply, async move {
803
                    client
804
                        .account()
805
                        .set_display_name(name.as_deref())
806
                        .await
807
                        .map_err(|source| Error::from_sdk("setting display name failed", source))
808
                });
809
            }
810
            Command::Account(AccountCommand::SetAvatar(avatar)) => {
811
                self.spawn_command("set avatar", reply, async move {
812
                    client
813
                        .account()
814
                        .set_avatar_url(avatar.as_deref())
815
                        .await
816
                        .map_err(|source| Error::from_sdk("setting avatar failed", source))
817
                });
818
            }
819
            Command::Account(AccountCommand::UploadAvatar { mimetype, bytes }) => {
820
                let max_bytes = self.config.max_media_bytes;
821
                self.spawn_command("upload avatar", reply, async move {
822
                    commands::upload_avatar(&client, &mimetype, bytes, max_bytes).await
823
                });
824
            }
825
            Command::Account(AccountCommand::UploadMedia { mimetype, bytes }) => {
826
                let max_bytes = self.config.max_media_bytes;
827
                self.tasks
828
                    .spawn(TaskContext::Command("upload media"), async move {
829
                        let uploaded =
830
                            commands::upload_media(&client, &mimetype, bytes, max_bytes).await;
831
                        reply.send(uploaded.map(CommandOutput::Mxc));
832
                    });
833
            }
834
            Command::Account(AccountCommand::IgnoreUser(user_id)) => enqueue(
835
                &self.mutations,
836
                Job {
837
                    command: Mutation::Ignore {
838
                        user_id,
839
                        ignored: true,
840
                    },
841
                    reply,
842
                },
843
            ),
844
            Command::Account(AccountCommand::UnignoreUser(user_id)) => enqueue(
845
                &self.mutations,
846
                Job {
847
                    command: Mutation::Ignore {
848
                        user_id,
849
                        ignored: false,
850
                    },
851
                    reply,
852
                },
853
            ),
854
            Command::Account(AccountCommand::ReorderSpaces(order)) => enqueue(
855
                &self.mutations,
856
                Job {
857
                    command: Mutation::ReorderSpaces(order),
858
                    reply,
859
                },
860
            ),
861
            Command::Account(AccountCommand::RenameDevice {
862
                device_id,
863
                display_name,
864
            }) => self.spawn_command("rename device", reply, async move {
865
                commands::rename_device(&client, &device_id, &display_name).await
866
            }),
867
            Command::Account(AccountCommand::DeleteDevices { device_ids, auth }) => {
868
                self.spawn_command("delete devices", reply, async move {
869
                    commands::delete_devices(&client, &device_ids, auth).await
870
                });
871
            }
872
            Command::Room(RoomCommand::Open(room_id)) => {
873
                if self.config.prepare_to_send {
874
                    self.prepare_quietly(room_id.clone());
875
                }
876
                self.open_room(room_id, reply).await;
877
            }
878
            Command::Room(RoomCommand::Close(room_id)) => self.close_room(room_id, reply).await,
879
            Command::Room(RoomCommand::PrepareToSend(room_id)) => {
880
                let flight = self.preparations.join(&client, room_id);
881
                self.spawn_command("prepare to send", reply, flight);
882
            }
883
            Command::Room(RoomCommand::SetTyping { room_id, typing }) => {
884
                if typing && self.config.prepare_to_send {
885
                    self.prepare_quietly(room_id.clone());
886
                }
887
                self.spawn_command("typing", reply, async move {
888
                    commands::set_typing(&client, &room_id, typing).await
889
                });
890
            }
891
            Command::Room(RoomCommand::CancelSend {
892
                room_id,
893
                transaction_id,
894
            }) => self.spawn_command("cancel send", reply, async move {
895
                commands::cancel_send(&client, &room_id, &transaction_id).await
896
            }),
897
            Command::Room(RoomCommand::RetrySend {
898
                room_id,
899
                transaction_id,
900
            }) => self.spawn_command("retry send", reply, async move {
901
                commands::retry_send(&client, &room_id, &transaction_id).await
902
            }),
903
            Command::Room(command) => enqueue(
904
                &self.mutations,
905
                Job {
906
                    command: Mutation::Room(command),
907
                    reply,
908
                },
909
            ),
910
            Command::Timeline(command) => self.route_timeline(command, reply),
911
            Command::EventContext(command) => {
912
                let (client, media, contexts) =
913
                    (client.clone(), self.media.clone(), self.contexts.clone());
914
                self.tasks
915
                    .spawn(TaskContext::Command("event context"), async move {
916
                        reply.send(context::execute(client, media, contexts, command).await);
917
                    });
918
            }
919
            Command::Mentions(command) => {
920
                let mentions = self.mentions.clone();
921
                self.tasks
922
                    .spawn(TaskContext::Command("mentions command"), async move {
923
                        reply.send(mentions.execute(command).await);
924
                    });
925
            }
926
            Command::Search(command) => {
927
                let search = self.search.clone();
928
                self.tasks
929
                    .spawn(TaskContext::Command("search command"), async move {
930
                        reply.send(search.execute(command).await);
931
                    });
932
            }
933
            Command::HistoryRepair(command) => self.route_repair(command, reply),
934
            Command::Media(MediaCommand::Upload(upload)) => self.upload(upload, reply),
935
            Command::Encryption(command) => enqueue(&self.encryption, Job { command, reply }),
936
            Command::Notification(command) => {
937
                enqueue(&self.notifications, Job { command, reply });
938
            }
939
            Command::Pack(command) => self.pack(command, reply),
940
            Command::ThreadList(command) => enqueue(&self.thread_lists, Job { command, reply }),
941
        }
942
        None
943
    }
944
945
    fn spawn_command(
946
        &mut self,
947
        label: &'static str,
948
        reply: Responder,
949
        work: impl Future<Output = Result<(), Error>> + SendOutsideWasm + 'static,
950
    ) {
951
        self.tasks.spawn(TaskContext::Command(label), async move {
952
            reply.done(work.await);
953
        });
954
    }
955
956
    fn prepare_quietly(&mut self, room_id: OwnedRoomId) {
957
        let flight = self.preparations.join(&self.client, room_id);
958
        self.tasks
959
            .spawn(TaskContext::Command("prepare to send"), async move {
960
                let _ = flight.await;
961
            });
962
    }
963
964
    fn pack(&mut self, command: PackCommand, reply: Responder) {
965
        let edit = |pack, edit| Mutation::Pack { pack, edit };
966
        let mutation = match command {
967
            PackCommand::CreateRoomPack {
968
                room_id,
969
                state_key,
970
                display_name,
971
            } => edit(
972
                ImagePackSource::Room { room_id, state_key },
973
                PackEdit::Create { display_name },
974
            ),
975
            PackCommand::Delete(pack) => edit(pack, PackEdit::Delete),
976
            PackCommand::SetMeta {
977
                pack,
978
                display_name,
979
                avatar,
980
                usage,
981
            } => edit(
982
                pack,
983
                PackEdit::Meta {
984
                    display_name,
985
                    avatar,
986
                    usage,
987
                },
988
            ),
989
            PackCommand::PutImage {
990
                pack,
991
                shortcode,
992
                mxc,
993
                body,
994
                usage,
995
                info,
996
            } => edit(
997
                pack,
998
                PackEdit::Put {
999
                    shortcode,
1000
                    mxc,
1001
                    body,
1002
                    usage,
1003
                    info,
1004
                },
1005
            ),
1006
            PackCommand::RenameImage { pack, from, to } => {
1007
                edit(pack, PackEdit::Rename { from, to })
1008
            }
1009
            PackCommand::RemoveImage { pack, shortcode } => {
1010
                edit(pack, PackEdit::Remove { shortcode })
1011
            }
1012
            PackCommand::SetEnabledEverywhere {
1013
                room_id,
1014
                state_key,
1015
                enabled,
1016
            } => Mutation::PackEnabledEverywhere {
1017
                room_id,
1018
                state_key,
1019
                enabled,
1020
            },
1021
            command => {
1022
                let client = self.client.clone();
1023
                let mutations = self.mutations.clone();
1024
                let max_bytes = self.config.max_media_bytes;
1025
                self.tasks.spawn(TaskContext::Command("pack"), async move {
1026
                    reply.send(packs::run_detached(client, mutations, max_bytes, command).await);
1027
                });
1028
                return;
1029
            }
1030
        };
1031
        enqueue(
1032
            &self.mutations,
1033
            Job {
1034
                command: mutation,
1035
                reply,
1036
            },
1037
        );
1038
    }
1039
1040
    fn upload(&mut self, upload: crate::AttachmentUpload, reply: Responder) {
1041
        let Some(room) = self.client.get_room(upload.timeline_id.room_id()) else {
1042
            return reply.done(Err(Error::room_unknown()));
1043
        };
1044
        let max_bytes = self.config.max_media_bytes;
1045
        self.spawn_command("upload", reply, async move {
1046
            media::queue_upload(room, upload, max_bytes).await
1047
        });
1048
    }
1049
1050
    fn route_timeline(&mut self, command: TimelineCommand, reply: Responder) {
1051
        let sender = match self.rooms.get(command.room_id()) {
1052
            Some(RoomSlot::Active { sender, .. }) => Some(sender.clone()),
1053
            _ => None,
1054
        };
1055
        match (sender, command) {
1056
            (Some(sender), command) => send_actor(&sender, ActorRequest::Execute(command, reply)),
1057
            (
1058
                None,
1059
                TimelineCommand::MarkRead {
1060
                    timeline_id,
1061
                    event_id,
1062
                },
1063
            ) => {
1064
                let client = self.client.clone();
1065
                self.spawn_command("mark read", reply, async move {
1066
                    commands::mark_read(&client, &timeline_id, event_id).await
1067
                });
1068
            }
1069
            (None, _) => reply.done(Err(Error::new(ErrorKind::RoomNotOpen, "room is not open"))),
1070
        }
1071
    }
1072
1073
    fn route_repair(&mut self, command: crate::HistoryRepairCommand, reply: Responder) {
1074
        use crate::HistoryRepairCommand;
1075
        let room_id = match &command {
1076
            HistoryRepairCommand::Cancel { room_id }
1077
            | HistoryRepairCommand::RetryRecent { room_id }
1078
            | HistoryRepairCommand::FillGap { room_id, .. } => room_id,
1079
        };
1080
        match self.rooms.get(room_id) {
1081
            Some(RoomSlot::Active { sender, .. }) => {
1082
                send_actor(sender, ActorRequest::Repair(command, reply))
1083
            }
1084
            Some(RoomSlot::Closing { .. }) => {
1085
                reply.done(Err(Error::new(ErrorKind::RoomBusy, "room is closing")))
1086
            }
1087
            None => reply.done(Err(Error::new(ErrorKind::RoomNotOpen, "room is not open"))),
1088
        }
1089
    }
1090
1091
    async fn open_room(&mut self, room_id: OwnedRoomId, reply: Responder) {
1092
        match self.rooms.get_mut(&room_id) {
1093
            Some(RoomSlot::Active { sender, .. }) => send_actor(sender, ActorRequest::Open(reply)),
1094
            Some(RoomSlot::Closing { pending, .. }) => {
1095
                defer(pending, Deferred::Open(reply), self.config.command_capacity);
1096
            }
1097
            None => self.start_actor(room_id, reply).await,
1098
        }
1099
    }
1100
1101
    async fn close_room(&mut self, room_id: OwnedRoomId, reply: Responder) {
1102
        let (generation, sender) = match self.rooms.get_mut(&room_id) {
1103
            None => return reply.done(Ok(())),
1104
            Some(RoomSlot::Closing { pending, .. }) => {
1105
                return defer(
1106
                    pending,
1107
                    Deferred::Close(reply),
1108
                    self.config.command_capacity,
1109
                );
1110
            }
1111
            Some(RoomSlot::Active { generation, sender }) => (*generation, sender.clone()),
1112
        };
1113
        match sender.try_send(ActorRequest::Close(reply)) {
1114
            Ok(()) => {}
1115
            // the actor already left on its own, which is closed enough
1116
            Err(mpsc::error::TrySendError::Closed(request)) => request.into_reply().done(Ok(())),
1117
            Err(mpsc::error::TrySendError::Full(request)) => {
1118
                return request.into_reply().done(Err(Error::new(
1119
                    ErrorKind::RoomBusy,
1120
                    "room command queue is full",
1121
                )));
1122
            }
1123
        }
1124
        self.rooms.insert(
1125
            room_id,
1126
            RoomSlot::Closing {
1127
                generation,
1128
                pending: VecDeque::new(),
1129
            },
1130
        );
1131
        self.update_room_subscriptions();
1132
    }
1133
1134
    async fn start_actor(&mut self, room_id: OwnedRoomId, reply: Responder) {
1135
        let Some(room) = self.client.get_room(&room_id) else {
1136
            return reply.done(Err(Error::room_unknown()));
1137
        };
1138
        self.next_generation = self.next_generation.wrapping_add(1);
1139
        let generation = self.next_generation;
1140
        let (sender, requests) = mpsc::channel(self.config.command_capacity);
1141
        let (supervisor, panics) = Supervisor::new();
1142
        let actor = RoomActor {
1143
            room_id: room_id.clone(),
1144
            generation,
1145
            room,
1146
            requests,
1147
            notes: self.note_sender.clone(),
1148
            context: SessionContext {
1149
                events: self.port.event_sender(),
1150
                media: self.media.clone(),
1151
                current_user_id: self.client.user_id().map(ToOwned::to_owned),
1152
                initial_history_limit: self.config.initial_history_limit,
1153
                supervisor,
1154
            },
1155
            panics,
1156
            opening: vec![reply],
1157
        };
1158
        self.rooms
1159
            .insert(room_id.clone(), RoomSlot::Active { generation, sender });
1160
        self.tasks.spawn(
1161
            TaskContext::RoomActor {
1162
                room_id,
1163
                generation,
1164
            },
1165
            actor.run(),
1166
        );
1167
        self.update_room_subscriptions();
1168
    }
1169
1170
    async fn actor_note(&mut self, note: ActorNote) {
1171
        let (room_id, generation) = match &note {
1172
            ActorNote::Leaving {
1173
                room_id,
1174
                generation,
1175
            }
1176
            | ActorNote::Exited {
1177
                room_id,
1178
                generation,
1179
                ..
1180
            } => (room_id.clone(), *generation),
1181
        };
1182
        if self
1183
            .rooms
1184
            .get(&room_id)
1185
            .is_none_or(|slot| slot.generation() != generation)
1186
        {
1187
            return;
1188
        }
1189
        let ActorNote::Exited { leftovers, .. } = note else {
1190
            if let Some(slot @ RoomSlot::Active { .. }) = self.rooms.get_mut(&room_id) {
1191
                *slot = RoomSlot::Closing {
1192
                    generation,
1193
                    pending: VecDeque::new(),
1194
                };
1195
                self.update_room_subscriptions();
1196
            }
1197
            return;
1198
        };
1199
        let pending = match self.rooms.remove(&room_id) {
1200
            Some(RoomSlot::Closing { pending, .. }) => pending,
1201
            _ => VecDeque::new(),
1202
        };
1203
        // leftovers were sent before anything deferred, so they go first
1204
        for request in leftovers {
1205
            match request {
1206
                ActorRequest::Open(reply) => self.open_room(room_id.clone(), reply).await,
1207
                ActorRequest::Close(reply) => reply.done(Ok(())),
1208
                ActorRequest::Execute(command, reply) => self.route_timeline(command, reply),
1209
                ActorRequest::Repair(command, reply) => self.route_repair(command, reply),
1210
            }
1211
        }
1212
        for deferred in pending {
1213
            match deferred {
1214
                Deferred::Open(reply) => self.open_room(room_id.clone(), reply).await,
1215
                Deferred::Close(reply) => self.close_room(room_id.clone(), reply).await,
1216
            }
1217
        }
1218
        self.update_room_subscriptions();
1219
    }
1220
1221
    fn update_room_subscriptions(&self) {
1222
        let room_ids = self
1223
            .rooms
1224
            .iter()
1225
            .filter(|(_, slot)| matches!(slot, RoomSlot::Active { .. }))
1226
            .map(|(room_id, _)| room_id.clone())
1227
            .collect();
1228
        self.subscriptions.send_replace(room_ids);
1229
    }
1230
1231
    async fn shutdown(&mut self) {
1232
        if let Some(handler) = self.verification_handler.take() {
1233
            self.client.remove_event_handler(handler);
1234
        }
1235
        self.port.close();
1236
        self.sync.stop().await;
1237
        if self.mentions_service.is_none() {
1238
            self.mentions.stop().await;
1239
        }
1240
        if self.search_service.is_none() {
1241
            self.search.stop().await;
1242
        }
1243
        self.tasks.shutdown().await;
1244
        self.rooms.clear();
1245
        self.contexts.clear().await;
1246
        self.media.clear().await;
1247
        self.port
1248
            .connection_sender()
1249
            .send_replace(ConnectionState::Stopped);
1250
        let _ = self
1251
            .port
1252
            .event_sender()
1253
            .send(Event::ConnectionChanged(ConnectionState::Stopped))
1254
            .await;
1255
    }
1256
}
1257
1258
fn send_actor(sender: &mpsc::Sender<ActorRequest>, request: ActorRequest) {
1259
    if let Err(error) = sender.try_send(request) {
1260
        let (request, error) = match error {
1261
            mpsc::error::TrySendError::Full(request) => (
1262
                request,
1263
                Error::new(ErrorKind::RoomBusy, "room command queue is full"),
1264
            ),
1265
            mpsc::error::TrySendError::Closed(request) => (
1266
                request,
1267
                Error::new(ErrorKind::RoomNotOpen, "room is not open"),
1268
            ),
1269
        };
1270
        request.into_reply().done(Err(error));
1271
    }
1272
}
1273
1274
fn defer(pending: &mut VecDeque<Deferred>, deferred: Deferred, capacity: usize) {
1275
    if pending.len() < capacity {
1276
        return pending.push_back(deferred);
1277
    }
1278
    let (Deferred::Open(reply) | Deferred::Close(reply)) = deferred;
1279
    reply.done(Err(Error::new(
1280
        ErrorKind::RoomBusy,
1281
        "room lifecycle queue is full",
1282
    )));
1283
}
1284
1285
async fn settled(logouts: &mut watch::Receiver<Logouts>) {
1286
    let _ = logouts.wait_for(|logouts| logouts.in_flight == 0).await;
1287
}
1288
1289
fn enqueue<T>(sender: &mpsc::Sender<Job<T>>, job: Job<T>) {
1290
    if let Err(error) = sender.try_send(job) {
1291
        let (job, error) = match error {
1292
            mpsc::error::TrySendError::Full(job) => (
1293
                job,
1294
                Error::new(ErrorKind::QueueFull, "command queue is full"),
1295
            ),
1296
            mpsc::error::TrySendError::Closed(job) => (job, Error::interrupted()),
1297
        };
1298
        job.reply.done(Err(error));
1299
    }
1300
}