atagen

macltok

public
Star0
Watch0
cbb715e119632f60c9b3ec1bc8803f1f33e39dce
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/mentions/service.rsRust1314 lines53.4 KB
1
use std::{
2
    collections::{BTreeMap, BTreeSet},
3
    sync::Arc,
4
    time::Duration,
5
};
6
7
use futures_util::{StreamExt, stream::FuturesUnordered};
8
use matrix_sdk::{
9
    Client, RoomState,
10
    sliding_sync::{ResponseCommitBatch, ResponseCommitHook},
11
};
12
use ruma::{
13
    OwnedRoomId, OwnedUserId,
14
    events::{AnySyncTimelineEvent, room::encrypted::OriginalSyncRoomEncryptedEvent},
15
    serde::Raw,
16
};
17
use serde_json::Value;
18
use tokio::sync::{mpsc, oneshot, watch};
19
20
use super::state::{Candidate, Data, SavedEvent, Window, id, invalid, now};
21
use crate::{
22
    CommandOutput, EngineConfig, Error, ErrorKind, Event, MentionCaptureState, MentionEntry,
23
    MentionGap, MentionKey, MentionReason, MentionsCommand, MentionsPage, MentionsUpdate,
24
    TimelineContent, TimelineItem,
25
    engine::{MediaService, MentionsStore, context, executor::EngineBoxFuture},
26
    normalise::{MediaRegistration, normalise_observed},
27
    transport::EventSender,
28
};
29
30
#[cfg(all(test, feature = "sqlite"))]
31
#[path = "service_tests.rs"]
32
mod tests;
33
34
#[derive(Clone)]
35
pub(crate) struct Handle(mpsc::Sender<Request>);
36
37
enum Request {
38
    Batch(ResponseCommitBatch, oneshot::Sender<Result<(), Error>>),
39
    Command(
40
        MentionsCommand,
41
        oneshot::Sender<Result<CommandOutput, Error>>,
42
    ),
43
    Stop(oneshot::Sender<()>),
44
}
45
46
pub(crate) struct Service {
47
    client: Client,
48
    account: OwnedUserId,
49
    media: Arc<MediaService>,
50
    store: Option<MentionsStore>,
51
    data: Data,
52
    config: EngineConfig,
53
    requests: mpsc::Receiver<Request>,
54
    updates: watch::Sender<MentionsPage>,
55
    recovery_budget: usize,
56
    latest_scan: bool,
57
    recovery_retry: u64,
58
}
59
60
enum Work {
61
    Capture(
62
        String,
63
        u64,
64
        Result<(crate::EventContext, Vec<MediaRegistration>, Vec<Value>), Error>,
65
    ),
66
    Candidate(String, Result<Value, Error>),
67
    History(
68
        bool,
69
        Option<String>,
70
        Result<ruma::api::client::push::get_notifications::v3::Response, Error>,
71
    ),
72
}
73
74
impl Handle {
75
    pub(crate) fn hook(&self) -> ResponseCommitHook {
76
        let handle = self.clone();
77
        ResponseCommitHook::new(move |batch| {
78
            let handle = handle.clone();
79
            async move {
80
                let (reply, response) = oneshot::channel();
81
                handle
82
                    .0
83
                    .send(Request::Batch(batch, reply))
84
                    .await
85
                    .map_err(|_| "mentions service stopped".to_owned())?;
86
                response
87
                    .await
88
                    .map_err(|_| "mentions commit acknowledgement dropped".to_owned())?
89
                    .map_err(|error| error.to_string())
90
            }
91
        })
92
    }
93
94
    pub(crate) async fn execute(&self, command: MentionsCommand) -> Result<CommandOutput, Error> {
95
        let (reply, response) = oneshot::channel();
96
        self.0
97
            .send(Request::Command(command, reply))
98
            .await
99
            .map_err(|_| Error::interrupted())?;
100
        response.await.map_err(|_| Error::interrupted())?
101
    }
102
103
    pub(crate) async fn stop(&self) {
104
        let (reply, response) = oneshot::channel();
105
        if self.0.send(Request::Stop(reply)).await.is_ok() {
106
            let _ = response.await;
107
        }
108
    }
109
}
110
111
impl Service {
112
    pub(crate) async fn new(
113
        client: Client,
114
        media: Arc<MediaService>,
115
        store: Option<MentionsStore>,
116
        config: EngineConfig,
117
    ) -> Result<(Handle, Self, watch::Receiver<MentionsPage>), Error> {
118
        let account = client
119
            .user_id()
120
            .ok_or_else(|| {
121
                Error::new(
122
                    ErrorKind::Unauthenticated,
123
                    "mentions require an authenticated account",
124
                )
125
            })?
126
            .to_owned();
127
        #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
128
        let data: Data = store
129
            .as_ref()
130
            .map(|store| store.read("inbox"))
131
            .transpose()?
132
            .flatten()
133
            .unwrap_or_default();
134
        #[cfg(not(any(feature = "sqlite", feature = "indexeddb")))]
135
        let data = Data::default();
136
        #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
137
        if store
138
            .as_ref()
139
            .is_some_and(|store| store.revision() != data.revision)
140
        {
141
            return Err(Error::new(
142
                ErrorKind::Store,
143
                "mentions revision does not match its checkpoint",
144
            ));
145
        }
146
        let page = data.page(&account, None, 50)?;
147
        media
148
            .register(
149
                page.entries
150
                    .iter()
151
                    .filter_map(|entry| data.events.get(&id(&entry.key)))
152
                    .flat_map(|event| event.media.clone())
153
                    .collect(),
154
            )
155
            .await;
156
        let (updates, receiver) = watch::channel(page);
157
        let (sender, requests) = mpsc::channel(config.command_capacity.max(1));
158
        Ok((
159
            Handle(sender),
160
            Self {
161
                client,
162
                account,
163
                media,
164
                store,
165
                data,
166
                config,
167
                requests,
168
                updates,
169
                recovery_budget: 20,
170
                latest_scan: true,
171
                recovery_retry: 0,
172
            },
173
            receiver,
174
        ))
175
    }
176
177
    async fn save(&mut self, mut next: Data) -> Result<(), Error> {
178
        next.revision = self
179
            .data
180
            .revision
181
            .checked_add(1)
182
            .ok_or_else(|| invalid("mentions revision exhausted"))?;
183
        next.bound(
184
            self.config.max_mentions_entries,
185
            self.config.max_mention_context_events,
186
            self.config.max_mention_candidates,
187
        );
188
        #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
189
        if let Some(store) = &mut self.store {
190
            loop {
191
                let value = serde_json::to_value(&next).map_err(|error| {
192
                    Error::with_source(
193
                        ErrorKind::Store,
194
                        "encoding the mentions inbox failed",
195
                        error,
196
                    )
197
                })?;
198
                match store
199
                    .commit(
200
                        self.data.revision,
201
                        vec![super::store::Write::Set("inbox".to_owned(), value)],
202
                    )
203
                    .await
204
                {
205
                    Ok(_) => break,
206
                    Err(error)
207
                        if error.message == "mentions storage limit exceeded"
208
                            && next.evict_one() => {}
209
                    Err(error) => return Err(error),
210
                }
211
            }
212
        }
213
        self.data = next;
214
        let page = self.data.page(&self.account, None, 50)?;
215
        self.media
216
            .register(
217
                page.entries
218
                    .iter()
219
                    .filter_map(|entry| self.data.events.get(&id(&entry.key)))
220
                    .flat_map(|event| event.media.clone())
221
                    .collect(),
222
            )
223
            .await;
224
        self.updates.send_replace(page);
225
        Ok(())
226
    }
227
228
    async fn command(&mut self, command: MentionsCommand) -> Result<CommandOutput, Error> {
229
        match command {
230
            MentionsCommand::Page { cursor, limit } => {
231
                let page = self.data.page(&self.account, cursor.as_deref(), limit)?;
232
                self.media
233
                    .register(
234
                        page.entries
235
                            .iter()
236
                            .filter_map(|entry| self.data.events.get(&id(&entry.key)))
237
                            .flat_map(|event| event.media.clone())
238
                            .collect(),
239
                    )
240
                    .await;
241
                Ok(CommandOutput::MentionsPage(page))
242
            }
243
            MentionsCommand::Context(key) => {
244
                let (context, media) = self.data.context(&key)?;
245
                self.media.register(media).await;
246
                Ok(CommandOutput::MentionContext(context))
247
            }
248
            MentionsCommand::RetryContext(key) => {
249
                let room = self
250
                    .client
251
                    .get_room(&key.room_id)
252
                    .filter(|room| room.state() == RoomState::Joined)
253
                    .ok_or_else(|| {
254
                        Error::new(
255
                            ErrorKind::Forbidden,
256
                            "saved context cannot be refetched after leaving the room",
257
                        )
258
                    })?;
259
                drop(room);
260
                let mut next = self.data.clone();
261
                let entry = next
262
                    .entries
263
                    .get_mut(&id(&key))
264
                    .ok_or_else(|| Error::new(ErrorKind::EventNotFound, "mention was not found"))?;
265
                entry.capture = MentionCaptureState::Pending;
266
                entry.revision = self.data.revision + 1;
267
                next.windows.entry(id(&key)).or_insert(Window {
268
                    before: Vec::new(),
269
                    target: Some(id(&key)),
270
                    after: Vec::new(),
271
                    start: None,
272
                    end: None,
273
                });
274
                self.save(next).await?;
275
                let (context, media) = self.data.context(&key)?;
276
                self.media.register(media).await;
277
                Ok(CommandOutput::MentionContext(context))
278
            }
279
            MentionsCommand::SetRead { keys, read } => {
280
                let mut next = self.data.clone();
281
                for key in keys {
282
                    if let Some(entry) = next.entries.get_mut(&id(&key)) {
283
                        entry.read = read;
284
                        entry.revision = self.data.revision + 1;
285
                    }
286
                }
287
                self.save(next).await?;
288
                Ok(CommandOutput::Done)
289
            }
290
            MentionsCommand::ReadThrough(boundary) => {
291
                if boundary.account != self.account || boundary.revision > self.data.revision {
292
                    return Err(invalid("invalid mentions read boundary"));
293
                }
294
                let mut next = self.data.clone();
295
                for entry in next
296
                    .entries
297
                    .values_mut()
298
                    .filter(|entry| entry.discovery_revision <= boundary.revision)
299
                {
300
                    entry.read = true;
301
                    entry.revision = self.data.revision + 1;
302
                }
303
                self.save(next).await?;
304
                Ok(CommandOutput::Done)
305
            }
306
            MentionsCommand::Recover => {
307
                self.recovery_budget = 20;
308
                self.latest_scan = true;
309
                self.recovery_retry = 0;
310
                let mut next = self.data.clone();
311
                next.coverage.recovering = true;
312
                next.coverage.historical_supported = None;
313
                self.save(next).await?;
314
                Ok(CommandOutput::Done)
315
            }
316
        }
317
    }
318
319
    fn gap(data: &mut Data, room_id: Option<OwnedRoomId>, reason: &str, token: Option<String>) {
320
        if !data
321
            .coverage
322
            .gaps
323
            .iter()
324
            .any(|gap| gap.room_id == room_id && gap.reason == reason && gap.token == token)
325
        {
326
            data.coverage.gaps.push(MentionGap {
327
                room_id,
328
                reason: reason.to_owned(),
329
                token,
330
            });
331
            if data.coverage.gaps.len() > 128 {
332
                data.coverage.gaps.remove(0);
333
                data.coverage.truncated = true;
334
            }
335
        }
336
    }
337
338
    async fn batch(&mut self, batch: ResponseCommitBatch) -> Result<(), Error> {
339
        let mut next = self.data.clone();
340
        if next.checkpoints.get(&batch.connection_id) == Some(&batch.next_pos) {
341
            return Ok(());
342
        }
343
        if next.checkpoints.get(&batch.connection_id) != batch.previous_pos.as_ref() {
344
            Self::gap(
345
                &mut next,
346
                None,
347
                "sync checkpoint continuity is unknown",
348
                None,
349
            );
350
        }
351
        for (room_id, raw) in &batch.raw_rooms {
352
            if raw.limited || !next.observed_rooms.contains(room_id) {
353
                Self::gap(
354
                    &mut next,
355
                    Some(room_id.clone()),
356
                    if raw.limited {
357
                        "limited sync timeline"
358
                    } else {
359
                        "initial room history is incomplete"
360
                    },
361
                    raw.prev_batch.clone(),
362
                );
363
            }
364
        }
365
        let room_ids: BTreeSet<_> = batch
366
            .raw_rooms
367
            .keys()
368
            .chain(batch.response.rooms.joined.keys())
369
            .cloned()
370
            .collect();
371
        for room_id in &room_ids {
372
            if !self
373
                .client
374
                .get_room(room_id)
375
                .is_some_and(|room| room.state() == RoomState::Joined)
376
            {
377
                continue;
378
            }
379
            next.observed_rooms.insert(room_id.clone());
380
            let mut observed = BTreeMap::new();
381
            if let Some(raw) = batch.raw_rooms.get(room_id) {
382
                for event in &raw.timeline {
383
                    let value: Value =
384
                        serde_json::from_str(event.json().get()).map_err(|error| {
385
                            Error::with_source(ErrorKind::Invalid, "invalid observed event", error)
386
                        })?;
387
                    if let Some(event_id) = value["event_id"].as_str() {
388
                        observed.insert(event_id.to_owned(), value);
389
                    }
390
                }
391
            }
392
            if let Some(update) = batch.response.rooms.joined.get(room_id) {
393
                for event in &update.timeline.events {
394
                    let value: Value =
395
                        serde_json::from_str(event.raw().json().get()).map_err(|error| {
396
                            Error::with_source(ErrorKind::Invalid, "invalid observed event", error)
397
                        })?;
398
                    if let Some(event_id) = value["event_id"].as_str() {
399
                        observed.insert(event_id.to_owned(), value);
400
                    }
401
                }
402
            }
403
            let mut observed: Vec<_> = observed.into_values().collect();
404
            observed.sort_by_key(|value| value["origin_server_ts"].as_u64().unwrap_or(0));
405
            for value in observed {
406
                self.observe(&mut next, room_id, value).await?;
407
            }
408
        }
409
        for room_id in batch.response.rooms.left.keys() {
410
            Self::gap(
411
                &mut next,
412
                Some(room_id.clone()),
413
                "room left, retained context is read only",
414
                None,
415
            );
416
            next.candidates
417
                .retain(|_, candidate| &candidate.room_id != room_id);
418
            for entry in next
419
                .entries
420
                .values_mut()
421
                .filter(|entry| &entry.key.room_id == room_id)
422
            {
423
                entry.capture = MentionCaptureState::Unavailable;
424
                entry.revision = next.revision + 1;
425
            }
426
        }
427
        let reevaluate: Vec<_> = next
428
            .entries
429
            .values()
430
            .filter(|entry| room_ids.contains(&entry.key.room_id))
431
            .filter_map(|entry| {
432
                next.events
433
                    .get(&id(&entry.key))
434
                    .and_then(|event| event.raw.clone())
435
                    .map(|raw| (entry.key.room_id.clone(), raw))
436
            })
437
            .collect();
438
        for (room_id, raw) in reevaluate {
439
            self.observe(&mut next, &room_id, raw).await?;
440
        }
441
        self.filter_ignored(&mut next).await;
442
        next.checkpoints.insert(batch.connection_id, batch.next_pos);
443
        self.save(next).await
444
    }
445
446
    async fn filter_ignored(&self, next: &mut Data) {
447
        let mut removed = Vec::new();
448
        for (key, entry) in &next.entries {
449
            if let TimelineItem::Event(event) = &entry.event {
450
                if self.client.is_user_ignored(&event.sender.user_id).await {
451
                    removed.push(key.clone());
452
                }
453
            }
454
        }
455
        for key in removed {
456
            next.entries.remove(&key);
457
            next.windows.remove(&key);
458
        }
459
        next.collect_events();
460
    }
461
462
    async fn observe(
463
        &self,
464
        data: &mut Data,
465
        room_id: &OwnedRoomId,
466
        mut value: Value,
467
    ) -> Result<(), Error> {
468
        let Some(event_id) = value["event_id"]
469
            .as_str()
470
            .and_then(|value| value.parse().ok())
471
        else {
472
            return Ok(());
473
        };
474
        let key = MentionKey {
475
            room_id: room_id.clone(),
476
            event_id,
477
        };
478
        let identity = id(&key);
479
        let Some(timestamp) = value["origin_server_ts"].as_u64() else {
480
            return Ok(());
481
        };
482
        data.coverage.earliest_timestamp_ms = Some(
483
            data.coverage
484
                .earliest_timestamp_ms
485
                .map_or(timestamp, |old| old.min(timestamp)),
486
        );
487
        if value["type"] == "m.room.redaction" {
488
            if let Some(target) = value["redacts"]
489
                .as_str()
490
                .or_else(|| value["content"]["redacts"].as_str())
491
                .and_then(|value| value.parse().ok())
492
            {
493
                let target = id(&MentionKey {
494
                    room_id: room_id.clone(),
495
                    event_id: target,
496
                });
497
                data.redacted.insert(target.clone(), timestamp);
498
                data.entries.remove(&target);
499
                data.windows.remove(&target);
500
                data.candidates.remove(&target);
501
                data.edits.remove(&target);
502
                data.candidates.retain(|_, candidate| {
503
                    candidate.room_id != *room_id
504
                        || candidate.raw["content"]["m.relates_to"]["event_id"].as_str()
505
                            != target.split_once('\n').map(|(_, event)| event)
506
                });
507
                if let Some(saved) = data.events.get_mut(&target) {
508
                    redact(saved);
509
                }
510
                data.touch_contexts(&target);
511
            }
512
            return Ok(());
513
        }
514
        if data.redacted.contains_key(&identity)
515
            || value["unsigned"].get("redacted_because").is_some()
516
        {
517
            data.redacted.insert(identity.clone(), timestamp);
518
            data.entries.remove(&identity);
519
            data.windows.remove(&identity);
520
            data.candidates.remove(&identity);
521
            if let Some(saved) = data.events.get_mut(&identity) {
522
                redact(saved);
523
            }
524
            data.touch_contexts(&identity);
525
            return Ok(());
526
        }
527
        if value["content"]["m.relates_to"]["rel_type"] == "m.replace" {
528
            if let Some(target) = value["content"]["m.relates_to"]["event_id"]
529
                .as_str()
530
                .and_then(|value| value.parse().ok())
531
            {
532
                let target = id(&MentionKey {
533
                    room_id: room_id.clone(),
534
                    event_id: target,
535
                });
536
                if data.redacted.contains_key(&target) {
537
                    return Ok(());
538
                }
539
                let newer = data.edits.get(&target).is_none_or(|old| {
540
                    (old["origin_server_ts"].as_u64(), old["event_id"].as_str())
541
                        < (Some(timestamp), value["event_id"].as_str())
542
                });
543
                if newer {
544
                    if let Some(original) =
545
                        data.events.get(&target).and_then(|saved| saved.raw.clone())
546
                    {
547
                        if original["sender"] != value["sender"] {
548
                            return Ok(());
549
                        }
550
                        data.edits.insert(target.clone(), value.clone());
551
                        value = edited(original, &value);
552
                        return Box::pin(self.observe(data, room_id, value)).await;
553
                    }
554
                    data.candidates.insert(
555
                        identity,
556
                        Candidate {
557
                            room_id: room_id.clone(),
558
                            raw: value,
559
                            attempts: 0,
560
                            retry_at_ms: 0,
561
                        },
562
                    );
563
                }
564
            }
565
            return Ok(());
566
        }
567
        if let Some(edit) = value["unsigned"]["m.relations"].get("m.replace").cloned() {
568
            if edit["sender"] == value["sender"]
569
                && edit["content"]["m.new_content"].is_object()
570
                && edit["content"]["m.relates_to"]["event_id"]
571
                    .as_str()
572
                    .is_none_or(|target| target == key.event_id.as_str())
573
                && edit["origin_server_ts"].as_u64().is_some()
574
                && data.edits.get(&identity).is_none_or(|old| {
575
                    (old["origin_server_ts"].as_u64(), old["event_id"].as_str())
576
                        < (edit["origin_server_ts"].as_u64(), edit["event_id"].as_str())
577
                })
578
            {
579
                data.edits.insert(identity.clone(), edit);
580
            }
581
        }
582
        if let Some(edit) = data.edits.get(&identity) {
583
            if value["sender"] == edit["sender"] {
584
                value = edited(value, edit);
585
            }
586
        }
587
        if let Some(edit) = value["unsigned"].get("macltok_edit").cloned() {
588
            data.edits.insert(identity.clone(), edit);
589
        }
590
        if value["type"] == "m.room.encrypted" {
591
            data.candidates.entry(identity).or_insert(Candidate {
592
                room_id: room_id.clone(),
593
                raw: value,
594
                attempts: 0,
595
                retry_at_ms: 0,
596
            });
597
            return Ok(());
598
        }
599
        let Some((item, media)) = normalise_observed(&value, Some(&self.account), &identity) else {
600
            return Ok(());
601
        };
602
        self.supplement(data, &key, &item, &media, &value);
603
        let (reasons, suppressed, unresolved) = self.classify(data, room_id, &value).await;
604
        if unresolved {
605
            data.candidates
606
                .entry(identity.clone())
607
                .or_insert(Candidate {
608
                    room_id: room_id.clone(),
609
                    raw: value.clone(),
610
                    attempts: 0,
611
                    retry_at_ms: now() + 30_000,
612
                });
613
        } else {
614
            data.candidates.remove(&identity);
615
        }
616
        if reasons.is_empty() {
617
            data.entries.remove(&identity);
618
            data.windows.remove(&identity);
619
            if data.events.contains_key(&identity) || data.edits.contains_key(&identity) {
620
                data.events.insert(
621
                    identity,
622
                    SavedEvent {
623
                        item,
624
                        media,
625
                        raw: Some(value),
626
                    },
627
                );
628
            }
629
            return Ok(());
630
        }
631
        let previous = data.entries.get(&identity);
632
        let changed = previous.is_none_or(|entry| {
633
            entry.event != item
634
                || entry.reasons != reasons
635
                || entry.notification_suppressed != suppressed
636
        });
637
        let entry = MentionEntry {
638
            key,
639
            timestamp_ms: timestamp,
640
            discovered_at_ms: previous.map_or_else(now, |entry| entry.discovered_at_ms),
641
            discovery_revision: previous
642
                .map_or(data.revision + 1, |entry| entry.discovery_revision),
643
            revision: if changed {
644
                data.revision + 1
645
            } else {
646
                previous.unwrap().revision
647
            },
648
            event: item.clone(),
649
            reasons,
650
            read: previous.is_some_and(|entry| entry.read),
651
            capture: previous.map_or(MentionCaptureState::Pending, |entry| {
652
                if changed {
653
                    MentionCaptureState::Pending
654
                } else {
655
                    entry.capture.clone()
656
                }
657
            }),
658
            room_name: self.client.get_room(room_id).and_then(|room| room.name()),
659
            notification_suppressed: suppressed,
660
        };
661
        data.entries.insert(identity.clone(), entry);
662
        data.windows.entry(identity.clone()).or_insert(Window {
663
            before: Vec::new(),
664
            target: Some(identity.clone()),
665
            after: Vec::new(),
666
            start: None,
667
            end: None,
668
        });
669
        data.events.insert(
670
            identity,
671
            SavedEvent {
672
                item,
673
                media,
674
                raw: Some(value),
675
            },
676
        );
677
        Ok(())
678
    }
679
680
    fn supplement(
681
        &self,
682
        data: &mut Data,
683
        key: &MentionKey,
684
        item: &TimelineItem,
685
        media: &[MediaRegistration],
686
        raw: &Value,
687
    ) {
688
        let TimelineItem::Event(event) = item else {
689
            return;
690
        };
691
        let identity = id(key);
692
        if data
693
            .events
694
            .get(&identity)
695
            .is_some_and(|saved| saved.item != *item)
696
        {
697
            data.touch_contexts(&identity);
698
        }
699
        let mut retained = data.events.contains_key(&identity);
700
        let saved_events = &data.events;
701
        for (target, window) in &mut data.windows {
702
            let Some(entry) = data.entries.get_mut(target) else {
703
                continue;
704
            };
705
            if entry.key.room_id != key.room_id
706
                || (entry.timestamp_ms, target.as_str()) >= (event.timestamp_ms, identity.as_str())
707
                || window.after.contains(&identity)
708
            {
709
                continue;
710
            }
711
            let previous = window.after.clone();
712
            window.after.push(identity.clone());
713
            window.after.sort_by_key(|key| {
714
                if key == &identity {
715
                    (event.timestamp_ms, key.clone())
716
                } else {
717
                    (
718
                        saved_events
719
                            .get(key)
720
                            .and_then(|saved| match &saved.item {
721
                                TimelineItem::Event(event) => Some(event.timestamp_ms),
722
                                _ => None,
723
                            })
724
                            .unwrap_or(0),
725
                        key.clone(),
726
                    )
727
                }
728
            });
729
            window.after.truncate(10);
730
            if window.after == previous {
731
                continue;
732
            }
733
            window.end = None;
734
            retained |= window.after.contains(&identity);
735
            if entry.capture == MentionCaptureState::Partial
736
                && window.after.len() == 10
737
                && (window.before.len() == 10 || window.start.is_none())
738
            {
739
                entry.capture = MentionCaptureState::Ready;
740
            }
741
            entry.revision = data.revision + 1;
742
        }
743
        if retained {
744
            data.events.insert(
745
                identity,
746
                SavedEvent {
747
                    item: item.clone(),
748
                    media: media.to_vec(),
749
                    raw: Some(raw.clone()),
750
                },
751
            );
752
        }
753
    }
754
755
    async fn classify(
756
        &self,
757
        data: &Data,
758
        room_id: &OwnedRoomId,
759
        value: &Value,
760
    ) -> (Vec<MentionReason>, bool, bool) {
761
        let Some(sender) = value["sender"]
762
            .as_str()
763
            .and_then(|sender| sender.parse::<OwnedUserId>().ok())
764
        else {
765
            return (vec![], false, false);
766
        };
767
        if sender == self.account
768
            || self.client.is_user_ignored(&sender).await
769
            || value["type"] != "m.room.message"
770
        {
771
            return (vec![], false, false);
772
        }
773
        let Some(room) = self
774
            .client
775
            .get_room(room_id)
776
            .filter(|room| room.state() == RoomState::Joined)
777
        else {
778
            return (vec![], false, false);
779
        };
780
        let raw: Raw<AnySyncTimelineEvent> =
781
            Raw::from_json_string(value.to_string()).expect("serialized event");
782
        let actions = room.event_push_actions(&raw).await.ok().flatten();
783
        let notify = actions.as_ref().is_some_and(|actions| {
784
            actions
785
                .iter()
786
                .any(|action| matches!(action, ruma::push::Action::Notify))
787
        });
788
        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)));
