atagen

macltok

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