atagen

macltok

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