atagen

macltok

public
Star0
Watch0
6f47f7cbe75aca541fb684d8bf35a7758c3a87d7
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/mod.rsRust1288 lines46.3 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 watchers: [(&'static str, EngineBoxFuture<'static, ()>); 15] = [
604
            (
605
                "upload tracker",
606
                Box::pin(watchers::uploads(
607
                    client.clone(),
608
                    client.send_queue().subscribe(),
609
                    events(),
610
                )),
611
            ),
612
            (
613
                "room subscription watcher",
614
                Box::pin(watchers::room_subscriptions(
615
                    self.sync.clone(),
616
                    self.subscriptions.subscribe(),
617
                )),
618
            ),
619
            (
620
                "session change watcher",
621
                Box::pin(session::session(
622
                    client.clone(),
623
                    queues.session_changes,
624
                    queues.persistence,
625
                    notices,
626
                    client
627
                        .device_id()
628
                        .expect("authentication was checked during construction")
629
                        .to_owned(),
630
                    self.logouts.clone(),
631
                )),
632
            ),
633
            (
634
                "session notice watcher",
635
                Box::pin(session::session_notices(
636
                    notice_queue,
637
                    events(),
638
                    self.stop.clone(),
639
                )),
640
            ),
641
            (
642
                "connection state watcher",
643
                Box::pin(watchers::connection(
644
                    self.sync.state(),
645
                    self.sync.clone(),
646
                    client.clone(),
647
                    events(),
648
                    self.port.connection_sender(),
649
                )),
650
            ),
651
            (
652
                "send queue watcher",
653
                Box::pin(watchers::send_queue_errors(
654
                    client.clone(),
655
                    client.send_queue().subscribe_errors(),
656
                    self.port.connection_sender().subscribe(),
657
                )),
658
            ),
659
            (
660
                "encryption state watcher",
661
                Box::pin(watchers::encryption(client.clone(), events())),
662
            ),
663
            (
664
                "room list watcher",
665
                Box::pin(watchers::rooms(client.clone(), events())),
666
            ),
667
            (
668
                "event context watcher",
669
                Box::pin(context::close_on_leave(
670
                    client.clone(),
671
                    self.media.clone(),
672
                    self.contexts.clone(),
673
                )),
674
            ),
675
            (
676
                "image pack watcher",
677
                Box::pin(watchers::image_packs(client.clone(), events())),
678
            ),
679
            (
680
                "notification settings watcher",
681
                Box::pin(watchers::notification_settings(client.clone(), events())),
682
            ),
683
            (
684
                "room command worker",
685
                Box::pin(mutations::run_scheduler(
686
                    client.clone(),
687
                    queues.mutations,
688
                    self.config.command_capacity,
689
                )),
690
            ),
691
            (
692
                "notification command worker",
693
                Box::pin(commands::run_notifications(
694
                    client.clone(),
695
                    queues.notifications,
696
                )),
697
            ),
698
            (
699
                "encryption command worker",
700
                Box::pin(commands::run_encryption(
701
                    client.clone(),
702
                    events(),
703
                    queues.encryption,
704
                    incoming_verifications,
705
                )),
706
            ),
707
            (
708
                "thread list worker",
709
                Box::pin(thread_list::run(
710
                    client.clone(),
711
                    events(),
712
                    self.media.clone(),
713
                    queues.thread_lists,
714
                )),
715
            ),
716
        ];
717
        for (label, task) in watchers {
718
            self.tasks.spawn(TaskContext::Watcher(label), task);
719
        }
720
    }
721
722
    fn verification_handler(
723
        &self,
724
        incoming: IncomingVerifications,
725
    ) -> matrix_sdk::event_handler::EventHandlerHandle {
726
        use matrix_sdk::ruma::events::key::verification::request::ToDeviceKeyVerificationRequestEvent;
727
        self.client.add_event_handler(
728
            move |event: ToDeviceKeyVerificationRequestEvent, client: Client| {
729
                let incoming = incoming.clone();
730
                async move {
731
                    let request = client
732
                        .encryption()
733
                        .get_verification_request(
734
                            &event.sender,
735
                            event.content.transaction_id.as_str(),
736
                        )
737
                        .await;
738
                    if let Some(request) = request {
739
                        let _ = incoming.send(request);
740
                    }
741
                }
742
            },
743
        )
744
    }
