atagen

macltok

public
Star0
Watch0
3dab4a4e73f48986ea8e34215bdeb7d5d3870773
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/normalise/timeline.rsRust486 lines20.2 KB
1
use std::sync::Arc;
2
3
use matrix_sdk_crypto::types::events::UtdCause as SdkUtdCause;
4
use matrix_sdk_ui::{
5
    eyeball_im::VectorDiff,
6
    timeline::{
7
        AnyOtherStateEventContentChange, EncryptedMessage, EventSendState, EventTimelineItem,
8
        MembershipChange as SdkMembershipChange, MsgLikeContent, MsgLikeKind, Profile,
9
        TimelineDetails, TimelineEventItemId, TimelineItem as SdkTimelineItem, TimelineItemContent,
10
        TimelineItemKind, VirtualTimelineItem,
11
    },
12
};
13
use ruma::{
14
    MxcUri, OwnedEventId, UserId,
15
    events::{
16
        StateEventContentChange,
17
        room::message::{MessageFormat, MessageType},
18
    },
19
};
20
21
use crate::{
22
    Change, HistoryGap, HistoryGapLifecycle, HistoryGapPause, MediaId, MembershipChange, Message,
23
    MessageKind, MessageMentions, Reaction, ReplyDetails, ReplyReference, SendState, Sender,
24
    ThreadSummary, TimelineContent, TimelineDiff, TimelineEvent, TimelineItem, TimelineItemId,
25
    UtdCause,
26
};
27
28
use super::link_preview::sender_previews;
29
use super::media::{
30
    MediaRegistration, MediaScope, media_descriptor_for_sticker, media_from_message,
31
    media_identity, media_registrations_for_item,
32
};
33
34
#[derive(Default)]
35
pub(crate) struct NormalisedTimeline {
36
    pub(crate) items: imbl::Vector<TimelineItem>,
37
    pub(crate) media: Vec<MediaRegistration>,
38
    /// replies whose target isn't loaded, so somebody should go and ask for it
39
    pub(crate) missing_replies: Vec<OwnedEventId>,
40
}
41
42
pub(crate) struct NormalisedTimelineDiffs {
43
    pub(crate) diffs: Vec<TimelineDiff>,
44
    pub(crate) media: Vec<MediaRegistration>,
45
    pub(crate) missing_replies: Vec<OwnedEventId>,
46
}
47
48
struct Normaliser<'a> {
49
    current_user_id: Option<&'a UserId>,
50
    scope: &'a MediaScope,
51
    media: Vec<MediaRegistration>,
52
    missing_replies: Vec<OwnedEventId>,
53
}
54
55
impl Normaliser<'_> {
56
    fn item(&mut self, item: &SdkTimelineItem) -> TimelineItem {
57
        self.media
58
            .extend(media_registrations_for_item(item, self.scope));
59
        match item.kind() {
60
            TimelineItemKind::Event(event) => {
61
                TimelineItem::Event(Box::new(self.event(item, event)))
62
            }
63
            TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(timestamp)) => {
64
                TimelineItem::DayDivider {
65
                    timestamp_ms: timestamp.get().into(),
66
                }
67
            }
68
            TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => TimelineItem::ReadMarker,
69
            TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => {
70
                TimelineItem::TimelineStart
71
            }
72
            TimelineItemKind::Virtual(VirtualTimelineItem::HistoryGap(gap)) => {
73
                use matrix_sdk::event_cache::HistoryGapStatus;
74
                let (lifecycle, failure) = match &gap.status {
75
                    // the sdk drops joined gaps before they reach a timeline
76
                    HistoryGapStatus::Idle | HistoryGapStatus::Joined => {
77
                        (HistoryGapLifecycle::Idle, None)
78
                    }
79
                    HistoryGapStatus::Paused => (
80
                        HistoryGapLifecycle::Paused {
81
                            reason: HistoryGapPause::CursorStalled,
82
                        },
83
                        None,
84
                    ),
85
                    HistoryGapStatus::BudgetSpent => (
86
                        HistoryGapLifecycle::Paused {
87
                            reason: HistoryGapPause::Budget,
88
                        },
89
                        None,
90
                    ),
91
                    HistoryGapStatus::Failed { message } => {
92
                        (HistoryGapLifecycle::Failed, Some(message.clone()))
93
                    }
94
                    HistoryGapStatus::Unavailable => (HistoryGapLifecycle::Unavailable, None),
95
                };
96
                TimelineItem::HistoryGap(HistoryGap {
97
                    id: gap.id.clone(),
98
                    older_boundary: gap.older_boundary.clone(),
99
                    newer_boundary: gap.newer_boundary.clone(),
100
                    lifecycle,
101
                    failure,
102
                })
103
            }
104
        }