789
        let direct = value["content"]["m.mentions"]["user_ids"]
790
            .as_array()
791
            .is_some_and(|users| {
792
                users
793
                    .iter()
794
                    .any(|user| user.as_str() == Some(self.account.as_str()))
795
            });
796
        let mut reasons = Vec::new();
797
        if direct {
798
            reasons.push(MentionReason::Direct);
799
        }
800
        let mut unresolved = false;
801
        if let Some(reply) = value["content"]["m.relates_to"]["m.in_reply_to"]["event_id"]
802
            .as_str()
803
            .and_then(|value| value.parse().ok())
804
        {
805
            let target = data.events.get(&id(&MentionKey {
806
                room_id: room_id.clone(),
807
                event_id: reply,
808
            }));
809
            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));
810
            if reply_to_self && (direct || (notify && highlight)) {
811
                reasons.push(MentionReason::Reply);
812
            } else if target.is_none()
813
                && value["unsigned"]["macltok_reply_sender"].as_str().is_none()
814
                && (direct || (notify && highlight))
815
            {
816
                unresolved = true;
817
            }
818
        }
819
        if value["content"]["m.mentions"]["room"] == true {
820
            match room.power_levels().await {
821
                Ok(levels) if levels.user_can_trigger_room_notification(&sender) => {
822
                    if notify && highlight {
823
                        reasons.push(MentionReason::Room);
824
                    } else if actions.is_none() {
825
                        unresolved = true;
826
                    }
827
                }
828
                Err(_) => unresolved = true,
829
                _ => {}
830
            }
831
        }
