atagen

macltok

public
Star0
Watch0
7c7832147a2ef1974fca8cc6abea628f157a4649
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/mentions/state.rsRust438 lines14.4 KB
1
use std::collections::{BTreeMap, BTreeSet};
2
3
use ruma::{OwnedEventId, OwnedUserId};
4
use serde_json::Value;
5
6
use crate::{
7
    Error, ErrorKind, EventContext, MentionCaptureState, MentionCoverage, MentionEntry, MentionGap,
8
    MentionGapReason, MentionKey, MentionsBoundary, MentionsPage, SavedMentionContext,
9
    TimelineItem, normalise::MediaRegistration,
10
};
11
12
const CANDIDATE_LIFETIME_MS: u64 = 7 * 24 * 60 * 60 * 1000;
13
14
#[derive(Clone)]
15
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16
pub(super) struct SavedEvent {
17
    pub item: TimelineItem,
18
    pub media: Vec<MediaRegistration>,
19
    pub raw: Option<Value>,
20
}
21
22
impl PartialEq for SavedEvent {
23
    fn eq(&self, other: &Self) -> bool {
24
        self.item == other.item
25
            && self.raw == other.raw
26
            && self
27
                .media
28
                .iter()
29
                .map(|media| &media.id)
30
                .eq(other.media.iter().map(|media| &media.id))
31
    }
32
}
33
34
#[derive(Clone, PartialEq)]
35
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36
pub(super) struct Candidate {
37
    pub room_id: ruma::OwnedRoomId,
38
    pub raw: Value,
39
    pub attempts: u32,
40
    pub retry_at_ms: u64,
41
    pub first_seen_ms: u64,
42
}
43
44
impl Candidate {
45
    pub fn new(room_id: ruma::OwnedRoomId, raw: Value, retry_at_ms: u64) -> Self {
46
        Self {
47
            room_id,
48
            raw,
49
            attempts: 0,
50
            retry_at_ms,
51
            first_seen_ms: now(),
52
        }
53
    }
54
55
    pub fn encrypted(&self) -> bool {
56
        self.raw["type"] == "m.room.encrypted"
57
    }
58
}
59
60
#[derive(Clone, PartialEq)]
61
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
62
pub(super) struct Window {
63
    pub before: Vec<String>,
64
    pub target: Option<String>,
65
    pub after: Vec<String>,
66
    pub start: Option<String>,
67
    pub end: Option<String>,
68
}
69
70
#[derive(Clone, Debug, Default, PartialEq)]
71
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72
pub(crate) struct RepairCoverage {
73
    pub(crate) last_confirmed_ms: Option<u64>,
74
    pub(crate) boundary_ms: Option<u64>,
75
    pub(crate) reliable: bool,
76
    pub(crate) unresolved: bool,
77
    pub(crate) latest_anchor: Option<OwnedEventId>,
78
}
79
80
impl RepairCoverage {
81
    pub(crate) fn duration_ms(&self) -> Option<u64> {
82
        if !self.reliable {
83
            return None;
84
        }
85
        self.boundary_ms?.checked_sub(self.last_confirmed_ms?)
86
    }
87
}
88
89
#[derive(Clone, Default, PartialEq)]
90
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
91
pub(super) struct Data {
92
    pub revision: u64,
93
    pub entries: BTreeMap<String, MentionEntry>,
94
    pub events: BTreeMap<String, SavedEvent>,
95
    pub windows: BTreeMap<String, Window>,
96
    pub capture_attempts: BTreeMap<String, u32>,
97
    pub candidates: BTreeMap<String, Candidate>,
98
    pub redacted: BTreeMap<String, u64>,
99
    pub edits: BTreeMap<String, Value>,
100
    pub coverage: MentionCoverage,
101
    pub history_cursor: Option<String>,
102
    pub history_exhausted: bool,
103
    pub latest_cursor: Option<String>,
104
    pub latest_anchor: Option<String>,
105
    pub latest_head: Option<String>,
106
    pub repair_policy: crate::HistoryRepairPolicy,
107
}
108
109
#[derive(Clone, Default, PartialEq)]
110
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
111
pub(super) struct SyncState {
112
    pub inbox_revision: u64,
113
    pub checkpoints: BTreeMap<String, Checkpoint>,
114
    pub observed_rooms: BTreeSet<ruma::OwnedRoomId>,
115
    pub repair_coverage: BTreeMap<ruma::OwnedRoomId, RepairCoverage>,
116
}
117
118
#[derive(Clone, Debug, PartialEq)]
119
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
120
pub(super) struct Checkpoint {
121
    pub pos: String,
122
    pub previous: Option<String>,
123
}
124
125
pub(super) fn id(key: &MentionKey) -> String {
126
    format!("{}\n{}", key.room_id, key.event_id)
127
}
128
129
pub(super) fn now() -> u64 {
130
    matrix_sdk::ruma::MilliSecondsSinceUnixEpoch::now()
131
        .get()
132
        .into()
133
}
134
135
pub(super) fn invalid(message: &'static str) -> Error {
136
    Error::new(ErrorKind::Invalid, message)
137
}
138
139
#[cfg(feature = "serde")]
140
pub(super) fn json_len(value: &impl serde::Serialize) -> usize {
141
    struct Count(usize);
142
    impl std::io::Write for Count {
143
        fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
144
            self.0 += bytes.len();
145
            Ok(bytes.len())
146
        }
147
        fn flush(&mut self) -> std::io::Result<()> {
148
            Ok(())
149
        }
150
    }
