atagen

macltok

public
Star0
Watch0
da8896938cf03d32f4a368e2061a9d37e675a255
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/normalise/context.rsRust249 lines9.1 KB
1
use std::collections::HashMap;
2
3
use ruma::{MxcUri, OwnedEventId, OwnedUserId, RoomId, UserId};
4
use serde_json::Value;
5
6
use super::{media::MediaRegistration, observed::normalise_observed};
7
use crate::{MediaId, Reaction, ReplyDetails, Sender, TimelineEvent, TimelineItem};
8
9
pub(crate) struct ContextPage {
10
    /// each with the index of the raw event it came from
11
    pub(crate) items: Vec<(usize, TimelineItem)>,
12
    pub(crate) media: Vec<MediaRegistration>,
13
}
14
15
struct Annotation<'a> {
16
    key: &'a str,
17
    sender: &'a str,
18
    shortcode: Option<&'a str>,
19
}
20
21
/// raw events in timeline order, shown the way the live room timeline shows them. edits and
22
/// reactions fold into their targets within the page, and redactions, unmatched relations
23
/// and thread replies go. `keep` is never dropped
24
pub(crate) fn normalise_context(
25
    values: &[Value],
26
    keep: Option<usize>,
27
    current_user_id: Option<&UserId>,
28
    room_id: &RoomId,
29
) -> ContextPage {
30
    let mut edits: HashMap<&str, Vec<&Value>> = HashMap::new();
31
    let mut annotations: HashMap<&str, Vec<Annotation<'_>>> = HashMap::new();
32
    let mut shown = Vec::new();
33
    for (index, value) in values.iter().enumerate() {
34
        let str_at = |pointer: &str| value.pointer(pointer).and_then(Value::as_str);
35
        let target = str_at("/content/m.relates_to/event_id");
36
        if Some(index) == keep {
37
            shown.push(index);
38
            continue;
39
        }
40
        match (
41
            str_at("/type"),
42
            str_at("/content/m.relates_to/rel_type"),
43
            target,
44
        ) {
45
            (Some("m.room.redaction"), ..) => {}
46
            (Some("m.reaction"), Some("m.annotation"), Some(target)) => {
47
                if let (Some(key), Some(sender)) =
48
                    (str_at("/content/m.relates_to/key"), str_at("/sender"))
49
                {
50
                    annotations.entry(target).or_default().push(Annotation {
51
                        key,
52
                        sender,
53
                        shortcode: str_at("/content/com.beeper.reaction.shortcode")
54
                            .or_else(|| str_at("/content/shortcode")),
55
                    });
56
                }
57
            }
58
            (Some("m.reaction"), ..) => {}
59
            (_, Some("m.replace"), Some(target)) => edits.entry(target).or_default().push(value),
60
            (_, Some("m.thread"), _) => {}
61
            _ => shown.push(index),
62
        }
63
    }
64
    let mut media = Vec::new();
65
    let items = shown
66
        .into_iter()
67
        .filter_map(|index| {
68
            let mut value = std::borrow::Cow::Borrowed(&values[index]);
69
            let event_id = value.get("event_id")?.as_str()?.to_owned();
70
            let latest_edit = edits.get(event_id.as_str()).and_then(|edits| {
71
                edits
72
                    .iter()
73
                    .rev()
74
                    .find(|edit| edit.get("sender") == value.get("sender"))
75
            });
76
            if let Some(edit) = latest_edit.copied()
77
                && value.pointer("/unsigned/m.relations/m.replace").is_none()
78
            {
79
                let unsigned = value
80
                    .to_mut()
81
                    .as_object_mut()?
82
                    .entry("unsigned")
83
                    .or_insert_with(|| Value::Object(Default::default()));
84
                let relations = unsigned
85
                    .as_object_mut()?
86
                    .entry("m.relations")
87
                    .or_insert_with(|| Value::Object(Default::default()));
88
                relations
89
                    .as_object_mut()?
90
                    .insert("m.replace".to_owned(), edit.clone());
91
            }
92
            let identity = format!("{room_id}\n{event_id}");
93
            let (mut item, registrations) = normalise_observed(&value, current_user_id, &identity)?;
94
            media.extend(registrations);
95
            if let (TimelineItem::Event(event), Some(annotations)) =
96
                (&mut item, annotations.get(event_id.as_str()))
97
            {
98
                event.reactions = reactions(annotations, current_user_id);
99
            }
100
            Some((index, item))
101
        })
102
        .collect();
103
    ContextPage { items, media }
104
}
105
106
fn reactions(annotations: &[Annotation<'_>], current_user_id: Option<&UserId>) -> Vec<Reaction> {
107
    let mut reactions: Vec<Reaction> = Vec::new();
108
    for annotation in annotations {
109
        let Ok(sender) = OwnedUserId::try_from(annotation.sender) else {
110
            continue;
111
        };
112
        let index = match reactions.iter().position(|r| r.key == annotation.key) {
113
            Some(index) => index,
114
            None => {
115
                reactions.push(Reaction {
116
                    key: annotation.key.to_owned(),
117
                    shortcode: None,
118
                    image: <&MxcUri>::from(annotation.key)
119
                        .is_valid()
120
                        .then(|| MediaId::plain(annotation.key)),
121
                    count: 0,
122
                    own: false,
123
                    senders: Vec::new(),
124
                });
125
                reactions.len() - 1
126
            }
127
        };
128
        let reaction = &mut reactions[index];
129
        if reaction.senders.contains(&sender) {
130
            continue;
131
        }
132
        reaction.own |= current_user_id == Some(sender.as_ref());
133
        reaction.senders.push(sender);
134
        reaction.count = reaction.senders.len();
135
        if reaction.shortcode.is_none() {
136
            reaction.shortcode = annotation.shortcode.map(str::to_owned);
137
        }
138
    }
139
    reactions
140
}
141
142
/// the sender's member data, for each event and every reply quoted in it
143
pub(crate) fn apply_profiles<'a>(
144
    events: impl Iterator<Item = &'a mut TimelineEvent>,
145
    profiles: &HashMap<OwnedUserId, Sender>,
146
) {
147
    for event in events {
148
        if let Some(sender) = profiles.get(&event.sender.user_id) {
149
            event.sender = sender.clone();
150
        }
151
    }
152
}
153
154
/// settles every reply, from `targets` or as unavailable
155
pub(crate) fn resolve_replies<'a>(
156
    events: impl Iterator<Item = &'a mut TimelineEvent>,