745
746
    /// hands back the shutdown request's responder, which is answered once we've stopped
747
    async fn dispatch(&mut self, Request { command, reply }: Request) -> Option<Responder> {
748
        let client = self.client.clone();
749
        match command {
750
            Command::Lifecycle(LifecycleCommand::Shutdown) => return Some(reply),
751
            Command::Account(AccountCommand::Logout) => {
752
                let events = self.port.event_sender();
753
                let stop = self.stop.clone();
754
                let logouts = self.logouts.clone();
755
                logouts.send_modify(|logouts| logouts.in_flight += 1);
756
                self.spawn_command("logout", reply, async move {
757
                    let result = client.logout().await;
758
                    if result.is_ok() {
759
                        let _ = events.send(Event::LoggedOut).await;
760
                        let _ = stop.send(Termination::LoggedOut);
761
                    }
762
                    let mut rejected = None;
763
                    logouts.send_if_modified(|logouts| {
764
                        if result.is_err() && logouts.in_flight == 1 {
765
                            rejected = logouts.rejected.take();
766
                        }
767
                        false
768
                    });
769
                    if let Some(soft_logout) = rejected {
770
                        session::reject_session(&events, &stop, soft_logout).await;
771
                    }
772
                    logouts.send_modify(|logouts| logouts.in_flight -= 1);
773
                    result.map_err(|source| Error::from_sdk("logout failed", source))
774
                });
775
            }
776
            Command::Account(AccountCommand::SetAccountData {
777
                event_type,
778
                content,
779
            }) => enqueue(
780
                &self.mutations,
781
                Job {
782
                    command: Mutation::GlobalAccountData {
783
                        event_type,
784
                        content,
785
                    },
786
                    reply,
787
                },
788
            ),
789
            Command::Account(AccountCommand::SetDisplayName(name)) => {
790
                self.spawn_command("set display name", reply, async move {
791
                    client
792
                        .account()
793
                        .set_display_name(name.as_deref())
794
                        .await
795
                        .map_err(|source| Error::from_sdk("setting display name failed", source))
796
                });
797
            }
798
            Command::Account(AccountCommand::SetAvatar(avatar)) => {
799
                self.spawn_command("set avatar", reply, async move {
800
                    client
801
                        .account()
802
                        .set_avatar_url(avatar.as_deref())
803
                        .await
804
                        .map_err(|source| Error::from_sdk("setting avatar failed", source))
805
                });
806
            }
807
            Command::Account(AccountCommand::UploadAvatar { mimetype, bytes }) => {
808
                let max_bytes = self.config.max_media_bytes;
809
                self.spawn_command("upload avatar", reply, async move {
810
                    commands::upload_avatar(&client, &mimetype, bytes, max_bytes).await
811
                });
812
            }
813
            Command::Account(AccountCommand::UploadMedia { mimetype, bytes }) => {
814
                let max_bytes = self.config.max_media_bytes;
815
                self.tasks
816
                    .spawn(TaskContext::Command("upload media"), async move {
817
                        let uploaded =
818
                            commands::upload_media(&client, &mimetype, bytes, max_bytes).await;
819
                        reply.send(uploaded.map(CommandOutput::Mxc));
820
                    });
821
            }
822
            Command::Account(AccountCommand::IgnoreUser(user_id)) => enqueue(
823
                &self.mutations,
824
                Job {
825
                    command: Mutation::Ignore {
826
                        user_id,
827
                        ignored: true,
828
                    },
829
                    reply,
830
                },
831
            ),
832
            Command::Account(AccountCommand::UnignoreUser(user_id)) => enqueue(
833
                &self.mutations,
834
                Job {
835
                    command: Mutation::Ignore {
836
                        user_id,
837
                        ignored: false,
838
                    },
839
                    reply,
840
                },
841
            ),
842
            Command::Account(AccountCommand::ReorderSpaces(order)) => enqueue(
843
                &self.mutations,
844
                Job {
845
                    command: Mutation::ReorderSpaces(order),
846
                    reply,
847
                },
848
            ),
849
            Command::Account(AccountCommand::RenameDevice {
850
                device_id,
851
                display_name,
852
            }) => self.spawn_command("rename device", reply, async move {
853
                commands::rename_device(&client, &device_id, &display_name).await
854
            }),
855
            Command::Account(AccountCommand::DeleteDevices { device_ids, auth }) => {
856
                self.spawn_command("delete devices", reply, async move {
857
                    commands::delete_devices(&client, &device_ids, auth).await
858
                });
859
            }
860
            Command::Room(RoomCommand::Open(room_id)) => {
861
                if self.config.prepare_to_send {
862
                    self.prepare_quietly(room_id.clone());
863
                }
864
                self.open_room(room_id, reply).await;
865
            }
866
            Command::Room(RoomCommand::Close(room_id)) => self.close_room(room_id, reply).await,
867
            Command::Room(RoomCommand::PrepareToSend(room_id)) => {
868
                let flight = self.preparations.join(&client, room_id);
869
                self.spawn_command("prepare to send", reply, flight);
870
            }
871
            Command::Room(RoomCommand::SetTyping { room_id, typing }) => {
872
                if typing && self.config.prepare_to_send {
873
                    self.prepare_quietly(room_id.clone());
874
                }
875
                self.spawn_command("typing", reply, async move {
876
                    commands::set_typing(&client, &room_id, typing).await
877
                });
878
            }
879
            Command::Room(RoomCommand::CancelSend {
880
                room_id,
881
                transaction_id,
882
            }) => self.spawn_command("cancel send", reply, async move {
883
                commands::cancel_send(&client, &room_id, &transaction_id).await
884
            }),
885
            Command::Room(RoomCommand::RetrySend {
886
                room_id,
887
                transaction_id,
888
            }) => self.spawn_command("retry send", reply, async move {
889
                commands::retry_send(&client, &room_id, &transaction_id).await
890
            }),
891
            Command::Room(command) => enqueue(
892
                &self.mutations,
893
                Job {
894
                    command: Mutation::Room(command),
895
                    reply,
896
                },
897
            ),
898
            Command::Timeline(command) => self.route_timeline(command, reply),
899
            Command::EventContext(command) => {
900
                let (client, media, contexts) =
901
                    (client.clone(), self.media.clone(), self.contexts.clone());
902
                self.tasks
903
                    .spawn(TaskContext::Command("event context"), async move {
904
                        reply.send(context::execute(client, media, contexts, command).await);
905
                    });
906
            }
907
            Command::Mentions(command) => {
908
                let mentions = self.mentions.clone();
909
                self.tasks
910
                    .spawn(TaskContext::Command("mentions command"), async move {
911
                        reply.send(mentions.execute(command).await);
912
                    });
913
            }
914
            Command::Search(command) => {
915
                let search = self.search.clone();
916
                self.tasks
917
                    .spawn(TaskContext::Command("search command"), async move {
918
                        reply.send(search.execute(command).await);
919
                    });
920
            }
921
            Command::HistoryRepair(command) => self.route_repair(command, reply),
922
            Command::Media(MediaCommand::Upload(upload)) => self.upload(upload, reply),
923
            Command::Encryption(command) => enqueue(&self.encryption, Job { command, reply }),
924
            Command::Notification(command) => {
925
                enqueue(&self.notifications, Job { command, reply });
926
            }
927
            Command::Pack(command) => self.pack(command, reply),
928
            Command::ThreadList(command) => enqueue(&self.thread_lists, Job { command, reply }),
929
        }
930
        None
931
    }
