atagen

macltok

public
Star0
Watch0
7cf3485212044ecdd5da3a45cd090c6e00b92597
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/timeline.rsRust463 lines16.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::{
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 { target, key, .. } => Box::pin(async move {
189
                timeline
190
                    .toggle_reaction(&item_id(target), &key)
191
                    .await
192
                    .map(drop)
193
                    .map_err(timeline_error)
194
            }),
195
            TimelineCommand::MarkRead { event_id, .. } => Box::pin(async move {
196
                let receipt_failed =
197
                    |source| Error::from_sdk("sending read receipt failed", source);
198
                timeline
199
                    .send_single_receipt(ReceiptType::Read, event_id.clone())
200
                    .await
201
                    .map_err(receipt_failed)?;
202
                if thread_root.is_none() {
203
                    timeline
204
                        .send_single_receipt(ReceiptType::FullyRead, event_id)
205
                        .await
206
                        .map_err(receipt_failed)?;
207
                }
208
                Ok(())
209
            }),
210
            command => return reply.done(self.execute_serial(command).await),
211
        };
212
        work.spawn(async move { reply.done(background.await) });
213
    }
214
215
    /// sends stay on the actor so they hit the send queue in the order they were asked for
216
    async fn execute_serial(&self, command: TimelineCommand) -> Result<(), Error> {
217
        let timeline = &self.timeline;
218
        match command {
219
            TimelineCommand::Send { content, .. } => {
220
                let body = match &content {
221
                    MessageContent::Text { content, .. }
222
                    | MessageContent::Emote { content, .. }
223
                    | MessageContent::Reply { content, .. } => &content.body,
224
                };
225
                if body.trim().is_empty() {
226
                    return Err(Error::new(ErrorKind::EmptyBody, "message body is empty"));
227
                }
228
                let content = match content {
229
                    MessageContent::Text { content, mentions } => {
230
                        message_content(content, mentions, false).with_relation(None)
231
                    }
232
                    MessageContent::Emote { content, mentions } => {
233
                        message_content(content, mentions, true).with_relation(None)
234
                    }
235
                    MessageContent::Reply {
236
                        event_id,
237
                        content,
238
                        mentions,
239
                        notify_replied_to,
240
                    } => {
241
                        self.reply_content(event_id, content, mentions, notify_replied_to)
242
                            .await?
243
                    }
244
                };
245
                timeline
246
                    .send(content.into())
247
                    .await
248
                    .map(drop)
249
                    .map_err(timeline_error)
250
            }
251
            TimelineCommand::Edit {
252
                target, content, ..
253
            } => {
254
                let (content, mentions, emote) = match content {
255
                    MessageEditContent::Text { content, mentions } => (content, mentions, false),
256
                    MessageEditContent::Emote { content, mentions } => (content, mentions, true),
257
                };
258
                if content.body.trim().is_empty() {
259
                    return Err(Error::new(ErrorKind::EmptyBody, "message body is empty"));
260
                }
261
                timeline
262
                    .edit(
263
                        &item_id(target),
264
                        EditedContent::RoomMessage(message_content(content, mentions, emote)),
265
                    )
266
                    .await
267
                    .map_err(timeline_error)
268
            }
269
            _ => Err(Error::new(
270
                ErrorKind::Other,
271
                "timeline command was misrouted",
272
            )),
273
        }
274
    }
275
276
    async fn reply_content(
277
        &self,
278
        event_id: OwnedEventId,
279
        content: RichText,
280
        mentions: MessageMentions,
281
        notify_replied_to: bool,
282
    ) -> Result<matrix_sdk::ruma::events::room::message::RoomMessageEventContent, Error> {
283
        let thread_root = self.id.thread_root();
284
        let content = self
285
            .timeline
286
            .room()
287
            .make_reply_event(
288
                message_content(content, mentions, false),
289
                SdkReply {
290
                    event_id,
291
                    enforce_thread: if thread_root.is_some() {
292
                        EnforceThread::Threaded(ReplyWithinThread::Yes)
293
                    } else {
294
                        EnforceThread::MaybeThreaded
295
                    },
296
                    add_mentions: if notify_replied_to {
297
                        AddMentions::Yes
298
                    } else {
299
                        AddMentions::No
300
                    },
301
                },
302
            )
303
            .await
304
            .map_err(|source| {
305
                Error::with_source(ErrorKind::Timeline, "reply operation failed", source)
306
            })?;
307
        if let Some(thread_root) = thread_root
308
            && !matches!(
309
                content.relates_to.as_ref(),
310
                Some(Relation::Thread(thread)) if &thread.event_id == thread_root
311
            )
312
        {
313
            return Err(Error::new(
314
                ErrorKind::Invalid,
315
                "reply target does not belong to the selected thread",
316
            ));
317
        }
318
        Ok(content)
319
    }
