atagen

macltok

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