932
933
    fn spawn_command(
934
        &mut self,
935
        label: &'static str,
936
        reply: Responder,
937
        work: impl Future<Output = Result<(), Error>> + SendOutsideWasm + 'static,
938
    ) {
939
        self.tasks.spawn(TaskContext::Command(label), async move {
940
            reply.done(work.await);
941
        });
942
    }
943
944
    fn prepare_quietly(&mut self, room_id: OwnedRoomId) {
945
        let flight = self.preparations.join(&self.client, room_id);
946
        self.tasks
947
            .spawn(TaskContext::Command("prepare to send"), async move {
948
                let _ = flight.await;
949
            });
950
    }
951
952
    fn pack(&mut self, command: PackCommand, reply: Responder) {
953
        let edit = |pack, edit| Mutation::Pack { pack, edit };
954
        let mutation = match command {
955
            PackCommand::CreateRoomPack {
956
                room_id,
957
                state_key,
958
                display_name,
959
            } => edit(
960
                ImagePackSource::Room { room_id, state_key },
961
                PackEdit::Create { display_name },
962
            ),
963
            PackCommand::Delete(pack) => edit(pack, PackEdit::Delete),
964
            PackCommand::SetMeta {
965
                pack,
966
                display_name,
967
                avatar,
968
                usage,
969
            } => edit(
970
                pack,
971
                PackEdit::Meta {
972
                    display_name,
973
                    avatar,
974
                    usage,
975
                },
976
            ),
977
            PackCommand::PutImage {
978
                pack,
979
                shortcode,
980
                mxc,
981
                body,
982
                usage,
983
                info,
984
            } => edit(
985
                pack,
986
                PackEdit::Put {
987
                    shortcode,
988
                    mxc,
989
                    body,
990
                    usage,
991
                    info,
992
                },
993
            ),
994
            PackCommand::RenameImage { pack, from, to } => {
995
                edit(pack, PackEdit::Rename { from, to })
996
            }
997
            PackCommand::RemoveImage { pack, shortcode } => {
998
                edit(pack, PackEdit::Remove { shortcode })
999
            }
1000
            PackCommand::SetEnabledEverywhere {
1001
                room_id,
1002
                state_key,
1003
                enabled,
1004
            } => Mutation::PackEnabledEverywhere {
1005
                room_id,
1006
                state_key,
1007
                enabled,
1008
            },
1009
            command => {
1010
                let client = self.client.clone();
1011
                let mutations = self.mutations.clone();
1012
                let max_bytes = self.config.max_media_bytes;
1013
                self.tasks.spawn(TaskContext::Command("pack"), async move {
1014
                    reply.send(packs::run_detached(client, mutations, max_bytes, command).await);
1015
                });
1016
                return;
1017
            }
1018
        };
1019
        enqueue(
1020
            &self.mutations,
1021
            Job {
1022
                command: mutation,
1023
                reply,
1024
            },
1025
        );
1026
    }
