atagen

macltok

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