atagen

macltok

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

Blame: src/mentions/service.rs

cbb715e11963atagen2026-10-24 23:24 UTC1
use std::{
cbb715e11963atagen2026-10-24 23:24 UTC2
    collections::{BTreeMap, BTreeSet},
cbb715e11963atagen2026-10-24 23:24 UTC3
    sync::Arc,
cbb715e11963atagen2026-10-24 23:24 UTC4
    time::Duration,
cbb715e11963atagen2026-10-24 23:24 UTC5
};
cbb715e11963atagen2026-10-24 23:24 UTC6
cbb715e11963atagen2026-10-24 23:24 UTC7
use futures_util::{StreamExt, stream::FuturesUnordered};
cbb715e11963atagen2026-10-24 23:24 UTC8
use matrix_sdk::{
cbb715e11963atagen2026-10-24 23:24 UTC9
    Client, RoomState,
cbb715e11963atagen2026-10-24 23:24 UTC10
    sliding_sync::{ResponseCommitBatch, ResponseCommitHook},
cbb715e11963atagen2026-10-24 23:24 UTC11
};
cbb715e11963atagen2026-10-24 23:24 UTC12
use ruma::{
80381e06a573atagen2026-10-25 03:02 UTC13
    OwnedEventId, OwnedRoomId, OwnedUserId,
cbb715e11963atagen2026-10-24 23:24 UTC14
    events::{AnySyncTimelineEvent, room::encrypted::OriginalSyncRoomEncryptedEvent},
cbb715e11963atagen2026-10-24 23:24 UTC15
    serde::Raw,
cbb715e11963atagen2026-10-24 23:24 UTC16
};
cbb715e11963atagen2026-10-24 23:24 UTC17
use serde_json::Value;
cbb715e11963atagen2026-10-24 23:24 UTC18
use tokio::sync::{mpsc, oneshot, watch};
cbb715e11963atagen2026-10-24 23:24 UTC19
80381e06a573atagen2026-10-25 03:02 UTC20
use super::state::{Candidate, Data, RepairCoverage, SavedEvent, Window, id, invalid, now};
cbb715e11963atagen2026-10-24 23:24 UTC21
use crate::{
80381e06a573atagen2026-10-25 03:02 UTC22
    CommandOutput, EngineConfig, Error, ErrorKind, Event, HistoryRepairPolicy, MentionCaptureState,
80381e06a573atagen2026-10-25 03:02 UTC23
    MentionEntry, MentionGap, MentionKey, MentionReason, MentionsCommand, MentionsPage,
80381e06a573atagen2026-10-25 03:02 UTC24
    MentionsUpdate, TimelineContent, TimelineItem,
cbb715e11963atagen2026-10-24 23:24 UTC25
    engine::{MediaService, MentionsStore, context, executor::EngineBoxFuture},
cbb715e11963atagen2026-10-24 23:24 UTC26
    normalise::{MediaRegistration, normalise_observed},
cbb715e11963atagen2026-10-24 23:24 UTC27
    transport::EventSender,
cbb715e11963atagen2026-10-24 23:24 UTC28
};
cbb715e11963atagen2026-10-24 23:24 UTC29
cbb715e11963atagen2026-10-24 23:24 UTC30
#[cfg(all(test, feature = "sqlite"))]
cbb715e11963atagen2026-10-24 23:24 UTC31
#[path = "service_tests.rs"]
cbb715e11963atagen2026-10-24 23:24 UTC32
mod tests;
cbb715e11963atagen2026-10-24 23:24 UTC33
cbb715e11963atagen2026-10-24 23:24 UTC34
#[derive(Clone)]
cbb715e11963atagen2026-10-24 23:24 UTC35
pub(crate) struct Handle(mpsc::Sender<Request>);
cbb715e11963atagen2026-10-24 23:24 UTC36
cbb715e11963atagen2026-10-24 23:24 UTC37
enum Request {
cbb715e11963atagen2026-10-24 23:24 UTC38
    Batch(ResponseCommitBatch, oneshot::Sender<Result<(), Error>>),
cbb715e11963atagen2026-10-24 23:24 UTC39
    Command(
cbb715e11963atagen2026-10-24 23:24 UTC40
        MentionsCommand,
cbb715e11963atagen2026-10-24 23:24 UTC41
        oneshot::Sender<Result<CommandOutput, Error>>,
cbb715e11963atagen2026-10-24 23:24 UTC42
    ),
80381e06a573atagen2026-10-25 03:02 UTC43
    GetRepairPolicy(oneshot::Sender<Result<HistoryRepairPolicy, Error>>),
80381e06a573atagen2026-10-25 03:02 UTC44
    SetRepairPolicy(HistoryRepairPolicy, oneshot::Sender<Result<(), Error>>),
80381e06a573atagen2026-10-25 03:02 UTC45
    GetRepairCoverage(OwnedRoomId, oneshot::Sender<Result<RepairCoverage, Error>>),
80381e06a573atagen2026-10-25 03:02 UTC46
    RepairResolved(OwnedRoomId, oneshot::Sender<Result<(), Error>>),
cbb715e11963atagen2026-10-24 23:24 UTC47
    Stop(oneshot::Sender<()>),
cbb715e11963atagen2026-10-24 23:24 UTC48
}
cbb715e11963atagen2026-10-24 23:24 UTC49
cbb715e11963atagen2026-10-24 23:24 UTC50
pub(crate) struct Service {
cbb715e11963atagen2026-10-24 23:24 UTC51
    client: Client,
cbb715e11963atagen2026-10-24 23:24 UTC52
    account: OwnedUserId,
cbb715e11963atagen2026-10-24 23:24 UTC53
    media: Arc<MediaService>,
cbb715e11963atagen2026-10-24 23:24 UTC54
    store: Option<MentionsStore>,
cbb715e11963atagen2026-10-24 23:24 UTC55
    data: Data,
cbb715e11963atagen2026-10-24 23:24 UTC56
    config: EngineConfig,
cbb715e11963atagen2026-10-24 23:24 UTC57
    requests: mpsc::Receiver<Request>,
cbb715e11963atagen2026-10-24 23:24 UTC58
    updates: watch::Sender<MentionsPage>,
cbb715e11963atagen2026-10-24 23:24 UTC59
    recovery_budget: usize,
cbb715e11963atagen2026-10-24 23:24 UTC60
    latest_scan: bool,
cbb715e11963atagen2026-10-24 23:24 UTC61
    recovery_retry: u64,
cbb715e11963atagen2026-10-24 23:24 UTC62
}
cbb715e11963atagen2026-10-24 23:24 UTC63
cbb715e11963atagen2026-10-24 23:24 UTC64
enum Work {
cbb715e11963atagen2026-10-24 23:24 UTC65
    Capture(
cbb715e11963atagen2026-10-24 23:24 UTC66
        String,
cbb715e11963atagen2026-10-24 23:24 UTC67
        u64,
cbb715e11963atagen2026-10-24 23:24 UTC68
        Result<(crate::EventContext, Vec<MediaRegistration>, Vec<Value>), Error>,
cbb715e11963atagen2026-10-24 23:24 UTC69
    ),
cbb715e11963atagen2026-10-24 23:24 UTC70
    Candidate(String, Result<Value, Error>),
cbb715e11963atagen2026-10-24 23:24 UTC71
    History(
cbb715e11963atagen2026-10-24 23:24 UTC72
        bool,
cbb715e11963atagen2026-10-24 23:24 UTC73
        Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC74
        Result<ruma::api::client::push::get_notifications::v3::Response, Error>,
cbb715e11963atagen2026-10-24 23:24 UTC75
    ),
cbb715e11963atagen2026-10-24 23:24 UTC76
}
cbb715e11963atagen2026-10-24 23:24 UTC77
cbb715e11963atagen2026-10-24 23:24 UTC78
impl Handle {
cbb715e11963atagen2026-10-24 23:24 UTC79
    pub(crate) fn hook(&self) -> ResponseCommitHook {
cbb715e11963atagen2026-10-24 23:24 UTC80
        let handle = self.clone();
cbb715e11963atagen2026-10-24 23:24 UTC81
        ResponseCommitHook::new(move |batch| {
cbb715e11963atagen2026-10-24 23:24 UTC82
            let handle = handle.clone();
cbb715e11963atagen2026-10-24 23:24 UTC83
            async move {
cbb715e11963atagen2026-10-24 23:24 UTC84
                let (reply, response) = oneshot::channel();
cbb715e11963atagen2026-10-24 23:24 UTC85
                handle
cbb715e11963atagen2026-10-24 23:24 UTC86
                    .0
cbb715e11963atagen2026-10-24 23:24 UTC87
                    .send(Request::Batch(batch, reply))
cbb715e11963atagen2026-10-24 23:24 UTC88
                    .await
cbb715e11963atagen2026-10-24 23:24 UTC89
                    .map_err(|_| "mentions service stopped".to_owned())?;
cbb715e11963atagen2026-10-24 23:24 UTC90
                response
cbb715e11963atagen2026-10-24 23:24 UTC91
                    .await
cbb715e11963atagen2026-10-24 23:24 UTC92
                    .map_err(|_| "mentions commit acknowledgement dropped".to_owned())?
cbb715e11963atagen2026-10-24 23:24 UTC93
                    .map_err(|error| error.to_string())
cbb715e11963atagen2026-10-24 23:24 UTC94
            }
cbb715e11963atagen2026-10-24 23:24 UTC95
        })
cbb715e11963atagen2026-10-24 23:24 UTC96
    }
cbb715e11963atagen2026-10-24 23:24 UTC97
cbb715e11963atagen2026-10-24 23:24 UTC98
    pub(crate) async fn execute(&self, command: MentionsCommand) -> Result<CommandOutput, Error> {
cbb715e11963atagen2026-10-24 23:24 UTC99
        let (reply, response) = oneshot::channel();
cbb715e11963atagen2026-10-24 23:24 UTC100
        self.0
cbb715e11963atagen2026-10-24 23:24 UTC101
            .send(Request::Command(command, reply))
cbb715e11963atagen2026-10-24 23:24 UTC102
            .await
cbb715e11963atagen2026-10-24 23:24 UTC103
            .map_err(|_| Error::interrupted())?;
cbb715e11963atagen2026-10-24 23:24 UTC104
        response.await.map_err(|_| Error::interrupted())?
cbb715e11963atagen2026-10-24 23:24 UTC105
    }
cbb715e11963atagen2026-10-24 23:24 UTC106
80381e06a573atagen2026-10-25 03:02 UTC107
    pub(crate) async fn repair_policy(&self) -> Result<HistoryRepairPolicy, Error> {
80381e06a573atagen2026-10-25 03:02 UTC108
        let (reply, response) = oneshot::channel();
80381e06a573atagen2026-10-25 03:02 UTC109
        self.0
80381e06a573atagen2026-10-25 03:02 UTC110
            .send(Request::GetRepairPolicy(reply))
80381e06a573atagen2026-10-25 03:02 UTC111
            .await
80381e06a573atagen2026-10-25 03:02 UTC112
            .map_err(|_| Error::interrupted())?;
80381e06a573atagen2026-10-25 03:02 UTC113
        response.await.map_err(|_| Error::interrupted())?
80381e06a573atagen2026-10-25 03:02 UTC114
    }
80381e06a573atagen2026-10-25 03:02 UTC115
80381e06a573atagen2026-10-25 03:02 UTC116
    pub(crate) async fn set_repair_policy(&self, policy: HistoryRepairPolicy) -> Result<(), Error> {
80381e06a573atagen2026-10-25 03:02 UTC117
        let (reply, response) = oneshot::channel();
80381e06a573atagen2026-10-25 03:02 UTC118
        self.0
80381e06a573atagen2026-10-25 03:02 UTC119
            .send(Request::SetRepairPolicy(policy, reply))
80381e06a573atagen2026-10-25 03:02 UTC120
            .await
80381e06a573atagen2026-10-25 03:02 UTC121
            .map_err(|_| Error::interrupted())?;
80381e06a573atagen2026-10-25 03:02 UTC122
        response.await.map_err(|_| Error::interrupted())?
80381e06a573atagen2026-10-25 03:02 UTC123
    }
80381e06a573atagen2026-10-25 03:02 UTC124
80381e06a573atagen2026-10-25 03:02 UTC125
    pub(crate) async fn repair_coverage(
80381e06a573atagen2026-10-25 03:02 UTC126
        &self,
80381e06a573atagen2026-10-25 03:02 UTC127
        room_id: OwnedRoomId,
80381e06a573atagen2026-10-25 03:02 UTC128
    ) -> Result<RepairCoverage, Error> {
80381e06a573atagen2026-10-25 03:02 UTC129
        let (reply, response) = oneshot::channel();
80381e06a573atagen2026-10-25 03:02 UTC130
        self.0
80381e06a573atagen2026-10-25 03:02 UTC131
            .send(Request::GetRepairCoverage(room_id, reply))
80381e06a573atagen2026-10-25 03:02 UTC132
            .await
80381e06a573atagen2026-10-25 03:02 UTC133
            .map_err(|_| Error::interrupted())?;
80381e06a573atagen2026-10-25 03:02 UTC134
        response.await.map_err(|_| Error::interrupted())?
80381e06a573atagen2026-10-25 03:02 UTC135
    }
80381e06a573atagen2026-10-25 03:02 UTC136
80381e06a573atagen2026-10-25 03:02 UTC137
    pub(crate) async fn repair_resolved(&self, room_id: OwnedRoomId) -> Result<(), Error> {
80381e06a573atagen2026-10-25 03:02 UTC138
        let (reply, response) = oneshot::channel();
80381e06a573atagen2026-10-25 03:02 UTC139
        self.0
80381e06a573atagen2026-10-25 03:02 UTC140
            .send(Request::RepairResolved(room_id, reply))
80381e06a573atagen2026-10-25 03:02 UTC141
            .await
80381e06a573atagen2026-10-25 03:02 UTC142
            .map_err(|_| Error::interrupted())?;
80381e06a573atagen2026-10-25 03:02 UTC143
        response.await.map_err(|_| Error::interrupted())?
80381e06a573atagen2026-10-25 03:02 UTC144
    }
80381e06a573atagen2026-10-25 03:02 UTC145
cbb715e11963atagen2026-10-24 23:24 UTC146
    pub(crate) async fn stop(&self) {
cbb715e11963atagen2026-10-24 23:24 UTC147
        let (reply, response) = oneshot::channel();
cbb715e11963atagen2026-10-24 23:24 UTC148
        if self.0.send(Request::Stop(reply)).await.is_ok() {
cbb715e11963atagen2026-10-24 23:24 UTC149
            let _ = response.await;
cbb715e11963atagen2026-10-24 23:24 UTC150
        }
cbb715e11963atagen2026-10-24 23:24 UTC151
    }
cbb715e11963atagen2026-10-24 23:24 UTC152
}
cbb715e11963atagen2026-10-24 23:24 UTC153
cbb715e11963atagen2026-10-24 23:24 UTC154
impl Service {
cbb715e11963atagen2026-10-24 23:24 UTC155
    pub(crate) async fn new(
cbb715e11963atagen2026-10-24 23:24 UTC156
        client: Client,
cbb715e11963atagen2026-10-24 23:24 UTC157
        media: Arc<MediaService>,
cbb715e11963atagen2026-10-24 23:24 UTC158
        store: Option<MentionsStore>,
cbb715e11963atagen2026-10-24 23:24 UTC159
        config: EngineConfig,
cbb715e11963atagen2026-10-24 23:24 UTC160
    ) -> Result<(Handle, Self, watch::Receiver<MentionsPage>), Error> {
cbb715e11963atagen2026-10-24 23:24 UTC161
        let account = client
cbb715e11963atagen2026-10-24 23:24 UTC162
            .user_id()
cbb715e11963atagen2026-10-24 23:24 UTC163
            .ok_or_else(|| {
cbb715e11963atagen2026-10-24 23:24 UTC164
                Error::new(
cbb715e11963atagen2026-10-24 23:24 UTC165
                    ErrorKind::Unauthenticated,
cbb715e11963atagen2026-10-24 23:24 UTC166
                    "mentions require an authenticated account",
cbb715e11963atagen2026-10-24 23:24 UTC167
                )
cbb715e11963atagen2026-10-24 23:24 UTC168
            })?
cbb715e11963atagen2026-10-24 23:24 UTC169
            .to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC170
        #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
cbb715e11963atagen2026-10-24 23:24 UTC171
        let data: Data = store
cbb715e11963atagen2026-10-24 23:24 UTC172
            .as_ref()
cbb715e11963atagen2026-10-24 23:24 UTC173
            .map(|store| store.read("inbox"))
cbb715e11963atagen2026-10-24 23:24 UTC174
            .transpose()?
cbb715e11963atagen2026-10-24 23:24 UTC175
            .flatten()
cbb715e11963atagen2026-10-24 23:24 UTC176
            .unwrap_or_default();
cbb715e11963atagen2026-10-24 23:24 UTC177
        #[cfg(not(any(feature = "sqlite", feature = "indexeddb")))]
cbb715e11963atagen2026-10-24 23:24 UTC178
        let data = Data::default();
cbb715e11963atagen2026-10-24 23:24 UTC179
        #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
cbb715e11963atagen2026-10-24 23:24 UTC180
        if store
cbb715e11963atagen2026-10-24 23:24 UTC181
            .as_ref()
cbb715e11963atagen2026-10-24 23:24 UTC182
            .is_some_and(|store| store.revision() != data.revision)
cbb715e11963atagen2026-10-24 23:24 UTC183
        {
cbb715e11963atagen2026-10-24 23:24 UTC184
            return Err(Error::new(
cbb715e11963atagen2026-10-24 23:24 UTC185
                ErrorKind::Store,
cbb715e11963atagen2026-10-24 23:24 UTC186
                "mentions revision does not match its checkpoint",
cbb715e11963atagen2026-10-24 23:24 UTC187
            ));
cbb715e11963atagen2026-10-24 23:24 UTC188
        }
cbb715e11963atagen2026-10-24 23:24 UTC189
        let page = data.page(&account, None, 50)?;
cbb715e11963atagen2026-10-24 23:24 UTC190
        media
cbb715e11963atagen2026-10-24 23:24 UTC191
            .register(
cbb715e11963atagen2026-10-24 23:24 UTC192
                page.entries
cbb715e11963atagen2026-10-24 23:24 UTC193
                    .iter()
cbb715e11963atagen2026-10-24 23:24 UTC194
                    .filter_map(|entry| data.events.get(&id(&entry.key)))
cbb715e11963atagen2026-10-24 23:24 UTC195
                    .flat_map(|event| event.media.clone())
cbb715e11963atagen2026-10-24 23:24 UTC196
                    .collect(),
cbb715e11963atagen2026-10-24 23:24 UTC197
            )
cbb715e11963atagen2026-10-24 23:24 UTC198
            .await;
cbb715e11963atagen2026-10-24 23:24 UTC199
        let (updates, receiver) = watch::channel(page);
cbb715e11963atagen2026-10-24 23:24 UTC200
        let (sender, requests) = mpsc::channel(config.command_capacity.max(1));
cbb715e11963atagen2026-10-24 23:24 UTC201
        Ok((
cbb715e11963atagen2026-10-24 23:24 UTC202
            Handle(sender),
cbb715e11963atagen2026-10-24 23:24 UTC203
            Self {
cbb715e11963atagen2026-10-24 23:24 UTC204
                client,
cbb715e11963atagen2026-10-24 23:24 UTC205
                account,
cbb715e11963atagen2026-10-24 23:24 UTC206
                media,
cbb715e11963atagen2026-10-24 23:24 UTC207
                store,
cbb715e11963atagen2026-10-24 23:24 UTC208
                data,
cbb715e11963atagen2026-10-24 23:24 UTC209
                config,
cbb715e11963atagen2026-10-24 23:24 UTC210
                requests,
cbb715e11963atagen2026-10-24 23:24 UTC211
                updates,
cbb715e11963atagen2026-10-24 23:24 UTC212
                recovery_budget: 20,
cbb715e11963atagen2026-10-24 23:24 UTC213
                latest_scan: true,
cbb715e11963atagen2026-10-24 23:24 UTC214
                recovery_retry: 0,
cbb715e11963atagen2026-10-24 23:24 UTC215
            },
cbb715e11963atagen2026-10-24 23:24 UTC216
            receiver,
cbb715e11963atagen2026-10-24 23:24 UTC217
        ))
cbb715e11963atagen2026-10-24 23:24 UTC218
    }
cbb715e11963atagen2026-10-24 23:24 UTC219
cbb715e11963atagen2026-10-24 23:24 UTC220
    async fn save(&mut self, mut next: Data) -> Result<(), Error> {
cbb715e11963atagen2026-10-24 23:24 UTC221
        next.revision = self
cbb715e11963atagen2026-10-24 23:24 UTC222
            .data
cbb715e11963atagen2026-10-24 23:24 UTC223
            .revision
cbb715e11963atagen2026-10-24 23:24 UTC224
            .checked_add(1)
cbb715e11963atagen2026-10-24 23:24 UTC225
            .ok_or_else(|| invalid("mentions revision exhausted"))?;
cbb715e11963atagen2026-10-24 23:24 UTC226
        next.bound(
cbb715e11963atagen2026-10-24 23:24 UTC227
            self.config.max_mentions_entries,
cbb715e11963atagen2026-10-24 23:24 UTC228
            self.config.max_mention_context_events,
cbb715e11963atagen2026-10-24 23:24 UTC229
            self.config.max_mention_candidates,
cbb715e11963atagen2026-10-24 23:24 UTC230
        );
cbb715e11963atagen2026-10-24 23:24 UTC231
        #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
cbb715e11963atagen2026-10-24 23:24 UTC232
        if let Some(store) = &mut self.store {
cbb715e11963atagen2026-10-24 23:24 UTC233
            loop {
cbb715e11963atagen2026-10-24 23:24 UTC234
                let value = serde_json::to_value(&next).map_err(|error| {
cbb715e11963atagen2026-10-24 23:24 UTC235
                    Error::with_source(
cbb715e11963atagen2026-10-24 23:24 UTC236
                        ErrorKind::Store,
cbb715e11963atagen2026-10-24 23:24 UTC237
                        "encoding the mentions inbox failed",
cbb715e11963atagen2026-10-24 23:24 UTC238
                        error,
cbb715e11963atagen2026-10-24 23:24 UTC239
                    )
cbb715e11963atagen2026-10-24 23:24 UTC240
                })?;
cbb715e11963atagen2026-10-24 23:24 UTC241
                match store
cbb715e11963atagen2026-10-24 23:24 UTC242
                    .commit(
cbb715e11963atagen2026-10-24 23:24 UTC243
                        self.data.revision,
cbb715e11963atagen2026-10-24 23:24 UTC244
                        vec![super::store::Write::Set("inbox".to_owned(), value)],
cbb715e11963atagen2026-10-24 23:24 UTC245
                    )
cbb715e11963atagen2026-10-24 23:24 UTC246
                    .await
cbb715e11963atagen2026-10-24 23:24 UTC247
                {
cbb715e11963atagen2026-10-24 23:24 UTC248
                    Ok(_) => break,
cbb715e11963atagen2026-10-24 23:24 UTC249
                    Err(error)
cbb715e11963atagen2026-10-24 23:24 UTC250
                        if error.message == "mentions storage limit exceeded"
cbb715e11963atagen2026-10-24 23:24 UTC251
                            && next.evict_one() => {}
cbb715e11963atagen2026-10-24 23:24 UTC252
                    Err(error) => return Err(error),
cbb715e11963atagen2026-10-24 23:24 UTC253
                }
cbb715e11963atagen2026-10-24 23:24 UTC254
            }
cbb715e11963atagen2026-10-24 23:24 UTC255
        }
cbb715e11963atagen2026-10-24 23:24 UTC256
        self.data = next;
cbb715e11963atagen2026-10-24 23:24 UTC257
        let page = self.data.page(&self.account, None, 50)?;
cbb715e11963atagen2026-10-24 23:24 UTC258
        self.media
cbb715e11963atagen2026-10-24 23:24 UTC259
            .register(
cbb715e11963atagen2026-10-24 23:24 UTC260
                page.entries
cbb715e11963atagen2026-10-24 23:24 UTC261
                    .iter()
cbb715e11963atagen2026-10-24 23:24 UTC262
                    .filter_map(|entry| self.data.events.get(&id(&entry.key)))
cbb715e11963atagen2026-10-24 23:24 UTC263
                    .flat_map(|event| event.media.clone())
cbb715e11963atagen2026-10-24 23:24 UTC264
                    .collect(),
cbb715e11963atagen2026-10-24 23:24 UTC265
            )
cbb715e11963atagen2026-10-24 23:24 UTC266
            .await;
cbb715e11963atagen2026-10-24 23:24 UTC267
        self.updates.send_replace(page);
cbb715e11963atagen2026-10-24 23:24 UTC268
        Ok(())
cbb715e11963atagen2026-10-24 23:24 UTC269
    }
cbb715e11963atagen2026-10-24 23:24 UTC270
80381e06a573atagen2026-10-25 03:02 UTC271
    async fn set_repair_policy(&mut self, policy: HistoryRepairPolicy) -> Result<(), Error> {
80381e06a573atagen2026-10-25 03:02 UTC272
        let mut next = self.data.clone();
80381e06a573atagen2026-10-25 03:02 UTC273
        next.repair_policy = policy;
80381e06a573atagen2026-10-25 03:02 UTC274
        self.save(next).await
80381e06a573atagen2026-10-25 03:02 UTC275
    }
80381e06a573atagen2026-10-25 03:02 UTC276
80381e06a573atagen2026-10-25 03:02 UTC277
    async fn repair_resolved(&mut self, room_id: OwnedRoomId) -> Result<(), Error> {
80381e06a573atagen2026-10-25 03:02 UTC278
        let mut next = self.data.clone();
80381e06a573atagen2026-10-25 03:02 UTC279
        let coverage = next.repair_coverage.entry(room_id.clone()).or_default();
80381e06a573atagen2026-10-25 03:02 UTC280
        coverage.boundary_ms = None;
80381e06a573atagen2026-10-25 03:02 UTC281
        coverage.last_confirmed_ms = Some(now());
80381e06a573atagen2026-10-25 03:02 UTC282
        coverage.reliable = false;
80381e06a573atagen2026-10-25 03:02 UTC283
        coverage.unresolved = false;
80381e06a573atagen2026-10-25 03:02 UTC284
        next.observed_rooms.insert(room_id);
80381e06a573atagen2026-10-25 03:02 UTC285
        self.save(next).await
80381e06a573atagen2026-10-25 03:02 UTC286
    }
80381e06a573atagen2026-10-25 03:02 UTC287
cbb715e11963atagen2026-10-24 23:24 UTC288
    async fn command(&mut self, command: MentionsCommand) -> Result<CommandOutput, Error> {
cbb715e11963atagen2026-10-24 23:24 UTC289
        match command {
cbb715e11963atagen2026-10-24 23:24 UTC290
            MentionsCommand::Page { cursor, limit } => {
cbb715e11963atagen2026-10-24 23:24 UTC291
                let page = self.data.page(&self.account, cursor.as_deref(), limit)?;
cbb715e11963atagen2026-10-24 23:24 UTC292
                self.media
cbb715e11963atagen2026-10-24 23:24 UTC293
                    .register(
cbb715e11963atagen2026-10-24 23:24 UTC294
                        page.entries
cbb715e11963atagen2026-10-24 23:24 UTC295
                            .iter()
cbb715e11963atagen2026-10-24 23:24 UTC296
                            .filter_map(|entry| self.data.events.get(&id(&entry.key)))
cbb715e11963atagen2026-10-24 23:24 UTC297
                            .flat_map(|event| event.media.clone())
cbb715e11963atagen2026-10-24 23:24 UTC298
                            .collect(),
cbb715e11963atagen2026-10-24 23:24 UTC299
                    )
cbb715e11963atagen2026-10-24 23:24 UTC300
                    .await;
cbb715e11963atagen2026-10-24 23:24 UTC301
                Ok(CommandOutput::MentionsPage(page))
cbb715e11963atagen2026-10-24 23:24 UTC302
            }
cbb715e11963atagen2026-10-24 23:24 UTC303
            MentionsCommand::Context(key) => {
cbb715e11963atagen2026-10-24 23:24 UTC304
                let (context, media) = self.data.context(&key)?;
cbb715e11963atagen2026-10-24 23:24 UTC305
                self.media.register(media).await;
cbb715e11963atagen2026-10-24 23:24 UTC306
                Ok(CommandOutput::MentionContext(context))
cbb715e11963atagen2026-10-24 23:24 UTC307
            }
cbb715e11963atagen2026-10-24 23:24 UTC308
            MentionsCommand::RetryContext(key) => {
cbb715e11963atagen2026-10-24 23:24 UTC309
                let room = self
cbb715e11963atagen2026-10-24 23:24 UTC310
                    .client
cbb715e11963atagen2026-10-24 23:24 UTC311
                    .get_room(&key.room_id)
cbb715e11963atagen2026-10-24 23:24 UTC312
                    .filter(|room| room.state() == RoomState::Joined)
cbb715e11963atagen2026-10-24 23:24 UTC313
                    .ok_or_else(|| {
cbb715e11963atagen2026-10-24 23:24 UTC314
                        Error::new(
cbb715e11963atagen2026-10-24 23:24 UTC315
                            ErrorKind::Forbidden,
cbb715e11963atagen2026-10-24 23:24 UTC316
                            "saved context cannot be refetched after leaving the room",
cbb715e11963atagen2026-10-24 23:24 UTC317
                        )
cbb715e11963atagen2026-10-24 23:24 UTC318
                    })?;
cbb715e11963atagen2026-10-24 23:24 UTC319
                drop(room);
cbb715e11963atagen2026-10-24 23:24 UTC320
                let mut next = self.data.clone();
cbb715e11963atagen2026-10-24 23:24 UTC321
                let entry = next
cbb715e11963atagen2026-10-24 23:24 UTC322
                    .entries
cbb715e11963atagen2026-10-24 23:24 UTC323
                    .get_mut(&id(&key))
cbb715e11963atagen2026-10-24 23:24 UTC324
                    .ok_or_else(|| Error::new(ErrorKind::EventNotFound, "mention was not found"))?;
cbb715e11963atagen2026-10-24 23:24 UTC325
                entry.capture = MentionCaptureState::Pending;
cbb715e11963atagen2026-10-24 23:24 UTC326
                entry.revision = self.data.revision + 1;
cbb715e11963atagen2026-10-24 23:24 UTC327
                next.windows.entry(id(&key)).or_insert(Window {
cbb715e11963atagen2026-10-24 23:24 UTC328
                    before: Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC329
                    target: Some(id(&key)),
cbb715e11963atagen2026-10-24 23:24 UTC330
                    after: Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC331
                    start: None,
cbb715e11963atagen2026-10-24 23:24 UTC332
                    end: None,
cbb715e11963atagen2026-10-24 23:24 UTC333
                });
cbb715e11963atagen2026-10-24 23:24 UTC334
                self.save(next).await?;
cbb715e11963atagen2026-10-24 23:24 UTC335
                let (context, media) = self.data.context(&key)?;
cbb715e11963atagen2026-10-24 23:24 UTC336
                self.media.register(media).await;
cbb715e11963atagen2026-10-24 23:24 UTC337
                Ok(CommandOutput::MentionContext(context))
cbb715e11963atagen2026-10-24 23:24 UTC338
            }
cbb715e11963atagen2026-10-24 23:24 UTC339
            MentionsCommand::SetRead { keys, read } => {
cbb715e11963atagen2026-10-24 23:24 UTC340
                let mut next = self.data.clone();
cbb715e11963atagen2026-10-24 23:24 UTC341
                for key in keys {
cbb715e11963atagen2026-10-24 23:24 UTC342
                    if let Some(entry) = next.entries.get_mut(&id(&key)) {
cbb715e11963atagen2026-10-24 23:24 UTC343
                        entry.read = read;
cbb715e11963atagen2026-10-24 23:24 UTC344
                        entry.revision = self.data.revision + 1;
cbb715e11963atagen2026-10-24 23:24 UTC345
                    }
cbb715e11963atagen2026-10-24 23:24 UTC346
                }
cbb715e11963atagen2026-10-24 23:24 UTC347
                self.save(next).await?;
cbb715e11963atagen2026-10-24 23:24 UTC348
                Ok(CommandOutput::Done)
cbb715e11963atagen2026-10-24 23:24 UTC349
            }
cbb715e11963atagen2026-10-24 23:24 UTC350
            MentionsCommand::ReadThrough(boundary) => {
cbb715e11963atagen2026-10-24 23:24 UTC351
                if boundary.account != self.account || boundary.revision > self.data.revision {
cbb715e11963atagen2026-10-24 23:24 UTC352
                    return Err(invalid("invalid mentions read boundary"));
cbb715e11963atagen2026-10-24 23:24 UTC353
                }
cbb715e11963atagen2026-10-24 23:24 UTC354
                let mut next = self.data.clone();
cbb715e11963atagen2026-10-24 23:24 UTC355
                for entry in next
cbb715e11963atagen2026-10-24 23:24 UTC356
                    .entries
cbb715e11963atagen2026-10-24 23:24 UTC357
                    .values_mut()
cbb715e11963atagen2026-10-24 23:24 UTC358
                    .filter(|entry| entry.discovery_revision <= boundary.revision)
cbb715e11963atagen2026-10-24 23:24 UTC359
                {
cbb715e11963atagen2026-10-24 23:24 UTC360
                    entry.read = true;
cbb715e11963atagen2026-10-24 23:24 UTC361
                    entry.revision = self.data.revision + 1;
cbb715e11963atagen2026-10-24 23:24 UTC362
                }
cbb715e11963atagen2026-10-24 23:24 UTC363
                self.save(next).await?;
cbb715e11963atagen2026-10-24 23:24 UTC364
                Ok(CommandOutput::Done)
cbb715e11963atagen2026-10-24 23:24 UTC365
            }
cbb715e11963atagen2026-10-24 23:24 UTC366
            MentionsCommand::Recover => {
cbb715e11963atagen2026-10-24 23:24 UTC367
                self.recovery_budget = 20;
cbb715e11963atagen2026-10-24 23:24 UTC368
                self.latest_scan = true;
cbb715e11963atagen2026-10-24 23:24 UTC369
                self.recovery_retry = 0;
cbb715e11963atagen2026-10-24 23:24 UTC370
                let mut next = self.data.clone();
cbb715e11963atagen2026-10-24 23:24 UTC371
                next.coverage.recovering = true;
cbb715e11963atagen2026-10-24 23:24 UTC372
                next.coverage.historical_supported = None;
cbb715e11963atagen2026-10-24 23:24 UTC373
                self.save(next).await?;
cbb715e11963atagen2026-10-24 23:24 UTC374
                Ok(CommandOutput::Done)
cbb715e11963atagen2026-10-24 23:24 UTC375
            }
cbb715e11963atagen2026-10-24 23:24 UTC376
        }
cbb715e11963atagen2026-10-24 23:24 UTC377
    }
cbb715e11963atagen2026-10-24 23:24 UTC378
cbb715e11963atagen2026-10-24 23:24 UTC379
    fn gap(data: &mut Data, room_id: Option<OwnedRoomId>, reason: &str, token: Option<String>) {
cbb715e11963atagen2026-10-24 23:24 UTC380
        if !data
cbb715e11963atagen2026-10-24 23:24 UTC381
            .coverage
cbb715e11963atagen2026-10-24 23:24 UTC382
            .gaps
cbb715e11963atagen2026-10-24 23:24 UTC383
            .iter()
cbb715e11963atagen2026-10-24 23:24 UTC384
            .any(|gap| gap.room_id == room_id && gap.reason == reason && gap.token == token)
cbb715e11963atagen2026-10-24 23:24 UTC385
        {
cbb715e11963atagen2026-10-24 23:24 UTC386
            data.coverage.gaps.push(MentionGap {
cbb715e11963atagen2026-10-24 23:24 UTC387
                room_id,
cbb715e11963atagen2026-10-24 23:24 UTC388
                reason: reason.to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC389
                token,
cbb715e11963atagen2026-10-24 23:24 UTC390
            });
cbb715e11963atagen2026-10-24 23:24 UTC391
            if data.coverage.gaps.len() > 128 {
cbb715e11963atagen2026-10-24 23:24 UTC392
                data.coverage.gaps.remove(0);
cbb715e11963atagen2026-10-24 23:24 UTC393
                data.coverage.truncated = true;
cbb715e11963atagen2026-10-24 23:24 UTC394
            }
cbb715e11963atagen2026-10-24 23:24 UTC395
        }
cbb715e11963atagen2026-10-24 23:24 UTC396
    }
cbb715e11963atagen2026-10-24 23:24 UTC397
cbb715e11963atagen2026-10-24 23:24 UTC398
    async fn batch(&mut self, batch: ResponseCommitBatch) -> Result<(), Error> {
cbb715e11963atagen2026-10-24 23:24 UTC399
        let mut next = self.data.clone();
cbb715e11963atagen2026-10-24 23:24 UTC400
        if next.checkpoints.get(&batch.connection_id) == Some(&batch.next_pos) {
cbb715e11963atagen2026-10-24 23:24 UTC401
            return Ok(());
cbb715e11963atagen2026-10-24 23:24 UTC402
        }
80381e06a573atagen2026-10-25 03:02 UTC403
        let observed_at_ms = now();
80381e06a573atagen2026-10-25 03:02 UTC404
        let checkpoint_discontinuity =
80381e06a573atagen2026-10-25 03:02 UTC405
            next.checkpoints.get(&batch.connection_id) != batch.previous_pos.as_ref();
80381e06a573atagen2026-10-25 03:02 UTC406
        if checkpoint_discontinuity {
cbb715e11963atagen2026-10-24 23:24 UTC407
            Self::gap(
cbb715e11963atagen2026-10-24 23:24 UTC408
                &mut next,
cbb715e11963atagen2026-10-24 23:24 UTC409
                None,
cbb715e11963atagen2026-10-24 23:24 UTC410
                "sync checkpoint continuity is unknown",
cbb715e11963atagen2026-10-24 23:24 UTC411
                None,
cbb715e11963atagen2026-10-24 23:24 UTC412
            );
80381e06a573atagen2026-10-25 03:02 UTC413
            let observed_rooms: Vec<_> = next.observed_rooms.iter().cloned().collect();
80381e06a573atagen2026-10-25 03:02 UTC414
            for room_id in observed_rooms {
80381e06a573atagen2026-10-25 03:02 UTC415
                Self::mark_unresolved(&mut next, &room_id, observed_at_ms);
80381e06a573atagen2026-10-25 03:02 UTC416
            }
cbb715e11963atagen2026-10-24 23:24 UTC417
        }
cbb715e11963atagen2026-10-24 23:24 UTC418
        for (room_id, raw) in &batch.raw_rooms {
cbb715e11963atagen2026-10-24 23:24 UTC419
            if raw.limited || !next.observed_rooms.contains(room_id) {
cbb715e11963atagen2026-10-24 23:24 UTC420
                Self::gap(
cbb715e11963atagen2026-10-24 23:24 UTC421
                    &mut next,
cbb715e11963atagen2026-10-24 23:24 UTC422
                    Some(room_id.clone()),
cbb715e11963atagen2026-10-24 23:24 UTC423
                    if raw.limited {
cbb715e11963atagen2026-10-24 23:24 UTC424
                        "limited sync timeline"
cbb715e11963atagen2026-10-24 23:24 UTC425
                    } else {
cbb715e11963atagen2026-10-24 23:24 UTC426
                        "initial room history is incomplete"
cbb715e11963atagen2026-10-24 23:24 UTC427
                    },
cbb715e11963atagen2026-10-24 23:24 UTC428
                    raw.prev_batch.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC429
                );
cbb715e11963atagen2026-10-24 23:24 UTC430
            }
cbb715e11963atagen2026-10-24 23:24 UTC431
        }
cbb715e11963atagen2026-10-24 23:24 UTC432
        let room_ids: BTreeSet<_> = batch
cbb715e11963atagen2026-10-24 23:24 UTC433
            .raw_rooms
cbb715e11963atagen2026-10-24 23:24 UTC434
            .keys()
cbb715e11963atagen2026-10-24 23:24 UTC435
            .chain(batch.response.rooms.joined.keys())
cbb715e11963atagen2026-10-24 23:24 UTC436
            .cloned()
cbb715e11963atagen2026-10-24 23:24 UTC437
            .collect();
cbb715e11963atagen2026-10-24 23:24 UTC438
        for room_id in &room_ids {
cbb715e11963atagen2026-10-24 23:24 UTC439
            if !self
cbb715e11963atagen2026-10-24 23:24 UTC440
                .client
cbb715e11963atagen2026-10-24 23:24 UTC441
                .get_room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC442
                .is_some_and(|room| room.state() == RoomState::Joined)
cbb715e11963atagen2026-10-24 23:24 UTC443
            {
cbb715e11963atagen2026-10-24 23:24 UTC444
                continue;
cbb715e11963atagen2026-10-24 23:24 UTC445
            }
80381e06a573atagen2026-10-25 03:02 UTC446
            let previously_observed = next.observed_rooms.contains(room_id);
80381e06a573atagen2026-10-25 03:02 UTC447
            let limited = batch.raw_rooms.get(room_id).is_some_and(|raw| raw.limited);
80381e06a573atagen2026-10-25 03:02 UTC448
            if checkpoint_discontinuity || limited || !previously_observed {
80381e06a573atagen2026-10-25 03:02 UTC449
                Self::mark_unresolved(&mut next, room_id, observed_at_ms);
80381e06a573atagen2026-10-25 03:02 UTC450
            } else {
80381e06a573atagen2026-10-25 03:02 UTC451
                let coverage = next.repair_coverage.entry(room_id.clone()).or_default();
80381e06a573atagen2026-10-25 03:02 UTC452
                if !coverage.unresolved {
80381e06a573atagen2026-10-25 03:02 UTC453
                    coverage.last_confirmed_ms = Some(observed_at_ms);
80381e06a573atagen2026-10-25 03:02 UTC454
                }
80381e06a573atagen2026-10-25 03:02 UTC455
                coverage.reliable = false;
80381e06a573atagen2026-10-25 03:02 UTC456
            }
cbb715e11963atagen2026-10-24 23:24 UTC457
            next.observed_rooms.insert(room_id.clone());
cbb715e11963atagen2026-10-24 23:24 UTC458
            let mut observed = BTreeMap::new();
80381e06a573atagen2026-10-25 03:02 UTC459
            let mut latest_anchor = None;
cbb715e11963atagen2026-10-24 23:24 UTC460
            if let Some(raw) = batch.raw_rooms.get(room_id) {
cbb715e11963atagen2026-10-24 23:24 UTC461
                for event in &raw.timeline {
cbb715e11963atagen2026-10-24 23:24 UTC462
                    let value: Value =
cbb715e11963atagen2026-10-24 23:24 UTC463
                        serde_json::from_str(event.json().get()).map_err(|error| {
cbb715e11963atagen2026-10-24 23:24 UTC464
                            Error::with_source(ErrorKind::Invalid, "invalid observed event", error)
cbb715e11963atagen2026-10-24 23:24 UTC465
                        })?;
cbb715e11963atagen2026-10-24 23:24 UTC466
                    if let Some(event_id) = value["event_id"].as_str() {
80381e06a573atagen2026-10-25 03:02 UTC467
                        latest_anchor = event_id.parse::<OwnedEventId>().ok();
cbb715e11963atagen2026-10-24 23:24 UTC468
                        observed.insert(event_id.to_owned(), value);
cbb715e11963atagen2026-10-24 23:24 UTC469
                    }
cbb715e11963atagen2026-10-24 23:24 UTC470
                }
cbb715e11963atagen2026-10-24 23:24 UTC471
            }
80381e06a573atagen2026-10-25 03:02 UTC472
            if let Some(latest_anchor) = latest_anchor {
80381e06a573atagen2026-10-25 03:02 UTC473
                next.repair_coverage
80381e06a573atagen2026-10-25 03:02 UTC474
                    .entry(room_id.clone())
80381e06a573atagen2026-10-25 03:02 UTC475
                    .or_default()
80381e06a573atagen2026-10-25 03:02 UTC476
                    .latest_anchor = Some(latest_anchor);
80381e06a573atagen2026-10-25 03:02 UTC477
            }
cbb715e11963atagen2026-10-24 23:24 UTC478
            if let Some(update) = batch.response.rooms.joined.get(room_id) {
cbb715e11963atagen2026-10-24 23:24 UTC479
                for event in &update.timeline.events {
cbb715e11963atagen2026-10-24 23:24 UTC480
                    let value: Value =
cbb715e11963atagen2026-10-24 23:24 UTC481
                        serde_json::from_str(event.raw().json().get()).map_err(|error| {
cbb715e11963atagen2026-10-24 23:24 UTC482
                            Error::with_source(ErrorKind::Invalid, "invalid observed event", error)
cbb715e11963atagen2026-10-24 23:24 UTC483
                        })?;
cbb715e11963atagen2026-10-24 23:24 UTC484
                    if let Some(event_id) = value["event_id"].as_str() {
cbb715e11963atagen2026-10-24 23:24 UTC485
                        observed.insert(event_id.to_owned(), value);
cbb715e11963atagen2026-10-24 23:24 UTC486
                    }
cbb715e11963atagen2026-10-24 23:24 UTC487
                }
cbb715e11963atagen2026-10-24 23:24 UTC488
            }
cbb715e11963atagen2026-10-24 23:24 UTC489
            let mut observed: Vec<_> = observed.into_values().collect();
cbb715e11963atagen2026-10-24 23:24 UTC490
            observed.sort_by_key(|value| value["origin_server_ts"].as_u64().unwrap_or(0));
cbb715e11963atagen2026-10-24 23:24 UTC491
            for value in observed {
cbb715e11963atagen2026-10-24 23:24 UTC492
                self.observe(&mut next, room_id, value).await?;
cbb715e11963atagen2026-10-24 23:24 UTC493
            }
cbb715e11963atagen2026-10-24 23:24 UTC494
        }
cbb715e11963atagen2026-10-24 23:24 UTC495
        for room_id in batch.response.rooms.left.keys() {
cbb715e11963atagen2026-10-24 23:24 UTC496
            Self::gap(
cbb715e11963atagen2026-10-24 23:24 UTC497
                &mut next,
cbb715e11963atagen2026-10-24 23:24 UTC498
                Some(room_id.clone()),
cbb715e11963atagen2026-10-24 23:24 UTC499
                "room left, retained context is read only",
cbb715e11963atagen2026-10-24 23:24 UTC500
                None,
cbb715e11963atagen2026-10-24 23:24 UTC501
            );
cbb715e11963atagen2026-10-24 23:24 UTC502
            next.candidates
cbb715e11963atagen2026-10-24 23:24 UTC503
                .retain(|_, candidate| &candidate.room_id != room_id);
cbb715e11963atagen2026-10-24 23:24 UTC504
            for entry in next
cbb715e11963atagen2026-10-24 23:24 UTC505
                .entries
cbb715e11963atagen2026-10-24 23:24 UTC506
                .values_mut()
cbb715e11963atagen2026-10-24 23:24 UTC507
                .filter(|entry| &entry.key.room_id == room_id)
cbb715e11963atagen2026-10-24 23:24 UTC508
            {
cbb715e11963atagen2026-10-24 23:24 UTC509
                entry.capture = MentionCaptureState::Unavailable;
cbb715e11963atagen2026-10-24 23:24 UTC510
                entry.revision = next.revision + 1;
cbb715e11963atagen2026-10-24 23:24 UTC511
            }
cbb715e11963atagen2026-10-24 23:24 UTC512
        }
cbb715e11963atagen2026-10-24 23:24 UTC513
        let reevaluate: Vec<_> = next
cbb715e11963atagen2026-10-24 23:24 UTC514
            .entries
cbb715e11963atagen2026-10-24 23:24 UTC515
            .values()
cbb715e11963atagen2026-10-24 23:24 UTC516
            .filter(|entry| room_ids.contains(&entry.key.room_id))
cbb715e11963atagen2026-10-24 23:24 UTC517
            .filter_map(|entry| {
cbb715e11963atagen2026-10-24 23:24 UTC518
                next.events
cbb715e11963atagen2026-10-24 23:24 UTC519
                    .get(&id(&entry.key))
cbb715e11963atagen2026-10-24 23:24 UTC520
                    .and_then(|event| event.raw.clone())
cbb715e11963atagen2026-10-24 23:24 UTC521
                    .map(|raw| (entry.key.room_id.clone(), raw))
cbb715e11963atagen2026-10-24 23:24 UTC522
            })
cbb715e11963atagen2026-10-24 23:24 UTC523
            .collect();
cbb715e11963atagen2026-10-24 23:24 UTC524
        for (room_id, raw) in reevaluate {
cbb715e11963atagen2026-10-24 23:24 UTC525
            self.observe(&mut next, &room_id, raw).await?;
cbb715e11963atagen2026-10-24 23:24 UTC526
        }
cbb715e11963atagen2026-10-24 23:24 UTC527
        self.filter_ignored(&mut next).await;
cbb715e11963atagen2026-10-24 23:24 UTC528
        next.checkpoints.insert(batch.connection_id, batch.next_pos);
cbb715e11963atagen2026-10-24 23:24 UTC529
        self.save(next).await
cbb715e11963atagen2026-10-24 23:24 UTC530
    }
cbb715e11963atagen2026-10-24 23:24 UTC531
80381e06a573atagen2026-10-25 03:02 UTC532
    fn mark_unresolved(data: &mut Data, room_id: &OwnedRoomId, observed_at_ms: u64) {
80381e06a573atagen2026-10-25 03:02 UTC533
        let coverage = data.repair_coverage.entry(room_id.clone()).or_default();
80381e06a573atagen2026-10-25 03:02 UTC534
        if !coverage.unresolved {
80381e06a573atagen2026-10-25 03:02 UTC535
            coverage.boundary_ms = Some(observed_at_ms);
80381e06a573atagen2026-10-25 03:02 UTC536
            coverage.unresolved = true;
80381e06a573atagen2026-10-25 03:02 UTC537
        }
80381e06a573atagen2026-10-25 03:02 UTC538
        coverage.reliable = false;
80381e06a573atagen2026-10-25 03:02 UTC539
    }
80381e06a573atagen2026-10-25 03:02 UTC540
cbb715e11963atagen2026-10-24 23:24 UTC541
    async fn filter_ignored(&self, next: &mut Data) {
cbb715e11963atagen2026-10-24 23:24 UTC542
        let mut removed = Vec::new();
cbb715e11963atagen2026-10-24 23:24 UTC543
        for (key, entry) in &next.entries {
cbb715e11963atagen2026-10-24 23:24 UTC544
            if let TimelineItem::Event(event) = &entry.event {
cbb715e11963atagen2026-10-24 23:24 UTC545
                if self.client.is_user_ignored(&event.sender.user_id).await {
cbb715e11963atagen2026-10-24 23:24 UTC546
                    removed.push(key.clone());
cbb715e11963atagen2026-10-24 23:24 UTC547
                }
cbb715e11963atagen2026-10-24 23:24 UTC548
            }
cbb715e11963atagen2026-10-24 23:24 UTC549
        }
cbb715e11963atagen2026-10-24 23:24 UTC550
        for key in removed {
cbb715e11963atagen2026-10-24 23:24 UTC551
            next.entries.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC552
            next.windows.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC553
        }
cbb715e11963atagen2026-10-24 23:24 UTC554
        next.collect_events();
cbb715e11963atagen2026-10-24 23:24 UTC555
    }
cbb715e11963atagen2026-10-24 23:24 UTC556
cbb715e11963atagen2026-10-24 23:24 UTC557
    async fn observe(
cbb715e11963atagen2026-10-24 23:24 UTC558
        &self,
cbb715e11963atagen2026-10-24 23:24 UTC559
        data: &mut Data,
cbb715e11963atagen2026-10-24 23:24 UTC560
        room_id: &OwnedRoomId,
cbb715e11963atagen2026-10-24 23:24 UTC561
        mut value: Value,
cbb715e11963atagen2026-10-24 23:24 UTC562
    ) -> Result<(), Error> {
cbb715e11963atagen2026-10-24 23:24 UTC563
        let Some(event_id) = value["event_id"]
cbb715e11963atagen2026-10-24 23:24 UTC564
            .as_str()
cbb715e11963atagen2026-10-24 23:24 UTC565
            .and_then(|value| value.parse().ok())
cbb715e11963atagen2026-10-24 23:24 UTC566
        else {
cbb715e11963atagen2026-10-24 23:24 UTC567
            return Ok(());
cbb715e11963atagen2026-10-24 23:24 UTC568
        };
cbb715e11963atagen2026-10-24 23:24 UTC569
        let key = MentionKey {
cbb715e11963atagen2026-10-24 23:24 UTC570
            room_id: room_id.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC571
            event_id,
cbb715e11963atagen2026-10-24 23:24 UTC572
        };
cbb715e11963atagen2026-10-24 23:24 UTC573
        let identity = id(&key);
cbb715e11963atagen2026-10-24 23:24 UTC574
        let Some(timestamp) = value["origin_server_ts"].as_u64() else {
cbb715e11963atagen2026-10-24 23:24 UTC575
            return Ok(());
cbb715e11963atagen2026-10-24 23:24 UTC576
        };
cbb715e11963atagen2026-10-24 23:24 UTC577
        data.coverage.earliest_timestamp_ms = Some(
cbb715e11963atagen2026-10-24 23:24 UTC578
            data.coverage
cbb715e11963atagen2026-10-24 23:24 UTC579
                .earliest_timestamp_ms
cbb715e11963atagen2026-10-24 23:24 UTC580
                .map_or(timestamp, |old| old.min(timestamp)),
cbb715e11963atagen2026-10-24 23:24 UTC581
        );
cbb715e11963atagen2026-10-24 23:24 UTC582
        if value["type"] == "m.room.redaction" {
cbb715e11963atagen2026-10-24 23:24 UTC583
            if let Some(target) = value["redacts"]
cbb715e11963atagen2026-10-24 23:24 UTC584
                .as_str()
cbb715e11963atagen2026-10-24 23:24 UTC585
                .or_else(|| value["content"]["redacts"].as_str())
cbb715e11963atagen2026-10-24 23:24 UTC586
                .and_then(|value| value.parse().ok())
cbb715e11963atagen2026-10-24 23:24 UTC587
            {
cbb715e11963atagen2026-10-24 23:24 UTC588
                let target = id(&MentionKey {
cbb715e11963atagen2026-10-24 23:24 UTC589
                    room_id: room_id.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC590
                    event_id: target,
cbb715e11963atagen2026-10-24 23:24 UTC591
                });
cbb715e11963atagen2026-10-24 23:24 UTC592
                data.redacted.insert(target.clone(), timestamp);
cbb715e11963atagen2026-10-24 23:24 UTC593
                data.entries.remove(&target);
cbb715e11963atagen2026-10-24 23:24 UTC594
                data.windows.remove(&target);
cbb715e11963atagen2026-10-24 23:24 UTC595
                data.candidates.remove(&target);
cbb715e11963atagen2026-10-24 23:24 UTC596
                data.edits.remove(&target);
cbb715e11963atagen2026-10-24 23:24 UTC597
                data.candidates.retain(|_, candidate| {
cbb715e11963atagen2026-10-24 23:24 UTC598
                    candidate.room_id != *room_id
cbb715e11963atagen2026-10-24 23:24 UTC599
                        || candidate.raw["content"]["m.relates_to"]["event_id"].as_str()
cbb715e11963atagen2026-10-24 23:24 UTC600
                            != target.split_once('\n').map(|(_, event)| event)
cbb715e11963atagen2026-10-24 23:24 UTC601
                });
cbb715e11963atagen2026-10-24 23:24 UTC602
                if let Some(saved) = data.events.get_mut(&target) {
cbb715e11963atagen2026-10-24 23:24 UTC603
                    redact(saved);
cbb715e11963atagen2026-10-24 23:24 UTC604
                }
cbb715e11963atagen2026-10-24 23:24 UTC605
                data.touch_contexts(&target);
cbb715e11963atagen2026-10-24 23:24 UTC606
            }
cbb715e11963atagen2026-10-24 23:24 UTC607
            return Ok(());
cbb715e11963atagen2026-10-24 23:24 UTC608
        }
cbb715e11963atagen2026-10-24 23:24 UTC609
        if data.redacted.contains_key(&identity)
cbb715e11963atagen2026-10-24 23:24 UTC610
            || value["unsigned"].get("redacted_because").is_some()
cbb715e11963atagen2026-10-24 23:24 UTC611
        {
cbb715e11963atagen2026-10-24 23:24 UTC612
            data.redacted.insert(identity.clone(), timestamp);
cbb715e11963atagen2026-10-24 23:24 UTC613
            data.entries.remove(&identity);
cbb715e11963atagen2026-10-24 23:24 UTC614
            data.windows.remove(&identity);
cbb715e11963atagen2026-10-24 23:24 UTC615
            data.candidates.remove(&identity);
cbb715e11963atagen2026-10-24 23:24 UTC616
            if let Some(saved) = data.events.get_mut(&identity) {
cbb715e11963atagen2026-10-24 23:24 UTC617
                redact(saved);
cbb715e11963atagen2026-10-24 23:24 UTC618
            }
cbb715e11963atagen2026-10-24 23:24 UTC619
            data.touch_contexts(&identity);
cbb715e11963atagen2026-10-24 23:24 UTC620
            return Ok(());
cbb715e11963atagen2026-10-24 23:24 UTC621
        }
cbb715e11963atagen2026-10-24 23:24 UTC622
        if value["content"]["m.relates_to"]["rel_type"] == "m.replace" {
cbb715e11963atagen2026-10-24 23:24 UTC623
            if let Some(target) = value["content"]["m.relates_to"]["event_id"]
cbb715e11963atagen2026-10-24 23:24 UTC624
                .as_str()
cbb715e11963atagen2026-10-24 23:24 UTC625
                .and_then(|value| value.parse().ok())
cbb715e11963atagen2026-10-24 23:24 UTC626
            {
cbb715e11963atagen2026-10-24 23:24 UTC627
                let target = id(&MentionKey {
cbb715e11963atagen2026-10-24 23:24 UTC628
                    room_id: room_id.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC629
                    event_id: target,
cbb715e11963atagen2026-10-24 23:24 UTC630
                });
cbb715e11963atagen2026-10-24 23:24 UTC631
                if data.redacted.contains_key(&target) {
cbb715e11963atagen2026-10-24 23:24 UTC632
                    return Ok(());
cbb715e11963atagen2026-10-24 23:24 UTC633
                }
cbb715e11963atagen2026-10-24 23:24 UTC634
                let newer = data.edits.get(&target).is_none_or(|old| {
cbb715e11963atagen2026-10-24 23:24 UTC635
                    (old["origin_server_ts"].as_u64(), old["event_id"].as_str())
cbb715e11963atagen2026-10-24 23:24 UTC636
                        < (Some(timestamp), value["event_id"].as_str())
cbb715e11963atagen2026-10-24 23:24 UTC637
                });
cbb715e11963atagen2026-10-24 23:24 UTC638
                if newer {
cbb715e11963atagen2026-10-24 23:24 UTC639
                    if let Some(original) =
cbb715e11963atagen2026-10-24 23:24 UTC640
                        data.events.get(&target).and_then(|saved| saved.raw.clone())
cbb715e11963atagen2026-10-24 23:24 UTC641
                    {
cbb715e11963atagen2026-10-24 23:24 UTC642
                        if original["sender"] != value["sender"] {
cbb715e11963atagen2026-10-24 23:24 UTC643
                            return Ok(());
cbb715e11963atagen2026-10-24 23:24 UTC644
                        }
cbb715e11963atagen2026-10-24 23:24 UTC645
                        data.edits.insert(target.clone(), value.clone());
cbb715e11963atagen2026-10-24 23:24 UTC646
                        value = edited(original, &value);
cbb715e11963atagen2026-10-24 23:24 UTC647
                        return Box::pin(self.observe(data, room_id, value)).await;
cbb715e11963atagen2026-10-24 23:24 UTC648
                    }
cbb715e11963atagen2026-10-24 23:24 UTC649
                    data.candidates.insert(
cbb715e11963atagen2026-10-24 23:24 UTC650
                        identity,
cbb715e11963atagen2026-10-24 23:24 UTC651
                        Candidate {
cbb715e11963atagen2026-10-24 23:24 UTC652
                            room_id: room_id.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC653
                            raw: value,
cbb715e11963atagen2026-10-24 23:24 UTC654
                            attempts: 0,
cbb715e11963atagen2026-10-24 23:24 UTC655
                            retry_at_ms: 0,
cbb715e11963atagen2026-10-24 23:24 UTC656
                        },
cbb715e11963atagen2026-10-24 23:24 UTC657
                    );
cbb715e11963atagen2026-10-24 23:24 UTC658
                }
cbb715e11963atagen2026-10-24 23:24 UTC659
            }
cbb715e11963atagen2026-10-24 23:24 UTC660
            return Ok(());
cbb715e11963atagen2026-10-24 23:24 UTC661
        }
cbb715e11963atagen2026-10-24 23:24 UTC662
        if let Some(edit) = value["unsigned"]["m.relations"].get("m.replace").cloned() {
cbb715e11963atagen2026-10-24 23:24 UTC663
            if edit["sender"] == value["sender"]
cbb715e11963atagen2026-10-24 23:24 UTC664
                && edit["content"]["m.new_content"].is_object()
cbb715e11963atagen2026-10-24 23:24 UTC665
                && edit["content"]["m.relates_to"]["event_id"]
cbb715e11963atagen2026-10-24 23:24 UTC666
                    .as_str()
cbb715e11963atagen2026-10-24 23:24 UTC667
                    .is_none_or(|target| target == key.event_id.as_str())
cbb715e11963atagen2026-10-24 23:24 UTC668
                && edit["origin_server_ts"].as_u64().is_some()
cbb715e11963atagen2026-10-24 23:24 UTC669
                && data.edits.get(&identity).is_none_or(|old| {
cbb715e11963atagen2026-10-24 23:24 UTC670
                    (old["origin_server_ts"].as_u64(), old["event_id"].as_str())
cbb715e11963atagen2026-10-24 23:24 UTC671
                        < (edit["origin_server_ts"].as_u64(), edit["event_id"].as_str())
cbb715e11963atagen2026-10-24 23:24 UTC672
                })
cbb715e11963atagen2026-10-24 23:24 UTC673
            {
cbb715e11963atagen2026-10-24 23:24 UTC674
                data.edits.insert(identity.clone(), edit);
cbb715e11963atagen2026-10-24 23:24 UTC675
            }
cbb715e11963atagen2026-10-24 23:24 UTC676
        }
cbb715e11963atagen2026-10-24 23:24 UTC677
        if let Some(edit) = data.edits.get(&identity) {
cbb715e11963atagen2026-10-24 23:24 UTC678
            if value["sender"] == edit["sender"] {
cbb715e11963atagen2026-10-24 23:24 UTC679
                value = edited(value, edit);
cbb715e11963atagen2026-10-24 23:24 UTC680
            }
cbb715e11963atagen2026-10-24 23:24 UTC681
        }
cbb715e11963atagen2026-10-24 23:24 UTC682
        if let Some(edit) = value["unsigned"].get("macltok_edit").cloned() {
cbb715e11963atagen2026-10-24 23:24 UTC683
            data.edits.insert(identity.clone(), edit);
cbb715e11963atagen2026-10-24 23:24 UTC684
        }
cbb715e11963atagen2026-10-24 23:24 UTC685
        if value["type"] == "m.room.encrypted" {
cbb715e11963atagen2026-10-24 23:24 UTC686
            data.candidates.entry(identity).or_insert(Candidate {
cbb715e11963atagen2026-10-24 23:24 UTC687
                room_id: room_id.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC688
                raw: value,
cbb715e11963atagen2026-10-24 23:24 UTC689
                attempts: 0,
cbb715e11963atagen2026-10-24 23:24 UTC690
                retry_at_ms: 0,
cbb715e11963atagen2026-10-24 23:24 UTC691
            });
cbb715e11963atagen2026-10-24 23:24 UTC692
            return Ok(());
cbb715e11963atagen2026-10-24 23:24 UTC693
        }
cbb715e11963atagen2026-10-24 23:24 UTC694
        let Some((item, media)) = normalise_observed(&value, Some(&self.account), &identity) else {
cbb715e11963atagen2026-10-24 23:24 UTC695
            return Ok(());
cbb715e11963atagen2026-10-24 23:24 UTC696
        };
cbb715e11963atagen2026-10-24 23:24 UTC697
        self.supplement(data, &key, &item, &media, &value);
cbb715e11963atagen2026-10-24 23:24 UTC698
        let (reasons, suppressed, unresolved) = self.classify(data, room_id, &value).await;
cbb715e11963atagen2026-10-24 23:24 UTC699
        if unresolved {
cbb715e11963atagen2026-10-24 23:24 UTC700
            data.candidates
cbb715e11963atagen2026-10-24 23:24 UTC701
                .entry(identity.clone())
cbb715e11963atagen2026-10-24 23:24 UTC702
                .or_insert(Candidate {
cbb715e11963atagen2026-10-24 23:24 UTC703
                    room_id: room_id.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC704
                    raw: value.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC705
                    attempts: 0,
cbb715e11963atagen2026-10-24 23:24 UTC706
                    retry_at_ms: now() + 30_000,
cbb715e11963atagen2026-10-24 23:24 UTC707
                });
cbb715e11963atagen2026-10-24 23:24 UTC708
        } else {
cbb715e11963atagen2026-10-24 23:24 UTC709
            data.candidates.remove(&identity);
cbb715e11963atagen2026-10-24 23:24 UTC710
        }
cbb715e11963atagen2026-10-24 23:24 UTC711
        if reasons.is_empty() {
cbb715e11963atagen2026-10-24 23:24 UTC712
            data.entries.remove(&identity);
cbb715e11963atagen2026-10-24 23:24 UTC713
            data.windows.remove(&identity);
cbb715e11963atagen2026-10-24 23:24 UTC714
            if data.events.contains_key(&identity) || data.edits.contains_key(&identity) {
cbb715e11963atagen2026-10-24 23:24 UTC715
                data.events.insert(
cbb715e11963atagen2026-10-24 23:24 UTC716
                    identity,
cbb715e11963atagen2026-10-24 23:24 UTC717
                    SavedEvent {
cbb715e11963atagen2026-10-24 23:24 UTC718
                        item,
cbb715e11963atagen2026-10-24 23:24 UTC719
                        media,
cbb715e11963atagen2026-10-24 23:24 UTC720
                        raw: Some(value),
cbb715e11963atagen2026-10-24 23:24 UTC721
                    },
cbb715e11963atagen2026-10-24 23:24 UTC722
                );
cbb715e11963atagen2026-10-24 23:24 UTC723
            }
cbb715e11963atagen2026-10-24 23:24 UTC724
            return Ok(());
cbb715e11963atagen2026-10-24 23:24 UTC725
        }
cbb715e11963atagen2026-10-24 23:24 UTC726
        let previous = data.entries.get(&identity);
cbb715e11963atagen2026-10-24 23:24 UTC727
        let changed = previous.is_none_or(|entry| {
cbb715e11963atagen2026-10-24 23:24 UTC728
            entry.event != item
cbb715e11963atagen2026-10-24 23:24 UTC729
                || entry.reasons != reasons
cbb715e11963atagen2026-10-24 23:24 UTC730
                || entry.notification_suppressed != suppressed
cbb715e11963atagen2026-10-24 23:24 UTC731
        });
cbb715e11963atagen2026-10-24 23:24 UTC732
        let entry = MentionEntry {
cbb715e11963atagen2026-10-24 23:24 UTC733
            key,
cbb715e11963atagen2026-10-24 23:24 UTC734
            timestamp_ms: timestamp,
cbb715e11963atagen2026-10-24 23:24 UTC735
            discovered_at_ms: previous.map_or_else(now, |entry| entry.discovered_at_ms),
cbb715e11963atagen2026-10-24 23:24 UTC736
            discovery_revision: previous
cbb715e11963atagen2026-10-24 23:24 UTC737
                .map_or(data.revision + 1, |entry| entry.discovery_revision),
cbb715e11963atagen2026-10-24 23:24 UTC738
            revision: if changed {
cbb715e11963atagen2026-10-24 23:24 UTC739
                data.revision + 1
cbb715e11963atagen2026-10-24 23:24 UTC740
            } else {
cbb715e11963atagen2026-10-24 23:24 UTC741
                previous.unwrap().revision
cbb715e11963atagen2026-10-24 23:24 UTC742
            },
cbb715e11963atagen2026-10-24 23:24 UTC743
            event: item.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC744
            reasons,
cbb715e11963atagen2026-10-24 23:24 UTC745
            read: previous.is_some_and(|entry| entry.read),
cbb715e11963atagen2026-10-24 23:24 UTC746
            capture: previous.map_or(MentionCaptureState::Pending, |entry| {
cbb715e11963atagen2026-10-24 23:24 UTC747
                if changed {
cbb715e11963atagen2026-10-24 23:24 UTC748
                    MentionCaptureState::Pending
cbb715e11963atagen2026-10-24 23:24 UTC749
                } else {
cbb715e11963atagen2026-10-24 23:24 UTC750
                    entry.capture.clone()
cbb715e11963atagen2026-10-24 23:24 UTC751
                }
cbb715e11963atagen2026-10-24 23:24 UTC752
            }),
cbb715e11963atagen2026-10-24 23:24 UTC753
            room_name: self.client.get_room(room_id).and_then(|room| room.name()),
cbb715e11963atagen2026-10-24 23:24 UTC754
            notification_suppressed: suppressed,
cbb715e11963atagen2026-10-24 23:24 UTC755
        };
cbb715e11963atagen2026-10-24 23:24 UTC756
        data.entries.insert(identity.clone(), entry);
cbb715e11963atagen2026-10-24 23:24 UTC757
        data.windows.entry(identity.clone()).or_insert(Window {
cbb715e11963atagen2026-10-24 23:24 UTC758
            before: Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC759
            target: Some(identity.clone()),
cbb715e11963atagen2026-10-24 23:24 UTC760
            after: Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC761
            start: None,
cbb715e11963atagen2026-10-24 23:24 UTC762
            end: None,
cbb715e11963atagen2026-10-24 23:24 UTC763
        });
cbb715e11963atagen2026-10-24 23:24 UTC764
        data.events.insert(
cbb715e11963atagen2026-10-24 23:24 UTC765
            identity,
cbb715e11963atagen2026-10-24 23:24 UTC766
            SavedEvent {
cbb715e11963atagen2026-10-24 23:24 UTC767
                item,
cbb715e11963atagen2026-10-24 23:24 UTC768
                media,
cbb715e11963atagen2026-10-24 23:24 UTC769
                raw: Some(value),
cbb715e11963atagen2026-10-24 23:24 UTC770
            },
cbb715e11963atagen2026-10-24 23:24 UTC771
        );
cbb715e11963atagen2026-10-24 23:24 UTC772
        Ok(())
cbb715e11963atagen2026-10-24 23:24 UTC773
    }
cbb715e11963atagen2026-10-24 23:24 UTC774
cbb715e11963atagen2026-10-24 23:24 UTC775
    fn supplement(
cbb715e11963atagen2026-10-24 23:24 UTC776
        &self,
cbb715e11963atagen2026-10-24 23:24 UTC777
        data: &mut Data,
cbb715e11963atagen2026-10-24 23:24 UTC778
        key: &MentionKey,
cbb715e11963atagen2026-10-24 23:24 UTC779
        item: &TimelineItem,
cbb715e11963atagen2026-10-24 23:24 UTC780
        media: &[MediaRegistration],
cbb715e11963atagen2026-10-24 23:24 UTC781
        raw: &Value,
cbb715e11963atagen2026-10-24 23:24 UTC782
    ) {
cbb715e11963atagen2026-10-24 23:24 UTC783
        let TimelineItem::Event(event) = item else {
cbb715e11963atagen2026-10-24 23:24 UTC784
            return;
cbb715e11963atagen2026-10-24 23:24 UTC785
        };
cbb715e11963atagen2026-10-24 23:24 UTC786
        let identity = id(key);
cbb715e11963atagen2026-10-24 23:24 UTC787
        if data
cbb715e11963atagen2026-10-24 23:24 UTC788
            .events
cbb715e11963atagen2026-10-24 23:24 UTC789
            .get(&identity)
cbb715e11963atagen2026-10-24 23:24 UTC790
            .is_some_and(|saved| saved.item != *item)
cbb715e11963atagen2026-10-24 23:24 UTC791
        {
cbb715e11963atagen2026-10-24 23:24 UTC792
            data.touch_contexts(&identity);
cbb715e11963atagen2026-10-24 23:24 UTC793
        }
cbb715e11963atagen2026-10-24 23:24 UTC794
        let mut retained = data.events.contains_key(&identity);
cbb715e11963atagen2026-10-24 23:24 UTC795
        let saved_events = &data.events;
cbb715e11963atagen2026-10-24 23:24 UTC796
        for (target, window) in &mut data.windows {
cbb715e11963atagen2026-10-24 23:24 UTC797
            let Some(entry) = data.entries.get_mut(target) else {
cbb715e11963atagen2026-10-24 23:24 UTC798
                continue;
cbb715e11963atagen2026-10-24 23:24 UTC799
            };
cbb715e11963atagen2026-10-24 23:24 UTC800
            if entry.key.room_id != key.room_id
cbb715e11963atagen2026-10-24 23:24 UTC801
                || (entry.timestamp_ms, target.as_str()) >= (event.timestamp_ms, identity.as_str())
cbb715e11963atagen2026-10-24 23:24 UTC802
                || window.after.contains(&identity)
cbb715e11963atagen2026-10-24 23:24 UTC803
            {
cbb715e11963atagen2026-10-24 23:24 UTC804
                continue;
cbb715e11963atagen2026-10-24 23:24 UTC805
            }
cbb715e11963atagen2026-10-24 23:24 UTC806
            let previous = window.after.clone();
cbb715e11963atagen2026-10-24 23:24 UTC807
            window.after.push(identity.clone());
cbb715e11963atagen2026-10-24 23:24 UTC808
            window.after.sort_by_key(|key| {
cbb715e11963atagen2026-10-24 23:24 UTC809
                if key == &identity {
cbb715e11963atagen2026-10-24 23:24 UTC810
                    (event.timestamp_ms, key.clone())
cbb715e11963atagen2026-10-24 23:24 UTC811
                } else {
cbb715e11963atagen2026-10-24 23:24 UTC812
                    (
cbb715e11963atagen2026-10-24 23:24 UTC813
                        saved_events
cbb715e11963atagen2026-10-24 23:24 UTC814
                            .get(key)
cbb715e11963atagen2026-10-24 23:24 UTC815
                            .and_then(|saved| match &saved.item {
cbb715e11963atagen2026-10-24 23:24 UTC816
                                TimelineItem::Event(event) => Some(event.timestamp_ms),
cbb715e11963atagen2026-10-24 23:24 UTC817
                                _ => None,
cbb715e11963atagen2026-10-24 23:24 UTC818
                            })
cbb715e11963atagen2026-10-24 23:24 UTC819
                            .unwrap_or(0),
cbb715e11963atagen2026-10-24 23:24 UTC820
                        key.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC821
                    )
cbb715e11963atagen2026-10-24 23:24 UTC822
                }
cbb715e11963atagen2026-10-24 23:24 UTC823
            });
cbb715e11963atagen2026-10-24 23:24 UTC824
            window.after.truncate(10);
cbb715e11963atagen2026-10-24 23:24 UTC825
            if window.after == previous {
cbb715e11963atagen2026-10-24 23:24 UTC826
                continue;
cbb715e11963atagen2026-10-24 23:24 UTC827
            }
cbb715e11963atagen2026-10-24 23:24 UTC828
            window.end = None;
cbb715e11963atagen2026-10-24 23:24 UTC829
            retained |= window.after.contains(&identity);
cbb715e11963atagen2026-10-24 23:24 UTC830
            if entry.capture == MentionCaptureState::Partial
cbb715e11963atagen2026-10-24 23:24 UTC831
                && window.after.len() == 10
cbb715e11963atagen2026-10-24 23:24 UTC832
                && (window.before.len() == 10 || window.start.is_none())
cbb715e11963atagen2026-10-24 23:24 UTC833
            {
cbb715e11963atagen2026-10-24 23:24 UTC834
                entry.capture = MentionCaptureState::Ready;
cbb715e11963atagen2026-10-24 23:24 UTC835
            }
cbb715e11963atagen2026-10-24 23:24 UTC836
            entry.revision = data.revision + 1;
cbb715e11963atagen2026-10-24 23:24 UTC837
        }
cbb715e11963atagen2026-10-24 23:24 UTC838
        if retained {
cbb715e11963atagen2026-10-24 23:24 UTC839
            data.events.insert(
cbb715e11963atagen2026-10-24 23:24 UTC840
                identity,
cbb715e11963atagen2026-10-24 23:24 UTC841
                SavedEvent {
cbb715e11963atagen2026-10-24 23:24 UTC842
                    item: item.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC843
                    media: media.to_vec(),
cbb715e11963atagen2026-10-24 23:24 UTC844
                    raw: Some(raw.clone()),
cbb715e11963atagen2026-10-24 23:24 UTC845
                },
cbb715e11963atagen2026-10-24 23:24 UTC846
            );
cbb715e11963atagen2026-10-24 23:24 UTC847
        }
cbb715e11963atagen2026-10-24 23:24 UTC848
    }
cbb715e11963atagen2026-10-24 23:24 UTC849
cbb715e11963atagen2026-10-24 23:24 UTC850
    async fn classify(
cbb715e11963atagen2026-10-24 23:24 UTC851
        &self,
cbb715e11963atagen2026-10-24 23:24 UTC852
        data: &Data,
cbb715e11963atagen2026-10-24 23:24 UTC853
        room_id: &OwnedRoomId,
cbb715e11963atagen2026-10-24 23:24 UTC854
        value: &Value,
cbb715e11963atagen2026-10-24 23:24 UTC855
    ) -> (Vec<MentionReason>, bool, bool) {
cbb715e11963atagen2026-10-24 23:24 UTC856
        let Some(sender) = value["sender"]
cbb715e11963atagen2026-10-24 23:24 UTC857
            .as_str()
cbb715e11963atagen2026-10-24 23:24 UTC858
            .and_then(|sender| sender.parse::<OwnedUserId>().ok())
cbb715e11963atagen2026-10-24 23:24 UTC859
        else {
cbb715e11963atagen2026-10-24 23:24 UTC860
            return (vec![], false, false);
cbb715e11963atagen2026-10-24 23:24 UTC861
        };
cbb715e11963atagen2026-10-24 23:24 UTC862
        if sender == self.account
cbb715e11963atagen2026-10-24 23:24 UTC863
            || self.client.is_user_ignored(&sender).await
cbb715e11963atagen2026-10-24 23:24 UTC864
            || value["type"] != "m.room.message"
cbb715e11963atagen2026-10-24 23:24 UTC865
        {
cbb715e11963atagen2026-10-24 23:24 UTC866
            return (vec![], false, false);
cbb715e11963atagen2026-10-24 23:24 UTC867
        }
cbb715e11963atagen2026-10-24 23:24 UTC868
        let Some(room) = self
cbb715e11963atagen2026-10-24 23:24 UTC869
            .client
cbb715e11963atagen2026-10-24 23:24 UTC870
            .get_room(room_id)
cbb715e11963atagen2026-10-24 23:24 UTC871
            .filter(|room| room.state() == RoomState::Joined)
cbb715e11963atagen2026-10-24 23:24 UTC872
        else {
cbb715e11963atagen2026-10-24 23:24 UTC873
            return (vec![], false, false);
cbb715e11963atagen2026-10-24 23:24 UTC874
        };
cbb715e11963atagen2026-10-24 23:24 UTC875
        let raw: Raw<AnySyncTimelineEvent> =
cbb715e11963atagen2026-10-24 23:24 UTC876
            Raw::from_json_string(value.to_string()).expect("serialized event");
cbb715e11963atagen2026-10-24 23:24 UTC877
        let actions = room.event_push_actions(&raw).await.ok().flatten();
cbb715e11963atagen2026-10-24 23:24 UTC878
        let notify = actions.as_ref().is_some_and(|actions| {
cbb715e11963atagen2026-10-24 23:24 UTC879
            actions
cbb715e11963atagen2026-10-24 23:24 UTC880
                .iter()
cbb715e11963atagen2026-10-24 23:24 UTC881
                .any(|action| matches!(action, ruma::push::Action::Notify))
cbb715e11963atagen2026-10-24 23:24 UTC882
        });
cbb715e11963atagen2026-10-24 23:24 UTC883
        let highlight = actions.as_ref().is_some_and(|actions| actions.iter().any(|action| matches!(action, ruma::push::Action::SetTweak(ruma::push::Tweak::Highlight(value)) if value != &ruma::push::HighlightTweakValue::No)));
cbb715e11963atagen2026-10-24 23:24 UTC884
        let direct = value["content"]["m.mentions"]["user_ids"]
cbb715e11963atagen2026-10-24 23:24 UTC885
            .as_array()
cbb715e11963atagen2026-10-24 23:24 UTC886
            .is_some_and(|users| {
cbb715e11963atagen2026-10-24 23:24 UTC887
                users
cbb715e11963atagen2026-10-24 23:24 UTC888
                    .iter()
cbb715e11963atagen2026-10-24 23:24 UTC889
                    .any(|user| user.as_str() == Some(self.account.as_str()))
cbb715e11963atagen2026-10-24 23:24 UTC890
            });
cbb715e11963atagen2026-10-24 23:24 UTC891
        let mut reasons = Vec::new();
cbb715e11963atagen2026-10-24 23:24 UTC892
        if direct {
cbb715e11963atagen2026-10-24 23:24 UTC893
            reasons.push(MentionReason::Direct);
cbb715e11963atagen2026-10-24 23:24 UTC894
        }
cbb715e11963atagen2026-10-24 23:24 UTC895
        let mut unresolved = false;
cbb715e11963atagen2026-10-24 23:24 UTC896
        if let Some(reply) = value["content"]["m.relates_to"]["m.in_reply_to"]["event_id"]
cbb715e11963atagen2026-10-24 23:24 UTC897
            .as_str()
cbb715e11963atagen2026-10-24 23:24 UTC898
            .and_then(|value| value.parse().ok())
cbb715e11963atagen2026-10-24 23:24 UTC899
        {
cbb715e11963atagen2026-10-24 23:24 UTC900
            let target = data.events.get(&id(&MentionKey {
cbb715e11963atagen2026-10-24 23:24 UTC901
                room_id: room_id.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC902
                event_id: reply,
cbb715e11963atagen2026-10-24 23:24 UTC903
            }));
cbb715e11963atagen2026-10-24 23:24 UTC904
            let reply_to_self = value["unsigned"]["macltok_reply_sender"].as_str() == Some(self.account.as_str()) || target.is_some_and(|target| matches!(&target.item, TimelineItem::Event(event) if event.sender.user_id == self.account));
cbb715e11963atagen2026-10-24 23:24 UTC905
            if reply_to_self && (direct || (notify && highlight)) {
cbb715e11963atagen2026-10-24 23:24 UTC906
                reasons.push(MentionReason::Reply);
cbb715e11963atagen2026-10-24 23:24 UTC907
            } else if target.is_none()
cbb715e11963atagen2026-10-24 23:24 UTC908
                && value["unsigned"]["macltok_reply_sender"].as_str().is_none()
cbb715e11963atagen2026-10-24 23:24 UTC909
                && (direct || (notify && highlight))
cbb715e11963atagen2026-10-24 23:24 UTC910
            {
cbb715e11963atagen2026-10-24 23:24 UTC911
                unresolved = true;
cbb715e11963atagen2026-10-24 23:24 UTC912
            }
cbb715e11963atagen2026-10-24 23:24 UTC913
        }
cbb715e11963atagen2026-10-24 23:24 UTC914
        if value["content"]["m.mentions"]["room"] == true {
cbb715e11963atagen2026-10-24 23:24 UTC915
            match room.power_levels().await {
cbb715e11963atagen2026-10-24 23:24 UTC916
                Ok(levels) if levels.user_can_trigger_room_notification(&sender) => {
cbb715e11963atagen2026-10-24 23:24 UTC917
                    if notify && highlight {
cbb715e11963atagen2026-10-24 23:24 UTC918
                        reasons.push(MentionReason::Room);
cbb715e11963atagen2026-10-24 23:24 UTC919
                    } else if actions.is_none() {
cbb715e11963atagen2026-10-24 23:24 UTC920
                        unresolved = true;
cbb715e11963atagen2026-10-24 23:24 UTC921
                    }
cbb715e11963atagen2026-10-24 23:24 UTC922
                }
cbb715e11963atagen2026-10-24 23:24 UTC923
                Err(_) => unresolved = true,
cbb715e11963atagen2026-10-24 23:24 UTC924
                _ => {}
cbb715e11963atagen2026-10-24 23:24 UTC925
            }
cbb715e11963atagen2026-10-24 23:24 UTC926
        }
cbb715e11963atagen2026-10-24 23:24 UTC927
        (reasons, !notify, unresolved)
cbb715e11963atagen2026-10-24 23:24 UTC928
    }
cbb715e11963atagen2026-10-24 23:24 UTC929
cbb715e11963atagen2026-10-24 23:24 UTC930
    pub(crate) async fn run(mut self) {
cbb715e11963atagen2026-10-24 23:24 UTC931
        let reports_client = self.client.clone();
cbb715e11963atagen2026-10-24 23:24 UTC932
        let reports = reports_client
cbb715e11963atagen2026-10-24 23:24 UTC933
            .event_cache()
cbb715e11963atagen2026-10-24 23:24 UTC934
            .subscribe_to_decryption_reports();
cbb715e11963atagen2026-10-24 23:24 UTC935
        futures_util::pin_mut!(reports);
cbb715e11963atagen2026-10-24 23:24 UTC936
        let mut ignores = self.client.subscribe_to_ignore_user_list_changes();
cbb715e11963atagen2026-10-24 23:24 UTC937
        let mut work: FuturesUnordered<EngineBoxFuture<'static, Work>> = FuturesUnordered::new();
cbb715e11963atagen2026-10-24 23:24 UTC938
        let mut captures = BTreeSet::new();
cbb715e11963atagen2026-10-24 23:24 UTC939
        let mut retries = BTreeSet::new();
cbb715e11963atagen2026-10-24 23:24 UTC940
        let mut history = false;
cbb715e11963atagen2026-10-24 23:24 UTC941
        let mut next_scan = now() + 60_000;
cbb715e11963atagen2026-10-24 23:24 UTC942
        let mut tick = Box::pin(matrix_sdk::sleep::sleep(Duration::from_secs(1)));
cbb715e11963atagen2026-10-24 23:24 UTC943
        loop {
cbb715e11963atagen2026-10-24 23:24 UTC944
            self.schedule(&mut work, &mut captures, &mut retries, &mut history);
cbb715e11963atagen2026-10-24 23:24 UTC945
            tokio::select! {
cbb715e11963atagen2026-10-24 23:24 UTC946
                biased;
cbb715e11963atagen2026-10-24 23:24 UTC947
                request = self.requests.recv() => match request {
cbb715e11963atagen2026-10-24 23:24 UTC948
                    Some(Request::Stop(reply)) => { let _ = reply.send(()); return; }
cbb715e11963atagen2026-10-24 23:24 UTC949
                    Some(Request::Batch(batch, reply)) => { let result = self.batch(batch).await; let _ = reply.send(result); }
cbb715e11963atagen2026-10-24 23:24 UTC950
                    Some(Request::Command(command, reply)) => { let result = self.command(command).await; let _ = reply.send(result); }
80381e06a573atagen2026-10-25 03:02 UTC951
                    Some(Request::GetRepairPolicy(reply)) => { let _ = reply.send(Ok(self.data.repair_policy.clone())); }
80381e06a573atagen2026-10-25 03:02 UTC952
                    Some(Request::SetRepairPolicy(policy, reply)) => { let result = self.set_repair_policy(policy).await; let _ = reply.send(result); }
80381e06a573atagen2026-10-25 03:02 UTC953
                    Some(Request::GetRepairCoverage(room_id, reply)) => { let coverage = self.data.repair_coverage.get(&room_id).cloned().unwrap_or_default(); let _ = reply.send(Ok(coverage)); }
80381e06a573atagen2026-10-25 03:02 UTC954
                    Some(Request::RepairResolved(room_id, reply)) => { let result = self.repair_resolved(room_id).await; let _ = reply.send(result); }
cbb715e11963atagen2026-10-24 23:24 UTC955
                    None => return,
cbb715e11963atagen2026-10-24 23:24 UTC956
                },
cbb715e11963atagen2026-10-24 23:24 UTC957
                Some(result) = work.next(), if !work.is_empty() => {
cbb715e11963atagen2026-10-24 23:24 UTC958
                    let mut next = self.data.clone();
cbb715e11963atagen2026-10-24 23:24 UTC959
                    let recovery = (self.latest_scan, self.recovery_budget);
cbb715e11963atagen2026-10-24 23:24 UTC960
                    match result {
cbb715e11963atagen2026-10-24 23:24 UTC961
                        Work::Capture(key, revision, result) => {
cbb715e11963atagen2026-10-24 23:24 UTC962
                            captures.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC963
                            let observed = result.as_ref().ok().map(|(_, _, observed)| observed.clone()).unwrap_or_default();
cbb715e11963atagen2026-10-24 23:24 UTC964
                            let room_id = next.entries.get(&key).filter(|entry| entry.revision == revision)
cbb715e11963atagen2026-10-24 23:24 UTC965
                                .map(|entry| entry.key.room_id.clone())
cbb715e11963atagen2026-10-24 23:24 UTC966
                                .filter(|room_id| self.client.get_room(room_id).is_some_and(|room| room.state() == RoomState::Joined));
cbb715e11963atagen2026-10-24 23:24 UTC967
                            self.captured(&mut next, key, revision, result);
cbb715e11963atagen2026-10-24 23:24 UTC968
                            if let Some(room_id) = room_id {
cbb715e11963atagen2026-10-24 23:24 UTC969
                                for value in observed {
cbb715e11963atagen2026-10-24 23:24 UTC970
                                    let identity = value["event_id"].as_str().map(|event_id| format!("{room_id}\n{event_id}"));
cbb715e11963atagen2026-10-24 23:24 UTC971
                                    if identity.as_ref().is_some_and(|key| next.events.get(key).is_some_and(|event| event.raw.is_some())) { continue; }
cbb715e11963atagen2026-10-24 23:24 UTC972
                                    self.observe(&mut next, &room_id, value).await.ok();
cbb715e11963atagen2026-10-24 23:24 UTC973
                                }
cbb715e11963atagen2026-10-24 23:24 UTC974
                            }
cbb715e11963atagen2026-10-24 23:24 UTC975
                        }
cbb715e11963atagen2026-10-24 23:24 UTC976
                        Work::Candidate(key, result) => {
cbb715e11963atagen2026-10-24 23:24 UTC977
                            retries.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC978
                            if let Some(mut candidate) = next.candidates.remove(&key) {
cbb715e11963atagen2026-10-24 23:24 UTC979
                                match result {
cbb715e11963atagen2026-10-24 23:24 UTC980
                                    Ok(value) => {
cbb715e11963atagen2026-10-24 23:24 UTC981
                                        if let Err(error) = self.observe(&mut next, &candidate.room_id, value).await { tracing::warn!(%error, "mention retry failed"); }
cbb715e11963atagen2026-10-24 23:24 UTC982
                                        if let Some(unresolved) = next.candidates.get_mut(&key) { unresolved.attempts = candidate.attempts.saturating_add(1); unresolved.retry_at_ms = now() + (1000u64 << unresolved.attempts.min(8)); }
cbb715e11963atagen2026-10-24 23:24 UTC983
                                    }
cbb715e11963atagen2026-10-24 23:24 UTC984
                                    Err(error) => {
cbb715e11963atagen2026-10-24 23:24 UTC985
                                        if candidate.raw["type"] != "m.room.encrypted" && (matches!(error.kind, ErrorKind::EventNotFound | ErrorKind::Forbidden) || error.server.as_ref().is_some_and(|server| matches!(server.status, 403 | 404))) {
cbb715e11963atagen2026-10-24 23:24 UTC986
                                            Self::gap(&mut next, Some(candidate.room_id), "mention notification context is unavailable", None);
cbb715e11963atagen2026-10-24 23:24 UTC987
                                        } else { candidate.attempts = candidate.attempts.saturating_add(1); candidate.retry_at_ms = retry_at(&error, 1000u64 << candidate.attempts.min(8)); next.candidates.insert(key, candidate); }
cbb715e11963atagen2026-10-24 23:24 UTC988
                                    }
cbb715e11963atagen2026-10-24 23:24 UTC989
                                }
cbb715e11963atagen2026-10-24 23:24 UTC990
                            }
cbb715e11963atagen2026-10-24 23:24 UTC991
                        }
cbb715e11963atagen2026-10-24 23:24 UTC992
                        Work::History(latest, from, result) => {
cbb715e11963atagen2026-10-24 23:24 UTC993
                            history = false; self.recovery_budget = self.recovery_budget.saturating_sub(1);
cbb715e11963atagen2026-10-24 23:24 UTC994
                            match result {
cbb715e11963atagen2026-10-24 23:24 UTC995
                                Ok(page) => {
cbb715e11963atagen2026-10-24 23:24 UTC996
                                    next.coverage.historical_supported = Some(true);
cbb715e11963atagen2026-10-24 23:24 UTC997
                                    let mut reached_anchor = false;
cbb715e11963atagen2026-10-24 23:24 UTC998
                                    for notification in page.notifications {
cbb715e11963atagen2026-10-24 23:24 UTC999
                                        if let Some(event_id) = notification.event.get_field::<String>("event_id").ok().flatten() {
cbb715e11963atagen2026-10-24 23:24 UTC1000
                                            let identity = format!("{}\n{}", notification.room_id, event_id);
cbb715e11963atagen2026-10-24 23:24 UTC1001
                                            if latest && from.is_none() && next.latest_head.is_none() { next.latest_head = Some(identity.clone()); }
cbb715e11963atagen2026-10-24 23:24 UTC1002
                                            if latest && next.latest_anchor.as_ref() == Some(&identity) { reached_anchor = true; break; }
cbb715e11963atagen2026-10-24 23:24 UTC1003
                                        }
cbb715e11963atagen2026-10-24 23:24 UTC1004
                                        if self.client.get_room(&notification.room_id).is_some_and(|room| room.state() == RoomState::Joined) {
cbb715e11963atagen2026-10-24 23:24 UTC1005
                                        if let Ok(value) = serde_json::from_str(notification.event.json().get()) { let _ = self.observe(&mut next, &notification.room_id, value).await; }
cbb715e11963atagen2026-10-24 23:24 UTC1006
                                    }}
cbb715e11963atagen2026-10-24 23:24 UTC1007
                                    let token = page.next_token.filter(|token| Some(token) != from.as_ref() && !reached_anchor);
cbb715e11963atagen2026-10-24 23:24 UTC1008
                                    if latest { next.latest_cursor = token.clone(); if token.is_none() { self.latest_scan = false; next.latest_anchor = next.latest_head.take().or(next.latest_anchor); } }
cbb715e11963atagen2026-10-24 23:24 UTC1009
                                    else { next.history_cursor = token.clone(); next.history_exhausted = token.is_none(); }
cbb715e11963atagen2026-10-24 23:24 UTC1010
                                }
cbb715e11963atagen2026-10-24 23:24 UTC1011
                                Err(error) => {
cbb715e11963atagen2026-10-24 23:24 UTC1012
                                    if error.server.as_ref().is_some_and(|server| matches!(server.status, 404 | 405 | 501)) { next.coverage.historical_supported = Some(false); self.recovery_budget = 0; }
cbb715e11963atagen2026-10-24 23:24 UTC1013
                                    if error.server.as_ref().is_some_and(|server| server.status == 400) {
cbb715e11963atagen2026-10-24 23:24 UTC1014
                                        if latest { next.latest_cursor = None; next.latest_head = None; } else { next.history_cursor = None; }
cbb715e11963atagen2026-10-24 23:24 UTC1015
                                    }
cbb715e11963atagen2026-10-24 23:24 UTC1016
                                    Self::gap(&mut next, None, "notification history could not be recovered", from);
cbb715e11963atagen2026-10-24 23:24 UTC1017
                                    self.recovery_retry = retry_at(&error, 60_000);
cbb715e11963atagen2026-10-24 23:24 UTC1018
                                }
cbb715e11963atagen2026-10-24 23:24 UTC1019
                            }
cbb715e11963atagen2026-10-24 23:24 UTC1020
                            next.coverage.recovering = self.recovery_budget > 0 && (self.latest_scan || !next.history_exhausted) && next.coverage.historical_supported != Some(false);
cbb715e11963atagen2026-10-24 23:24 UTC1021
                        }
cbb715e11963atagen2026-10-24 23:24 UTC1022
                    }
cbb715e11963atagen2026-10-24 23:24 UTC1023
                    if let Err(error) = self.save(next).await { tracing::error!(%error, "saving mention background work failed"); self.latest_scan = recovery.0; self.recovery_budget = recovery.1; self.recovery_retry = now() + 60_000; }
cbb715e11963atagen2026-10-24 23:24 UTC1024
                }
cbb715e11963atagen2026-10-24 23:24 UTC1025
                Some(_) = reports.next() => { for candidate in self.data.candidates.values_mut() { candidate.retry_at_ms = 0; } }
cbb715e11963atagen2026-10-24 23:24 UTC1026
                Some(_) = ignores.next() => {
cbb715e11963atagen2026-10-24 23:24 UTC1027
                    let mut next = self.data.clone(); self.filter_ignored(&mut next).await;
cbb715e11963atagen2026-10-24 23:24 UTC1028
                    if let Err(error) = self.save(next).await { tracing::error!(%error, "saving ignored mention removals failed"); }
cbb715e11963atagen2026-10-24 23:24 UTC1029
                }
cbb715e11963atagen2026-10-24 23:24 UTC1030
                () = &mut tick => {
cbb715e11963atagen2026-10-24 23:24 UTC1031
                    tick = Box::pin(matrix_sdk::sleep::sleep(Duration::from_secs(1)));
cbb715e11963atagen2026-10-24 23:24 UTC1032
                    if now() >= next_scan && !history { self.recovery_budget = 20; self.latest_scan = true; next_scan = now() + 60_000; }
cbb715e11963atagen2026-10-24 23:24 UTC1033
                }
cbb715e11963atagen2026-10-24 23:24 UTC1034
            }
cbb715e11963atagen2026-10-24 23:24 UTC1035
        }
cbb715e11963atagen2026-10-24 23:24 UTC1036
    }
cbb715e11963atagen2026-10-24 23:24 UTC1037
cbb715e11963atagen2026-10-24 23:24 UTC1038
    fn schedule(
cbb715e11963atagen2026-10-24 23:24 UTC1039
        &mut self,
cbb715e11963atagen2026-10-24 23:24 UTC1040
        work: &mut FuturesUnordered<EngineBoxFuture<'static, Work>>,
cbb715e11963atagen2026-10-24 23:24 UTC1041
        captures: &mut BTreeSet<String>,
cbb715e11963atagen2026-10-24 23:24 UTC1042
        retries: &mut BTreeSet<String>,
cbb715e11963atagen2026-10-24 23:24 UTC1043
        history: &mut bool,
cbb715e11963atagen2026-10-24 23:24 UTC1044
    ) {
cbb715e11963atagen2026-10-24 23:24 UTC1045
        for (key, entry) in self.data.entries.iter().filter(|(_, entry)| matches!(entry.capture, MentionCaptureState::Pending) || matches!(entry.capture, MentionCaptureState::Failed {retry_at_ms, ..} if retry_at_ms <= now())).take(128) {
cbb715e11963atagen2026-10-24 23:24 UTC1046
            if captures.len() >= 2 { break; }
cbb715e11963atagen2026-10-24 23:24 UTC1047
            if captures.contains(key) { continue; }
cbb715e11963atagen2026-10-24 23:24 UTC1048
            let Some(room) = self.client.get_room(&entry.key.room_id).filter(|room| room.state() == RoomState::Joined) else { continue; };
cbb715e11963atagen2026-10-24 23:24 UTC1049
            captures.insert(key.clone());
cbb715e11963atagen2026-10-24 23:24 UTC1050
            let key = key.clone(); let event_id = entry.key.event_id.clone(); let account = self.account.clone(); let revision = entry.revision;
cbb715e11963atagen2026-10-24 23:24 UTC1051
            work.push(Box::pin(async move { Work::Capture(key, revision, context::capture(room, Some(account), event_id, 10, 10).await) }));
cbb715e11963atagen2026-10-24 23:24 UTC1052
        }
cbb715e11963atagen2026-10-24 23:24 UTC1053
        for (key, candidate) in &self.data.candidates {
cbb715e11963atagen2026-10-24 23:24 UTC1054
            if retries.len() >= 2 {
cbb715e11963atagen2026-10-24 23:24 UTC1055
                break;
cbb715e11963atagen2026-10-24 23:24 UTC1056
            }
cbb715e11963atagen2026-10-24 23:24 UTC1057
            if candidate.retry_at_ms > now() || retries.contains(key) {
cbb715e11963atagen2026-10-24 23:24 UTC1058
                continue;
cbb715e11963atagen2026-10-24 23:24 UTC1059
            }
cbb715e11963atagen2026-10-24 23:24 UTC1060
            let Some(room) = self
cbb715e11963atagen2026-10-24 23:24 UTC1061
                .client
cbb715e11963atagen2026-10-24 23:24 UTC1062
                .get_room(&candidate.room_id)
cbb715e11963atagen2026-10-24 23:24 UTC1063
                .filter(|room| room.state() == RoomState::Joined)
cbb715e11963atagen2026-10-24 23:24 UTC1064
            else {
cbb715e11963atagen2026-10-24 23:24 UTC1065
                continue;
cbb715e11963atagen2026-10-24 23:24 UTC1066
            };
cbb715e11963atagen2026-10-24 23:24 UTC1067
            let key = key.clone();
cbb715e11963atagen2026-10-24 23:24 UTC1068
            let candidate = candidate.clone();
cbb715e11963atagen2026-10-24 23:24 UTC1069
            let key_for_work = key.clone();
cbb715e11963atagen2026-10-24 23:24 UTC1070
            retries.insert(key);
cbb715e11963atagen2026-10-24 23:24 UTC1071
            work.push(Box::pin(async move {
cbb715e11963atagen2026-10-24 23:24 UTC1072
                let result = async {
cbb715e11963atagen2026-10-24 23:24 UTC1073
                    if candidate.raw["type"] == "m.room.encrypted" {
cbb715e11963atagen2026-10-24 23:24 UTC1074
                        let raw: Raw<OriginalSyncRoomEncryptedEvent> =
cbb715e11963atagen2026-10-24 23:24 UTC1075
                            Raw::from_json_string(candidate.raw.to_string()).map_err(|error| {
cbb715e11963atagen2026-10-24 23:24 UTC1076
                                Error::with_source(
cbb715e11963atagen2026-10-24 23:24 UTC1077
                                    ErrorKind::Invalid,
cbb715e11963atagen2026-10-24 23:24 UTC1078
                                    "invalid encrypted mention candidate",
cbb715e11963atagen2026-10-24 23:24 UTC1079
                                    error,
cbb715e11963atagen2026-10-24 23:24 UTC1080
                                )
cbb715e11963atagen2026-10-24 23:24 UTC1081
                            })?;
cbb715e11963atagen2026-10-24 23:24 UTC1082
                        let event = room.decrypt_event(&raw, None).await.map_err(|error| {
cbb715e11963atagen2026-10-24 23:24 UTC1083
                            Error::from_sdk("decrypting a mention candidate failed", error)
cbb715e11963atagen2026-10-24 23:24 UTC1084
                        })?;
cbb715e11963atagen2026-10-24 23:24 UTC1085
                        let value: Value =
cbb715e11963atagen2026-10-24 23:24 UTC1086
                            serde_json::from_str(event.raw().json().get()).map_err(|error| {
cbb715e11963atagen2026-10-24 23:24 UTC1087
                                Error::with_source(
cbb715e11963atagen2026-10-24 23:24 UTC1088
                                    ErrorKind::Invalid,
cbb715e11963atagen2026-10-24 23:24 UTC1089
                                    "invalid decrypted mention",
cbb715e11963atagen2026-10-24 23:24 UTC1090
                                    error,
cbb715e11963atagen2026-10-24 23:24 UTC1091
                                )
cbb715e11963atagen2026-10-24 23:24 UTC1092
                            })?;
cbb715e11963atagen2026-10-24 23:24 UTC1093
                        if value["type"] == "m.room.encrypted" {
cbb715e11963atagen2026-10-24 23:24 UTC1094
                            return Err(Error::new(
cbb715e11963atagen2026-10-24 23:24 UTC1095
                                ErrorKind::Encryption,
cbb715e11963atagen2026-10-24 23:24 UTC1096
                                "mention candidate still awaits keys",
cbb715e11963atagen2026-10-24 23:24 UTC1097
                            ));
cbb715e11963atagen2026-10-24 23:24 UTC1098
                        }
cbb715e11963atagen2026-10-24 23:24 UTC1099
                        return Ok(value);
cbb715e11963atagen2026-10-24 23:24 UTC1100
                    }
cbb715e11963atagen2026-10-24 23:24 UTC1101
                    let target = candidate.raw["content"]["m.relates_to"]["event_id"]
cbb715e11963atagen2026-10-24 23:24 UTC1102
                        .as_str()
cbb715e11963atagen2026-10-24 23:24 UTC1103
                        .filter(|_| {
cbb715e11963atagen2026-10-24 23:24 UTC1104
                            candidate.raw["content"]["m.relates_to"]["rel_type"] == "m.replace"
cbb715e11963atagen2026-10-24 23:24 UTC1105
                        });
cbb715e11963atagen2026-10-24 23:24 UTC1106
                    if let Some(target) =
cbb715e11963atagen2026-10-24 23:24 UTC1107
                        target.and_then(|target| target.parse::<ruma::OwnedEventId>().ok())
cbb715e11963atagen2026-10-24 23:24 UTC1108
                    {
cbb715e11963atagen2026-10-24 23:24 UTC1109
                        let event = room.event(&target, None).await.map_err(|error| {
cbb715e11963atagen2026-10-24 23:24 UTC1110
                            Error::from_sdk("fetching an edited mention failed", error)
cbb715e11963atagen2026-10-24 23:24 UTC1111
                        })?;
cbb715e11963atagen2026-10-24 23:24 UTC1112
                        let original: Value = serde_json::from_str(event.raw().json().get())
cbb715e11963atagen2026-10-24 23:24 UTC1113
                            .map_err(|error| {
cbb715e11963atagen2026-10-24 23:24 UTC1114
                                Error::with_source(
cbb715e11963atagen2026-10-24 23:24 UTC1115
                                    ErrorKind::Invalid,
cbb715e11963atagen2026-10-24 23:24 UTC1116
                                    "invalid edited mention",
cbb715e11963atagen2026-10-24 23:24 UTC1117
                                    error,
cbb715e11963atagen2026-10-24 23:24 UTC1118
                                )
cbb715e11963atagen2026-10-24 23:24 UTC1119
                            })?;
cbb715e11963atagen2026-10-24 23:24 UTC1120
                        if original["sender"] == candidate.raw["sender"] {
cbb715e11963atagen2026-10-24 23:24 UTC1121
                            let mut original = edited(original, &candidate.raw);
cbb715e11963atagen2026-10-24 23:24 UTC1122
                            original["unsigned"]["macltok_edit"] = candidate.raw;
cbb715e11963atagen2026-10-24 23:24 UTC1123
                            return Ok(original);
cbb715e11963atagen2026-10-24 23:24 UTC1124
                        }
cbb715e11963atagen2026-10-24 23:24 UTC1125
                        return Err(Error::new(
cbb715e11963atagen2026-10-24 23:24 UTC1126
                            ErrorKind::Forbidden,
cbb715e11963atagen2026-10-24 23:24 UTC1127
                            "an edit belongs to a different sender",
cbb715e11963atagen2026-10-24 23:24 UTC1128
                        ));
cbb715e11963atagen2026-10-24 23:24 UTC1129
                    }
cbb715e11963atagen2026-10-24 23:24 UTC1130
                    let mut value = candidate.raw;
cbb715e11963atagen2026-10-24 23:24 UTC1131
                    if let Some(target) =
cbb715e11963atagen2026-10-24 23:24 UTC1132
                        value["content"]["m.relates_to"]["m.in_reply_to"]["event_id"]
cbb715e11963atagen2026-10-24 23:24 UTC1133
                            .as_str()
cbb715e11963atagen2026-10-24 23:24 UTC1134
                            .and_then(|target| target.parse::<ruma::OwnedEventId>().ok())
cbb715e11963atagen2026-10-24 23:24 UTC1135
                    {
cbb715e11963atagen2026-10-24 23:24 UTC1136
                        let event = room.event(&target, None).await.map_err(|error| {
cbb715e11963atagen2026-10-24 23:24 UTC1137
                            Error::from_sdk("fetching a mention reply target failed", error)
cbb715e11963atagen2026-10-24 23:24 UTC1138
                        })?;
cbb715e11963atagen2026-10-24 23:24 UTC1139
                        let target: Value = serde_json::from_str(event.raw().json().get())
cbb715e11963atagen2026-10-24 23:24 UTC1140
                            .map_err(|error| {
cbb715e11963atagen2026-10-24 23:24 UTC1141
                                Error::with_source(
cbb715e11963atagen2026-10-24 23:24 UTC1142
                                    ErrorKind::Invalid,
cbb715e11963atagen2026-10-24 23:24 UTC1143
                                    "invalid mention reply target",
cbb715e11963atagen2026-10-24 23:24 UTC1144
                                    error,
cbb715e11963atagen2026-10-24 23:24 UTC1145
                                )
cbb715e11963atagen2026-10-24 23:24 UTC1146
                            })?;
cbb715e11963atagen2026-10-24 23:24 UTC1147
                        value["unsigned"]["macltok_reply_sender"] = target["sender"].clone();
cbb715e11963atagen2026-10-24 23:24 UTC1148
                    }
cbb715e11963atagen2026-10-24 23:24 UTC1149
                    Ok(value)
cbb715e11963atagen2026-10-24 23:24 UTC1150
                }
cbb715e11963atagen2026-10-24 23:24 UTC1151
                .await;
cbb715e11963atagen2026-10-24 23:24 UTC1152
                Work::Candidate(key_for_work, result)
cbb715e11963atagen2026-10-24 23:24 UTC1153
            }));
cbb715e11963atagen2026-10-24 23:24 UTC1154
        }
cbb715e11963atagen2026-10-24 23:24 UTC1155
        if !*history
cbb715e11963atagen2026-10-24 23:24 UTC1156
            && self.recovery_budget > 0
cbb715e11963atagen2026-10-24 23:24 UTC1157
            && now() >= self.recovery_retry
cbb715e11963atagen2026-10-24 23:24 UTC1158
            && (self.latest_scan || !self.data.history_exhausted)
cbb715e11963atagen2026-10-24 23:24 UTC1159
            && self.data.coverage.historical_supported != Some(false)
cbb715e11963atagen2026-10-24 23:24 UTC1160
        {
cbb715e11963atagen2026-10-24 23:24 UTC1161
            *history = true;
cbb715e11963atagen2026-10-24 23:24 UTC1162
            let latest = self.latest_scan;
cbb715e11963atagen2026-10-24 23:24 UTC1163
            let from = if latest {
cbb715e11963atagen2026-10-24 23:24 UTC1164
                self.data.latest_cursor.clone()
cbb715e11963atagen2026-10-24 23:24 UTC1165
            } else {
cbb715e11963atagen2026-10-24 23:24 UTC1166
                self.data.history_cursor.clone()
cbb715e11963atagen2026-10-24 23:24 UTC1167
            };
cbb715e11963atagen2026-10-24 23:24 UTC1168
            let client = self.client.clone();
cbb715e11963atagen2026-10-24 23:24 UTC1169
            work.push(Box::pin(async move {
cbb715e11963atagen2026-10-24 23:24 UTC1170
                let mut request = ruma::api::client::push::get_notifications::v3::Request::new();
cbb715e11963atagen2026-10-24 23:24 UTC1171
                request.from = from.clone();
cbb715e11963atagen2026-10-24 23:24 UTC1172
                request.limit = Some(ruma::uint!(50));
cbb715e11963atagen2026-10-24 23:24 UTC1173
                let result = client.send(request).await.map_err(|error| {
cbb715e11963atagen2026-10-24 23:24 UTC1174
                    Error::from_sdk("fetching mention history failed", error.into())
cbb715e11963atagen2026-10-24 23:24 UTC1175
                });
cbb715e11963atagen2026-10-24 23:24 UTC1176
                Work::History(latest, from, result)
cbb715e11963atagen2026-10-24 23:24 UTC1177
            }));
cbb715e11963atagen2026-10-24 23:24 UTC1178
        }
cbb715e11963atagen2026-10-24 23:24 UTC1179
    }
cbb715e11963atagen2026-10-24 23:24 UTC1180
cbb715e11963atagen2026-10-24 23:24 UTC1181
    fn captured(
cbb715e11963atagen2026-10-24 23:24 UTC1182
        &self,
cbb715e11963atagen2026-10-24 23:24 UTC1183
        data: &mut Data,
cbb715e11963atagen2026-10-24 23:24 UTC1184
        key: String,
cbb715e11963atagen2026-10-24 23:24 UTC1185
        revision: u64,
cbb715e11963atagen2026-10-24 23:24 UTC1186
        result: Result<(crate::EventContext, Vec<MediaRegistration>, Vec<Value>), Error>,
cbb715e11963atagen2026-10-24 23:24 UTC1187
    ) {
cbb715e11963atagen2026-10-24 23:24 UTC1188
        let Some(entry) = data.entries.get(&key) else {
cbb715e11963atagen2026-10-24 23:24 UTC1189
            return;
cbb715e11963atagen2026-10-24 23:24 UTC1190
        };
cbb715e11963atagen2026-10-24 23:24 UTC1191
        if entry.revision != revision {
cbb715e11963atagen2026-10-24 23:24 UTC1192
            return;
cbb715e11963atagen2026-10-24 23:24 UTC1193
        }
cbb715e11963atagen2026-10-24 23:24 UTC1194
        let room_id = entry.key.room_id.clone();
cbb715e11963atagen2026-10-24 23:24 UTC1195
        if !self
cbb715e11963atagen2026-10-24 23:24 UTC1196
            .client
cbb715e11963atagen2026-10-24 23:24 UTC1197
            .get_room(&room_id)
cbb715e11963atagen2026-10-24 23:24 UTC1198
            .is_some_and(|room| room.state() == RoomState::Joined)
cbb715e11963atagen2026-10-24 23:24 UTC1199
        {
cbb715e11963atagen2026-10-24 23:24 UTC1200
            return;
cbb715e11963atagen2026-10-24 23:24 UTC1201
        }
cbb715e11963atagen2026-10-24 23:24 UTC1202
        match result {
cbb715e11963atagen2026-10-24 23:24 UTC1203
            Ok((context, media, _)) => {
cbb715e11963atagen2026-10-24 23:24 UTC1204
                data.capture_attempts.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC1205
                let live_after = data
cbb715e11963atagen2026-10-24 23:24 UTC1206
                    .windows
cbb715e11963atagen2026-10-24 23:24 UTC1207
                    .get(&key)
cbb715e11963atagen2026-10-24 23:24 UTC1208
                    .map(|window| window.after.clone())
cbb715e11963atagen2026-10-24 23:24 UTC1209
                    .unwrap_or_default();
cbb715e11963atagen2026-10-24 23:24 UTC1210
                let mut insert = |item: TimelineItem| -> Option<String> {
cbb715e11963atagen2026-10-24 23:24 UTC1211
                    let TimelineItem::Event(event) = &item else {
cbb715e11963atagen2026-10-24 23:24 UTC1212
                        return None;
cbb715e11963atagen2026-10-24 23:24 UTC1213
                    };
cbb715e11963atagen2026-10-24 23:24 UTC1214
                    let event_id = event.event_id.clone()?;
cbb715e11963atagen2026-10-24 23:24 UTC1215
                    let event_key = id(&MentionKey {
cbb715e11963atagen2026-10-24 23:24 UTC1216
                        room_id: room_id.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC1217
                        event_id,
cbb715e11963atagen2026-10-24 23:24 UTC1218
                    });
cbb715e11963atagen2026-10-24 23:24 UTC1219
                    let registrations = media_for_item(&item, &media);
cbb715e11963atagen2026-10-24 23:24 UTC1220
                    let mut saved = SavedEvent {
cbb715e11963atagen2026-10-24 23:24 UTC1221
                        item,
cbb715e11963atagen2026-10-24 23:24 UTC1222
                        media: registrations,
cbb715e11963atagen2026-10-24 23:24 UTC1223
                        raw: None,
cbb715e11963atagen2026-10-24 23:24 UTC1224
                    };
cbb715e11963atagen2026-10-24 23:24 UTC1225
                    if let Some(existing) = data.events.get(&event_key) {
cbb715e11963atagen2026-10-24 23:24 UTC1226
                        saved = existing.clone();
cbb715e11963atagen2026-10-24 23:24 UTC1227
                    }
cbb715e11963atagen2026-10-24 23:24 UTC1228
                    if data.redacted.contains_key(&event_key) {
cbb715e11963atagen2026-10-24 23:24 UTC1229
                        redact(&mut saved);
cbb715e11963atagen2026-10-24 23:24 UTC1230
                    }
cbb715e11963atagen2026-10-24 23:24 UTC1231
                    data.events.insert(event_key.clone(), saved);
cbb715e11963atagen2026-10-24 23:24 UTC1232
                    Some(event_key)
cbb715e11963atagen2026-10-24 23:24 UTC1233
                };
cbb715e11963atagen2026-10-24 23:24 UTC1234
                let before: Vec<_> = context
cbb715e11963atagen2026-10-24 23:24 UTC1235
                    .events_before
cbb715e11963atagen2026-10-24 23:24 UTC1236
                    .into_iter()
cbb715e11963atagen2026-10-24 23:24 UTC1237
                    .filter_map(&mut insert)
cbb715e11963atagen2026-10-24 23:24 UTC1238
                    .collect();
cbb715e11963atagen2026-10-24 23:24 UTC1239
                let target = context.event.and_then(&mut insert);
cbb715e11963atagen2026-10-24 23:24 UTC1240
                let mut after: Vec<_> = context
cbb715e11963atagen2026-10-24 23:24 UTC1241
                    .events_after
cbb715e11963atagen2026-10-24 23:24 UTC1242
                    .into_iter()
cbb715e11963atagen2026-10-24 23:24 UTC1243
                    .filter_map(&mut insert)
cbb715e11963atagen2026-10-24 23:24 UTC1244
                    .collect();
cbb715e11963atagen2026-10-24 23:24 UTC1245
                let extended = live_after.iter().any(|key| !after.contains(key));
cbb715e11963atagen2026-10-24 23:24 UTC1246
                after.extend(live_after);
cbb715e11963atagen2026-10-24 23:24 UTC1247
                after.sort_by_key(|key| {
cbb715e11963atagen2026-10-24 23:24 UTC1248
                    (
cbb715e11963atagen2026-10-24 23:24 UTC1249
                        data.events
cbb715e11963atagen2026-10-24 23:24 UTC1250
                            .get(key)
cbb715e11963atagen2026-10-24 23:24 UTC1251
                            .and_then(|saved| match &saved.item {
cbb715e11963atagen2026-10-24 23:24 UTC1252
                                TimelineItem::Event(event) => Some(event.timestamp_ms),
cbb715e11963atagen2026-10-24 23:24 UTC1253
                                _ => None,
cbb715e11963atagen2026-10-24 23:24 UTC1254
                            })
cbb715e11963atagen2026-10-24 23:24 UTC1255
                            .unwrap_or(0),
cbb715e11963atagen2026-10-24 23:24 UTC1256
                        key.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC1257
                    )
cbb715e11963atagen2026-10-24 23:24 UTC1258
                });
cbb715e11963atagen2026-10-24 23:24 UTC1259
                after.dedup();
cbb715e11963atagen2026-10-24 23:24 UTC1260
                after.truncate(10);
cbb715e11963atagen2026-10-24 23:24 UTC1261
                let entry = data.entries.get_mut(&key).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC1262
                entry.capture = if target.is_none() {
cbb715e11963atagen2026-10-24 23:24 UTC1263
                    MentionCaptureState::Unavailable
cbb715e11963atagen2026-10-24 23:24 UTC1264
                } else if after.len() < 10 || (before.len() < 10 && context.start_token.is_some()) {
cbb715e11963atagen2026-10-24 23:24 UTC1265
                    MentionCaptureState::Partial
cbb715e11963atagen2026-10-24 23:24 UTC1266
                } else {
cbb715e11963atagen2026-10-24 23:24 UTC1267
                    MentionCaptureState::Ready
cbb715e11963atagen2026-10-24 23:24 UTC1268
                };
cbb715e11963atagen2026-10-24 23:24 UTC1269
                entry.revision = data.revision + 1;
cbb715e11963atagen2026-10-24 23:24 UTC1270
                data.windows.insert(
cbb715e11963atagen2026-10-24 23:24 UTC1271
                    key,
cbb715e11963atagen2026-10-24 23:24 UTC1272
                    Window {
cbb715e11963atagen2026-10-24 23:24 UTC1273
                        before,
cbb715e11963atagen2026-10-24 23:24 UTC1274
                        target,
cbb715e11963atagen2026-10-24 23:24 UTC1275
                        after,
cbb715e11963atagen2026-10-24 23:24 UTC1276
                        start: context.start_token,
cbb715e11963atagen2026-10-24 23:24 UTC1277
                        end: if extended { None } else { context.end_token },
cbb715e11963atagen2026-10-24 23:24 UTC1278
                    },
cbb715e11963atagen2026-10-24 23:24 UTC1279
                );
cbb715e11963atagen2026-10-24 23:24 UTC1280
            }
cbb715e11963atagen2026-10-24 23:24 UTC1281
            Err(error) => {
cbb715e11963atagen2026-10-24 23:24 UTC1282
                let attempts = data.capture_attempts.entry(key.clone()).or_default();
cbb715e11963atagen2026-10-24 23:24 UTC1283
                *attempts = attempts.saturating_add(1);
cbb715e11963atagen2026-10-24 23:24 UTC1284
                let delay = (1000u64 << (*attempts).min(11)).min(3_600_000);
cbb715e11963atagen2026-10-24 23:24 UTC1285
                let entry = data.entries.get_mut(&key).unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC1286
                entry.capture = if matches!(
cbb715e11963atagen2026-10-24 23:24 UTC1287
                    error.kind,
cbb715e11963atagen2026-10-24 23:24 UTC1288
                    ErrorKind::EventNotFound | ErrorKind::Forbidden | ErrorKind::RoomUnknown
cbb715e11963atagen2026-10-24 23:24 UTC1289
                ) || error
cbb715e11963atagen2026-10-24 23:24 UTC1290
                    .server
cbb715e11963atagen2026-10-24 23:24 UTC1291
                    .as_ref()
cbb715e11963atagen2026-10-24 23:24 UTC1292
                    .is_some_and(|server| matches!(server.status, 403 | 404))
cbb715e11963atagen2026-10-24 23:24 UTC1293
                {
cbb715e11963atagen2026-10-24 23:24 UTC1294
                    MentionCaptureState::Unavailable
cbb715e11963atagen2026-10-24 23:24 UTC1295
                } else {
cbb715e11963atagen2026-10-24 23:24 UTC1296
                    MentionCaptureState::Failed {
cbb715e11963atagen2026-10-24 23:24 UTC1297
                        message: error.to_string(),
cbb715e11963atagen2026-10-24 23:24 UTC1298
                        retry_at_ms: retry_at(&error, delay),
cbb715e11963atagen2026-10-24 23:24 UTC1299
                    }
cbb715e11963atagen2026-10-24 23:24 UTC1300
                };
cbb715e11963atagen2026-10-24 23:24 UTC1301
                entry.revision = data.revision + 1;
cbb715e11963atagen2026-10-24 23:24 UTC1302
            }
cbb715e11963atagen2026-10-24 23:24 UTC1303
        }
cbb715e11963atagen2026-10-24 23:24 UTC1304
    }
cbb715e11963atagen2026-10-24 23:24 UTC1305
}
cbb715e11963atagen2026-10-24 23:24 UTC1306
cbb715e11963atagen2026-10-24 23:24 UTC1307
fn edited(mut original: Value, edit: &Value) -> Value {
cbb715e11963atagen2026-10-24 23:24 UTC1308
    original["content"] = edit["content"]["m.new_content"].clone();
cbb715e11963atagen2026-10-24 23:24 UTC1309
    original["unsigned"]["macltok_edited"] = Value::Bool(true);
cbb715e11963atagen2026-10-24 23:24 UTC1310
    original
cbb715e11963atagen2026-10-24 23:24 UTC1311
}
cbb715e11963atagen2026-10-24 23:24 UTC1312
cbb715e11963atagen2026-10-24 23:24 UTC1313
fn retry_at(error: &Error, fallback_ms: u64) -> u64 {
cbb715e11963atagen2026-10-24 23:24 UTC1314
    let mut source = std::error::Error::source(error);
cbb715e11963atagen2026-10-24 23:24 UTC1315
    while let Some(current) = source {
cbb715e11963atagen2026-10-24 23:24 UTC1316
        if let Some(sdk) = current.downcast_ref::<matrix_sdk::Error>() {
cbb715e11963atagen2026-10-24 23:24 UTC1317
            if let Some(ruma::api::error::ErrorKind::LimitExceeded(data)) =
cbb715e11963atagen2026-10-24 23:24 UTC1318
                sdk.client_api_error_kind()
cbb715e11963atagen2026-10-24 23:24 UTC1319
            {
cbb715e11963atagen2026-10-24 23:24 UTC1320
                if let Some(delay) = data.retry_after {
cbb715e11963atagen2026-10-24 23:24 UTC1321
                    let delay = match delay {
cbb715e11963atagen2026-10-24 23:24 UTC1322
                        ruma::api::error::RetryAfter::Delay(delay) => delay,
cbb715e11963atagen2026-10-24 23:24 UTC1323
                        ruma::api::error::RetryAfter::DateTime(time) => time
cbb715e11963atagen2026-10-24 23:24 UTC1324
                            .duration_since(ruma::time::SystemTime::now())
cbb715e11963atagen2026-10-24 23:24 UTC1325
                            .unwrap_or_default(),
cbb715e11963atagen2026-10-24 23:24 UTC1326
                    };
cbb715e11963atagen2026-10-24 23:24 UTC1327
                    return now().saturating_add(
cbb715e11963atagen2026-10-24 23:24 UTC1328
                        u64::try_from(delay.as_millis())
cbb715e11963atagen2026-10-24 23:24 UTC1329
                            .unwrap_or(u64::MAX)
cbb715e11963atagen2026-10-24 23:24 UTC1330
                            .max(fallback_ms),
cbb715e11963atagen2026-10-24 23:24 UTC1331
                    );
cbb715e11963atagen2026-10-24 23:24 UTC1332
                }
cbb715e11963atagen2026-10-24 23:24 UTC1333
            }
cbb715e11963atagen2026-10-24 23:24 UTC1334
        }
cbb715e11963atagen2026-10-24 23:24 UTC1335
        source = current.source();
cbb715e11963atagen2026-10-24 23:24 UTC1336
    }
cbb715e11963atagen2026-10-24 23:24 UTC1337
    now().saturating_add(fallback_ms)
cbb715e11963atagen2026-10-24 23:24 UTC1338
}
cbb715e11963atagen2026-10-24 23:24 UTC1339
cbb715e11963atagen2026-10-24 23:24 UTC1340
fn redact(event: &mut SavedEvent) {
cbb715e11963atagen2026-10-24 23:24 UTC1341
    if let TimelineItem::Event(item) = &mut event.item {
cbb715e11963atagen2026-10-24 23:24 UTC1342
        item.content = TimelineContent::Redacted;
cbb715e11963atagen2026-10-24 23:24 UTC1343
        item.reply_to = None;
cbb715e11963atagen2026-10-24 23:24 UTC1344
        item.reactions.clear();
cbb715e11963atagen2026-10-24 23:24 UTC1345
    }
cbb715e11963atagen2026-10-24 23:24 UTC1346
    event.media.clear();
cbb715e11963atagen2026-10-24 23:24 UTC1347
    event.raw = None;
cbb715e11963atagen2026-10-24 23:24 UTC1348
}
cbb715e11963atagen2026-10-24 23:24 UTC1349
cbb715e11963atagen2026-10-24 23:24 UTC1350
fn media_for_item(
cbb715e11963atagen2026-10-24 23:24 UTC1351
    item: &TimelineItem,
cbb715e11963atagen2026-10-24 23:24 UTC1352
    registrations: &[MediaRegistration],
cbb715e11963atagen2026-10-24 23:24 UTC1353
) -> Vec<MediaRegistration> {
cbb715e11963atagen2026-10-24 23:24 UTC1354
    let TimelineItem::Event(event) = item else {
cbb715e11963atagen2026-10-24 23:24 UTC1355
        return Vec::new();
cbb715e11963atagen2026-10-24 23:24 UTC1356
    };
cbb715e11963atagen2026-10-24 23:24 UTC1357
    let mut ids = Vec::new();
cbb715e11963atagen2026-10-24 23:24 UTC1358
    match &event.content {
cbb715e11963atagen2026-10-24 23:24 UTC1359
        TimelineContent::Message(message) => {
cbb715e11963atagen2026-10-24 23:24 UTC1360
            ids.extend(message.media.iter().map(|media| &media.id));
cbb715e11963atagen2026-10-24 23:24 UTC1361
            ids.extend(
cbb715e11963atagen2026-10-24 23:24 UTC1362
                message
cbb715e11963atagen2026-10-24 23:24 UTC1363
                    .link_previews
cbb715e11963atagen2026-10-24 23:24 UTC1364
                    .iter()
cbb715e11963atagen2026-10-24 23:24 UTC1365
                    .filter_map(|preview| preview.image.as_ref().map(|image| &image.media_id)),
cbb715e11963atagen2026-10-24 23:24 UTC1366
            );
cbb715e11963atagen2026-10-24 23:24 UTC1367
        }
cbb715e11963atagen2026-10-24 23:24 UTC1368
        TimelineContent::Sticker { media, .. } => ids.push(&media.id),
cbb715e11963atagen2026-10-24 23:24 UTC1369
        _ => {}
cbb715e11963atagen2026-10-24 23:24 UTC1370
    }
cbb715e11963atagen2026-10-24 23:24 UTC1371
    registrations
cbb715e11963atagen2026-10-24 23:24 UTC1372
        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC1373
        .filter(|registration| ids.contains(&&registration.id))
cbb715e11963atagen2026-10-24 23:24 UTC1374
        .cloned()
cbb715e11963atagen2026-10-24 23:24 UTC1375
        .collect()
cbb715e11963atagen2026-10-24 23:24 UTC1376
}
cbb715e11963atagen2026-10-24 23:24 UTC1377
cbb715e11963atagen2026-10-24 23:24 UTC1378
pub(crate) async fn watch_updates(mut updates: watch::Receiver<MentionsPage>, events: EventSender) {
cbb715e11963atagen2026-10-24 23:24 UTC1379
    let mut previous: BTreeMap<String, MentionEntry> = BTreeMap::new();
cbb715e11963atagen2026-10-24 23:24 UTC1380
    loop {
cbb715e11963atagen2026-10-24 23:24 UTC1381
        let snapshot = updates.borrow_and_update().clone();
cbb715e11963atagen2026-10-24 23:24 UTC1382
        let current: BTreeMap<_, _> = snapshot
cbb715e11963atagen2026-10-24 23:24 UTC1383
            .entries
cbb715e11963atagen2026-10-24 23:24 UTC1384
            .iter()
cbb715e11963atagen2026-10-24 23:24 UTC1385
            .map(|entry| (id(&entry.key), entry.clone()))
cbb715e11963atagen2026-10-24 23:24 UTC1386
            .collect();
cbb715e11963atagen2026-10-24 23:24 UTC1387
        let upserts = current
cbb715e11963atagen2026-10-24 23:24 UTC1388
            .iter()
cbb715e11963atagen2026-10-24 23:24 UTC1389
            .filter(|(key, entry)| previous.get(*key) != Some(entry))
cbb715e11963atagen2026-10-24 23:24 UTC1390
            .map(|(_, entry)| entry.clone())
cbb715e11963atagen2026-10-24 23:24 UTC1391
            .collect();
cbb715e11963atagen2026-10-24 23:24 UTC1392
        let removed = previous
cbb715e11963atagen2026-10-24 23:24 UTC1393
            .iter()
cbb715e11963atagen2026-10-24 23:24 UTC1394
            .filter(|(key, _)| !current.contains_key(*key))
cbb715e11963atagen2026-10-24 23:24 UTC1395
            .map(|(_, entry)| entry.key.clone())
cbb715e11963atagen2026-10-24 23:24 UTC1396
            .collect();
cbb715e11963atagen2026-10-24 23:24 UTC1397
        previous = current;
cbb715e11963atagen2026-10-24 23:24 UTC1398
        if events
cbb715e11963atagen2026-10-24 23:24 UTC1399
            .send(Event::MentionsChanged(MentionsUpdate {
cbb715e11963atagen2026-10-24 23:24 UTC1400
                snapshot,
cbb715e11963atagen2026-10-24 23:24 UTC1401
                upserts,
cbb715e11963atagen2026-10-24 23:24 UTC1402
                removed,
cbb715e11963atagen2026-10-24 23:24 UTC1403
            }))
cbb715e11963atagen2026-10-24 23:24 UTC1404
            .await
cbb715e11963atagen2026-10-24 23:24 UTC1405
            .is_err()
cbb715e11963atagen2026-10-24 23:24 UTC1406
        {
cbb715e11963atagen2026-10-24 23:24 UTC1407
            return;
cbb715e11963atagen2026-10-24 23:24 UTC1408
        }
cbb715e11963atagen2026-10-24 23:24 UTC1409
        if updates.changed().await.is_err() {
cbb715e11963atagen2026-10-24 23:24 UTC1410
            return;
cbb715e11963atagen2026-10-24 23:24 UTC1411
        }
cbb715e11963atagen2026-10-24 23:24 UTC1412
    }
cbb715e11963atagen2026-10-24 23:24 UTC1413
}