1027
1028
    fn upload(&mut self, upload: crate::AttachmentUpload, reply: Responder) {
1029
        let Some(room) = self.client.get_room(upload.timeline_id.room_id()) else {
1030
            return reply.done(Err(Error::room_unknown()));
1031
        };
1032
        let max_bytes = self.config.max_media_bytes;
1033
        self.spawn_command("upload", reply, async move {
1034
            media::queue_upload(room, upload, max_bytes).await
1035
        });
1036
    }
1037
1038
    fn route_timeline(&mut self, command: TimelineCommand, reply: Responder) {
1039
        let sender = match self.rooms.get(command.room_id()) {
1040
            Some(RoomSlot::Active { sender, .. }) => Some(sender.clone()),
1041
            _ => None,
1042
        };
1043
        match (sender, command) {
1044
            (Some(sender), command) => send_actor(&sender, ActorRequest::Execute(command, reply)),
1045
            (
1046
                None,
1047
                TimelineCommand::MarkRead {
1048
                    timeline_id,
1049
                    event_id,
1050
                },
1051
            ) => {
1052
                let client = self.client.clone();
1053
                self.spawn_command("mark read", reply, async move {
1054
                    commands::mark_read(&client, &timeline_id, event_id).await
1055
                });
1056
            }
1057
            (None, _) => reply.done(Err(Error::new(ErrorKind::RoomNotOpen, "room is not open"))),
1058
        }
1059
    }
1060
1061
    fn route_repair(&mut self, command: crate::HistoryRepairCommand, reply: Responder) {
1062
        use crate::HistoryRepairCommand;
1063
        let room_id = match &command {
1064
            HistoryRepairCommand::Cancel { room_id }
1065
            | HistoryRepairCommand::RetryRecent { room_id }
1066
            | HistoryRepairCommand::FillGap { room_id, .. } => room_id,
1067
        };
1068
        match self.rooms.get(room_id) {
1069
            Some(RoomSlot::Active { sender, .. }) => {
1070
                send_actor(sender, ActorRequest::Repair(command, reply))
1071
            }
1072
            Some(RoomSlot::Closing { .. }) => {
1073
                reply.done(Err(Error::new(ErrorKind::RoomBusy, "room is closing")))
1074
            }
1075
            None => reply.done(Err(Error::new(ErrorKind::RoomNotOpen, "room is not open"))),
1076
        }
1077
    }
