atagen

macltok

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