| 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 | } |