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