157
    targets: &HashMap<OwnedEventId, TimelineEvent>,
158
) {
159
    for event in events {
160
        let Some(reply) = event.reply_to.as_mut() else {
161
            continue;
162
        };
163
        if !matches!(reply.details, ReplyDetails::Pending) {
164
            continue;
165
        }
166
        reply.details = match targets.get(&reply.event_id) {
167
            Some(target) => ReplyDetails::Ready {
168
                sender: target.sender.clone(),
169
                content: Box::new(target.content.clone()),
170
                timestamp_ms: target.timestamp_ms,
171
            },
172
            None => ReplyDetails::Unavailable,
173
        };
174
    }
175
}
176
177
#[cfg(test)]
178
mod tests {
179
    use ruma::{room_id, user_id};
180
    use serde_json::json;
181
182
    use super::*;
183
    use crate::TimelineContent;
184
185
    fn message(id: &str, sender: &str, body: &str) -> Value {
186
        json!({
187
            "type": "m.room.message",
188
            "event_id": id,
189
            "sender": sender,
190
            "origin_server_ts": 1,
191
            "content": { "msgtype": "m.text", "body": body },
192
        })
193
    }
194
195
    #[test]
196
    fn edits_and_reactions_fold_into_their_targets() {
197
        let mut edit = message("$edit", "@a:x", "* fixed");
198
        edit["content"]["m.new_content"] = json!({ "msgtype": "m.text", "body": "fixed" });
199
        edit["content"]["m.relates_to"] = json!({ "rel_type": "m.replace", "event_id": "$orig" });
200
        let mut foreign_edit = message("$foreign", "@b:x", "* hijack");
201
        foreign_edit["content"]["m.new_content"] = json!({ "msgtype": "m.text", "body": "hijack" });
202
        foreign_edit["content"]["m.relates_to"] =
203
            json!({ "rel_type": "m.replace", "event_id": "$orig" });
204
        let reaction = |id: &str, sender: &str| {
205
            json!({
206
                "type": "m.reaction",
207
                "event_id": id,
208
                "sender": sender,
209
                "origin_server_ts": 1,
210
                "content": { "m.relates_to": {
211
                    "rel_type": "m.annotation", "event_id": "$orig", "key": "👍",
212
                }},
213
            })
214
        };
215
        let mut threaded = message("$threaded", "@b:x", "in a thread");
216
        threaded["content"]["m.relates_to"] =
217
            json!({ "rel_type": "m.thread", "event_id": "$orig" });
218
        let values = [
219
            message("$orig", "@a:x", "typo"),
220
            edit,
221
            foreign_edit,
222
            reaction("$r1", "@b:x"),
223
            reaction("$r2", "@b:x"),
224
            reaction("$r3", "@a:x"),
225
            threaded,
226
            json!({
227
                "type": "m.room.redaction",
228
                "event_id": "$redaction",
229
                "sender": "@a:x",
230
                "origin_server_ts": 1,
231
                "redacts": "$gone",
232
                "content": {},
233
            }),
234
        ];
235
        let page = normalise_context(&values, None, Some(user_id!("@a:x")), room_id!("!room:x"));
236
        assert_eq!(page.items.len(), 1);
237
        let TimelineItem::Event(event) = &page.items[0].1 else {
238
            panic!("expected an event");
239
        };
240
        let TimelineContent::Message(message) = &event.content else {
241
            panic!("expected a message");
242
        };
243
        assert_eq!(message.body, "fixed");
244
        assert!(message.edited);
245
        assert_eq!(event.reactions.len(), 1);
246
        assert_eq!(event.reactions[0].count, 2);
247
        assert!(event.reactions[0].own);
248
    }
249
}