atagen

macltok

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