1078
1079
    async fn open_room(&mut self, room_id: OwnedRoomId, reply: Responder) {
1080
        match self.rooms.get_mut(&room_id) {
1081
            Some(RoomSlot::Active { sender, .. }) => send_actor(sender, ActorRequest::Open(reply)),
1082
            Some(RoomSlot::Closing { pending, .. }) => {
1083
                defer(pending, Deferred::Open(reply), self.config.command_capacity);
1084
            }
1085
            None => self.start_actor(room_id, reply).await,
1086
        }
1087
    }
1088
1089
    async fn close_room(&mut self, room_id: OwnedRoomId, reply: Responder) {
1090
        let (generation, sender) = match self.rooms.get_mut(&room_id) {
1091
            None => return reply.done(Ok(())),
1092
            Some(RoomSlot::Closing { pending, .. }) => {
1093
                return defer(
1094
                    pending,
1095
                    Deferred::Close(reply),
1096
                    self.config.command_capacity,
1097
                );
1098
            }
1099
            Some(RoomSlot::Active { generation, sender }) => (*generation, sender.clone()),
1100
        };
1101
        match sender.try_send(ActorRequest::Close(reply)) {
1102
            Ok(()) => {}
1103
            // the actor already left on its own, which is closed enough
1104
            Err(mpsc::error::TrySendError::Closed(request)) => request.into_reply().done(Ok(())),
1105
            Err(mpsc::error::TrySendError::Full(request)) => {
1106
                return request.into_reply().done(Err(Error::new(
1107
                    ErrorKind::RoomBusy,
1108
                    "room command queue is full",
1109
                )));
1110
            }
1111
        }
1112
        self.rooms.insert(
1113
            room_id,
1114
            RoomSlot::Closing {
1115
                generation,
1116
                pending: VecDeque::new(),
1117
            },
1118
        );
1119
        self.update_room_subscriptions();
1120
    }
1121
1122
    async fn start_actor(&mut self, room_id: OwnedRoomId, reply: Responder) {
1123
        let Some(room) = self.client.get_room(&room_id) else {
1124
            return reply.done(Err(Error::room_unknown()));
1125
        };
1126
        self.next_generation = self.next_generation.wrapping_add(1);
1127
        let generation = self.next_generation;
1128
        let (sender, requests) = mpsc::channel(self.config.command_capacity);
1129
        let (supervisor, panics) = Supervisor::new();
1130
        let actor = RoomActor {
1131
            room_id: room_id.clone(),
1132
            generation,
1133
            room,
1134
            requests,
1135
            notes: self.note_sender.clone(),
1136
            context: SessionContext {
1137
                events: self.port.event_sender(),
1138
                media: self.media.clone(),
1139
                current_user_id: self.client.user_id().map(ToOwned::to_owned),
1140
                initial_history_limit: self.config.initial_history_limit,
1141
                supervisor,
1142
            },
1143
            panics,
1144
            opening: vec![reply],
1145
        };
1146
        self.rooms
1147
            .insert(room_id.clone(), RoomSlot::Active { generation, sender });
1148
        self.tasks.spawn(
1149
            TaskContext::RoomActor {
1150
                room_id,
1151
                generation,
1152
            },
1153
            actor.run(),
1154
        );
1155
        self.update_room_subscriptions();
1156
    }