832
        (reasons, !notify, unresolved)
833
    }
834
835
    pub(crate) async fn run(mut self) {
836
        let reports_client = self.client.clone();
837
        let reports = reports_client
838
            .event_cache()
839
            .subscribe_to_decryption_reports();
840
        futures_util::pin_mut!(reports);
841
        let mut ignores = self.client.subscribe_to_ignore_user_list_changes();
842
        let mut work: FuturesUnordered<EngineBoxFuture<'static, Work>> = FuturesUnordered::new();
843
        let mut captures = BTreeSet::new();
844
        let mut retries = BTreeSet::new();
845
        let mut history = false;
846
        let mut next_scan = now() + 60_000;
847
        let mut tick = Box::pin(matrix_sdk::sleep::sleep(Duration::from_secs(1)));
848
        loop {
849
            self.schedule(&mut work, &mut captures, &mut retries, &mut history);
850
            tokio::select! {
851
                biased;
852
                request = self.requests.recv() => match request {
853
                    Some(Request::Stop(reply)) => { let _ = reply.send(()); return; }
854
                    Some(Request::Batch(batch, reply)) => { let result = self.batch(batch).await; let _ = reply.send(result); }
855
                    Some(Request::Command(command, reply)) => { let result = self.command(command).await; let _ = reply.send(result); }
856
                    None => return,
857
                },
858
                Some(result) = work.next(), if !work.is_empty() => {
859
                    let mut next = self.data.clone();
860
                    let recovery = (self.latest_scan, self.recovery_budget);
861
                    match result {
862
                        Work::Capture(key, revision, result) => {
863
                            captures.remove(&key);
864
                            let observed = result.as_ref().ok().map(|(_, _, observed)| observed.clone()).unwrap_or_default();
865
                            let room_id = next.entries.get(&key).filter(|entry| entry.revision == revision)
866
                                .map(|entry| entry.key.room_id.clone())
867
                                .filter(|room_id| self.client.get_room(room_id).is_some_and(|room| room.state() == RoomState::Joined));
868
                            self.captured(&mut next, key, revision, result);
869
                            if let Some(room_id) = room_id {
870
                                for value in observed {
871
                                    let identity = value["event_id"].as_str().map(|event_id| format!("{room_id}\n{event_id}"));
872
                                    if identity.as_ref().is_some_and(|key| next.events.get(key).is_some_and(|event| event.raw.is_some())) { continue; }
873
                                    self.observe(&mut next, &room_id, value).await.ok();
874
                                }
875
                            }
876
                        }
877
                        Work::Candidate(key, result) => {
878
                            retries.remove(&key);
879
                            if let Some(mut candidate) = next.candidates.remove(&key) {
880
                                match result {
881
                                    Ok(value) => {
882
                                        if let Err(error) = self.observe(&mut next, &candidate.room_id, value).await { tracing::warn!(%error, "mention retry failed"); }
883
                                        if let Some(unresolved) = next.candidates.get_mut(&key) { unresolved.attempts = candidate.attempts.saturating_add(1); unresolved.retry_at_ms = now() + (1000u64 << unresolved.attempts.min(8)); }
884
                                    }
885
                                    Err(error) => {
886
                                        if candidate.raw["type"] != "m.room.encrypted" && (matches!(error.kind, ErrorKind::EventNotFound | ErrorKind::Forbidden) || error.server.as_ref().is_some_and(|server| matches!(server.status, 403 | 404))) {
887
                                            Self::gap(&mut next, Some(candidate.room_id), "mention notification context is unavailable", None);
888
                                        } else { candidate.attempts = candidate.attempts.saturating_add(1); candidate.retry_at_ms = retry_at(&error, 1000u64 << candidate.attempts.min(8)); next.candidates.insert(key, candidate); }
889
                                    }
890
                                }
891
                            }
892
                        }
893
                        Work::History(latest, from, result) => {
894
                            history = false; self.recovery_budget = self.recovery_budget.saturating_sub(1);
895
                            match result {
896
                                Ok(page) => {
897
                                    next.coverage.historical_supported = Some(true);
898
                                    let mut reached_anchor = false;
899
                                    for notification in page.notifications {
900
                                        if let Some(event_id) = notification.event.get_field::<String>("event_id").ok().flatten() {
901
                                            let identity = format!("{}\n{}", notification.room_id, event_id);
902
                                            if latest && from.is_none() && next.latest_head.is_none() { next.latest_head = Some(identity.clone()); }
903
                                            if latest && next.latest_anchor.as_ref() == Some(&identity) { reached_anchor = true; break; }
904
                                        }
905
                                        if self.client.get_room(&notification.room_id).is_some_and(|room| room.state() == RoomState::Joined) {
906
                                        if let Ok(value) = serde_json::from_str(notification.event.json().get()) { let _ = self.observe(&mut next, &notification.room_id, value).await; }
907
                                    }}
908
                                    let token = page.next_token.filter(|token| Some(token) != from.as_ref() && !reached_anchor);
909
                                    if latest { next.latest_cursor = token.clone(); if token.is_none() { self.latest_scan = false; next.latest_anchor = next.latest_head.take().or(next.latest_anchor); } }
910
                                    else { next.history_cursor = token.clone(); next.history_exhausted = token.is_none(); }
911
                                }
912
                                Err(error) => {
913
                                    if error.server.as_ref().is_some_and(|server| matches!(server.status, 404 | 405 | 501)) { next.coverage.historical_supported = Some(false); self.recovery_budget = 0; }
914
                                    if error.server.as_ref().is_some_and(|server| server.status == 400) {
915
                                        if latest { next.latest_cursor = None; next.latest_head = None; } else { next.history_cursor = None; }
916
                                    }
917
                                    Self::gap(&mut next, None, "notification history could not be recovered", from);
918
                                    self.recovery_retry = retry_at(&error, 60_000);
919
                                }
920
                            }
921
                            next.coverage.recovering = self.recovery_budget > 0 && (self.latest_scan || !next.history_exhausted) && next.coverage.historical_supported != Some(false);
922
                        }
923
                    }
924
                    if let Err(error) = self.save(next).await { tracing::error!(%error, "saving mention background work failed"); self.latest_scan = recovery.0; self.recovery_budget = recovery.1; self.recovery_retry = now() + 60_000; }
925
                }
926
                Some(_) = reports.next() => { for candidate in self.data.candidates.values_mut() { candidate.retry_at_ms = 0; } }
927
                Some(_) = ignores.next() => {
928
                    let mut next = self.data.clone(); self.filter_ignored(&mut next).await;
929
                    if let Err(error) = self.save(next).await { tracing::error!(%error, "saving ignored mention removals failed"); }
930
                }
931
                () = &mut tick => {
932
                    tick = Box::pin(matrix_sdk::sleep::sleep(Duration::from_secs(1)));
933
                    if now() >= next_scan && !history { self.recovery_budget = 20; self.latest_scan = true; next_scan = now() + 60_000; }
934
                }
935
            }
936
        }
937
    }
