atagen

macltok

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

Blame: src/mentions/service.rs

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