atagen

macltok

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