atagen

macltok

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