atagen

macltok

public
Star0
Watch0
93d4735d70cff581f48583d0b0aeb43b08e1d882
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/normalise/observed.rsRust340 lines14.0 KB
1
use ruma::{
2
    OwnedEventId, UserId,
3
    events::{
4
        poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
5
        room::{
6
            member::{MembershipChange as RumaMembershipChange, OriginalSyncRoomMemberEvent},
7
            message::{
8
                MessageType, Relation, RoomMessageEventContent,
9
                RoomMessageEventContentWithoutRelation,
10
            },
11
        },
12
        sticker::StickerEventContent,
13
    },
14
};
15
use serde_json::Value;
16
17
use super::{
18
    link_preview::sender_previews,
19
    media::{
20
        MediaRegistration, media_descriptor_for_sticker, media_registration,
21
        media_sticker_registration,
22
    },
23
    timeline::formatted_body,
24
};
25
use crate::{
26
    Change, MediaId, MembershipChange, Message, MessageMentions, ReplyDetails, ReplyReference,
27
    SendState, Sender, TimelineContent, TimelineEvent, TimelineItem, TimelineItemId, UtdCause,
28
};
29
30
pub(crate) fn normalise_observed(
31
    value: &Value,
32
    current_user_id: Option<&UserId>,
33
    identity: &str,
34
) -> Option<(TimelineItem, Vec<MediaRegistration>)> {
35
    let event_id: OwnedEventId = serde_json::from_value(value.get("event_id")?.clone()).ok()?;
36
    let sender_id: ruma::OwnedUserId = serde_json::from_value(value.get("sender")?.clone()).ok()?;
37
    let timestamp_ms = value.get("origin_server_ts")?.as_u64()?;
38
    let event_type = value.get("type")?.as_str()?.to_owned();
39
    let own = current_user_id.is_some_and(|user_id| user_id.as_str() == sender_id.as_str());
40
    let redacted = value
41
        .get("unsigned")
42
        .and_then(|unsigned| unsigned.get("redacted_because"))
43
        .is_some();
44
    let mut media = Vec::new();
45
    let (content, reply_to, thread_root) = if redacted {
46
        (TimelineContent::Redacted, None, None)
47
    } else if event_type == "m.room.message" {
48
        match value
49
            .get("content")
50
            .cloned()
51
            .and_then(|content| serde_json::from_value::<RoomMessageEventContent>(content).ok())
52
        {
53
            Some(mut message) => {
54
                let (reply_to, thread_root) = relation_refs(message.relates_to.as_ref());
55
                let relation_replacement = match &message.relates_to {
56
                    Some(Relation::Replacement(replacement)) => {
57
                        Some(replacement.new_content.clone())
58
                    }
59
                    _ => None,
60
                };
61
                let bundled_replacement = value
62
                    .get("unsigned")
63
                    .and_then(|unsigned| unsigned.get("m.relations"))
64
                    .and_then(|relations| relations.get("m.replace"))
65
                    .and_then(|edit| {
66
                        let edit_sender = edit.get("sender")?.as_str()?;
67
                        if edit_sender != sender_id.as_str() {
68
                            return None;
69
                        }
70
                        edit.get("content")?.get("m.new_content").cloned()
71
                    })
72
                    .and_then(|content| {
73
                        serde_json::from_value::<RoomMessageEventContentWithoutRelation>(content)
74
                            .ok()
75
                    });
76
                let edited = relation_replacement.is_some()
77
                    || bundled_replacement.is_some()
78
                    || value
79
                        .get("unsigned")
80
                        .and_then(|unsigned| unsigned.get("macltok_edited"))
81
                        .and_then(Value::as_bool)
82
                        .unwrap_or(false);
83
                if let Some(replacement) = relation_replacement.or(bundled_replacement) {
84
                    message.apply_replacement(replacement);
85
                }
86
                let message_type = &message.msgtype;
87
                media.extend(media_registration(identity, message_type));
88
                let previews = sender_previews(Some(identity), message_type);
89
                media.extend(
90
                    previews
91
                        .iter()
92
                        .filter_map(|(_, registration)| registration.clone()),
93
                );
94
                (
95
                    TimelineContent::Message(Message {
96
                        kind: super::message_kind(message_type),
97
                        body: message.body().to_owned(),
98
                        geo_uri: match message_type {
99
                            MessageType::Location(location) => Some(location.geo_uri.clone()),
100
                            _ => None,
101
                        },
102
                        formatted_body: formatted_body(message_type),
103
                        mentions: message.mentions.as_ref().map(|mentions| MessageMentions {
104
                            user_ids: mentions.user_ids.iter().cloned().collect(),
105
                            room: mentions.room,
106
                        }),
107
                        edited,
108
                        media: super::media::media_from_message(identity, message_type),
109
                        link_previews: previews.into_iter().map(|(preview, _)| preview).collect(),
110
                    }),
111
                    reply_to,
112
                    thread_root,
113
                )
114
            }
115
            None => (TimelineContent::Other { event_type }, None, None),
116
        }
117
    } else if event_type == "m.room.encrypted" {
118
        (
119
            TimelineContent::UnableToDecrypt(UtdCause::Unknown),
120
            None,
121
            None,
122
        )
123
    } else if event_type == "m.sticker" {
124
        match value
125
            .get("content")
126
            .cloned()
127
            .and_then(|content| serde_json::from_value::<StickerEventContent>(content).ok())
128
        {
129
            Some(sticker) => {
130
                let (reply_to, thread_root) = relation_refs(sticker.relates_to.as_ref());
131
                media.extend(media_sticker_registration(identity, &sticker));
132
                (
133
                    TimelineContent::Sticker {
134
                        body: sticker.body.clone(),
135
                        media: media_descriptor_for_sticker(identity, &sticker),
136
                    },
137
                    reply_to,
138
                    thread_root,
139
                )
140
            }
141
            None => (TimelineContent::Other { event_type }, None, None),
142
        }
143
    } else if event_type == "m.poll.start" || event_type == "org.matrix.msc3381.poll.start" {
144
        let poll_content = value.get("content");
145
        let question = poll_content.and_then(|content| poll_question(content, &event_type));
146
        match question {
147
            Some(question) => {
148
                let (reply_to, thread_root) =
149
                    poll_content.map(raw_relation_refs).unwrap_or_default();
150
                (TimelineContent::Poll { question }, reply_to, thread_root)
151
            }
152
            None => (TimelineContent::Other { event_type }, None, None),
153
        }
154
    } else if let Some(state_key) = value.get("state_key").and_then(Value::as_str) {
155
        let content = state_content(value, &event_type).unwrap_or(TimelineContent::OtherState {
156
            event_type,
157
            state_key: state_key.to_owned(),
158
        });
159
        (content, None, None)
160
    } else {
161
        (TimelineContent::Other { event_type }, None, None)
162
    };
163
    let event = TimelineEvent {
164
        item_id: TimelineItemId::new(identity.to_owned()),
165
        event_id: Some(event_id),
166
        transaction_id: None,
167
        sender: Sender {
168
            user_id: sender_id,
169
            display_name: None,
170
            avatar: None,
171
            name_ambiguous: false,
172
        },
173
        timestamp_ms,
174
        own,
175
        send_state: SendState::Sent,
176
        content,
177
        reply_to: reply_to.map(|event_id| ReplyReference {
178
            event_id,
179
            details: ReplyDetails::Pending,
180
        }),
181
        thread_root,
182
        thread_summary: None,
183
        reactions: Vec::new(),
184
        read_by: Vec::new(),
185
        editable: false,
186
        highlight: false,
187
    };
188
    Some((TimelineItem::Event(Box::new(event)), media))
189
}
190
191
fn relation_refs<C>(
192
    relation: Option<&Relation<C>>,
193
) -> (Option<OwnedEventId>, Option<OwnedEventId>) {
194
    match relation {
195
        Some(Relation::Reply(reply)) => (Some(reply.in_reply_to.event_id.clone()), None),
196
        Some(Relation::Thread(thread)) => (
197
            thread
198
                .in_reply_to
199
                .as_ref()
200
                .map(|reply| reply.event_id.clone()),
201
            Some(thread.event_id.clone()),
202
        ),
203
        _ => (None, None),
204
    }
205
}
206
207
fn raw_relation_refs(content: &Value) -> (Option<OwnedEventId>, Option<OwnedEventId>) {
208
    let Some(relation) = content.get("m.relates_to") else {
209
        return (None, None);
210
    };
211
    let reply_to = relation
212
        .get("m.in_reply_to")
213
        .and_then(|reply| reply.get("event_id"))
214
        .and_then(Value::as_str)
215
        .and_then(|event_id| event_id.parse().ok());
216
    let thread_root = (relation.get("rel_type").and_then(Value::as_str) == Some("m.thread"))
217
        .then(|| relation.get("event_id").and_then(Value::as_str))
218
        .flatten()
219
        .and_then(|event_id| event_id.parse().ok());
220
    (reply_to, thread_root)
221
}
222
223
pub(crate) fn poll_question(content: &Value, event_type: &str) -> Option<String> {
224
    let typed_question = match event_type {
225
        "m.poll.start" => serde_json::from_value::<PollStartEventContent>(content.clone())
226
            .ok()
227
            .and_then(|poll| poll.poll.question.text.find_plain().map(str::to_owned)),
228
        "org.matrix.msc3381.poll.start" => {
229
            serde_json::from_value::<UnstablePollStartEventContent>(content.clone())
230
                .ok()
231
                .map(|poll| poll.poll_start().question.text.clone())
232
        }
233
        _ => None,
234
    };
235
    typed_question
236
        .or_else(|| {
237
            content
238
                .get("m.poll")
239
                .and_then(|poll| poll.get("question"))
240
                .and_then(|question| question.get("m.text"))
241
                .and_then(generic_text)
242
        })
243
        .or_else(|| {
244
            content
245
                .get("org.matrix.msc3381.poll.start")
246
                .and_then(|poll| poll.get("question"))
247
                .and_then(|question| question.get("org.matrix.msc1767.text"))
248
                .and_then(generic_text)
249
        })
250
        .or_else(|| {
251
            ["org.matrix.msc1767.text", "m.text", "question", "body"]
252
                .iter()
253
                .find_map(|key| content.get(*key).and_then(generic_text))
254
        })
255
}
256
257
fn generic_text(value: &Value) -> Option<String> {
258
    match value {
259
        Value::String(text) => Some(text.clone()),
260
        Value::Array(representations) => representations
261
            .iter()
262
            .find(|representation| {
263
                representation.get("mimetype").and_then(Value::as_str) == Some("text/plain")
264
            })
265
            .or_else(|| representations.first())
266
            .and_then(|representation| representation.get("body"))
267
            .and_then(Value::as_str)
268
            .map(str::to_owned),
269
        Value::Object(_) => value.get("body").and_then(Value::as_str).map(str::to_owned),
270
        _ => None,
271
    }
272
}
273
274
/// the typed state rows the live timeline gives these events
275
fn state_content(value: &Value, event_type: &str) -> Option<TimelineContent> {
276
    let content = value.get("content")?;
277
    let text = |key: &str| {
278
        content
279
            .get(key)
280
            .and_then(Value::as_str)
281
            .filter(|text| !text.is_empty())
282
            .map(str::to_owned)
283
    };
284
    Some(match event_type {
285
        "m.room.member" => {
286
            let event: OriginalSyncRoomMemberEvent = serde_json::from_value(value.clone()).ok()?;
287
            let user_id = event.state_key.clone();
288
            match event.membership_change() {
289
                RumaMembershipChange::ProfileChanged {
290
                    displayname_change,
291
                    avatar_url_change,
292
                } => TimelineContent::Profile {
293
                    user_id,
294
                    display_name: displayname_change.map(|change| Change {
295
                        old: change.old.map(str::to_owned),
296
                        new: change.new.map(str::to_owned),
297
                    }),
298
                    avatar: avatar_url_change.map(|change| Change {
299
                        old: change.old.map(MediaId::plain),
300
                        new: change.new.map(MediaId::plain),
301
                    }),
302
                },
303
                change => TimelineContent::Membership {
304
                    user_id,
305
                    display_name: event.content.displayname.clone(),
306
                    change: membership_change(&change),
307
                },
308
            }
309
        }
310
        "m.room.name" => TimelineContent::RoomName(text("name")),
311
        "m.room.topic" => TimelineContent::RoomTopic(text("topic")),
312
        "m.room.avatar" => TimelineContent::RoomAvatar(text("url").map(|url| MediaId::plain(&url))),
313
        "m.room.encryption" => TimelineContent::EncryptionEnabled,
314
        "m.room.create" => TimelineContent::RoomCreated,
315
        "m.room.tombstone" => TimelineContent::RoomUpgraded {
316
            successor: serde_json::from_value(content.get("replacement_room")?.clone()).ok()?,
317
        },
318
        _ => return None,
319
    })
320
}
321
322
const fn membership_change(change: &RumaMembershipChange<'_>) -> MembershipChange {
323
    match change {
324
        RumaMembershipChange::Joined => MembershipChange::Joined,
325
        RumaMembershipChange::Left => MembershipChange::Left,
326
        RumaMembershipChange::Banned => MembershipChange::Banned,
327
        RumaMembershipChange::Unbanned => MembershipChange::Unbanned,
328
        RumaMembershipChange::Kicked => MembershipChange::Kicked,
329
        RumaMembershipChange::KickedAndBanned => MembershipChange::KickedAndBanned,
330
        RumaMembershipChange::Invited => MembershipChange::Invited,
331
        RumaMembershipChange::InvitationAccepted => MembershipChange::InvitationAccepted,
332
        RumaMembershipChange::InvitationRejected => MembershipChange::InvitationRejected,
333
        RumaMembershipChange::InvitationRevoked => MembershipChange::InvitationRevoked,
334
        RumaMembershipChange::Knocked => MembershipChange::Knocked,
335
        RumaMembershipChange::KnockAccepted => MembershipChange::KnockAccepted,
336
        RumaMembershipChange::KnockRetracted => MembershipChange::KnockRetracted,
337
        RumaMembershipChange::KnockDenied => MembershipChange::KnockDenied,
338
        _ => MembershipChange::Other,
339
    }
340
}