atagen

macltok

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