1157
1158
    async fn actor_note(&mut self, note: ActorNote) {
1159
        let (room_id, generation) = match &note {
1160
            ActorNote::Leaving {
1161
                room_id,
1162
                generation,
1163
            }
1164
            | ActorNote::Exited {
1165
                room_id,
1166
                generation,
1167
                ..
1168
            } => (room_id.clone(), *generation),
1169
        };
1170
        if self
1171
            .rooms
1172
            .get(&room_id)
1173
            .is_none_or(|slot| slot.generation() != generation)
1174
        {
1175
            return;
1176
        }
1177
        let ActorNote::Exited { leftovers, .. } = note else {
1178
            if let Some(slot @ RoomSlot::Active { .. }) = self.rooms.get_mut(&room_id) {
1179
                *slot = RoomSlot::Closing {
1180
                    generation,
1181
                    pending: VecDeque::new(),
1182
                };
1183
                self.update_room_subscriptions();
1184
            }
1185
            return;
1186
        };
1187
        let pending = match self.rooms.remove(&room_id) {
1188
            Some(RoomSlot::Closing { pending, .. }) => pending,
1189
            _ => VecDeque::new(),
1190
        };
1191
        // leftovers were sent before anything deferred, so they go first
1192
        for request in leftovers {
1193
            match request {
1194
                ActorRequest::Open(reply) => self.open_room(room_id.clone(), reply).await,
1195
                ActorRequest::Close(reply) => reply.done(Ok(())),
1196
                ActorRequest::Execute(command, reply) => self.route_timeline(command, reply),
1197
                ActorRequest::Repair(command, reply) => self.route_repair(command, reply),
1198
            }
1199
        }
1200
        for deferred in pending {
1201
            match deferred {
1202
                Deferred::Open(reply) => self.open_room(room_id.clone(), reply).await,
1203
                Deferred::Close(reply) => self.close_room(room_id.clone(), reply).await,
1204
            }
1205
        }
1206
        self.update_room_subscriptions();
1207
    }
1208
1209
    fn update_room_subscriptions(&self) {
1210
        let room_ids = self
1211
            .rooms
1212
            .iter()
1213
            .filter(|(_, slot)| matches!(slot, RoomSlot::Active { .. }))
1214
            .map(|(room_id, _)| room_id.clone())
1215
            .collect();
1216
        self.subscriptions.send_replace(room_ids);
1217
    }
1218
1219
    async fn shutdown(&mut self) {
1220
        if let Some(handler) = self.verification_handler.take() {
1221
            self.client.remove_event_handler(handler);
1222
        }
1223
        self.port.close();
1224
        self.sync.stop().await;
1225
        if self.mentions_service.is_none() {
1226
            self.mentions.stop().await;
1227
        }
1228
        if self.search_service.is_none() {
1229
            self.search.stop().await;
1230
        }
1231
        self.tasks.shutdown().await;
1232
        self.rooms.clear();
1233
        self.contexts.clear().await;
1234
        self.media.clear().await;
1235
        self.port
1236
            .connection_sender()
1237
            .send_replace(ConnectionState::Stopped);
1238
        let _ = self
1239
            .port
1240
            .event_sender()
1241
            .send(Event::ConnectionChanged(ConnectionState::Stopped))
1242
            .await;
1243
    }
1244
}
1245
1246
fn send_actor(sender: &mpsc::Sender<ActorRequest>, request: ActorRequest) {
1247
    if let Err(error) = sender.try_send(request) {
1248
        let (request, error) = match error {
1249
            mpsc::error::TrySendError::Full(request) => (
1250
                request,
1251
                Error::new(ErrorKind::RoomBusy, "room command queue is full"),
1252
            ),
1253
            mpsc::error::TrySendError::Closed(request) => (
1254
                request,
1255
                Error::new(ErrorKind::RoomNotOpen, "room is not open"),
1256
            ),
1257
        };
1258
        request.into_reply().done(Err(error));
1259
    }
1260
}
1261
1262
fn defer(pending: &mut VecDeque<Deferred>, deferred: Deferred, capacity: usize) {
1263
    if pending.len() < capacity {
1264
        return pending.push_back(deferred);
1265
    }
1266
    let (Deferred::Open(reply) | Deferred::Close(reply)) = deferred;
1267
    reply.done(Err(Error::new(
1268
        ErrorKind::RoomBusy,
1269
        "room lifecycle queue is full",
1270
    )));
1271
}
1272
1273
async fn settled(logouts: &mut watch::Receiver<Logouts>) {
1274
    let _ = logouts.wait_for(|logouts| logouts.in_flight == 0).await;
1275
}
1276
1277
fn enqueue<T>(sender: &mpsc::Sender<Job<T>>, job: Job<T>) {
1278
    if let Err(error) = sender.try_send(job) {
1279
        let (job, error) = match error {
1280
            mpsc::error::TrySendError::Full(job) => (
1281
                job,
1282
                Error::new(ErrorKind::QueueFull, "command queue is full"),
1283
            ),
1284
            mpsc::error::TrySendError::Closed(job) => (job, Error::interrupted()),
1285
        };
1286
        job.reply.done(Err(error));
1287
    }
1288
}