atagen

macltok

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