atagen

macltok

public
Star0
Watch0
cf4b01bf62fa44d38ba284645beb2b3586fc6a5b
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>,
82d37c369846atagen2026-10-25 10:55 UTC94
}
82d37c369846atagen2026-10-25 10:55 UTC95
82d37c369846atagen2026-10-25 10:55 UTC96
/// what a set of rooms holds in the inbox, compared before and after a change so an
82d37c369846atagen2026-10-25 10:55 UTC97
/// unchanged inbox costs only the rooms it touched
82d37c369846atagen2026-10-25 10:55 UTC98
#[derive(PartialEq)]
82d37c369846atagen2026-10-25 10:55 UTC99
pub(super) struct Scope {
82d37c369846atagen2026-10-25 10:55 UTC100
    rooms: Vec<RoomScope>,
82d37c369846atagen2026-10-25 10:55 UTC101
    coverage: MentionCoverage,
82d37c369846atagen2026-10-25 10:55 UTC102
    cursors: (
82d37c369846atagen2026-10-25 10:55 UTC103
        Option<String>,
82d37c369846atagen2026-10-25 10:55 UTC104
        bool,
82d37c369846atagen2026-10-25 10:55 UTC105
        Option<String>,
82d37c369846atagen2026-10-25 10:55 UTC106
        Option<String>,
82d37c369846atagen2026-10-25 10:55 UTC107
        Option<String>,
82d37c369846atagen2026-10-25 10:55 UTC108
    ),
82d37c369846atagen2026-10-25 10:55 UTC109
}
82d37c369846atagen2026-10-25 10:55 UTC110
82d37c369846atagen2026-10-25 10:55 UTC111
#[derive(PartialEq)]
82d37c369846atagen2026-10-25 10:55 UTC112
struct RoomScope {
82d37c369846atagen2026-10-25 10:55 UTC113
    entries: Vec<MentionEntry>,
82d37c369846atagen2026-10-25 10:55 UTC114
    events: Vec<SavedEvent>,
82d37c369846atagen2026-10-25 10:55 UTC115
    windows: Vec<Window>,
82d37c369846atagen2026-10-25 10:55 UTC116
    capture_attempts: Vec<u32>,
82d37c369846atagen2026-10-25 10:55 UTC117
    candidates: Vec<Candidate>,
82d37c369846atagen2026-10-25 10:55 UTC118
    redacted: Vec<u64>,
82d37c369846atagen2026-10-25 10:55 UTC119
    edits: Vec<Value>,
82d37c369846atagen2026-10-25 10:55 UTC120
    keys: Vec<String>,
82d37c369846atagen2026-10-25 10:55 UTC121
}
82d37c369846atagen2026-10-25 10:55 UTC122
82d37c369846atagen2026-10-25 10:55 UTC123
fn in_room<V: Clone>(
82d37c369846atagen2026-10-25 10:55 UTC124
    map: &BTreeMap<String, V>,
82d37c369846atagen2026-10-25 10:55 UTC125
    room: &ruma::RoomId,
82d37c369846atagen2026-10-25 10:55 UTC126
    keys: &mut Vec<String>,
82d37c369846atagen2026-10-25 10:55 UTC127
) -> Vec<V> {
82d37c369846atagen2026-10-25 10:55 UTC128
    // no room id holds a newline, so a room's "room\nevent" keys sit below "room\x0b"
82d37c369846atagen2026-10-25 10:55 UTC129
    map.range(format!("{room}\n")..format!("{room}\u{b}"))
82d37c369846atagen2026-10-25 10:55 UTC130
        .map(|(key, value)| {
82d37c369846atagen2026-10-25 10:55 UTC131
            keys.push(key.clone());
82d37c369846atagen2026-10-25 10:55 UTC132
            value.clone()
82d37c369846atagen2026-10-25 10:55 UTC133
        })
82d37c369846atagen2026-10-25 10:55 UTC134
        .collect()
7cdaa86b9489atagen2026-10-25 08:43 UTC135
}
7cdaa86b9489atagen2026-10-25 08:43 UTC136
7cdaa86b9489atagen2026-10-25 08:43 UTC137
#[derive(Clone, Debug, PartialEq)]
7cdaa86b9489atagen2026-10-25 08:43 UTC138
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7cdaa86b9489atagen2026-10-25 08:43 UTC139
pub(super) struct Checkpoint {
7cdaa86b9489atagen2026-10-25 08:43 UTC140
    pub pos: String,
7cdaa86b9489atagen2026-10-25 08:43 UTC141
    pub previous: Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC142
}
cbb715e11963atagen2026-10-24 23:24 UTC143
cbb715e11963atagen2026-10-24 23:24 UTC144
pub(super) fn id(key: &MentionKey) -> String {
cbb715e11963atagen2026-10-24 23:24 UTC145
    format!("{}\n{}", key.room_id, key.event_id)
cbb715e11963atagen2026-10-24 23:24 UTC146
}
cbb715e11963atagen2026-10-24 23:24 UTC147
cbb715e11963atagen2026-10-24 23:24 UTC148
pub(super) fn now() -> u64 {
cbb715e11963atagen2026-10-24 23:24 UTC149
    matrix_sdk::ruma::MilliSecondsSinceUnixEpoch::now()
cbb715e11963atagen2026-10-24 23:24 UTC150
        .get()
cbb715e11963atagen2026-10-24 23:24 UTC151
        .into()
cbb715e11963atagen2026-10-24 23:24 UTC152
}
cbb715e11963atagen2026-10-24 23:24 UTC153
cbb715e11963atagen2026-10-24 23:24 UTC154
pub(super) fn invalid(message: &'static str) -> Error {
cbb715e11963atagen2026-10-24 23:24 UTC155
    Error::new(ErrorKind::Invalid, message)
cbb715e11963atagen2026-10-24 23:24 UTC156
}
cbb715e11963atagen2026-10-24 23:24 UTC157
7cdaa86b9489atagen2026-10-25 08:43 UTC158
#[cfg(feature = "serde")]
7cdaa86b9489atagen2026-10-25 08:43 UTC159
pub(super) fn json_len(value: &impl serde::Serialize) -> usize {
7cdaa86b9489atagen2026-10-25 08:43 UTC160
    struct Count(usize);
7cdaa86b9489atagen2026-10-25 08:43 UTC161
    impl std::io::Write for Count {
7cdaa86b9489atagen2026-10-25 08:43 UTC162
        fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
7cdaa86b9489atagen2026-10-25 08:43 UTC163
            self.0 += bytes.len();
7cdaa86b9489atagen2026-10-25 08:43 UTC164
            Ok(bytes.len())
7cdaa86b9489atagen2026-10-25 08:43 UTC165
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC166
        fn flush(&mut self) -> std::io::Result<()> {
7cdaa86b9489atagen2026-10-25 08:43 UTC167
            Ok(())
7cdaa86b9489atagen2026-10-25 08:43 UTC168
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC169
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC170
    let mut count = Count(0);
7cdaa86b9489atagen2026-10-25 08:43 UTC171
    let _ = serde_json::to_writer(&mut count, value);
7cdaa86b9489atagen2026-10-25 08:43 UTC172
    count.0
7cdaa86b9489atagen2026-10-25 08:43 UTC173
}
7cdaa86b9489atagen2026-10-25 08:43 UTC174
7cdaa86b9489atagen2026-10-25 08:43 UTC175
#[cfg(not(feature = "serde"))]
7cdaa86b9489atagen2026-10-25 08:43 UTC176
pub(super) fn json_len<T>(_: &T) -> usize {
7cdaa86b9489atagen2026-10-25 08:43 UTC177
    0
7cdaa86b9489atagen2026-10-25 08:43 UTC178
}
7cdaa86b9489atagen2026-10-25 08:43 UTC179
7cdaa86b9489atagen2026-10-25 08:43 UTC180
fn timestamp(value: &Value) -> u64 {
7cdaa86b9489atagen2026-10-25 08:43 UTC181
    value["origin_server_ts"].as_u64().unwrap_or(0)
7cdaa86b9489atagen2026-10-25 08:43 UTC182
}
7cdaa86b9489atagen2026-10-25 08:43 UTC183
cbb715e11963atagen2026-10-24 23:24 UTC184
impl Data {
82d37c369846atagen2026-10-25 10:55 UTC185
    pub fn scope<'a>(&self, rooms: impl IntoIterator<Item = &'a ruma::OwnedRoomId>) -> Scope {
82d37c369846atagen2026-10-25 10:55 UTC186
        Scope {
82d37c369846atagen2026-10-25 10:55 UTC187
            rooms: rooms
82d37c369846atagen2026-10-25 10:55 UTC188
                .into_iter()
82d37c369846atagen2026-10-25 10:55 UTC189
                .map(|room| {
82d37c369846atagen2026-10-25 10:55 UTC190
                    let mut keys = Vec::new();
82d37c369846atagen2026-10-25 10:55 UTC191
                    RoomScope {
82d37c369846atagen2026-10-25 10:55 UTC192
                        entries: in_room(&self.entries, room, &mut keys),
82d37c369846atagen2026-10-25 10:55 UTC193
                        events: in_room(&self.events, room, &mut keys),
82d37c369846atagen2026-10-25 10:55 UTC194
                        windows: in_room(&self.windows, room, &mut keys),
82d37c369846atagen2026-10-25 10:55 UTC195
                        capture_attempts: in_room(&self.capture_attempts, room, &mut keys),
82d37c369846atagen2026-10-25 10:55 UTC196
                        candidates: in_room(&self.candidates, room, &mut keys),
82d37c369846atagen2026-10-25 10:55 UTC197
                        redacted: in_room(&self.redacted, room, &mut keys),
82d37c369846atagen2026-10-25 10:55 UTC198
                        edits: in_room(&self.edits, room, &mut keys),
82d37c369846atagen2026-10-25 10:55 UTC199
                        keys,
82d37c369846atagen2026-10-25 10:55 UTC200
                    }
82d37c369846atagen2026-10-25 10:55 UTC201
                })
82d37c369846atagen2026-10-25 10:55 UTC202
                .collect(),
82d37c369846atagen2026-10-25 10:55 UTC203
            coverage: self.coverage.clone(),
82d37c369846atagen2026-10-25 10:55 UTC204
            cursors: (
82d37c369846atagen2026-10-25 10:55 UTC205
                self.history_cursor.clone(),
82d37c369846atagen2026-10-25 10:55 UTC206
                self.history_exhausted,
82d37c369846atagen2026-10-25 10:55 UTC207
                self.latest_cursor.clone(),
82d37c369846atagen2026-10-25 10:55 UTC208
                self.latest_anchor.clone(),
82d37c369846atagen2026-10-25 10:55 UTC209
                self.latest_head.clone(),
82d37c369846atagen2026-10-25 10:55 UTC210
            ),
82d37c369846atagen2026-10-25 10:55 UTC211
        }
82d37c369846atagen2026-10-25 10:55 UTC212
    }
82d37c369846atagen2026-10-25 10:55 UTC213
82d37c369846atagen2026-10-25 10:55 UTC214
    /// folds an inbox built in memory while the store was unreachable into the one the
82d37c369846atagen2026-10-25 10:55 UTC215
    /// store held, keeping the newer side where both have a key
82d37c369846atagen2026-10-25 10:55 UTC216
    pub fn absorb(&mut self, newer: Data) {
82d37c369846atagen2026-10-25 10:55 UTC217
        self.revision = self.revision.max(newer.revision);
82d37c369846atagen2026-10-25 10:55 UTC218
        self.entries.extend(newer.entries);
82d37c369846atagen2026-10-25 10:55 UTC219
        self.events.extend(newer.events);
82d37c369846atagen2026-10-25 10:55 UTC220
        self.windows.extend(newer.windows);
82d37c369846atagen2026-10-25 10:55 UTC221
        self.capture_attempts.extend(newer.capture_attempts);
82d37c369846atagen2026-10-25 10:55 UTC222
        self.candidates.extend(newer.candidates);
82d37c369846atagen2026-10-25 10:55 UTC223
        self.redacted.extend(newer.redacted);
82d37c369846atagen2026-10-25 10:55 UTC224
        self.edits.extend(newer.edits);
82d37c369846atagen2026-10-25 10:55 UTC225
        for gap in newer.coverage.gaps {
82d37c369846atagen2026-10-25 10:55 UTC226
            self.gap(gap.room_id, gap.reason, gap.token);
82d37c369846atagen2026-10-25 10:55 UTC227
        }
82d37c369846atagen2026-10-25 10:55 UTC228
        self.coverage.earliest_timestamp_ms = self
82d37c369846atagen2026-10-25 10:55 UTC229
            .coverage
82d37c369846atagen2026-10-25 10:55 UTC230
            .earliest_timestamp_ms
82d37c369846atagen2026-10-25 10:55 UTC231
            .into_iter()
82d37c369846atagen2026-10-25 10:55 UTC232
            .chain(newer.coverage.earliest_timestamp_ms)
82d37c369846atagen2026-10-25 10:55 UTC233
            .min();
82d37c369846atagen2026-10-25 10:55 UTC234
        self.coverage.truncated |= newer.coverage.truncated;
82d37c369846atagen2026-10-25 10:55 UTC235
        self.history_cursor = newer.history_cursor.or(self.history_cursor.take());
82d37c369846atagen2026-10-25 10:55 UTC236
        self.latest_cursor = newer.latest_cursor.or(self.latest_cursor.take());
82d37c369846atagen2026-10-25 10:55 UTC237
        self.latest_anchor = newer.latest_anchor.or(self.latest_anchor.take());
82d37c369846atagen2026-10-25 10:55 UTC238
        self.latest_head = newer.latest_head.or(self.latest_head.take());
82d37c369846atagen2026-10-25 10:55 UTC239
    }
82d37c369846atagen2026-10-25 10:55 UTC240
7cdaa86b9489atagen2026-10-25 08:43 UTC241
    pub fn gap(
7cdaa86b9489atagen2026-10-25 08:43 UTC242
        &mut self,
7cdaa86b9489atagen2026-10-25 08:43 UTC243
        room_id: Option<ruma::OwnedRoomId>,
7cdaa86b9489atagen2026-10-25 08:43 UTC244
        reason: MentionGapReason,
7cdaa86b9489atagen2026-10-25 08:43 UTC245
        token: Option<String>,
7cdaa86b9489atagen2026-10-25 08:43 UTC246
    ) {
7cdaa86b9489atagen2026-10-25 08:43 UTC247
        if self
7cdaa86b9489atagen2026-10-25 08:43 UTC248
            .coverage
7cdaa86b9489atagen2026-10-25 08:43 UTC249
            .gaps
7cdaa86b9489atagen2026-10-25 08:43 UTC250
            .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC251
            .any(|gap| gap.room_id == room_id && gap.reason == reason && gap.token == token)
7cdaa86b9489atagen2026-10-25 08:43 UTC252
        {
7cdaa86b9489atagen2026-10-25 08:43 UTC253
            return;
7cdaa86b9489atagen2026-10-25 08:43 UTC254
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC255
        self.coverage.gaps.push(MentionGap {
7cdaa86b9489atagen2026-10-25 08:43 UTC256
            room_id,
7cdaa86b9489atagen2026-10-25 08:43 UTC257
            reason,
7cdaa86b9489atagen2026-10-25 08:43 UTC258
            token,
7cdaa86b9489atagen2026-10-25 08:43 UTC259
        });
7cdaa86b9489atagen2026-10-25 08:43 UTC260
        if self.coverage.gaps.len() > 128 {
7cdaa86b9489atagen2026-10-25 08:43 UTC261
            self.coverage.gaps.remove(0);
7cdaa86b9489atagen2026-10-25 08:43 UTC262
            self.coverage.truncated = true;
7cdaa86b9489atagen2026-10-25 08:43 UTC263
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC264
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC265
7cdaa86b9489atagen2026-10-25 08:43 UTC266
    pub fn expire_candidates(&mut self, now_ms: u64) -> bool {
7cdaa86b9489atagen2026-10-25 08:43 UTC267
        let expired: Vec<_> = self
7cdaa86b9489atagen2026-10-25 08:43 UTC268
            .candidates
7cdaa86b9489atagen2026-10-25 08:43 UTC269
            .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC270
            .filter(|(_, candidate)| {
7cdaa86b9489atagen2026-10-25 08:43 UTC271
                now_ms.saturating_sub(candidate.first_seen_ms) > CANDIDATE_LIFETIME_MS
7cdaa86b9489atagen2026-10-25 08:43 UTC272
            })
7cdaa86b9489atagen2026-10-25 08:43 UTC273
            .map(|(key, candidate)| (key.clone(), candidate.room_id.clone()))
7cdaa86b9489atagen2026-10-25 08:43 UTC274
            .collect();
7cdaa86b9489atagen2026-10-25 08:43 UTC275
        for (key, room_id) in &expired {
7cdaa86b9489atagen2026-10-25 08:43 UTC276
            self.candidates.remove(key);
7cdaa86b9489atagen2026-10-25 08:43 UTC277
            self.gap(
7cdaa86b9489atagen2026-10-25 08:43 UTC278
                Some(room_id.clone()),
7cdaa86b9489atagen2026-10-25 08:43 UTC279
                MentionGapReason::CandidateExpired,
7cdaa86b9489atagen2026-10-25 08:43 UTC280
                None,
7cdaa86b9489atagen2026-10-25 08:43 UTC281
            );
7cdaa86b9489atagen2026-10-25 08:43 UTC282
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC283
        !expired.is_empty()
7cdaa86b9489atagen2026-10-25 08:43 UTC284
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC285
cbb715e11963atagen2026-10-24 23:24 UTC286
    pub fn touch_contexts(&mut self, identity: &str) {
cbb715e11963atagen2026-10-24 23:24 UTC287
        for (key, window) in &self.windows {
cbb715e11963atagen2026-10-24 23:24 UTC288
            if window.target.as_deref() == Some(identity)
cbb715e11963atagen2026-10-24 23:24 UTC289
                || window
cbb715e11963atagen2026-10-24 23:24 UTC290
                    .before
cbb715e11963atagen2026-10-24 23:24 UTC291
                    .iter()
cbb715e11963atagen2026-10-24 23:24 UTC292
                    .chain(&window.after)
cbb715e11963atagen2026-10-24 23:24 UTC293
                    .any(|key| key == identity)
cbb715e11963atagen2026-10-24 23:24 UTC294
            {
cbb715e11963atagen2026-10-24 23:24 UTC295
                if let Some(entry) = self.entries.get_mut(key) {
cbb715e11963atagen2026-10-24 23:24 UTC296
                    entry.revision = self.revision + 1;
cbb715e11963atagen2026-10-24 23:24 UTC297
                }
cbb715e11963atagen2026-10-24 23:24 UTC298
            }
cbb715e11963atagen2026-10-24 23:24 UTC299
        }
cbb715e11963atagen2026-10-24 23:24 UTC300
    }
cbb715e11963atagen2026-10-24 23:24 UTC301
cbb715e11963atagen2026-10-24 23:24 UTC302
    pub fn page(
cbb715e11963atagen2026-10-24 23:24 UTC303
        &self,
cbb715e11963atagen2026-10-24 23:24 UTC304
        account: &OwnedUserId,
cbb715e11963atagen2026-10-24 23:24 UTC305
        cursor: Option<&str>,
cbb715e11963atagen2026-10-24 23:24 UTC306
        limit: u16,
cbb715e11963atagen2026-10-24 23:24 UTC307
    ) -> Result<MentionsPage, Error> {
cbb715e11963atagen2026-10-24 23:24 UTC308
        let (boundary, last) = if let Some(cursor) = cursor {
cbb715e11963atagen2026-10-24 23:24 UTC309
            let value: Value =
cbb715e11963atagen2026-10-24 23:24 UTC310
                serde_json::from_str(cursor).map_err(|_| invalid("invalid mentions cursor"))?;
cbb715e11963atagen2026-10-24 23:24 UTC311
            if value["account"].as_str() != Some(account.as_str()) {
cbb715e11963atagen2026-10-24 23:24 UTC312
                return Err(invalid("mentions cursor belongs to a different account"));
cbb715e11963atagen2026-10-24 23:24 UTC313
            }
cbb715e11963atagen2026-10-24 23:24 UTC314
            let boundary = value["revision"]
cbb715e11963atagen2026-10-24 23:24 UTC315
                .as_u64()
cbb715e11963atagen2026-10-24 23:24 UTC316
                .ok_or_else(|| invalid("invalid mentions cursor"))?;
cbb715e11963atagen2026-10-24 23:24 UTC317
            let timestamp = value["timestamp"]
cbb715e11963atagen2026-10-24 23:24 UTC318
                .as_u64()
cbb715e11963atagen2026-10-24 23:24 UTC319
                .ok_or_else(|| invalid("invalid mentions cursor"))?;
cbb715e11963atagen2026-10-24 23:24 UTC320
            let key = value["key"]
cbb715e11963atagen2026-10-24 23:24 UTC321
                .as_str()
cbb715e11963atagen2026-10-24 23:24 UTC322
                .ok_or_else(|| invalid("invalid mentions cursor"))?
cbb715e11963atagen2026-10-24 23:24 UTC323
                .to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC324
            if boundary > self.revision {
cbb715e11963atagen2026-10-24 23:24 UTC325
                return Err(invalid("mentions cursor is newer than this store"));
cbb715e11963atagen2026-10-24 23:24 UTC326
            }
cbb715e11963atagen2026-10-24 23:24 UTC327
            (boundary, Some((timestamp, key)))
cbb715e11963atagen2026-10-24 23:24 UTC328
        } else {
cbb715e11963atagen2026-10-24 23:24 UTC329
            (self.revision, None)
cbb715e11963atagen2026-10-24 23:24 UTC330
        };
cbb715e11963atagen2026-10-24 23:24 UTC331
        let mut entries: Vec<_> = self
cbb715e11963atagen2026-10-24 23:24 UTC332
            .entries
cbb715e11963atagen2026-10-24 23:24 UTC333
            .iter()
cbb715e11963atagen2026-10-24 23:24 UTC334
            .filter(|(key, entry)| {
cbb715e11963atagen2026-10-24 23:24 UTC335
                entry.discovery_revision <= boundary
cbb715e11963atagen2026-10-24 23:24 UTC336
                    && last.as_ref().is_none_or(|last| {
cbb715e11963atagen2026-10-24 23:24 UTC337
                        (entry.timestamp_ms, key.as_str()) < (last.0, last.1.as_str())
cbb715e11963atagen2026-10-24 23:24 UTC338
                    })
cbb715e11963atagen2026-10-24 23:24 UTC339
            })
cbb715e11963atagen2026-10-24 23:24 UTC340
            .map(|(_, entry)| entry.clone())
cbb715e11963atagen2026-10-24 23:24 UTC341
            .collect();
cbb715e11963atagen2026-10-24 23:24 UTC342
        entries.sort_by(|a, b| (b.timestamp_ms, id(&b.key)).cmp(&(a.timestamp_ms, id(&a.key))));
cbb715e11963atagen2026-10-24 23:24 UTC343
        let limit = usize::from(limit.clamp(1, 50));
cbb715e11963atagen2026-10-24 23:24 UTC344
        let end_reached = entries.len() <= limit;
cbb715e11963atagen2026-10-24 23:24 UTC345
        entries.truncate(limit);
cbb715e11963atagen2026-10-24 23:24 UTC346
        let next_cursor = if end_reached {
cbb715e11963atagen2026-10-24 23:24 UTC347
            None
cbb715e11963atagen2026-10-24 23:24 UTC348
        } else {
cbb715e11963atagen2026-10-24 23:24 UTC349
            entries.last().map(|entry| serde_json::json!({
cbb715e11963atagen2026-10-24 23:24 UTC350
            "account": account.as_str(), "revision": boundary, "timestamp": entry.timestamp_ms, "key": id(&entry.key)
cbb715e11963atagen2026-10-24 23:24 UTC351
        }).to_string())
cbb715e11963atagen2026-10-24 23:24 UTC352
        };
cbb715e11963atagen2026-10-24 23:24 UTC353
        let mut coverage = self.coverage.clone();
cbb715e11963atagen2026-10-24 23:24 UTC354
        coverage.unresolved_candidates = self.candidates.len();
cbb715e11963atagen2026-10-24 23:24 UTC355
        Ok(MentionsPage {
cbb715e11963atagen2026-10-24 23:24 UTC356
            entries,
cbb715e11963atagen2026-10-24 23:24 UTC357
            next_cursor,
cbb715e11963atagen2026-10-24 23:24 UTC358
            end_reached,
cbb715e11963atagen2026-10-24 23:24 UTC359
            coverage,
cbb715e11963atagen2026-10-24 23:24 UTC360
            unread_count: self.entries.values().filter(|entry| !entry.read).count(),
cbb715e11963atagen2026-10-24 23:24 UTC361
            revision: self.revision,
cbb715e11963atagen2026-10-24 23:24 UTC362
            boundary: MentionsBoundary {
cbb715e11963atagen2026-10-24 23:24 UTC363
                account: account.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC364
                revision: boundary,
cbb715e11963atagen2026-10-24 23:24 UTC365
            },
cbb715e11963atagen2026-10-24 23:24 UTC366
        })
cbb715e11963atagen2026-10-24 23:24 UTC367
    }
cbb715e11963atagen2026-10-24 23:24 UTC368
cbb715e11963atagen2026-10-24 23:24 UTC369
    pub fn context(
cbb715e11963atagen2026-10-24 23:24 UTC370
        &self,
cbb715e11963atagen2026-10-24 23:24 UTC371
        key: &MentionKey,
cbb715e11963atagen2026-10-24 23:24 UTC372
    ) -> Result<(SavedMentionContext, Vec<MediaRegistration>), Error> {
cbb715e11963atagen2026-10-24 23:24 UTC373
        let entry = self
cbb715e11963atagen2026-10-24 23:24 UTC374
            .entries
cbb715e11963atagen2026-10-24 23:24 UTC375
            .get(&id(key))
cbb715e11963atagen2026-10-24 23:24 UTC376
            .ok_or_else(|| Error::new(ErrorKind::EventNotFound, "mention was not found"))?;
cbb715e11963atagen2026-10-24 23:24 UTC377
        let mut media = Vec::new();
cbb715e11963atagen2026-10-24 23:24 UTC378
        let mut item = |key: &String| {
cbb715e11963atagen2026-10-24 23:24 UTC379
            self.events.get(key).map(|event| {
cbb715e11963atagen2026-10-24 23:24 UTC380
                media.extend(event.media.clone());
cbb715e11963atagen2026-10-24 23:24 UTC381
                event.item.clone()
cbb715e11963atagen2026-10-24 23:24 UTC382
            })
cbb715e11963atagen2026-10-24 23:24 UTC383
        };
258c08ec2168atagen2026-10-25 09:15 UTC384
        let context = self.windows.get(&id(key)).and_then(|window| {
258c08ec2168atagen2026-10-25 09:15 UTC385
            let event = window.target.as_ref().and_then(&mut item)?;
258c08ec2168atagen2026-10-25 09:15 UTC386
            Some(EventContext {
258c08ec2168atagen2026-10-25 09:15 UTC387
                events_before: window.before.iter().filter_map(&mut item).collect(),
258c08ec2168atagen2026-10-25 09:15 UTC388
                event,
258c08ec2168atagen2026-10-25 09:15 UTC389
                events_after: window.after.iter().filter_map(&mut item).collect(),
258c08ec2168atagen2026-10-25 09:15 UTC390
                start_token: window.start.clone(),
258c08ec2168atagen2026-10-25 09:15 UTC391
                end_token: window.end.clone(),
258c08ec2168atagen2026-10-25 09:15 UTC392
            })
cbb715e11963atagen2026-10-24 23:24 UTC393
        });
cbb715e11963atagen2026-10-24 23:24 UTC394
        Ok((
cbb715e11963atagen2026-10-24 23:24 UTC395
            SavedMentionContext {
cbb715e11963atagen2026-10-24 23:24 UTC396
                key: key.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC397
                state: entry.capture.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC398
                context,
cbb715e11963atagen2026-10-24 23:24 UTC399
                revision: self.revision,
cbb715e11963atagen2026-10-24 23:24 UTC400
            },
cbb715e11963atagen2026-10-24 23:24 UTC401
            media,
cbb715e11963atagen2026-10-24 23:24 UTC402
        ))
cbb715e11963atagen2026-10-24 23:24 UTC403
    }
cbb715e11963atagen2026-10-24 23:24 UTC404
7cdaa86b9489atagen2026-10-25 08:43 UTC405
    pub fn collect_events(&mut self) -> usize {
cbb715e11963atagen2026-10-24 23:24 UTC406
        let mut used: BTreeSet<String> = self.entries.keys().cloned().collect();
cbb715e11963atagen2026-10-24 23:24 UTC407
        for window in self.windows.values() {
cbb715e11963atagen2026-10-24 23:24 UTC408
            used.extend(window.before.iter().chain(&window.after).cloned());
cbb715e11963atagen2026-10-24 23:24 UTC409
            used.extend(window.target.iter().cloned());
cbb715e11963atagen2026-10-24 23:24 UTC410
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC411
        let mut freed = 0;
7cdaa86b9489atagen2026-10-25 08:43 UTC412
        self.events.retain(|key, event| {
7cdaa86b9489atagen2026-10-25 08:43 UTC413
            let keep = used.contains(key);
7cdaa86b9489atagen2026-10-25 08:43 UTC414
            if !keep {
7cdaa86b9489atagen2026-10-25 08:43 UTC415
                freed += key.len() + json_len(event);
7cdaa86b9489atagen2026-10-25 08:43 UTC416
            }
7cdaa86b9489atagen2026-10-25 08:43 UTC417
            keep
7cdaa86b9489atagen2026-10-25 08:43 UTC418
        });
cbb715e11963atagen2026-10-24 23:24 UTC419
        self.capture_attempts
cbb715e11963atagen2026-10-24 23:24 UTC420
            .retain(|key, _| self.entries.contains_key(key));
7cdaa86b9489atagen2026-10-25 08:43 UTC421
        freed
cbb715e11963atagen2026-10-24 23:24 UTC422
    }
cbb715e11963atagen2026-10-24 23:24 UTC423
7cdaa86b9489atagen2026-10-25 08:43 UTC424
    /// returns roughly how many plaintext JSON bytes the eviction freed
7cdaa86b9489atagen2026-10-25 08:43 UTC425
    pub fn evict_one(&mut self) -> Option<usize> {
cbb715e11963atagen2026-10-24 23:24 UTC426
        let oldest = |keys: Vec<String>, entries: &BTreeMap<String, MentionEntry>| {
cbb715e11963atagen2026-10-24 23:24 UTC427
            keys.into_iter()
cbb715e11963atagen2026-10-24 23:24 UTC428
                .min_by_key(|key| entries.get(key).map_or(0, |entry| entry.timestamp_ms))
cbb715e11963atagen2026-10-24 23:24 UTC429
        };
7cdaa86b9489atagen2026-10-25 08:43 UTC430
        let freed = if let Some(key) = oldest(
cbb715e11963atagen2026-10-24 23:24 UTC431
            self.windows
cbb715e11963atagen2026-10-24 23:24 UTC432
                .keys()
cbb715e11963atagen2026-10-24 23:24 UTC433
                .filter(|key| self.entries.get(*key).is_some_and(|entry| entry.read))
cbb715e11963atagen2026-10-24 23:24 UTC434
                .cloned()
cbb715e11963atagen2026-10-24 23:24 UTC435
                .collect(),
cbb715e11963atagen2026-10-24 23:24 UTC436
            &self.entries,
cbb715e11963atagen2026-10-24 23:24 UTC437
        )
cbb715e11963atagen2026-10-24 23:24 UTC438
        .or_else(|| oldest(self.windows.keys().cloned().collect(), &self.entries))
cbb715e11963atagen2026-10-24 23:24 UTC439
        {
7cdaa86b9489atagen2026-10-25 08:43 UTC440
            let window = self.windows.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC441
            if let Some(entry) = self.entries.get_mut(&key) {
cbb715e11963atagen2026-10-24 23:24 UTC442
                entry.capture = MentionCaptureState::Unavailable;
cbb715e11963atagen2026-10-24 23:24 UTC443
                entry.revision = self.revision;
cbb715e11963atagen2026-10-24 23:24 UTC444
            }
7cdaa86b9489atagen2026-10-25 08:43 UTC445
            key.len() + json_len(&window)
cbb715e11963atagen2026-10-24 23:24 UTC446
        } else if let Some(key) = self
cbb715e11963atagen2026-10-24 23:24 UTC447
            .entries
cbb715e11963atagen2026-10-24 23:24 UTC448
            .iter()
cbb715e11963atagen2026-10-24 23:24 UTC449
            .min_by_key(|(_, entry)| (!entry.read, entry.timestamp_ms))
cbb715e11963atagen2026-10-24 23:24 UTC450
            .map(|(key, _)| key.clone())
cbb715e11963atagen2026-10-24 23:24 UTC451
        {
7cdaa86b9489atagen2026-10-25 08:43 UTC452
            key.len() + json_len(&self.entries.remove(&key))
7cdaa86b9489atagen2026-10-25 08:43 UTC453
        } else if let Some(key) = self
7cdaa86b9489atagen2026-10-25 08:43 UTC454
            .candidates
7cdaa86b9489atagen2026-10-25 08:43 UTC455
            .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC456
            .min_by_key(|(_, candidate)| timestamp(&candidate.raw))
7cdaa86b9489atagen2026-10-25 08:43 UTC457
            .map(|(key, _)| key.clone())
7cdaa86b9489atagen2026-10-25 08:43 UTC458
        {
7cdaa86b9489atagen2026-10-25 08:43 UTC459
            key.len() + json_len(&self.candidates.remove(&key))
cbb715e11963atagen2026-10-24 23:24 UTC460
        } else if let Some(key) = self.edits.keys().next().cloned() {
7cdaa86b9489atagen2026-10-25 08:43 UTC461
            key.len() + json_len(&self.edits.remove(&key))
cbb715e11963atagen2026-10-24 23:24 UTC462
        } else {
7cdaa86b9489atagen2026-10-25 08:43 UTC463
            let key = self.redacted.keys().next().cloned()?;
7cdaa86b9489atagen2026-10-25 08:43 UTC464
            self.redacted.remove(&key);
7cdaa86b9489atagen2026-10-25 08:43 UTC465
            key.len() + 24
7cdaa86b9489atagen2026-10-25 08:43 UTC466
        };
cbb715e11963atagen2026-10-24 23:24 UTC467
        self.coverage.truncated = true;
7cdaa86b9489atagen2026-10-25 08:43 UTC468
        Some(freed + self.collect_events())
cbb715e11963atagen2026-10-24 23:24 UTC469
    }
cbb715e11963atagen2026-10-24 23:24 UTC470
cbb715e11963atagen2026-10-24 23:24 UTC471
    pub fn bound(&mut self, entries: usize, events: usize, candidates: usize) {
cbb715e11963atagen2026-10-24 23:24 UTC472
        while self.entries.len() > entries {
cbb715e11963atagen2026-10-24 23:24 UTC473
            if let Some(key) = self
cbb715e11963atagen2026-10-24 23:24 UTC474
                .entries
cbb715e11963atagen2026-10-24 23:24 UTC475
                .iter()
cbb715e11963atagen2026-10-24 23:24 UTC476
                .min_by_key(|(_, entry)| (!entry.read, entry.timestamp_ms))
cbb715e11963atagen2026-10-24 23:24 UTC477
                .map(|(key, _)| key.clone())
cbb715e11963atagen2026-10-24 23:24 UTC478
            {
cbb715e11963atagen2026-10-24 23:24 UTC479
                self.entries.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC480
                self.windows.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC481
                self.coverage.truncated = true;
cbb715e11963atagen2026-10-24 23:24 UTC482
            }
cbb715e11963atagen2026-10-24 23:24 UTC483
        }
cbb715e11963atagen2026-10-24 23:24 UTC484
        self.collect_events();
7cdaa86b9489atagen2026-10-25 08:43 UTC485
        while self.events.len() > events && self.evict_one().is_some() {}
7cdaa86b9489atagen2026-10-25 08:43 UTC486
        if self.candidates.len() > candidates {
7cdaa86b9489atagen2026-10-25 08:43 UTC487
            let mut oldest: Vec<_> = self
7cdaa86b9489atagen2026-10-25 08:43 UTC488
                .candidates
7cdaa86b9489atagen2026-10-25 08:43 UTC489
                .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC490
                .map(|(key, candidate)| (timestamp(&candidate.raw), key.clone()))
7cdaa86b9489atagen2026-10-25 08:43 UTC491
                .collect();
7cdaa86b9489atagen2026-10-25 08:43 UTC492
            oldest.sort();
7cdaa86b9489atagen2026-10-25 08:43 UTC493
            for (_, key) in oldest.into_iter().take(self.candidates.len() - candidates) {
7cdaa86b9489atagen2026-10-25 08:43 UTC494
                self.candidates.remove(&key);
7cdaa86b9489atagen2026-10-25 08:43 UTC495
            }
cbb715e11963atagen2026-10-24 23:24 UTC496
            self.coverage.truncated = true;
cbb715e11963atagen2026-10-24 23:24 UTC497
        }
cbb715e11963atagen2026-10-24 23:24 UTC498
        for map in [&mut self.redacted] {
cbb715e11963atagen2026-10-24 23:24 UTC499
            while map.len() > entries + candidates {
cbb715e11963atagen2026-10-24 23:24 UTC500
                let key = map
cbb715e11963atagen2026-10-24 23:24 UTC501
                    .iter()
cbb715e11963atagen2026-10-24 23:24 UTC502
                    .min_by_key(|(_, timestamp)| *timestamp)
cbb715e11963atagen2026-10-24 23:24 UTC503
                    .map(|(key, _)| key.clone())
cbb715e11963atagen2026-10-24 23:24 UTC504
                    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC505
                map.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC506
                self.coverage.truncated = true;
cbb715e11963atagen2026-10-24 23:24 UTC507
            }
cbb715e11963atagen2026-10-24 23:24 UTC508
        }
cbb715e11963atagen2026-10-24 23:24 UTC509
        while self.edits.len() > candidates {
cbb715e11963atagen2026-10-24 23:24 UTC510
            let key = self.edits.keys().next().cloned().unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC511
            self.edits.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC512
            self.coverage.truncated = true;
cbb715e11963atagen2026-10-24 23:24 UTC513
        }
cbb715e11963atagen2026-10-24 23:24 UTC514
        self.coverage.unresolved_candidates = self.candidates.len();
cbb715e11963atagen2026-10-24 23:24 UTC515
    }
cbb715e11963atagen2026-10-24 23:24 UTC516
}