atagen

macltok

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