atagen

macltok

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