105
    }
106
107
    fn event(&mut self, item: &SdkTimelineItem, event: &EventTimelineItem) -> TimelineEvent {
108
        let msg_like = match event.content() {
109
            TimelineItemContent::MsgLike(content) => Some(content),
110
            _ => None,
111
        };
112
        let reply_to = msg_like
113
            .and_then(|content| content.in_reply_to.as_ref())
114
            .map(|reply| {
115
                let details = match &reply.event {
116
                    TimelineDetails::Ready(embedded) => ReplyDetails::Ready {
117
                        sender: sender(&embedded.sender, &embedded.sender_profile),
118
                        content: Box::new(content(&embedded.content, None)),
119
                        timestamp_ms: embedded.timestamp.get().into(),
120
                    },
121
                    TimelineDetails::Unavailable => {
122
                        self.missing_replies
123
                            .extend(event.event_id().map(ToOwned::to_owned));
124
                        ReplyDetails::Pending
125
                    }
126
                    TimelineDetails::Pending => ReplyDetails::Pending,
127
                    TimelineDetails::Error(_) => ReplyDetails::Unavailable,
128
                };
129
                ReplyReference {
130
                    event_id: reply.event_id.clone(),
131
                    details,
132
                }
133
            });
134
        let mut read_by: Vec<_> = event
135
            .read_receipts()
136
            .keys()
137
            .filter(|user_id| *user_id != event.sender())
138
            .cloned()
139
            .collect();
140
        read_by.sort();
141
        TimelineEvent {
142
            item_id: TimelineItemId::new(item.unique_id().0.clone()),
143
            event_id: event.event_id().map(ToOwned::to_owned),
144
            transaction_id: event.transaction_id().map(ToOwned::to_owned),
145
            sender: sender(event.sender(), event.sender_profile()),
146
            timestamp_ms: event.timestamp().get().into(),
147
            own: event.is_own(),
148
            send_state: send_state(event.send_state()),
149
            content: content(event.content(), Some(&media_identity(item, self.scope))),
150
            reply_to,
151
            thread_root: msg_like.and_then(|content| content.thread_root.clone()),
152
            thread_summary: msg_like.and_then(thread_summary),
153
            reactions: reactions(event, self.current_user_id),
154
            read_by,
155
            editable: event.is_editable(),
156
            highlight: event.is_highlighted(),
157
        }
158
    }
159
}
160
161
pub(crate) fn normalise_timeline<'a>(
162
    items: impl Iterator<Item = &'a Arc<SdkTimelineItem>>,
163
    current_user_id: Option<&UserId>,
164
    scope: &MediaScope,
