atagen

macltok

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