atagen

macltok

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