938
939
    fn schedule(
940
        &mut self,
941
        work: &mut FuturesUnordered<EngineBoxFuture<'static, Work>>,
942
        captures: &mut BTreeSet<String>,
943
        retries: &mut BTreeSet<String>,
944
        history: &mut bool,
945
    ) {
946
        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) {
947
            if captures.len() >= 2 { break; }
948
            if captures.contains(key) { continue; }
949
            let Some(room) = self.client.get_room(&entry.key.room_id).filter(|room| room.state() == RoomState::Joined) else { continue; };
950
            captures.insert(key.clone());
951
            let key = key.clone(); let event_id = entry.key.event_id.clone(); let account = self.account.clone(); let revision = entry.revision;
952
            work.push(Box::pin(async move { Work::Capture(key, revision, context::capture(room, Some(account), event_id, 10, 10).await) }));
953
        }
954
        for (key, candidate) in &self.data.candidates {
955
            if retries.len() >= 2 {
956
                break;
957
            }
958
            if candidate.retry_at_ms > now() || retries.contains(key) {
959
                continue;
960
            }
961
            let Some(room) = self
962
                .client
963
                .get_room(&candidate.room_id)
964
                .filter(|room| room.state() == RoomState::Joined)
965
            else {
966
                continue;
967
            };
968
            let key = key.clone();
969
            let candidate = candidate.clone();
970
            let key_for_work = key.clone();
971
            retries.insert(key);
972
            work.push(Box::pin(async move {
973
                let result = async {
974
                    if candidate.raw["type"] == "m.room.encrypted" {
975
                        let raw: Raw<OriginalSyncRoomEncryptedEvent> =
976
                            Raw::from_json_string(candidate.raw.to_string()).map_err(|error| {
977
                                Error::with_source(
978
                                    ErrorKind::Invalid,
979
                                    "invalid encrypted mention candidate",
980
                                    error,
981
                                )
982
                            })?;
983
                        let event = room.decrypt_event(&raw, None).await.map_err(|error| {
984
                            Error::from_sdk("decrypting a mention candidate failed", error)
985
                        })?;
986
                        let value: Value =
987
                            serde_json::from_str(event.raw().json().get()).map_err(|error| {
988
                                Error::with_source(
989
                                    ErrorKind::Invalid,
990
                                    "invalid decrypted mention",
991
                                    error,
992
                                )
993
                            })?;
994
                        if value["type"] == "m.room.encrypted" {
995
                            return Err(Error::new(
996
                                ErrorKind::Encryption,
997
                                "mention candidate still awaits keys",
998
                            ));
999
                        }
1000
                        return Ok(value);
1001
                    }
1002
                    let target = candidate.raw["content"]["m.relates_to"]["event_id"]
1003
                        .as_str()
1004
                        .filter(|_| {
1005
                            candidate.raw["content"]["m.relates_to"]["rel_type"] == "m.replace"
1006
                        });
1007
                    if let Some(target) =
1008
                        target.and_then(|target| target.parse::<ruma::OwnedEventId>().ok())
1009
                    {
1010
                        let event = room.event(&target, None).await.map_err(|error| {
1011
                            Error::from_sdk("fetching an edited mention failed", error)
1012
                        })?;
1013
                        let original: Value = serde_json::from_str(event.raw().json().get())
1014
                            .map_err(|error| {
1015
                                Error::with_source(
1016
                                    ErrorKind::Invalid,
1017
                                    "invalid edited mention",
1018
                                    error,
1019
                                )
1020
                            })?;
1021
                        if original["sender"] == candidate.raw["sender"] {
1022
                            let mut original = edited(original, &candidate.raw);
1023
                            original["unsigned"]["macltok_edit"] = candidate.raw;
1024
                            return Ok(original);
1025
                        }
1026
                        return Err(Error::new(
1027
                            ErrorKind::Forbidden,
1028
                            "an edit belongs to a different sender",
1029
                        ));
1030
                    }
1031
                    let mut value = candidate.raw;
1032
                    if let Some(target) =
1033
                        value["content"]["m.relates_to"]["m.in_reply_to"]["event_id"]
1034
                            .as_str()
1035
                            .and_then(|target| target.parse::<ruma::OwnedEventId>().ok())
1036
                    {
1037
                        let event = room.event(&target, None).await.map_err(|error| {
1038
                            Error::from_sdk("fetching a mention reply target failed", error)
1039
                        })?;
1040
                        let target: Value = serde_json::from_str(event.raw().json().get())
1041
                            .map_err(|error| {
1042
                                Error::with_source(
1043
                                    ErrorKind::Invalid,
1044
                                    "invalid mention reply target",
1045
                                    error,
1046
                                )
1047
                            })?;
1048
                        value["unsigned"]["macltok_reply_sender"] = target["sender"].clone();
1049
                    }
1050
                    Ok(value)
1051
                }
1052
                .await;
1053
                Work::Candidate(key_for_work, result)
1054
            }));
1055
        }