165
) -> NormalisedTimeline {
166
    let mut normaliser = Normaliser {
167
        current_user_id,
168
        scope,
169
        media: Vec::new(),
170
        missing_replies: Vec::new(),
171
    };
172
    let items = items.map(|item| normaliser.item(item)).collect();
173
    NormalisedTimeline {
174
        items,
175
        media: normaliser.media,
176
        missing_replies: normaliser.missing_replies,
177
    }
178
}
179
180
pub(crate) fn normalise_timeline_diffs(
181
    diffs: Vec<VectorDiff<Arc<SdkTimelineItem>>>,
182
    current_user_id: Option<&UserId>,
183
    scope: &MediaScope,
184
) -> NormalisedTimelineDiffs {
185
    let mut normaliser = Normaliser {
186
        current_user_id,
187
        scope,
188
        media: Vec::new(),
189
        missing_replies: Vec::new(),
190
    };
191
    let diffs = diffs
192
        .into_iter()
193
        .map(|diff| match diff.map(|item| normaliser.item(&item)) {
194
            VectorDiff::Append { values } => TimelineDiff::Append {
195
                values: values.into_iter().collect(),
196
            },
197
            VectorDiff::Clear => TimelineDiff::Clear,
198
            VectorDiff::PushFront { value } => TimelineDiff::PushFront { value },
199
            VectorDiff::PushBack { value } => TimelineDiff::PushBack { value },
200
            VectorDiff::PopFront => TimelineDiff::PopFront,
201
            VectorDiff::PopBack => TimelineDiff::PopBack,
202
            VectorDiff::Insert { index, value } => TimelineDiff::Insert { index, value },
203
            VectorDiff::Set { index, value } => TimelineDiff::Set { index, value },
204
            VectorDiff::Remove { index } => TimelineDiff::Remove { index },
205
            VectorDiff::Truncate { length } => TimelineDiff::Truncate { length },
206
            VectorDiff::Reset { values } => TimelineDiff::Reset {
207
                values: values.into_iter().collect(),
208
            },
209
        })
210
        .collect();
211
    NormalisedTimelineDiffs {
212
        diffs,
213
        media: normaliser.media,
214
        missing_replies: normaliser.missing_replies,
215
    }
216
}
217
218
/// `media_identity` is `None` for embedded reply content, which never gets fetchable media of its own
219
fn content(content: &TimelineItemContent, media_identity: Option<&str>) -> TimelineContent {
220
    match content {
221
        TimelineItemContent::MsgLike(content) => msg_like(content, media_identity),
222
        TimelineItemContent::MembershipChange(change) => TimelineContent::Membership {
223
            user_id: change.user_id().to_owned(),
224
            display_name: change.display_name(),
225
            change: change
226
                .change()
227
                .map_or(MembershipChange::Other, membership_change),
228
        },
229
        TimelineItemContent::ProfileChange(change) => TimelineContent::Profile {
230
            user_id: change.user_id().to_owned(),
231
            display_name: change.displayname_change().map(|change| Change {
232
                old: change.old.clone(),
233
                new: change.new.clone(),
234
            }),
235
            avatar: change.avatar_url_change().map(|change| Change {
236
                old: change.old.as_deref().map(MediaId::plain),
237
                new: change.new.as_deref().map(MediaId::plain),
238
            }),
239
        },
240
        TimelineItemContent::OtherState(state) => match state.content() {
241
            AnyOtherStateEventContentChange::RoomName(change) => TimelineContent::RoomName(
242
                original(change).and_then(|content| non_empty(&content.name)),
243
            ),
244
            AnyOtherStateEventContentChange::RoomTopic(change) => TimelineContent::RoomTopic(
245
                original(change).and_then(|content| non_empty(&content.topic)),
246
            ),
247
            AnyOtherStateEventContentChange::RoomAvatar(change) => TimelineContent::RoomAvatar(
248
                original(change).and_then(|content| content.url.as_deref().map(MediaId::plain)),
249
            ),
250
            AnyOtherStateEventContentChange::RoomEncryption(_) => {
251
                TimelineContent::EncryptionEnabled
252
            }
253
            AnyOtherStateEventContentChange::RoomCreate(_) => TimelineContent::RoomCreated,
254
            AnyOtherStateEventContentChange::RoomTombstone(change) => match original(change) {
255
                Some(content) => TimelineContent::RoomUpgraded {
256
                    successor: content.replacement_room.clone(),
257
                },
258
                None => other_state(state.content().event_type().to_string(), state.state_key()),
259
            },
260
            other => other_state(other.event_type().to_string(), state.state_key()),
261
        },
262
        TimelineItemContent::FailedToParseMessageLike { event_type, .. } => {
263
            TimelineContent::Other {
264
                event_type: event_type.to_string(),
265
            }
266
        }
267
        TimelineItemContent::FailedToParseState {
268
            event_type,
269
            state_key,
270
            ..
271
        } => other_state(event_type.to_string(), state_key),
272
        TimelineItemContent::CallInvite => TimelineContent::Other {
273
            event_type: "m.call.invite".to_owned(),
274
        },
275
        TimelineItemContent::RtcNotification { .. } => TimelineContent::Other {
276
            event_type: "org.matrix.msc4075.rtc.notification".to_owned(),
277
        },
278
    }
279
}
280
281
/// a cleared name or topic arrives as an empty string
282
fn non_empty(value: &str) -> Option<String> {
283
    (!value.is_empty()).then(|| value.to_owned())
284
}
285
286
fn other_state(event_type: String, state_key: &str) -> TimelineContent {
287
    TimelineContent::OtherState {
288
        event_type,
289
        state_key: state_key.to_owned(),
290
    }
291
}
292
293
fn original<C>(change: &StateEventContentChange<C>) -> Option<&C>
294
where
295
    C: ruma::events::StaticStateEventContent + ruma::events::RedactContent,