151
    let mut count = Count(0);
152
    let _ = serde_json::to_writer(&mut count, value);
153
    count.0
154
}
155
156
#[cfg(not(feature = "serde"))]
157
pub(super) fn json_len<T>(_: &T) -> usize {
158
    0
159
}
160
161
fn timestamp(value: &Value) -> u64 {
162
    value["origin_server_ts"].as_u64().unwrap_or(0)
163
}
164
165
impl Data {
166
    pub fn gap(
167
        &mut self,
168
        room_id: Option<ruma::OwnedRoomId>,
169
        reason: MentionGapReason,
170
        token: Option<String>,
171
    ) {
172
        if self
173
            .coverage
174
            .gaps
175
            .iter()
176
            .any(|gap| gap.room_id == room_id && gap.reason == reason && gap.token == token)
177
        {
178
            return;
179
        }
180
        self.coverage.gaps.push(MentionGap {
181
            room_id,
182
            reason,
183
            token,
184
        });
185
        if self.coverage.gaps.len() > 128 {
186
            self.coverage.gaps.remove(0);
187
            self.coverage.truncated = true;
188
        }
189
    }
190
191
    pub fn expire_candidates(&mut self, now_ms: u64) -> bool {
192
        let expired: Vec<_> = self
193
            .candidates
194
            .iter()
195
            .filter(|(_, candidate)| {
196
                now_ms.saturating_sub(candidate.first_seen_ms) > CANDIDATE_LIFETIME_MS
197
            })
198
            .map(|(key, candidate)| (key.clone(), candidate.room_id.clone()))
199
            .collect();
200
        for (key, room_id) in &expired {
201
            self.candidates.remove(key);
202
            self.gap(
203
                Some(room_id.clone()),
204
                MentionGapReason::CandidateExpired,
205
                None,
206
            );
207
        }
208
        !expired.is_empty()
209
    }
210
211
    pub fn touch_contexts(&mut self, identity: &str) {
212
        for (key, window) in &self.windows {
213
            if window.target.as_deref() == Some(identity)
214
                || window
215
                    .before
216
                    .iter()
217
                    .chain(&window.after)
218
                    .any(|key| key == identity)
219
            {
220
                if let Some(entry) = self.entries.get_mut(key) {
221
                    entry.revision = self.revision + 1;
222
                }
223
            }
224
        }
225
    }
226
227
    pub fn page(
228
        &self,
229
        account: &OwnedUserId,
230
        cursor: Option<&str>,
231
        limit: u16,
232
    ) -> Result<MentionsPage, Error> {
233
        let (boundary, last) = if let Some(cursor) = cursor {
234
            let value: Value =
235
                serde_json::from_str(cursor).map_err(|_| invalid("invalid mentions cursor"))?;
236
            if value["account"].as_str() != Some(account.as_str()) {
237
                return Err(invalid("mentions cursor belongs to a different account"));
238
            }
239
            let boundary = value["revision"]
240
                .as_u64()
241
                .ok_or_else(|| invalid("invalid mentions cursor"))?;
242
            let timestamp = value["timestamp"]
243
                .as_u64()
244
                .ok_or_else(|| invalid("invalid mentions cursor"))?;
245
            let key = value["key"]
246
                .as_str()
247
                .ok_or_else(|| invalid("invalid mentions cursor"))?
248
                .to_owned();
249
            if boundary > self.revision {
250
                return Err(invalid("mentions cursor is newer than this store"));
251
            }
252
            (boundary, Some((timestamp, key)))
253
        } else {
254
            (self.revision, None)
255
        };
256
        let mut entries: Vec<_> = self
257
            .entries
258
            .iter()
259
            .filter(|(key, entry)| {
260
                entry.discovery_revision <= boundary
261
                    && last.as_ref().is_none_or(|last| {
262
                        (entry.timestamp_ms, key.as_str()) < (last.0, last.1.as_str())
263
                    })
264
            })
265
            .map(|(_, entry)| entry.clone())
266
            .collect();
267
        entries.sort_by(|a, b| (b.timestamp_ms, id(&b.key)).cmp(&(a.timestamp_ms, id(&a.key))));
268
        let limit = usize::from(limit.clamp(1, 50));
269
        let end_reached = entries.len() <= limit;
270
        entries.truncate(limit);
271
        let next_cursor = if end_reached {
272
            None
273
        } else {
274
            entries.last().map(|entry| serde_json::json!({
275
            "account": account.as_str(), "revision": boundary, "timestamp": entry.timestamp_ms, "key": id(&entry.key)
276
        }).to_string())
277
        };
278
        let mut coverage = self.coverage.clone();
279
        coverage.unresolved_candidates = self.candidates.len();
280
        Ok(MentionsPage {
281
            entries,
282
            next_cursor,
283
            end_reached,
284
            coverage,
285
            unread_count: self.entries.values().filter(|entry| !entry.read).count(),
286
            revision: self.revision,
287
            boundary: MentionsBoundary {
288
                account: account.clone(),
289
                revision: boundary,
290
            },
291
        })
292
    }
293
294
    pub fn context(
295
        &self,
296
        key: &MentionKey,
297
    ) -> Result<(SavedMentionContext, Vec<MediaRegistration>), Error> {
298
        let entry = self
299
            .entries
300
            .get(&id(key))
301
            .ok_or_else(|| Error::new(ErrorKind::EventNotFound, "mention was not found"))?;
302
        let mut media = Vec::new();
303
        let mut item = |key: &String| {
304
            self.events.get(key).map(|event| {
305
                media.extend(event.media.clone());
306
                event.item.clone()
307
            })
308
        };
309
        let context = self.windows.get(&id(key)).map(|window| EventContext {
310
            events_before: window.before.iter().filter_map(&mut item).collect(),
311
            event: window.target.as_ref().and_then(&mut item),
312
            events_after: window.after.iter().filter_map(&mut item).collect(),
313
            start_token: window.start.clone(),
314
            end_token: window.end.clone(),
315
        });
316
        Ok((
317
            SavedMentionContext {
318
                key: key.clone(),
319
                state: entry.capture.clone(),
320
                context,
321
                revision: self.revision,
322
            },
323
            media,
324
        ))
325
    }
326
327
    pub fn collect_events(&mut self) -> usize {
328
        let mut used: BTreeSet<String> = self.entries.keys().cloned().collect();
329
        for window in self.windows.values() {
330
            used.extend(window.before.iter().chain(&window.after).cloned());
331
            used.extend(window.target.iter().cloned());
332
        }
333
        let mut freed = 0;
334
        self.events.retain(|key, event| {
335
            let keep = used.contains(key);
336
            if !keep {
337
                freed += key.len() + json_len(event);
338
            }
339
            keep
340
        });
341
        self.capture_attempts
342
            .retain(|key, _| self.entries.contains_key(key));
343
        freed
344
    }
345
346
    /// returns roughly how many plaintext JSON bytes the eviction freed
347
    pub fn evict_one(&mut self) -> Option<usize> {
348
        let oldest = |keys: Vec<String>, entries: &BTreeMap<String, MentionEntry>| {
349
            keys.into_iter()
350
                .min_by_key(|key| entries.get(key).map_or(0, |entry| entry.timestamp_ms))
351
        };
352
        let freed = if let Some(key) = oldest(
353
            self.windows
354
                .keys()
355
                .filter(|key| self.entries.get(*key).is_some_and(|entry| entry.read))
356
                .cloned()
357
                .collect(),
358
            &self.entries,
359
        )
360
        .or_else(|| oldest(self.windows.keys().cloned().collect(), &self.entries))
361
        {
362
            let window = self.windows.remove(&key);
363
            if let Some(entry) = self.entries.get_mut(&key) {
364
                entry.capture = MentionCaptureState::Unavailable;
365
                entry.revision = self.revision;
366
            }
367
            key.len() + json_len(&window)
368
        } else if let Some(key) = self
369
            .entries
370
            .iter()
371
            .min_by_key(|(_, entry)| (!entry.read, entry.timestamp_ms))
372
            .map(|(key, _)| key.clone())
373
        {
374
            key.len() + json_len(&self.entries.remove(&key))
375
        } else if let Some(key) = self
376
            .candidates
377
            .iter()
378
            .min_by_key(|(_, candidate)| timestamp(&candidate.raw))
379
            .map(|(key, _)| key.clone())
380
        {
381
            key.len() + json_len(&self.candidates.remove(&key))
382
        } else if let Some(key) = self.edits.keys().next().cloned() {
383
            key.len() + json_len(&self.edits.remove(&key))
384
        } else {
385
            let key = self.redacted.keys().next().cloned()?;
386
            self.redacted.remove(&key);
387
            key.len() + 24
388
        };
389
        self.coverage.truncated = true;
390
        Some(freed + self.collect_events())
391
    }
392
393
    pub fn bound(&mut self, entries: usize, events: usize, candidates: usize) {
394
        while self.entries.len() > entries {
395
            if let Some(key) = self
396
                .entries
397
                .iter()
398
                .min_by_key(|(_, entry)| (!entry.read, entry.timestamp_ms))
399
                .map(|(key, _)| key.clone())
400
            {
401
                self.entries.remove(&key);
402
                self.windows.remove(&key);
403
                self.coverage.truncated = true;
404
            }
405
        }
406
        self.collect_events();
407
        while self.events.len() > events && self.evict_one().is_some() {}
408
        if self.candidates.len() > candidates {
409
            let mut oldest: Vec<_> = self
410
                .candidates
411
                .iter()
412
                .map(|(key, candidate)| (timestamp(&candidate.raw), key.clone()))
413
                .collect();
414
            oldest.sort();
415
            for (_, key) in oldest.into_iter().take(self.candidates.len() - candidates) {
416
                self.candidates.remove(&key);
417
            }
418
            self.coverage.truncated = true;
419
        }
420
        for map in [&mut self.redacted] {
421
            while map.len() > entries + candidates {
422
                let key = map
423
                    .iter()
424
                    .min_by_key(|(_, timestamp)| *timestamp)
425
                    .map(|(key, _)| key.clone())
426
                    .unwrap();
427
                map.remove(&key);
428
                self.coverage.truncated = true;
429
            }
430
        }
431
        while self.edits.len() > candidates {
432
            let key = self.edits.keys().next().cloned().unwrap();
433
            self.edits.remove(&key);
434
            self.coverage.truncated = true;
435
        }
436
        self.coverage.unresolved_candidates = self.candidates.len();
437
    }
438
}