1056
        if !*history
1057
            && self.recovery_budget > 0
1058
            && now() >= self.recovery_retry
1059
            && (self.latest_scan || !self.data.history_exhausted)
1060
            && self.data.coverage.historical_supported != Some(false)
1061
        {
1062
            *history = true;
1063
            let latest = self.latest_scan;
1064
            let from = if latest {
1065
                self.data.latest_cursor.clone()
1066
            } else {
1067
                self.data.history_cursor.clone()
1068
            };
1069
            let client = self.client.clone();
1070
            work.push(Box::pin(async move {
1071
                let mut request = ruma::api::client::push::get_notifications::v3::Request::new();
1072
                request.from = from.clone();
1073
                request.limit = Some(ruma::uint!(50));
1074
                let result = client.send(request).await.map_err(|error| {
1075
                    Error::from_sdk("fetching mention history failed", error.into())
1076
                });
1077
                Work::History(latest, from, result)
1078
            }));
1079
        }
1080
    }
1081
1082
    fn captured(
1083
        &self,
1084
        data: &mut Data,
1085
        key: String,
1086
        revision: u64,
1087
        result: Result<(crate::EventContext, Vec<MediaRegistration>, Vec<Value>), Error>,
1088
    ) {
1089
        let Some(entry) = data.entries.get(&key) else {
1090
            return;
1091
        };
1092
        if entry.revision != revision {
1093
            return;
1094
        }
1095
        let room_id = entry.key.room_id.clone();
1096
        if !self
1097
            .client
1098
            .get_room(&room_id)
1099
            .is_some_and(|room| room.state() == RoomState::Joined)
1100
        {
1101
            return;
1102
        }
1103
        match result {
1104
            Ok((context, media, _)) => {
1105
                data.capture_attempts.remove(&key);
1106
                let live_after = data
1107
                    .windows
1108
                    .get(&key)
1109
                    .map(|window| window.after.clone())
1110
                    .unwrap_or_default();
1111
                let mut insert = |item: TimelineItem| -> Option<String> {
1112
                    let TimelineItem::Event(event) = &item else {
1113
                        return None;
1114
                    };
1115
                    let event_id = event.event_id.clone()?;
1116
                    let event_key = id(&MentionKey {
1117
                        room_id: room_id.clone(),
1118
                        event_id,
1119
                    });
1120
                    let registrations = media_for_item(&item, &media);
1121
                    let mut saved = SavedEvent {
1122
                        item,
1123
                        media: registrations,
1124
                        raw: None,
1125
                    };
1126
                    if let Some(existing) = data.events.get(&event_key) {
1127
                        saved = existing.clone();
1128
                    }
1129
                    if data.redacted.contains_key(&event_key) {
1130
                        redact(&mut saved);
1131
                    }
1132
                    data.events.insert(event_key.clone(), saved);
1133
                    Some(event_key)
1134
                };
1135
                let before: Vec<_> = context
1136
                    .events_before
1137
                    .into_iter()
1138
                    .filter_map(&mut insert)
1139
                    .collect();
1140
                let target = context.event.and_then(&mut insert);
1141
                let mut after: Vec<_> = context
1142
                    .events_after
1143
                    .into_iter()
1144
                    .filter_map(&mut insert)
1145
                    .collect();
1146
                let extended = live_after.iter().any(|key| !after.contains(key));
1147
                after.extend(live_after);
1148
                after.sort_by_key(|key| {
1149
                    (
1150
                        data.events
1151
                            .get(key)
1152
                            .and_then(|saved| match &saved.item {
1153
                                TimelineItem::Event(event) => Some(event.timestamp_ms),
1154
                                _ => None,
1155
                            })
1156
                            .unwrap_or(0),
1157
                        key.clone(),
1158
                    )
1159
                });
1160
                after.dedup();
1161
                after.truncate(10);
1162
                let entry = data.entries.get_mut(&key).unwrap();
1163
                entry.capture = if target.is_none() {
1164
                    MentionCaptureState::Unavailable
1165
                } else if after.len() < 10 || (before.len() < 10 && context.start_token.is_some()) {
1166
                    MentionCaptureState::Partial
1167
                } else {
1168
                    MentionCaptureState::Ready
1169
                };
1170
                entry.revision = data.revision + 1;
1171
                data.windows.insert(
1172
                    key,
1173
                    Window {
1174
                        before,
1175
                        target,
1176
                        after,
1177
                        start: context.start_token,
1178
                        end: if extended { None } else { context.end_token },
1179
                    },
1180
                );
1181
            }
1182
            Err(error) => {
1183
                let attempts = data.capture_attempts.entry(key.clone()).or_default();
1184
                *attempts = attempts.saturating_add(1);
1185
                let delay = (1000u64 << (*attempts).min(11)).min(3_600_000);
1186
                let entry = data.entries.get_mut(&key).unwrap();
1187
                entry.capture = if matches!(
1188
                    error.kind,
1189
                    ErrorKind::EventNotFound | ErrorKind::Forbidden | ErrorKind::RoomUnknown
1190
                ) || error
1191
                    .server
1192
                    .as_ref()
1193
                    .is_some_and(|server| matches!(server.status, 403 | 404))
1194
                {
1195
                    MentionCaptureState::Unavailable
1196
                } else {
1197
                    MentionCaptureState::Failed {
1198
                        message: error.to_string(),
1199
                        retry_at_ms: retry_at(&error, delay),
1200
                    }
1201
                };
1202
                entry.revision = data.revision + 1;
1203
            }
1204
        }
1205
    }
