atagen

macltok

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