atagen

macltok

public
Star0
Watch0
cff1b0a4eb30290a6fe32fea825c816b696fe4f4
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/search/feed.rsRust289 lines8.7 KB
1
use std::collections::{HashSet, VecDeque};
2
3
use ruma::events::room::message::sanitize::remove_plain_reply_fallback;
4
use serde_json::Value;
5
6
use super::{MessageIndex, index::Doc};
7
use crate::normalise::poll_question;
8
9
const REMEMBERED_REDACTIONS: usize = 10_000;
10
11
#[derive(Clone, Debug, Eq, PartialEq)]
12
pub(crate) struct Edit {
13
    pub(crate) id: String,
14
    pub(crate) target: String,
15
    pub(crate) sender: String,
16
    pub(crate) ts: u64,
17
    pub(crate) text: String,
18
}
19
20
#[derive(Clone, Debug, Eq, PartialEq)]
21
pub(crate) enum Observed {
22
    Message {
23
        key: String,
24
        sender: String,
25
        ts: u64,
26
        text: String,
27
        bundled: Option<Edit>,
28
    },
29
    Edit(Edit),
30
    Redaction {
31
        target: String,
32
    },
33
    Redacted {
34
        key: String,
35
    },
36
    Encrypted {
37
        key: String,
38
        session_id: String,
39
        ts: u64,
40
    },
41
    Other,
42
}
43
44
#[derive(Debug, Eq, PartialEq)]
45
pub(crate) enum Effect {
46
    Indexed(u64),
47
    Removed,
48
    /// an edit was redacted out from under its original, which has to be read again
49
    Refetch(String),
50
    Nothing,
51
}
52
53
/// redactions seen lately, so a stale copy of what they removed can't come back
54
#[derive(Default)]
55
pub(crate) struct Redactions {
56
    order: VecDeque<String>,
57
    seen: HashSet<String>,
58
}
59
60
impl Redactions {
61
    fn insert(&mut self, key: String) {
62
        if self.seen.insert(key.clone()) {
63
            self.order.push_back(key);
64
            if self.order.len() > REMEMBERED_REDACTIONS
65
                && let Some(oldest) = self.order.pop_front()
66
            {
67
                self.seen.remove(&oldest);
68
            }
69
        }
70
    }
71
72
    fn contains(&self, key: &str) -> bool {
73
        self.seen.contains(key)
74
    }
75
}
76
77
fn text_of(event_type: &str, content: &Value) -> Option<String> {
78
    let text = match event_type {
79
        "m.room.message" => {
80
            let msgtype = content.get("msgtype")?.as_str()?;
81
            if msgtype == "m.key.verification.request" {
82
                return None;
83
            }
84
            let mut body = content.get("body")?.as_str()?.to_owned();
85
            if content.pointer("/m.relates_to/m.in_reply_to").is_some() {
86
                body = remove_plain_reply_fallback(&body).to_owned();
87
            }
88
            match content.get("filename").and_then(Value::as_str) {
89
                Some(filename) if filename != body => format!("{body}\n{filename}"),
90
                _ => body,
91
            }
92
        }
93
        "m.sticker" => content.get("body")?.as_str()?.to_owned(),
94
        "m.poll.start" | "org.matrix.msc3381.poll.start" => poll_question(content, event_type)?,
95
        _ => return None,
96
    };
97
    (!text.trim().is_empty()).then_some(text)
98
}
99
100
fn edit_of(value: &Value, event_type: &str) -> Option<Edit> {
101
    let content = value.get("content")?;
102
    if content.pointer("/m.relates_to/rel_type")?.as_str()? != "m.replace" {
103
        return None;
104
    }
105
    Some(Edit {
106
        id: value.get("event_id")?.as_str()?.to_owned(),
107
        target: content
108
            .pointer("/m.relates_to/event_id")?
109
            .as_str()?
110
            .to_owned(),
111
        sender: value.get("sender")?.as_str()?.to_owned(),
112
        ts: value.get("origin_server_ts")?.as_u64()?,
113
        text: text_of(event_type, content.get("m.new_content")?)?,
114
    })
115
}
116
117
pub(crate) fn observe(value: &Value) -> Observed {
118
    observe_inner(value).unwrap_or(Observed::Other)
119
}
120
121
fn observe_inner(value: &Value) -> Option<Observed> {
122
    let event_type = value.get("type")?.as_str()?;
123
    let key = value.get("event_id")?.as_str()?.to_owned();
124
    if event_type == "m.room.redaction" {
125
        let target = value
126
            .get("redacts")
127
            .or_else(|| value.pointer("/content/redacts"))?
128
            .as_str()?
129
            .to_owned();
130
        return Some(Observed::Redaction { target });
131
    }
132
    if value.pointer("/unsigned/redacted_because").is_some() {
133
        return Some(Observed::Redacted { key });
134
    }
135
    let ts = value.get("origin_server_ts")?.as_u64()?;
136
    if event_type == "m.room.encrypted" {
137
        let session_id = value.pointer("/content/session_id")?.as_str()?.to_owned();
138
        return Some(Observed::Encrypted {
139
            key,
140
            session_id,
141
            ts,
142
        });
143
    }
144
    if value
145
        .pointer("/content/m.relates_to/rel_type")
146
        .and_then(Value::as_str)
147
        == Some("m.replace")
148
    {
149
        return edit_of(value, event_type).map(Observed::Edit);
150
    }
151
    let content = value.get("content")?;
152
    let text = text_of(event_type, content)?;
153
    let sender = value.get("sender")?.as_str()?.to_owned();
154
    let bundled = value
155
        .pointer("/unsigned/m.relations/m.replace")
156
        .and_then(|edit| edit_of(edit, event_type))
157
        .filter(|edit| edit.target == key);
158
    Some(Observed::Message {
159
        key,
160
        sender,
161
        ts,
162
        text,
163
        bundled,
164
    })
165
}
166
167
fn newer(ts: u64, id: &str, than: &Doc) -> bool {
168
    (ts, id) > (than.content_ts, than.content.as_str())
169
}
170
171
/// what `observed` does to `room`'s part of the index. edits land on their original under
172
/// its key, the latest by its own sender wins, and one arriving first holds the key
173
/// provisionally until the original turns up
174
pub(crate) fn apply(
175
    index: &mut dyn MessageIndex,
176
    redactions: &mut Redactions,
177
    room: &str,
178
    observed: Observed,
179
) -> Effect {
180
    match observed {
181
        Observed::Message {
182
            key,
183
            sender,
184
            ts,
185
            text,
186
            bundled,
187
        } => {
188
            if redactions.contains(&key) {
189
                return Effect::Nothing;
190
            }
191
            let mut doc = Doc {
192
                key: key.clone(),
193
                room: room.to_owned(),
194
                sender: sender.clone(),
195
                ts,
196
                content: key.clone(),
197
                content_ts: ts,
198
                text,
199
                provisional: false,
200
            };
201
            if let Some(edit) =
202
                bundled.filter(|edit| edit.sender == sender && !redactions.contains(&edit.id))
203
            {
204
                doc.content = edit.id;
205
                doc.content_ts = edit.ts;
206
                doc.text = edit.text;
207
            }
208
            match index.get(&key) {
209
                Some(existing) if !existing.provisional => {
210
                    if !newer(doc.content_ts, &doc.content, &existing) {
211
                        return Effect::Nothing;
212
                    }
213
                }
214
                Some(existing) => {
215
                    if existing.sender == sender && !newer(doc.content_ts, &doc.content, &existing)
216
                    {
217
                        doc.content = existing.content;
218
                        doc.content_ts = existing.content_ts;
219
                        doc.text = existing.text;
220
                    }
221
                }
222
                None => {}
223
            }
224
            index.put(doc);
225
            Effect::Indexed(ts)
226
        }
227
        Observed::Edit(edit) => {
228
            if redactions.contains(&edit.target) || redactions.contains(&edit.id) {
229
                return Effect::Nothing;
230
            }
231
            let doc = match index.get(&edit.target) {
232
                Some(existing) => {
233
                    if existing.sender != edit.sender || !newer(edit.ts, &edit.id, &existing) {
234
                        return Effect::Nothing;
235
                    }
236
                    Doc {
237
                        content: edit.id,
238
                        content_ts: edit.ts,
239
                        text: edit.text,
240
                        ..existing
241
                    }
242
                }
243
                None => Doc {
244
                    key: edit.target,
245
                    room: room.to_owned(),
246
                    sender: edit.sender,
247
                    ts: edit.ts,
248
                    content: edit.id,
249
                    content_ts: edit.ts,
250
                    text: edit.text,
251
                    provisional: true,
252
                },
253
            };
254
            let ts = doc.ts;
255
            index.put(doc);
256
            Effect::Indexed(ts)
257
        }
258
        Observed::Redaction { target } => {
259
            redactions.insert(target.clone());
260
            if index.remove(&target) {
261
                return Effect::Removed;
262
            }
263
            match index.by_content(&target) {
264
                Some(doc) => {
265
                    index.remove(&doc.key);
266
                    if doc.provisional {
267
                        Effect::Removed
268
                    } else {
269
                        Effect::Refetch(doc.key)
270
                    }
271
                }
272
                None => Effect::Nothing,
273
            }
274
        }
275
        Observed::Redacted { key } => {
276
            redactions.insert(key.clone());
277
            if index.remove(&key) {
278
                Effect::Removed
279
            } else {
280
                Effect::Nothing
281
            }
282
        }
283
        Observed::Encrypted { .. } | Observed::Other => Effect::Nothing,
284
    }
285
}
286
287
#[cfg(test)]
288
#[path = "feed_tests.rs"]
289
mod tests;