atagen

macltok

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