atagen

macltok

public
Star0
Watch0
924b7c0115912af6bd8ab5191841544afcfda9c5
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: src/mentions/state.rs

cbb715e11963atagen2026-10-24 23:24 UTC1
use std::collections::{BTreeMap, BTreeSet};
cbb715e11963atagen2026-10-24 23:24 UTC2
2c13c8db6174atagen2026-10-25 09:12 UTC3
use ruma::OwnedUserId;
cbb715e11963atagen2026-10-24 23:24 UTC4
use serde_json::Value;
cbb715e11963atagen2026-10-24 23:24 UTC5
cbb715e11963atagen2026-10-24 23:24 UTC6
use crate::{
7cdaa86b9489atagen2026-10-25 08:43 UTC7
    Error, ErrorKind, EventContext, MentionCaptureState, MentionCoverage, MentionEntry, MentionGap,
7cdaa86b9489atagen2026-10-25 08:43 UTC8
    MentionGapReason, MentionKey, MentionsBoundary, MentionsPage, SavedMentionContext,
7cdaa86b9489atagen2026-10-25 08:43 UTC9
    TimelineItem, normalise::MediaRegistration,
cbb715e11963atagen2026-10-24 23:24 UTC10
};
cbb715e11963atagen2026-10-24 23:24 UTC11
7cdaa86b9489atagen2026-10-25 08:43 UTC12
const CANDIDATE_LIFETIME_MS: u64 = 7 * 24 * 60 * 60 * 1000;
7cdaa86b9489atagen2026-10-25 08:43 UTC13
cbb715e11963atagen2026-10-24 23:24 UTC14
#[derive(Clone)]
cbb715e11963atagen2026-10-24 23:24 UTC15
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
cbb715e11963atagen2026-10-24 23:24 UTC16
pub(super) struct SavedEvent {
cbb715e11963atagen2026-10-24 23:24 UTC17
    pub item: TimelineItem,
cbb715e11963atagen2026-10-24 23:24 UTC18
    pub media: Vec<MediaRegistration>,
cbb715e11963atagen2026-10-24 23:24 UTC19
    pub raw: Option<Value>,
cbb715e11963atagen2026-10-24 23:24 UTC20
}
cbb715e11963atagen2026-10-24 23:24 UTC21
7cdaa86b9489atagen2026-10-25 08:43 UTC22
impl PartialEq for SavedEvent {
7cdaa86b9489atagen2026-10-25 08:43 UTC23
    fn eq(&self, other: &Self) -> bool {
7cdaa86b9489atagen2026-10-25 08:43 UTC24
        self.item == other.item
7cdaa86b9489atagen2026-10-25 08:43 UTC25
            && self.raw == other.raw
7cdaa86b9489atagen2026-10-25 08:43 UTC26
            && self
7cdaa86b9489atagen2026-10-25 08:43 UTC27
                .media
7cdaa86b9489atagen2026-10-25 08:43 UTC28
                .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC29
                .map(|media| &media.id)
7cdaa86b9489atagen2026-10-25 08:43 UTC30
                .eq(other.media.iter().map(|media| &media.id))
7cdaa86b9489atagen2026-10-25 08:43 UTC31
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC32
}
7cdaa86b9489atagen2026-10-25 08:43 UTC33
7cdaa86b9489atagen2026-10-25 08:43 UTC34
#[derive(Clone, PartialEq)]
cbb715e11963atagen2026-10-24 23:24 UTC35
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
cbb715e11963atagen2026-10-24 23:24 UTC36
pub(super) struct Candidate {
cbb715e11963atagen2026-10-24 23:24 UTC37
    pub room_id: ruma::OwnedRoomId,
cbb715e11963atagen2026-10-24 23:24 UTC38
    pub raw: Value,
cbb715e11963atagen2026-10-24 23:24 UTC39
    pub attempts: u32,
cbb715e11963atagen2026-10-24 23:24 UTC40
    pub retry_at_ms: u64,
7cdaa86b9489atagen2026-10-25 08:43 UTC41
    pub first_seen_ms: u64,
cbb715e11963atagen2026-10-24 23:24 UTC42
}
cbb715e11963atagen2026-10-24 23:24 UTC43
7cdaa86b9489atagen2026-10-25 08:43 UTC44
impl Candidate {
7cdaa86b9489atagen2026-10-25 08:43 UTC45
    pub fn new(room_id: ruma::OwnedRoomId, raw: Value, retry_at_ms: u64) -> Self {
7cdaa86b9489atagen2026-10-25 08:43 UTC46
        Self {
7cdaa86b9489atagen2026-10-25 08:43 UTC47
            room_id,
7cdaa86b9489atagen2026-10-25 08:43 UTC48
            raw,
7cdaa86b9489atagen2026-10-25 08:43 UTC49
            attempts: 0,
7cdaa86b9489atagen2026-10-25 08:43 UTC50
            retry_at_ms,
7cdaa86b9489atagen2026-10-25 08:43 UTC51
            first_seen_ms: now(),
7cdaa86b9489atagen2026-10-25 08:43 UTC52
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC53
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC54
7cdaa86b9489atagen2026-10-25 08:43 UTC55
    pub fn encrypted(&self) -> bool {
7cdaa86b9489atagen2026-10-25 08:43 UTC56
        self.raw["type"] == "m.room.encrypted"
7cdaa86b9489atagen2026-10-25 08:43 UTC57
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC58
}
7cdaa86b9489atagen2026-10-25 08:43 UTC59
7cdaa86b9489atagen2026-10-25 08:43 UTC60
#[derive(Clone, PartialEq)]
cbb715e11963atagen2026-10-24 23:24 UTC61
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
cbb715e11963atagen2026-10-24 23:24 UTC62
pub(super) struct Window {
cbb715e11963atagen2026-10-24 23:24 UTC63
    pub before: Vec<String>,
cbb715e11963atagen2026-10-24 23:24 UTC64
    pub target: Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC65
    pub after: Vec<String>,
cbb715e11963atagen2026-10-24 23:24 UTC66
    pub start: Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC67
    pub end: Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC68
}
cbb715e11963atagen2026-10-24 23:24 UTC69
7cdaa86b9489atagen2026-10-25 08:43 UTC70
#[derive(Clone, Default, PartialEq)]
cbb715e11963atagen2026-10-24 23:24 UTC71
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
cbb715e11963atagen2026-10-24 23:24 UTC72
pub(super) struct Data {
cbb715e11963atagen2026-10-24 23:24 UTC73
    pub revision: u64,
cbb715e11963atagen2026-10-24 23:24 UTC74
    pub entries: BTreeMap<String, MentionEntry>,
cbb715e11963atagen2026-10-24 23:24 UTC75
    pub events: BTreeMap<String, SavedEvent>,
cbb715e11963atagen2026-10-24 23:24 UTC76
    pub windows: BTreeMap<String, Window>,
cbb715e11963atagen2026-10-24 23:24 UTC77
    pub capture_attempts: BTreeMap<String, u32>,
cbb715e11963atagen2026-10-24 23:24 UTC78
    pub candidates: BTreeMap<String, Candidate>,
cbb715e11963atagen2026-10-24 23:24 UTC79
    pub redacted: BTreeMap<String, u64>,
cbb715e11963atagen2026-10-24 23:24 UTC80
    pub edits: BTreeMap<String, Value>,
cbb715e11963atagen2026-10-24 23:24 UTC81
    pub coverage: MentionCoverage,
cbb715e11963atagen2026-10-24 23:24 UTC82
    pub history_cursor: Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC83
    pub history_exhausted: bool,
cbb715e11963atagen2026-10-24 23:24 UTC84
    pub latest_cursor: Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC85
    pub latest_anchor: Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC86
    pub latest_head: Option<String>,
7cdaa86b9489atagen2026-10-25 08:43 UTC87
}
7cdaa86b9489atagen2026-10-25 08:43 UTC88
7cdaa86b9489atagen2026-10-25 08:43 UTC89
#[derive(Clone, Default, PartialEq)]
7cdaa86b9489atagen2026-10-25 08:43 UTC90
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7cdaa86b9489atagen2026-10-25 08:43 UTC91
pub(super) struct SyncState {
7cdaa86b9489atagen2026-10-25 08:43 UTC92
    pub inbox_revision: u64,
7cdaa86b9489atagen2026-10-25 08:43 UTC93
    pub checkpoints: BTreeMap<String, Checkpoint>,
7cdaa86b9489atagen2026-10-25 08:43 UTC94
    pub observed_rooms: BTreeSet<ruma::OwnedRoomId>,
7cdaa86b9489atagen2026-10-25 08:43 UTC95
}
7cdaa86b9489atagen2026-10-25 08:43 UTC96
7cdaa86b9489atagen2026-10-25 08:43 UTC97
#[derive(Clone, Debug, PartialEq)]
7cdaa86b9489atagen2026-10-25 08:43 UTC98
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7cdaa86b9489atagen2026-10-25 08:43 UTC99
pub(super) struct Checkpoint {
7cdaa86b9489atagen2026-10-25 08:43 UTC100
    pub pos: String,
7cdaa86b9489atagen2026-10-25 08:43 UTC101
    pub previous: Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC102
}
cbb715e11963atagen2026-10-24 23:24 UTC103
cbb715e11963atagen2026-10-24 23:24 UTC104
pub(super) fn id(key: &MentionKey) -> String {
cbb715e11963atagen2026-10-24 23:24 UTC105
    format!("{}\n{}", key.room_id, key.event_id)
cbb715e11963atagen2026-10-24 23:24 UTC106
}
cbb715e11963atagen2026-10-24 23:24 UTC107
cbb715e11963atagen2026-10-24 23:24 UTC108
pub(super) fn now() -> u64 {
cbb715e11963atagen2026-10-24 23:24 UTC109
    matrix_sdk::ruma::MilliSecondsSinceUnixEpoch::now()
cbb715e11963atagen2026-10-24 23:24 UTC110
        .get()
cbb715e11963atagen2026-10-24 23:24 UTC111
        .into()
cbb715e11963atagen2026-10-24 23:24 UTC112
}
cbb715e11963atagen2026-10-24 23:24 UTC113
cbb715e11963atagen2026-10-24 23:24 UTC114
pub(super) fn invalid(message: &'static str) -> Error {
cbb715e11963atagen2026-10-24 23:24 UTC115
    Error::new(ErrorKind::Invalid, message)
cbb715e11963atagen2026-10-24 23:24 UTC116
}
cbb715e11963atagen2026-10-24 23:24 UTC117
7cdaa86b9489atagen2026-10-25 08:43 UTC118
#[cfg(feature = "serde")]
7cdaa86b9489atagen2026-10-25 08:43 UTC119
pub(super) fn json_len(value: &impl serde::Serialize) -> usize {
7cdaa86b9489atagen2026-10-25 08:43 UTC120
    struct Count(usize);
7cdaa86b9489atagen2026-10-25 08:43 UTC121
    impl std::io::Write for Count {
7cdaa86b9489atagen2026-10-25 08:43 UTC122
        fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
7cdaa86b9489atagen2026-10-25 08:43 UTC123
            self.0 += bytes.len();
7cdaa86b9489atagen2026-10-25 08:43 UTC124
            Ok(bytes.len())
7cdaa86b9489atagen2026-10-25 08:43 UTC125
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC126
        fn flush(&mut self) -> std::io::Result<()> {
7cdaa86b9489atagen2026-10-25 08:43 UTC127
            Ok(())
7cdaa86b9489atagen2026-10-25 08:43 UTC128
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC129
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC130
    let mut count = Count(0);
7cdaa86b9489atagen2026-10-25 08:43 UTC131
    let _ = serde_json::to_writer(&mut count, value);
7cdaa86b9489atagen2026-10-25 08:43 UTC132
    count.0
7cdaa86b9489atagen2026-10-25 08:43 UTC133
}
7cdaa86b9489atagen2026-10-25 08:43 UTC134
7cdaa86b9489atagen2026-10-25 08:43 UTC135
#[cfg(not(feature = "serde"))]
7cdaa86b9489atagen2026-10-25 08:43 UTC136
pub(super) fn json_len<T>(_: &T) -> usize {
7cdaa86b9489atagen2026-10-25 08:43 UTC137
    0
7cdaa86b9489atagen2026-10-25 08:43 UTC138
}
7cdaa86b9489atagen2026-10-25 08:43 UTC139
7cdaa86b9489atagen2026-10-25 08:43 UTC140
fn timestamp(value: &Value) -> u64 {
7cdaa86b9489atagen2026-10-25 08:43 UTC141
    value["origin_server_ts"].as_u64().unwrap_or(0)
7cdaa86b9489atagen2026-10-25 08:43 UTC142
}
7cdaa86b9489atagen2026-10-25 08:43 UTC143
cbb715e11963atagen2026-10-24 23:24 UTC144
impl Data {
7cdaa86b9489atagen2026-10-25 08:43 UTC145
    pub fn gap(
7cdaa86b9489atagen2026-10-25 08:43 UTC146
        &mut self,
7cdaa86b9489atagen2026-10-25 08:43 UTC147
        room_id: Option<ruma::OwnedRoomId>,
7cdaa86b9489atagen2026-10-25 08:43 UTC148
        reason: MentionGapReason,
7cdaa86b9489atagen2026-10-25 08:43 UTC149
        token: Option<String>,
7cdaa86b9489atagen2026-10-25 08:43 UTC150
    ) {
7cdaa86b9489atagen2026-10-25 08:43 UTC151
        if self
7cdaa86b9489atagen2026-10-25 08:43 UTC152
            .coverage
7cdaa86b9489atagen2026-10-25 08:43 UTC153
            .gaps
7cdaa86b9489atagen2026-10-25 08:43 UTC154
            .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC155
            .any(|gap| gap.room_id == room_id && gap.reason == reason && gap.token == token)
7cdaa86b9489atagen2026-10-25 08:43 UTC156
        {
7cdaa86b9489atagen2026-10-25 08:43 UTC157
            return;
7cdaa86b9489atagen2026-10-25 08:43 UTC158
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC159
        self.coverage.gaps.push(MentionGap {
7cdaa86b9489atagen2026-10-25 08:43 UTC160
            room_id,
7cdaa86b9489atagen2026-10-25 08:43 UTC161
            reason,
7cdaa86b9489atagen2026-10-25 08:43 UTC162
            token,
7cdaa86b9489atagen2026-10-25 08:43 UTC163
        });
7cdaa86b9489atagen2026-10-25 08:43 UTC164
        if self.coverage.gaps.len() > 128 {
7cdaa86b9489atagen2026-10-25 08:43 UTC165
            self.coverage.gaps.remove(0);
7cdaa86b9489atagen2026-10-25 08:43 UTC166
            self.coverage.truncated = true;
7cdaa86b9489atagen2026-10-25 08:43 UTC167
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC168
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC169
7cdaa86b9489atagen2026-10-25 08:43 UTC170
    pub fn expire_candidates(&mut self, now_ms: u64) -> bool {
7cdaa86b9489atagen2026-10-25 08:43 UTC171
        let expired: Vec<_> = self
7cdaa86b9489atagen2026-10-25 08:43 UTC172
            .candidates
7cdaa86b9489atagen2026-10-25 08:43 UTC173
            .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC174
            .filter(|(_, candidate)| {
7cdaa86b9489atagen2026-10-25 08:43 UTC175
                now_ms.saturating_sub(candidate.first_seen_ms) > CANDIDATE_LIFETIME_MS
7cdaa86b9489atagen2026-10-25 08:43 UTC176
            })
7cdaa86b9489atagen2026-10-25 08:43 UTC177
            .map(|(key, candidate)| (key.clone(), candidate.room_id.clone()))
7cdaa86b9489atagen2026-10-25 08:43 UTC178
            .collect();
7cdaa86b9489atagen2026-10-25 08:43 UTC179
        for (key, room_id) in &expired {
7cdaa86b9489atagen2026-10-25 08:43 UTC180
            self.candidates.remove(key);
7cdaa86b9489atagen2026-10-25 08:43 UTC181
            self.gap(
7cdaa86b9489atagen2026-10-25 08:43 UTC182
                Some(room_id.clone()),
7cdaa86b9489atagen2026-10-25 08:43 UTC183
                MentionGapReason::CandidateExpired,
7cdaa86b9489atagen2026-10-25 08:43 UTC184
                None,
7cdaa86b9489atagen2026-10-25 08:43 UTC185
            );
7cdaa86b9489atagen2026-10-25 08:43 UTC186
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC187
        !expired.is_empty()
7cdaa86b9489atagen2026-10-25 08:43 UTC188
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC189
cbb715e11963atagen2026-10-24 23:24 UTC190
    pub fn touch_contexts(&mut self, identity: &str) {
cbb715e11963atagen2026-10-24 23:24 UTC191
        for (key, window) in &self.windows {
cbb715e11963atagen2026-10-24 23:24 UTC192
            if window.target.as_deref() == Some(identity)
cbb715e11963atagen2026-10-24 23:24 UTC193
                || window
cbb715e11963atagen2026-10-24 23:24 UTC194
                    .before
cbb715e11963atagen2026-10-24 23:24 UTC195
                    .iter()
cbb715e11963atagen2026-10-24 23:24 UTC196
                    .chain(&window.after)
cbb715e11963atagen2026-10-24 23:24 UTC197
                    .any(|key| key == identity)
cbb715e11963atagen2026-10-24 23:24 UTC198
            {
cbb715e11963atagen2026-10-24 23:24 UTC199
                if let Some(entry) = self.entries.get_mut(key) {
cbb715e11963atagen2026-10-24 23:24 UTC200
                    entry.revision = self.revision + 1;
cbb715e11963atagen2026-10-24 23:24 UTC201
                }
cbb715e11963atagen2026-10-24 23:24 UTC202
            }
cbb715e11963atagen2026-10-24 23:24 UTC203
        }
cbb715e11963atagen2026-10-24 23:24 UTC204
    }
cbb715e11963atagen2026-10-24 23:24 UTC205
cbb715e11963atagen2026-10-24 23:24 UTC206
    pub fn page(
cbb715e11963atagen2026-10-24 23:24 UTC207
        &self,
cbb715e11963atagen2026-10-24 23:24 UTC208
        account: &OwnedUserId,
cbb715e11963atagen2026-10-24 23:24 UTC209
        cursor: Option<&str>,
cbb715e11963atagen2026-10-24 23:24 UTC210
        limit: u16,
cbb715e11963atagen2026-10-24 23:24 UTC211
    ) -> Result<MentionsPage, Error> {
cbb715e11963atagen2026-10-24 23:24 UTC212
        let (boundary, last) = if let Some(cursor) = cursor {
cbb715e11963atagen2026-10-24 23:24 UTC213
            let value: Value =
cbb715e11963atagen2026-10-24 23:24 UTC214
                serde_json::from_str(cursor).map_err(|_| invalid("invalid mentions cursor"))?;
cbb715e11963atagen2026-10-24 23:24 UTC215
            if value["account"].as_str() != Some(account.as_str()) {
cbb715e11963atagen2026-10-24 23:24 UTC216
                return Err(invalid("mentions cursor belongs to a different account"));
cbb715e11963atagen2026-10-24 23:24 UTC217
            }
cbb715e11963atagen2026-10-24 23:24 UTC218
            let boundary = value["revision"]
cbb715e11963atagen2026-10-24 23:24 UTC219
                .as_u64()
cbb715e11963atagen2026-10-24 23:24 UTC220
                .ok_or_else(|| invalid("invalid mentions cursor"))?;
cbb715e11963atagen2026-10-24 23:24 UTC221
            let timestamp = value["timestamp"]
cbb715e11963atagen2026-10-24 23:24 UTC222
                .as_u64()
cbb715e11963atagen2026-10-24 23:24 UTC223
                .ok_or_else(|| invalid("invalid mentions cursor"))?;
cbb715e11963atagen2026-10-24 23:24 UTC224
            let key = value["key"]
cbb715e11963atagen2026-10-24 23:24 UTC225
                .as_str()
cbb715e11963atagen2026-10-24 23:24 UTC226
                .ok_or_else(|| invalid("invalid mentions cursor"))?
cbb715e11963atagen2026-10-24 23:24 UTC227
                .to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC228
            if boundary > self.revision {
cbb715e11963atagen2026-10-24 23:24 UTC229
                return Err(invalid("mentions cursor is newer than this store"));
cbb715e11963atagen2026-10-24 23:24 UTC230
            }
cbb715e11963atagen2026-10-24 23:24 UTC231
            (boundary, Some((timestamp, key)))
cbb715e11963atagen2026-10-24 23:24 UTC232
        } else {
cbb715e11963atagen2026-10-24 23:24 UTC233
            (self.revision, None)
cbb715e11963atagen2026-10-24 23:24 UTC234
        };
cbb715e11963atagen2026-10-24 23:24 UTC235
        let mut entries: Vec<_> = self
cbb715e11963atagen2026-10-24 23:24 UTC236
            .entries
cbb715e11963atagen2026-10-24 23:24 UTC237
            .iter()
cbb715e11963atagen2026-10-24 23:24 UTC238
            .filter(|(key, entry)| {
cbb715e11963atagen2026-10-24 23:24 UTC239
                entry.discovery_revision <= boundary
cbb715e11963atagen2026-10-24 23:24 UTC240
                    && last.as_ref().is_none_or(|last| {
cbb715e11963atagen2026-10-24 23:24 UTC241
                        (entry.timestamp_ms, key.as_str()) < (last.0, last.1.as_str())
cbb715e11963atagen2026-10-24 23:24 UTC242
                    })
cbb715e11963atagen2026-10-24 23:24 UTC243
            })
cbb715e11963atagen2026-10-24 23:24 UTC244
            .map(|(_, entry)| entry.clone())
cbb715e11963atagen2026-10-24 23:24 UTC245
            .collect();
cbb715e11963atagen2026-10-24 23:24 UTC246
        entries.sort_by(|a, b| (b.timestamp_ms, id(&b.key)).cmp(&(a.timestamp_ms, id(&a.key))));
cbb715e11963atagen2026-10-24 23:24 UTC247
        let limit = usize::from(limit.clamp(1, 50));
cbb715e11963atagen2026-10-24 23:24 UTC248
        let end_reached = entries.len() <= limit;
cbb715e11963atagen2026-10-24 23:24 UTC249
        entries.truncate(limit);
cbb715e11963atagen2026-10-24 23:24 UTC250
        let next_cursor = if end_reached {
cbb715e11963atagen2026-10-24 23:24 UTC251
            None
cbb715e11963atagen2026-10-24 23:24 UTC252
        } else {
cbb715e11963atagen2026-10-24 23:24 UTC253
            entries.last().map(|entry| serde_json::json!({
cbb715e11963atagen2026-10-24 23:24 UTC254
            "account": account.as_str(), "revision": boundary, "timestamp": entry.timestamp_ms, "key": id(&entry.key)
cbb715e11963atagen2026-10-24 23:24 UTC255
        }).to_string())
cbb715e11963atagen2026-10-24 23:24 UTC256
        };
cbb715e11963atagen2026-10-24 23:24 UTC257
        let mut coverage = self.coverage.clone();
cbb715e11963atagen2026-10-24 23:24 UTC258
        coverage.unresolved_candidates = self.candidates.len();
cbb715e11963atagen2026-10-24 23:24 UTC259
        Ok(MentionsPage {
cbb715e11963atagen2026-10-24 23:24 UTC260
            entries,
cbb715e11963atagen2026-10-24 23:24 UTC261
            next_cursor,
cbb715e11963atagen2026-10-24 23:24 UTC262
            end_reached,
cbb715e11963atagen2026-10-24 23:24 UTC263
            coverage,
cbb715e11963atagen2026-10-24 23:24 UTC264
            unread_count: self.entries.values().filter(|entry| !entry.read).count(),
cbb715e11963atagen2026-10-24 23:24 UTC265
            revision: self.revision,
cbb715e11963atagen2026-10-24 23:24 UTC266
            boundary: MentionsBoundary {
cbb715e11963atagen2026-10-24 23:24 UTC267
                account: account.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC268
                revision: boundary,
cbb715e11963atagen2026-10-24 23:24 UTC269
            },
cbb715e11963atagen2026-10-24 23:24 UTC270
        })
cbb715e11963atagen2026-10-24 23:24 UTC271
    }
cbb715e11963atagen2026-10-24 23:24 UTC272
cbb715e11963atagen2026-10-24 23:24 UTC273
    pub fn context(
cbb715e11963atagen2026-10-24 23:24 UTC274
        &self,
cbb715e11963atagen2026-10-24 23:24 UTC275
        key: &MentionKey,
cbb715e11963atagen2026-10-24 23:24 UTC276
    ) -> Result<(SavedMentionContext, Vec<MediaRegistration>), Error> {
cbb715e11963atagen2026-10-24 23:24 UTC277
        let entry = self
cbb715e11963atagen2026-10-24 23:24 UTC278
            .entries
cbb715e11963atagen2026-10-24 23:24 UTC279
            .get(&id(key))
cbb715e11963atagen2026-10-24 23:24 UTC280
            .ok_or_else(|| Error::new(ErrorKind::EventNotFound, "mention was not found"))?;
cbb715e11963atagen2026-10-24 23:24 UTC281
        let mut media = Vec::new();
cbb715e11963atagen2026-10-24 23:24 UTC282
        let mut item = |key: &String| {
cbb715e11963atagen2026-10-24 23:24 UTC283
            self.events.get(key).map(|event| {
cbb715e11963atagen2026-10-24 23:24 UTC284
                media.extend(event.media.clone());
cbb715e11963atagen2026-10-24 23:24 UTC285
                event.item.clone()
cbb715e11963atagen2026-10-24 23:24 UTC286
            })
cbb715e11963atagen2026-10-24 23:24 UTC287
        };
258c08ec2168atagen2026-10-25 09:15 UTC288
        let context = self.windows.get(&id(key)).and_then(|window| {
258c08ec2168atagen2026-10-25 09:15 UTC289
            let event = window.target.as_ref().and_then(&mut item)?;
258c08ec2168atagen2026-10-25 09:15 UTC290
            Some(EventContext {
258c08ec2168atagen2026-10-25 09:15 UTC291
                events_before: window.before.iter().filter_map(&mut item).collect(),
258c08ec2168atagen2026-10-25 09:15 UTC292
                event,
258c08ec2168atagen2026-10-25 09:15 UTC293
                events_after: window.after.iter().filter_map(&mut item).collect(),
258c08ec2168atagen2026-10-25 09:15 UTC294
                start_token: window.start.clone(),
258c08ec2168atagen2026-10-25 09:15 UTC295
                end_token: window.end.clone(),
258c08ec2168atagen2026-10-25 09:15 UTC296
            })
cbb715e11963atagen2026-10-24 23:24 UTC297
        });
cbb715e11963atagen2026-10-24 23:24 UTC298
        Ok((
cbb715e11963atagen2026-10-24 23:24 UTC299
            SavedMentionContext {
cbb715e11963atagen2026-10-24 23:24 UTC300
                key: key.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC301
                state: entry.capture.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC302
                context,
cbb715e11963atagen2026-10-24 23:24 UTC303
                revision: self.revision,
cbb715e11963atagen2026-10-24 23:24 UTC304
            },
cbb715e11963atagen2026-10-24 23:24 UTC305
            media,
cbb715e11963atagen2026-10-24 23:24 UTC306
        ))
cbb715e11963atagen2026-10-24 23:24 UTC307
    }
cbb715e11963atagen2026-10-24 23:24 UTC308
7cdaa86b9489atagen2026-10-25 08:43 UTC309
    pub fn collect_events(&mut self) -> usize {
cbb715e11963atagen2026-10-24 23:24 UTC310
        let mut used: BTreeSet<String> = self.entries.keys().cloned().collect();
cbb715e11963atagen2026-10-24 23:24 UTC311
        for window in self.windows.values() {
cbb715e11963atagen2026-10-24 23:24 UTC312
            used.extend(window.before.iter().chain(&window.after).cloned());
cbb715e11963atagen2026-10-24 23:24 UTC313
            used.extend(window.target.iter().cloned());
cbb715e11963atagen2026-10-24 23:24 UTC314
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC315
        let mut freed = 0;
7cdaa86b9489atagen2026-10-25 08:43 UTC316
        self.events.retain(|key, event| {
7cdaa86b9489atagen2026-10-25 08:43 UTC317
            let keep = used.contains(key);
7cdaa86b9489atagen2026-10-25 08:43 UTC318
            if !keep {
7cdaa86b9489atagen2026-10-25 08:43 UTC319
                freed += key.len() + json_len(event);
7cdaa86b9489atagen2026-10-25 08:43 UTC320
            }
7cdaa86b9489atagen2026-10-25 08:43 UTC321
            keep
7cdaa86b9489atagen2026-10-25 08:43 UTC322
        });
cbb715e11963atagen2026-10-24 23:24 UTC323
        self.capture_attempts
cbb715e11963atagen2026-10-24 23:24 UTC324
            .retain(|key, _| self.entries.contains_key(key));
7cdaa86b9489atagen2026-10-25 08:43 UTC325
        freed
cbb715e11963atagen2026-10-24 23:24 UTC326
    }
cbb715e11963atagen2026-10-24 23:24 UTC327
7cdaa86b9489atagen2026-10-25 08:43 UTC328
    /// returns roughly how many plaintext JSON bytes the eviction freed
7cdaa86b9489atagen2026-10-25 08:43 UTC329
    pub fn evict_one(&mut self) -> Option<usize> {
cbb715e11963atagen2026-10-24 23:24 UTC330
        let oldest = |keys: Vec<String>, entries: &BTreeMap<String, MentionEntry>| {
cbb715e11963atagen2026-10-24 23:24 UTC331
            keys.into_iter()
cbb715e11963atagen2026-10-24 23:24 UTC332
                .min_by_key(|key| entries.get(key).map_or(0, |entry| entry.timestamp_ms))
cbb715e11963atagen2026-10-24 23:24 UTC333
        };
7cdaa86b9489atagen2026-10-25 08:43 UTC334
        let freed = if let Some(key) = oldest(
cbb715e11963atagen2026-10-24 23:24 UTC335
            self.windows
cbb715e11963atagen2026-10-24 23:24 UTC336
                .keys()
cbb715e11963atagen2026-10-24 23:24 UTC337
                .filter(|key| self.entries.get(*key).is_some_and(|entry| entry.read))
cbb715e11963atagen2026-10-24 23:24 UTC338
                .cloned()
cbb715e11963atagen2026-10-24 23:24 UTC339
                .collect(),
cbb715e11963atagen2026-10-24 23:24 UTC340
            &self.entries,
cbb715e11963atagen2026-10-24 23:24 UTC341
        )
cbb715e11963atagen2026-10-24 23:24 UTC342
        .or_else(|| oldest(self.windows.keys().cloned().collect(), &self.entries))
cbb715e11963atagen2026-10-24 23:24 UTC343
        {
7cdaa86b9489atagen2026-10-25 08:43 UTC344
            let window = self.windows.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC345
            if let Some(entry) = self.entries.get_mut(&key) {
cbb715e11963atagen2026-10-24 23:24 UTC346
                entry.capture = MentionCaptureState::Unavailable;
cbb715e11963atagen2026-10-24 23:24 UTC347
                entry.revision = self.revision;
cbb715e11963atagen2026-10-24 23:24 UTC348
            }
7cdaa86b9489atagen2026-10-25 08:43 UTC349
            key.len() + json_len(&window)
cbb715e11963atagen2026-10-24 23:24 UTC350
        } else if let Some(key) = self
cbb715e11963atagen2026-10-24 23:24 UTC351
            .entries
cbb715e11963atagen2026-10-24 23:24 UTC352
            .iter()
cbb715e11963atagen2026-10-24 23:24 UTC353
            .min_by_key(|(_, entry)| (!entry.read, entry.timestamp_ms))
cbb715e11963atagen2026-10-24 23:24 UTC354
            .map(|(key, _)| key.clone())
cbb715e11963atagen2026-10-24 23:24 UTC355
        {
7cdaa86b9489atagen2026-10-25 08:43 UTC356
            key.len() + json_len(&self.entries.remove(&key))
7cdaa86b9489atagen2026-10-25 08:43 UTC357
        } else if let Some(key) = self
7cdaa86b9489atagen2026-10-25 08:43 UTC358
            .candidates
7cdaa86b9489atagen2026-10-25 08:43 UTC359
            .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC360
            .min_by_key(|(_, candidate)| timestamp(&candidate.raw))
7cdaa86b9489atagen2026-10-25 08:43 UTC361
            .map(|(key, _)| key.clone())
7cdaa86b9489atagen2026-10-25 08:43 UTC362
        {
7cdaa86b9489atagen2026-10-25 08:43 UTC363
            key.len() + json_len(&self.candidates.remove(&key))
cbb715e11963atagen2026-10-24 23:24 UTC364
        } else if let Some(key) = self.edits.keys().next().cloned() {
7cdaa86b9489atagen2026-10-25 08:43 UTC365
            key.len() + json_len(&self.edits.remove(&key))
cbb715e11963atagen2026-10-24 23:24 UTC366
        } else {
7cdaa86b9489atagen2026-10-25 08:43 UTC367
            let key = self.redacted.keys().next().cloned()?;
7cdaa86b9489atagen2026-10-25 08:43 UTC368
            self.redacted.remove(&key);
7cdaa86b9489atagen2026-10-25 08:43 UTC369
            key.len() + 24
7cdaa86b9489atagen2026-10-25 08:43 UTC370
        };
cbb715e11963atagen2026-10-24 23:24 UTC371
        self.coverage.truncated = true;
7cdaa86b9489atagen2026-10-25 08:43 UTC372
        Some(freed + self.collect_events())
cbb715e11963atagen2026-10-24 23:24 UTC373
    }
cbb715e11963atagen2026-10-24 23:24 UTC374
cbb715e11963atagen2026-10-24 23:24 UTC375
    pub fn bound(&mut self, entries: usize, events: usize, candidates: usize) {
cbb715e11963atagen2026-10-24 23:24 UTC376
        while self.entries.len() > entries {
cbb715e11963atagen2026-10-24 23:24 UTC377
            if let Some(key) = self
cbb715e11963atagen2026-10-24 23:24 UTC378
                .entries
cbb715e11963atagen2026-10-24 23:24 UTC379
                .iter()
cbb715e11963atagen2026-10-24 23:24 UTC380
                .min_by_key(|(_, entry)| (!entry.read, entry.timestamp_ms))
cbb715e11963atagen2026-10-24 23:24 UTC381
                .map(|(key, _)| key.clone())
cbb715e11963atagen2026-10-24 23:24 UTC382
            {
cbb715e11963atagen2026-10-24 23:24 UTC383
                self.entries.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC384
                self.windows.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC385
                self.coverage.truncated = true;
cbb715e11963atagen2026-10-24 23:24 UTC386
            }
cbb715e11963atagen2026-10-24 23:24 UTC387
        }
cbb715e11963atagen2026-10-24 23:24 UTC388
        self.collect_events();
7cdaa86b9489atagen2026-10-25 08:43 UTC389
        while self.events.len() > events && self.evict_one().is_some() {}
7cdaa86b9489atagen2026-10-25 08:43 UTC390
        if self.candidates.len() > candidates {
7cdaa86b9489atagen2026-10-25 08:43 UTC391
            let mut oldest: Vec<_> = self
7cdaa86b9489atagen2026-10-25 08:43 UTC392
                .candidates
7cdaa86b9489atagen2026-10-25 08:43 UTC393
                .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC394
                .map(|(key, candidate)| (timestamp(&candidate.raw), key.clone()))
7cdaa86b9489atagen2026-10-25 08:43 UTC395
                .collect();
7cdaa86b9489atagen2026-10-25 08:43 UTC396
            oldest.sort();
7cdaa86b9489atagen2026-10-25 08:43 UTC397
            for (_, key) in oldest.into_iter().take(self.candidates.len() - candidates) {
7cdaa86b9489atagen2026-10-25 08:43 UTC398
                self.candidates.remove(&key);
7cdaa86b9489atagen2026-10-25 08:43 UTC399
            }
cbb715e11963atagen2026-10-24 23:24 UTC400
            self.coverage.truncated = true;
cbb715e11963atagen2026-10-24 23:24 UTC401
        }
cbb715e11963atagen2026-10-24 23:24 UTC402
        for map in [&mut self.redacted] {
cbb715e11963atagen2026-10-24 23:24 UTC403
            while map.len() > entries + candidates {
cbb715e11963atagen2026-10-24 23:24 UTC404
                let key = map
cbb715e11963atagen2026-10-24 23:24 UTC405
                    .iter()
cbb715e11963atagen2026-10-24 23:24 UTC406
                    .min_by_key(|(_, timestamp)| *timestamp)
cbb715e11963atagen2026-10-24 23:24 UTC407
                    .map(|(key, _)| key.clone())
cbb715e11963atagen2026-10-24 23:24 UTC408
                    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC409
                map.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC410
                self.coverage.truncated = true;
cbb715e11963atagen2026-10-24 23:24 UTC411
            }
cbb715e11963atagen2026-10-24 23:24 UTC412
        }
cbb715e11963atagen2026-10-24 23:24 UTC413
        while self.edits.len() > candidates {
cbb715e11963atagen2026-10-24 23:24 UTC414
            let key = self.edits.keys().next().cloned().unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC415
            self.edits.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC416
            self.coverage.truncated = true;
cbb715e11963atagen2026-10-24 23:24 UTC417
        }
cbb715e11963atagen2026-10-24 23:24 UTC418
        self.coverage.unresolved_candidates = self.candidates.len();
cbb715e11963atagen2026-10-24 23:24 UTC419
    }
cbb715e11963atagen2026-10-24 23:24 UTC420
}