320
}
321
322
impl Drop for TimelineSession {
323
    fn drop(&mut self) {
324
        self.watcher.abort();
325
        self.pagination_worker.abort();
326
    }
327
}
328
329
fn item_id(target: EventRef) -> TimelineEventItemId {
330
    match target {
331
        EventRef::Remote(event_id) => TimelineEventItemId::EventId(event_id),
332
        EventRef::Local(transaction_id) => TimelineEventItemId::TransactionId(transaction_id),
333
    }
334
}
335
336
/// the sdk leaves a reply's target unloaded until someone asks, so the watcher asks, once per event
337
struct ReplyFetcher {
338
    timeline: Arc<Timeline>,
339
    asked: HashSet<OwnedEventId>,
340
    fetches: TaskSet<()>,
341
}
342
343
impl ReplyFetcher {
344
    fn fetch(&mut self, missing: Vec<OwnedEventId>) {
345
        for event_id in missing {
346
            if self.asked.insert(event_id.clone()) {
347
                let timeline = self.timeline.clone();
348
                self.fetches.spawn(async move {
349
                    if let Err(error) = timeline.fetch_details_for_event(&event_id).await {
350
                        tracing::debug!(%event_id, %error, "fetching reply details failed");
351
                    }
352
                });
353
            }
354
        }
355
    }
356
}
357
358
async fn start_watcher(
359
    id: &TimelineId,
360
    timeline: &Arc<Timeline>,
361
    context: &SessionContext,
362
    scope: &MediaScope,
363
) -> Result<JoinHandle<()>, SendEventError> {
364
    let (items, mut stream) = timeline.subscribe().await;
365
    let current_user_id = context.current_user_id.clone();
366
    let normalised = normalise_timeline(items.iter(), current_user_id.as_deref(), scope);
367
    context.media.register(normalised.media).await;
368
    context
369
        .events
370
        .send(Event::TimelineReset {
371
            timeline_id: id.clone(),
372
            items: normalised.items,
373
        })
374
        .await?;
375
    let mut replies = ReplyFetcher {
376
        timeline: timeline.clone(),
377
        asked: HashSet::new(),
378
        fetches: TaskSet::new(),
379
    };
380
    replies.fetch(normalised.missing_replies);
381
    let id = id.clone();
382
    let events = context.events.clone();
383
    let media = context.media.clone();
384
    let scope = scope.clone();
385
    Ok(context.supervisor.spawn("timeline watcher", async move {
386
        loop {
387
            let diffs = tokio::select! {
388
                diffs = stream.next() => match diffs {
389
                    Some(diffs) => diffs,
390
                    None => return,
391
                },
392
                Some(_) = replies.fetches.join_next(), if !replies.fetches.is_empty() => continue,
393
            };
394
            let normalised = normalise_timeline_diffs(diffs, current_user_id.as_deref(), &scope);
395
            media.register(normalised.media).await;
396
            replies.fetch(normalised.missing_replies);
397
            let event = Event::TimelineChanged {
398
                timeline_id: id.clone(),
399
                diffs: normalised.diffs,
400
            };
401
            if events.send(event).await.is_err() {
402
                return;
403
            }
404
        }
405
    }))
406
}
407
408
async fn run_pagination(
409
    mut requests: mpsc::Receiver<PaginationRequest>,
410
    timeline: Arc<Timeline>,
411
    events: EventSender,
412
    timeline_id: TimelineId,
413
) {
414
    while let Some(PaginationRequest { limit, reply }) = requests.recv().await {
415
        let history = |state| Event::HistoryChanged {
416
            timeline_id: timeline_id.clone(),
417
            state,
418
        };
419
        if events.send(history(HistoryState::Loading)).await.is_err() {
420
            return;
421
        }
422
        let result = timeline
423
            .paginate_backwards(limit.max(1))
424
            .await
425
            .map_err(timeline_error);
426
        let state = match &result {
427
            Ok(true) => HistoryState::StartReached,
428
            Ok(false) => HistoryState::Idle,
429
            Err(error) => HistoryState::Failed(error.to_string()),
430
        };
431
        let _ = events.send(history(state)).await;
432
        if let Some(reply) = reply {
433
            reply.done(result.map(drop));
434
        }
435
    }
436
}
437
438
fn message_content(
439
    content: RichText,
440
    mentions: MessageMentions,
441
    emote: bool,
442
) -> RoomMessageEventContentWithoutRelation {
443
    let content = match content.formatted_body {
444
        Some(formatted_body) if emote => {
445
            RoomMessageEventContentWithoutRelation::emote_html(content.body, formatted_body)
446
        }
447
        Some(formatted_body) => {
448
            RoomMessageEventContentWithoutRelation::text_html(content.body, formatted_body)
449
        }
450
        None if emote => RoomMessageEventContentWithoutRelation::emote_plain(content.body),
451
        None => RoomMessageEventContentWithoutRelation::text_plain(content.body),
452
    };
453
    let content = content.add_mentions(Mentions::with_user_ids(mentions.user_ids));
454
    if mentions.room {
455
        content.add_mentions(Mentions::with_room_mention())
456
    } else {
457
        content
458
    }
459
}
460
461
pub(super) fn timeline_error(source: matrix_sdk_ui::timeline::Error) -> Error {
462
    Error::with_source(ErrorKind::Timeline, "timeline operation failed", source)
463
}