1206
}
1207
1208
fn edited(mut original: Value, edit: &Value) -> Value {
1209
    original["content"] = edit["content"]["m.new_content"].clone();
1210
    original["unsigned"]["macltok_edited"] = Value::Bool(true);
1211
    original
1212
}
1213
1214
fn retry_at(error: &Error, fallback_ms: u64) -> u64 {
1215
    let mut source = std::error::Error::source(error);
1216
    while let Some(current) = source {
1217
        if let Some(sdk) = current.downcast_ref::<matrix_sdk::Error>() {
1218
            if let Some(ruma::api::error::ErrorKind::LimitExceeded(data)) =
1219
                sdk.client_api_error_kind()
1220
            {
1221
                if let Some(delay) = data.retry_after {
1222
                    let delay = match delay {
1223
                        ruma::api::error::RetryAfter::Delay(delay) => delay,
1224
                        ruma::api::error::RetryAfter::DateTime(time) => time
1225
                            .duration_since(ruma::time::SystemTime::now())
1226
                            .unwrap_or_default(),
1227
                    };
1228
                    return now().saturating_add(
1229
                        u64::try_from(delay.as_millis())
1230
                            .unwrap_or(u64::MAX)
1231
                            .max(fallback_ms),
1232
                    );
1233
                }
1234
            }
1235
        }
1236
        source = current.source();
1237
    }
1238
    now().saturating_add(fallback_ms)
1239
}
1240
1241
fn redact(event: &mut SavedEvent) {
1242
    if let TimelineItem::Event(item) = &mut event.item {
1243
        item.content = TimelineContent::Redacted;
1244
        item.reply_to = None;
1245
        item.reactions.clear();
1246
    }
1247
    event.media.clear();
1248
    event.raw = None;
1249
}
1250
1251
fn media_for_item(
1252
    item: &TimelineItem,
1253
    registrations: &[MediaRegistration],
1254
) -> Vec<MediaRegistration> {
1255
    let TimelineItem::Event(event) = item else {
1256
        return Vec::new();
1257
    };
1258
    let mut ids = Vec::new();
1259
    match &event.content {
1260
        TimelineContent::Message(message) => {
1261
            ids.extend(message.media.iter().map(|media| &media.id));
1262
            ids.extend(
1263
                message
1264
                    .link_previews
1265
                    .iter()
1266
                    .filter_map(|preview| preview.image.as_ref().map(|image| &image.media_id)),
1267
            );
1268
        }
1269
        TimelineContent::Sticker { media, .. } => ids.push(&media.id),
1270
        _ => {}
1271
    }
1272
    registrations
1273
        .iter()
1274
        .filter(|registration| ids.contains(&&registration.id))
1275
        .cloned()
1276
        .collect()
1277
}
1278
1279
pub(crate) async fn watch_updates(mut updates: watch::Receiver<MentionsPage>, events: EventSender) {
1280
    let mut previous: BTreeMap<String, MentionEntry> = BTreeMap::new();
1281
    loop {
1282
        let snapshot = updates.borrow_and_update().clone();
1283
        let current: BTreeMap<_, _> = snapshot
1284
            .entries
1285
            .iter()
1286
            .map(|entry| (id(&entry.key), entry.clone()))
1287
            .collect();
1288
        let upserts = current
1289
            .iter()
1290
            .filter(|(key, entry)| previous.get(*key) != Some(entry))
1291
            .map(|(_, entry)| entry.clone())
1292
            .collect();
1293
        let removed = previous
1294
            .iter()
1295
            .filter(|(key, _)| !current.contains_key(*key))
1296
            .map(|(_, entry)| entry.key.clone())
1297
            .collect();
1298
        previous = current;
1299
        if events
1300
            .send(Event::MentionsChanged(MentionsUpdate {
1301
                snapshot,
1302
                upserts,
1303
                removed,
1304
            }))
1305
            .await
1306
            .is_err()
1307
        {
1308
            return;
1309
        }
1310
        if updates.changed().await.is_err() {
1311
            return;
1312
        }
1313
    }
1314
}