atagen

macltok

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