atagen

macltok

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