atagen

macltok

public
Star0
Watch0
5b585cd970fd8c14c399754bfd12500c872c8c08
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/timeline.rsRust543 lines19.3 KB
1
use std::{collections::HashSet, future::Future, panic::AssertUnwindSafe, sync::Arc};
2
3
use futures_util::{FutureExt, StreamExt};
4
use matrix_sdk::SendOutsideWasm;
5
use matrix_sdk::{
6
    room::edit::EditedContent,
7
    room::reply::{EnforceThread, Reply as SdkReply},
8
    ruma::{
9
        api::client::receipt::create_receipt::v3::ReceiptType,
10
        events::room::message::{
11
            AddMentions, Relation, ReplyWithinThread, RoomMessageEventContentWithoutRelation,
12
        },
13
    },
14
};
15
use matrix_sdk_ui::timeline::{Timeline, TimelineEventItemId};
16
use ruma::{OwnedEventId, OwnedUserId};
17
use tokio::sync::{mpsc, oneshot};
18
19
use super::executor::{EngineBoxFuture, JoinHandle, TaskSet, spawn};
20
use crate::{
21
    Error, ErrorKind, Event, EventRef, HistoryState, MessageContent, MessageEditContent,
22
    MessageMentions, RichText, TimelineCommand, TimelineId,
23
    engine::MediaService,
24
    normalise::{MediaScope, normalise_timeline, normalise_timeline_diffs},
25
    transport::{EventSender, Responder, SendEventError},
26
    upload::sdk_mentions,
27
};
28
29
const PAGINATION_QUEUE: usize = 32;
30
31
/// long-lived tasks inside a room actor report panics here so the actor can die loudly
32
#[derive(Clone)]
33
pub(super) struct Supervisor(mpsc::UnboundedSender<&'static str>);
34
35
impl Supervisor {
36
    pub(super) fn new() -> (Self, mpsc::UnboundedReceiver<&'static str>) {
37
        let (sender, receiver) = mpsc::unbounded_channel();
38
        (Self(sender), receiver)
39
    }
40
41
    pub(super) fn spawn(
42
        &self,
43
        label: &'static str,
44
        task: impl Future<Output = ()> + SendOutsideWasm + 'static,
45
    ) -> JoinHandle<()> {
46
        let panics = self.0.clone();
47
        spawn(async move {
48
            if AssertUnwindSafe(task).catch_unwind().await.is_err() {
49
                let _ = panics.send(label);
50
            }
51
        })
52
    }
53
}
54
55
/// aborts its task when dropped, so an actor that dies mid-flight takes its helpers with it
56
pub(super) struct TaskGuard(pub(super) JoinHandle<()>);
57
58
impl Drop for TaskGuard {
59
    fn drop(&mut self) {
60
        self.0.abort();
61
    }
62
}
63
64
/// what a timeline session needs from the room that owns it
65
#[derive(Clone)]
66
pub(super) struct SessionContext {
67
    pub(super) events: EventSender,
68
    pub(super) media: Arc<MediaService>,
69
    pub(super) current_user_id: Option<OwnedUserId>,
70
    pub(super) initial_history_limit: u16,
71
    pub(super) supervisor: Supervisor,
72
}
73
74
pub(super) struct TimelineSession {
75
    pub(super) id: TimelineId,
76
    timeline: Arc<Timeline>,
77
    context: SessionContext,
78
    scope: MediaScope,
79
    watcher: JoinHandle<()>,
80
    fences: mpsc::UnboundedSender<Fence>,
81
    pagination: mpsc::Sender<PaginationRequest>,
82
    pagination_worker: JoinHandle<()>,
83
    member_fetch: JoinHandle<()>,
84
}
85
86
struct PaginationRequest {
87
    limit: u16,
88
    reply: Option<Responder>,
89
}
90
91
impl TimelineSession {
92
    pub(super) async fn start(
93
        id: TimelineId,
94
        timeline: Timeline,
95
        context: SessionContext,
96
    ) -> Result<Self, SendEventError> {
97
        let timeline = Arc::new(timeline);
98
        let scope = MediaScope::new();
99
        let (watcher, fences) = start_watcher(&id, &timeline, &context, &scope).await?;
100
        let (pagination, pagination_worker) = start_pagination_worker(&id, &timeline, &context);
101
        // under lazy-loaded members the sdk leaves sender profiles unavailable until someone
102
        // asks, and a /members fetched any other way never reaches the timeline's items
103
        let member_fetch = context.supervisor.spawn("member fetch", {
104
            let timeline = timeline.clone();
105
            async move { timeline.fetch_members().await }
106
        });
107
        let session = Self {
108
            id,
109
            timeline,
110
            scope,
111
            watcher,
112
            fences,
113
            pagination,
114
            pagination_worker,
115
            member_fetch,
116
            context,
117
        };
118
        if session.context.initial_history_limit > 0 && !matches!(session.id, TimelineId::Room(_)) {
119
            session.paginate(session.context.initial_history_limit, None);
120
        }
121
        Ok(session)
122
    }
123
124
    /// swaps in a fresh watcher. the old one is dead before the reset goes out,
125
    /// so no stale diff can land after it
126
    pub(super) async fn resync(&mut self) -> Result<(), Error> {
127
        self.watcher.abort();
128
        let _ = (&mut self.watcher).await;
129
        (self.watcher, self.fences) =
130
            start_watcher(&self.id, &self.timeline, &self.context, &self.scope)
131
                .await
132
                .map_err(|_| Error::new(ErrorKind::Timeline, "timeline reset could not be sent"))?;
133
        Ok(())
134
    }
135
136
    /// returns once the watcher has passed on the history the room cache committed
137
    pub(super) async fn publish_history(&self) -> Result<(), Error> {
138
        self.timeline
139
            .flush_history()
140
            .await
141
            .map_err(timeline_error)?;
142
        let (fence, drained) = oneshot::channel();
143
        self.fences.send(fence).map_err(|_| Error::interrupted())?;
144
        drained.await.map_err(|_| Error::interrupted())
145
    }
146
147
    pub(super) fn paginate(&self, limit: u16, reply: Option<Responder>) {
148
        if let Err(error) = self.pagination.try_send(PaginationRequest { limit, reply }) {
149
            let (request, error) = match error {
150
                mpsc::error::TrySendError::Full(request) => (
151
                    request,
152
                    Error::new(ErrorKind::QueueFull, "pagination queue is full"),
153
                ),
154
                mpsc::error::TrySendError::Closed(request) => (
155
                    request,
156
                    Error::new(ErrorKind::TimelineNotOpen, "timeline is closing"),
157
                ),
158
            };
159
            if let Some(reply) = request.reply {
160
                reply.done(Err(error));
161
            }
162
        }
163
    }
164
165
    async fn stop_without_close(&mut self) {
166
        self.member_fetch.abort();
167
        self.pagination_worker.abort();
168
        self.watcher.abort();
169
        let _ = (&mut self.member_fetch).await;
170
        let _ = (&mut self.pagination_worker).await;
171
        let _ = (&mut self.watcher).await;
172
        // after the watcher, or a last diff could register media for a timeline that's gone
173
        self.context.media.release(&self.scope).await;
174
    }
175
176
    /// pending paginations answer `Interrupted` as their responders drop
177
    pub(super) async fn close(mut self) {
178
        self.stop_without_close().await;
179
        let _ = self
180
            .context
181
            .events
182
            .send(Event::TimelineClosed {
183
                timeline_id: self.id.clone(),
184
            })
185
            .await;
186
    }
187
188
    pub(super) async fn execute(
189
        &self,
190
        command: TimelineCommand,
191
        reply: Responder,
192
        work: &mut TaskSet<()>,
193
    ) {
194
        let timeline = self.timeline.clone();
195
        let thread_root = self.id.thread_root().cloned();
196
        let background: EngineBoxFuture<'static, Result<(), Error>> = match command {
197
            TimelineCommand::Paginate { limit, .. } => return self.paginate(limit, Some(reply)),
198
            // network-bound, so it runs beside the actor instead of queueing the room behind it
199
            TimelineCommand::Redact { target, reason, .. } => Box::pin(async move {
200
                timeline
201
                    .redact(&item_id(target), reason.as_deref())
202
                    .await
203
                    .map_err(timeline_error)
204
            }),
205
            TimelineCommand::ToggleReaction {
206
                target,
207
                key,
208
                shortcode,
209
                ..
210
            } => Box::pin(async move {
211
                timeline
212
                    .toggle_reaction_with_shortcode(&item_id(target), &key, shortcode.as_deref())
213
                    .await
214
                    .map(drop)
215
                    .map_err(timeline_error)
216
            }),
217
            TimelineCommand::MarkRead { event_id, .. } => Box::pin(async move {
218
                let receipt_failed =
219
                    |source| Error::from_sdk("sending read receipt failed", source);
220
                timeline
221
                    .send_single_receipt(ReceiptType::Read, event_id.clone())
222
                    .await
223
                    .map_err(receipt_failed)?;
224
                if thread_root.is_none() {
225
                    timeline
226
                        .send_single_receipt(ReceiptType::FullyRead, event_id)
227
                        .await
228
                        .map_err(receipt_failed)?;
229
                }
230
                Ok(())
231
            }),
232
            command => return reply.done(self.execute_serial(command).await),
233
        };
234
        work.spawn(async move { reply.done(background.await) });
235
    }
236
237
    /// sends stay on the actor so they hit the send queue in the order they were asked for
238
    async fn execute_serial(&self, command: TimelineCommand) -> Result<(), Error> {
239
        let timeline = &self.timeline;
240
        match command {
241
            TimelineCommand::Send { content, .. } => {
242
                let body = match &content {
243
                    MessageContent::Text { content, .. }
244
                    | MessageContent::Emote { content, .. }
245
                    | MessageContent::Notice { content, .. }
246
                    | MessageContent::Reply { content, .. } => &content.body,
247
                };
248
                if body.trim().is_empty() {
249
                    return Err(Error::new(ErrorKind::EmptyBody, "message body is empty"));
250
                }
251
                let content = match content {
252
                    MessageContent::Text { content, mentions } => {
253
                        message_content(content, mentions, Msgtype::Text).with_relation(None)
254
                    }
255
                    MessageContent::Emote { content, mentions } => {
256
                        message_content(content, mentions, Msgtype::Emote).with_relation(None)
257
                    }
258
                    MessageContent::Notice { content, mentions } => {
259
                        message_content(content, mentions, Msgtype::Notice).with_relation(None)
260
                    }
261
                    MessageContent::Reply {
262
                        event_id,
263
                        content,
264
                        mentions,
265
                        notify_replied_to,
266
                    } => {
267
                        self.reply_content(event_id, content, mentions, notify_replied_to)
268
                            .await?
269
                    }
270
                };
271
                timeline
272
                    .send(content.into())
273
                    .await
274
                    .map(drop)
275
                    .map_err(timeline_error)
276
            }
277
            TimelineCommand::Edit {
278
                target, content, ..
279
            } => {
280
                let (content, mentions, msgtype) = match content {
281
                    MessageEditContent::Text { content, mentions } => {
282
                        (content, mentions, Msgtype::Text)
283
                    }
284
                    MessageEditContent::Emote { content, mentions } => {
285
                        (content, mentions, Msgtype::Emote)
286
                    }
287
                };
288
                if content.body.trim().is_empty() {
289
                    return Err(Error::new(ErrorKind::EmptyBody, "message body is empty"));
290
                }
291
                timeline
292
                    .edit(
293
                        &item_id(target),
294
                        EditedContent::RoomMessage(message_content(content, mentions, msgtype)),
295
                    )
296
                    .await
297
                    .map_err(timeline_error)
298
            }
299
            _ => Err(Error::new(
300
                ErrorKind::Other,
301
                "timeline command was misrouted",
302
            )),
303
        }
304
    }
305
306
    async fn reply_content(
307
        &self,
308
        event_id: OwnedEventId,
309
        content: RichText,
310
        mentions: MessageMentions,
311
        notify_replied_to: bool,
312
    ) -> Result<matrix_sdk::ruma::events::room::message::RoomMessageEventContent, Error> {
313
        let thread_root = self.id.thread_root();
314
        let content = self
315
            .timeline
316
            .room()
317
            .make_reply_event(
318
                message_content(content, mentions, Msgtype::Text),
319
                SdkReply {
320
                    event_id,
321
                    enforce_thread: if thread_root.is_some() {
322
                        EnforceThread::Threaded(ReplyWithinThread::Yes)
323
                    } else {
324
                        EnforceThread::MaybeThreaded
325
                    },
326
                    add_mentions: if notify_replied_to {
327
                        AddMentions::Yes
328
                    } else {
329
                        AddMentions::No
330
                    },
331
                },
332
            )
333
            .await
334
            .map_err(|source| {
335
                Error::with_source(ErrorKind::Timeline, "reply operation failed", source)
336
            })?;
337
        if let Some(thread_root) = thread_root
338
            && !matches!(
339
                content.relates_to.as_ref(),
340
                Some(Relation::Thread(thread)) if &thread.event_id == thread_root
341
            )
342
        {
343
            return Err(Error::new(
344
                ErrorKind::Invalid,
345
                "reply target does not belong to the selected thread",
346
            ));
347
        }
348
        Ok(content)
349
    }
350
}
351
352
fn start_pagination_worker(
353
    id: &TimelineId,
354
    timeline: &Arc<Timeline>,
355
    context: &SessionContext,
356
) -> (mpsc::Sender<PaginationRequest>, JoinHandle<()>) {
357
    let (pagination, requests) = mpsc::channel(PAGINATION_QUEUE);
358
    let worker = context.supervisor.spawn(
359
        "pagination worker",
360
        run_pagination(
361
            requests,
362
            timeline.clone(),
363
            context.events.clone(),
364
            id.clone(),
365
        ),
366
    );
367
    (pagination, worker)
368
}
369
370
impl Drop for TimelineSession {
371
    fn drop(&mut self) {
372
        self.watcher.abort();
373
        self.pagination_worker.abort();
374
        self.member_fetch.abort();
375
    }
376
}
377
378
fn item_id(target: EventRef) -> TimelineEventItemId {
379
    match target {
380
        EventRef::Remote(event_id) => TimelineEventItemId::EventId(event_id),
381
        EventRef::Local(transaction_id) => TimelineEventItemId::TransactionId(transaction_id),
382
    }
383
}
384
385
/// the sdk leaves a reply's target unloaded until someone asks, so the watcher asks, once per event
386
struct ReplyFetcher {
387
    timeline: Arc<Timeline>,
388
    asked: HashSet<OwnedEventId>,
389
    fetches: TaskSet<()>,
390
}
391
392
impl ReplyFetcher {
393
    fn fetch(&mut self, missing: Vec<OwnedEventId>) {
394
        for event_id in missing {
395
            if self.asked.insert(event_id.clone()) {
396
                let timeline = self.timeline.clone();
397
                self.fetches.spawn(async move {
398
                    if let Err(error) = timeline.fetch_details_for_event(&event_id).await {
399
                        tracing::debug!(%event_id, %error, "fetching reply details failed");
400
                    }
401
                });
402
            }
403
        }
404
    }
405
}
406
407
/// asks the watcher to pass on every diff the timeline already holds, then answers
408
type Fence = oneshot::Sender<()>;
409
410
async fn start_watcher(
411
    id: &TimelineId,
412
    timeline: &Arc<Timeline>,
413
    context: &SessionContext,
414
    scope: &MediaScope,
415
) -> Result<(JoinHandle<()>, mpsc::UnboundedSender<Fence>), SendEventError> {
416
    let (items, mut stream) = timeline.subscribe().await;
417
    let current_user_id = context.current_user_id.clone();
418
    let (normalised, mut hidden) =
419
        normalise_timeline(items.iter(), current_user_id.as_deref(), scope);
420
    context.media.register(scope, normalised.media).await;
421
    context
422
        .events
423
        .send(Event::TimelineReset {
424
            timeline_id: id.clone(),
425
            items: normalised.items,
426
        })
427
        .await?;
428
    let mut replies = ReplyFetcher {
429
        timeline: timeline.clone(),
430
        asked: HashSet::new(),
431
        fetches: TaskSet::new(),
432
    };
433
    replies.fetch(normalised.missing_replies);
434
    let (fences, mut fence_requests) = mpsc::unbounded_channel::<Fence>();
435
    let id = id.clone();
436
    let events = context.events.clone();
437
    let media = context.media.clone();
438
    let scope = scope.clone();
439
    let task = context.supervisor.spawn("timeline watcher", async move {
440
        loop {
441
            let (diffs, fence) = tokio::select! {
442
                diffs = stream.next() => match diffs {
443
                    Some(diffs) => (vec![diffs], None),
444
                    None => return,
445
                },
446
                Some(fence) = fence_requests.recv() => {
447
                    let mut ready = Vec::new();
448
                    while let Some(diffs) = stream.next().now_or_never() {
449
                        match diffs {
450
                            Some(diffs) => ready.push(diffs),
451
                            None => return,
452
                        }
453
                    }
454
                    (ready, Some(fence))
455
                }
456
                Some(_) = replies.fetches.join_next(), if !replies.fetches.is_empty() => continue,
457
            };
458
            for diffs in diffs {
459
                let normalised = normalise_timeline_diffs(
460
                    diffs,
461
                    current_user_id.as_deref(),
462
                    &scope,
463
                    &mut hidden,
464
                );
465
                media.register(&scope, normalised.media).await;
466
                replies.fetch(normalised.missing_replies);
467
                if normalised.diffs.is_empty() {
468
                    continue;
469
                }
470
                let event = Event::TimelineChanged {
471
                    timeline_id: id.clone(),
472
                    diffs: normalised.diffs,
473
                };
474
                if events.send(event).await.is_err() {
475
                    return;
476
                }
477
            }
478
            if let Some(fence) = fence {
479
                let _ = fence.send(());
480
            }
481
        }
482
    });
483
    Ok((task, fences))
484
}
485
486
async fn run_pagination(
487
    mut requests: mpsc::Receiver<PaginationRequest>,
488
    timeline: Arc<Timeline>,
489
    events: EventSender,
490
    timeline_id: TimelineId,
491
) {
492
    while let Some(PaginationRequest { limit, reply }) = requests.recv().await {
493
        let history = |state| Event::HistoryChanged {
494
            timeline_id: timeline_id.clone(),
495
            state,
496
        };
497
        if events.send(history(HistoryState::Loading)).await.is_err() {
498
            return;
499
        }
500
        let result = timeline
501
            .paginate_backwards(limit.max(1))
502
            .await
503
            .map_err(timeline_error);
504
        let state = match &result {
505
            Ok(true) => HistoryState::StartReached,
506
            Ok(false) => HistoryState::Idle,
507
            Err(error) => HistoryState::Failed(error.to_string()),
508
        };
509
        let _ = events.send(history(state)).await;
510
        if let Some(reply) = reply {
511
            reply.done(result.map(drop));
512
        }
513
    }
514
}
515
516
#[derive(Clone, Copy)]
517
enum Msgtype {
518
    Text,
519
    Emote,
520
    Notice,
521
}
522
523
fn message_content(
524
    content: RichText,
525
    mentions: MessageMentions,
526
    msgtype: Msgtype,
527
) -> RoomMessageEventContentWithoutRelation {
528
    type Content = RoomMessageEventContentWithoutRelation;
529
    let (body, formatted_body) = (content.body, content.formatted_body);
530
    let content = match (msgtype, formatted_body) {
531
        (Msgtype::Text, Some(html)) => Content::text_html(body, html),
532
        (Msgtype::Text, None) => Content::text_plain(body),
533
        (Msgtype::Emote, Some(html)) => Content::emote_html(body, html),
534
        (Msgtype::Emote, None) => Content::emote_plain(body),
535
        (Msgtype::Notice, Some(html)) => Content::notice_html(body, html),
536
        (Msgtype::Notice, None) => Content::notice_plain(body),
537
    };
538
    content.add_mentions(sdk_mentions(mentions))
539
}
540
541
pub(super) fn timeline_error(source: matrix_sdk_ui::timeline::Error) -> Error {
542
    Error::with_source(ErrorKind::Timeline, "timeline operation failed", source)
543
}