296
{
297
    match change {
298
        StateEventContentChange::Original { content, .. } => Some(content),
299
        StateEventContentChange::Redacted(_) => None,
300
    }
301
}
302
303
fn msg_like(content: &MsgLikeContent, media_identity: Option<&str>) -> TimelineContent {
304
    match &content.kind {
305
        MsgLikeKind::Message(message) => TimelineContent::Message(Message {
306
            kind: message_kind(message.msgtype()),
307
            body: message.body().to_owned(),
308
            geo_uri: match message.msgtype() {
309
                MessageType::Location(location) => Some(location.geo_uri.clone()),
310
                _ => None,
311
            },
312
            formatted_body: formatted_body(message.msgtype()),
313
            mentions: message.mentions().map(|mentions| MessageMentions {
314
                user_ids: mentions.user_ids.iter().cloned().collect(),
315
                room: mentions.room,
316
            }),
317
            edited: message.is_edited(),
318
            media: media_identity
319
                .and_then(|identity| media_from_message(identity, message.msgtype())),
320
            link_previews: sender_previews(media_identity, message.msgtype())
321
                .into_iter()
322
                .map(|(preview, _)| preview)
323
                .collect(),
324
        }),
325
        MsgLikeKind::Sticker(sticker) => {
326
            let sticker = sticker.content();
327
            match media_identity {
328
                Some(identity) => TimelineContent::Sticker {
329
                    body: sticker.body.clone(),
330
                    media: media_descriptor_for_sticker(identity, sticker),
331
                },
332
                None => TimelineContent::Other {
333
                    event_type: "m.sticker".to_owned(),
334
                },
335
            }
336
        }
337
        MsgLikeKind::Poll(poll) => TimelineContent::Poll {
338
            question: poll.results().question,
339
        },
340
        MsgLikeKind::Redacted => TimelineContent::Redacted,
341
        MsgLikeKind::UnableToDecrypt(encrypted) => {
342
            TimelineContent::UnableToDecrypt(match encrypted {
343
                EncryptedMessage::MegolmV1AesSha2 { cause, .. } => utd_cause(*cause),
344
                _ => UtdCause::Unknown,
345
            })
346
        }
347
        MsgLikeKind::Other(other) => TimelineContent::Other {
348
            event_type: other.event_type().to_string(),
349
        },
350
        MsgLikeKind::LiveLocation(_) => TimelineContent::Other {
351
            event_type: "m.beacon_info".to_owned(),
352
        },
353
    }
354
}
355
356
pub(crate) fn message_kind(message: &MessageType) -> MessageKind {
357
    match message {
358
        MessageType::Text(_) => MessageKind::Text,
359
        MessageType::Emote(_) => MessageKind::Emote,
360
        MessageType::Notice(_) => MessageKind::Notice,
361
        MessageType::ServerNotice(_) => MessageKind::ServerNotice,
362
        MessageType::Image(_) => MessageKind::Image,
363
        MessageType::Video(_) => MessageKind::Video,
364
        MessageType::Audio(_) => MessageKind::Audio,
365
        MessageType::File(_) => MessageKind::File,
366
        MessageType::Location(_) => MessageKind::Location,
367
        MessageType::VerificationRequest(_) => MessageKind::VerificationRequest,
368
        other => MessageKind::Other(other.msgtype().to_owned()),
369
    }
370
}
371
372
const fn membership_change(change: SdkMembershipChange) -> MembershipChange {
373
    match change {
374
        SdkMembershipChange::Joined => MembershipChange::Joined,
375
        SdkMembershipChange::Left => MembershipChange::Left,
376
        SdkMembershipChange::Banned => MembershipChange::Banned,
377
        SdkMembershipChange::Unbanned => MembershipChange::Unbanned,
378
        SdkMembershipChange::Kicked => MembershipChange::Kicked,
379
        SdkMembershipChange::KickedAndBanned => MembershipChange::KickedAndBanned,
380
        SdkMembershipChange::Invited => MembershipChange::Invited,
381
        SdkMembershipChange::InvitationAccepted => MembershipChange::InvitationAccepted,
382
        SdkMembershipChange::InvitationRejected => MembershipChange::InvitationRejected,
383
        SdkMembershipChange::InvitationRevoked => MembershipChange::InvitationRevoked,
384
        SdkMembershipChange::Knocked => MembershipChange::Knocked,
385
        SdkMembershipChange::KnockAccepted => MembershipChange::KnockAccepted,
386
        SdkMembershipChange::KnockRetracted => MembershipChange::KnockRetracted,
387
        SdkMembershipChange::KnockDenied => MembershipChange::KnockDenied,
388
        SdkMembershipChange::None
389
        | SdkMembershipChange::Error
390
        | SdkMembershipChange::NotImplemented => MembershipChange::Other,
391
    }
392
}
393
394
const fn utd_cause(cause: SdkUtdCause) -> UtdCause {
395
    match cause {
396
        SdkUtdCause::Unknown => UtdCause::Unknown,
397
        SdkUtdCause::SentBeforeWeJoined => UtdCause::SentBeforeWeJoined,
398
        SdkUtdCause::VerificationViolation => UtdCause::VerificationViolation,
399
        SdkUtdCause::UnsignedDevice => UtdCause::UnsignedDevice,
400
        SdkUtdCause::UnknownDevice => UtdCause::UnknownDevice,
401
        SdkUtdCause::HistoricalMessageAndBackupIsDisabled => {
402
            UtdCause::HistoricalMessageAndBackupIsDisabled
403
        }
404
        SdkUtdCause::WithheldForUnverifiedOrInsecureDevice => {
405
            UtdCause::WithheldForUnverifiedOrInsecureDevice
406
        }
407
        SdkUtdCause::WithheldBySender => UtdCause::WithheldBySender,
408
        SdkUtdCause::HistoricalMessageAndDeviceIsUnverified => {
409
            UtdCause::HistoricalMessageAndDeviceIsUnverified
410
        }
411
    }
412
}
413
414
fn sender(user_id: &UserId, profile: &TimelineDetails<Profile>) -> Sender {
415
    let profile = match profile {
416
        TimelineDetails::Ready(profile) => Some(profile),
417
        _ => None,
418
    };
419
    Sender {
420
        user_id: user_id.to_owned(),
421
        display_name: profile.and_then(|profile| profile.display_name.clone()),
422
        avatar: profile.and_then(|profile| profile.avatar_url.as_deref().map(MediaId::plain)),
423
        name_ambiguous: profile.is_some_and(|profile| profile.display_name_ambiguous),
424
    }
425
}
426
427
fn send_state(state: Option<&EventSendState>) -> SendState {
428
    match state {
429
        None | Some(EventSendState::Sent { .. }) => SendState::Sent,
430
        Some(EventSendState::NotSentYet { .. }) => SendState::Sending,
431
        // recoverable means the sdk parks the queue and we unpark it, nothing for the user to do
432
        Some(EventSendState::SendingFailed { is_recoverable, .. }) => SendState::Failed {
433
            will_retry: *is_recoverable,
434
        },
435
    }
436
}
437
438
fn thread_summary(content: &MsgLikeContent) -> Option<ThreadSummary> {
439
    content.thread_summary.as_ref().map(|summary| {
440
        let latest_event = match &summary.latest_event {
441
            TimelineDetails::Ready(event) => Some(event.as_ref()),
442
            _ => None,
443
        };
444
        ThreadSummary {
445
            reply_count: summary.num_replies,
446
            latest_event_id: latest_event.and_then(|event| match &event.identifier {
447
                TimelineEventItemId::EventId(event_id) => Some(event_id.clone()),
448
                TimelineEventItemId::TransactionId(_) => None,
449
            }),
450
            latest_event_timestamp_ms: latest_event.map(|event| event.timestamp.get().into()),
451
        }
452
    })
453
}
454
455
fn reactions(event: &EventTimelineItem, current_user_id: Option<&UserId>) -> Vec<Reaction> {
456
    event
457
        .content()
458
        .reactions()
459
        .into_iter()
460
        .flat_map(|reactions| reactions.iter())
461
        .map(|(key, senders)| Reaction {
462
            key: key.clone(),
463
            shortcode: senders.values().find_map(|info| info.shortcode.clone()),
464
            image: <&MxcUri>::from(key.as_str())
465
                .is_valid()
466
                .then(|| MediaId::plain(key)),
467
            count: senders.len(),
468
            own: current_user_id.is_some_and(|user_id| senders.contains_key(user_id)),
469
            senders: senders.keys().cloned().collect(),
470
        })
471
        .collect()
472
}
473
474
pub(super) fn formatted_body(message: &MessageType) -> Option<String> {
475
    let formatted = match message {
476
        MessageType::Text(content) => content.formatted.as_ref(),
477
        MessageType::Emote(content) => content.formatted.as_ref(),
478
        MessageType::Notice(content) => content.formatted.as_ref(),
479
        MessageType::Audio(content) => content.formatted_caption(),
480
        MessageType::File(content) => content.formatted_caption(),
481
        MessageType::Image(content) => content.formatted_caption(),
482
        MessageType::Video(content) => content.formatted_caption(),
483
        _ => None,
484
    }?;
485
    (formatted.format == MessageFormat::Html).then(|| formatted.body.clone())
486
}