atagen

macltok

public
Star0
Watch0
21b9227729a49cccc12924f2ca63c0f1890c6b89
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/normalise/thread.rsRust219 lines7.5 KB
1
use std::collections::{BTreeMap, HashSet};
2
3
use matrix_sdk::{
4
    Room, RoomState, cross_process_lock::MappedCrossProcessLockState,
5
    deserialized_responses::TimelineEvent,
6
};
7
use matrix_sdk_base::event_cache::store::EventCacheStoreLockGuard;
8
use ruma::{
9
    OwnedEventId,
10
    events::{
11
        receipt::{ReceiptThread, ReceiptType},
12
        relation::RelationType,
13
    },
14
};
15
use serde_json::{Value, json};
16
17
use crate::ThreadUnread;
18
19
fn key(room: &Room) -> Vec<u8> {
20
    format!("macltok.thread_unreads.{}", room.room_id()).into_bytes()
21
}
22
23
/// what the last recount left, so a room list never touches the event cache
24
pub(crate) async fn stored_thread_unreads(room: &Room) -> BTreeMap<OwnedEventId, ThreadUnread> {
25
    if room.state() != RoomState::Joined {
26
        return BTreeMap::new();
27
    }
28
    match room.client().state_store().get_custom_value(&key(room)).await {
29
        Ok(Some(bytes)) => decode(&bytes),
30
        Ok(None) => BTreeMap::new(),
31
        Err(error) => {
32
            tracing::debug!(room_id = %room.room_id(), %error, "reading thread unreads failed");
33
            BTreeMap::new()
34
        }
35
    }
36
}
37
38
/// the sdk leaves thread events out of the room's counts once threads are on and keeps none
39
/// of its own for them. this counts `touched` and every thread that still had something
40
/// unread from the event cache store, which holds every reply it ever took in whatever is
41
/// in memory, against a receipt for that thread (msc3771), an unthreaded one or the user's
42
/// own reply. `None` when nothing moved
43
pub(crate) async fn recount_thread_unreads(
44
    room: &Room,
45
    touched: HashSet<OwnedEventId>,
46
) -> Option<BTreeMap<OwnedEventId, ThreadUnread>> {
47
    let known = stored_thread_unreads(room).await;
48
    let mut roots = touched;
49
    roots.extend(known.keys().cloned());
50
    if roots.is_empty() || room.state() != RoomState::Joined {
51
        return None;
52
    }
53
    let store = match room.client().event_cache_store().lock().await {
54
        Ok(MappedCrossProcessLockState::Clean(store) | MappedCrossProcessLockState::Dirty(store)) => {
55
            store
56
        }
57
        Err(error) => {
58
            tracing::debug!(room_id = %room.room_id(), %error, "locking the event cache store failed");
59
            return None;
60
        }
61
    };
62
    let own_user_id = room.own_user_id();
63
    let unthreaded = read_through(room, &store, ReceiptThread::Unthreaded).await;
64
    let mut counted = BTreeMap::new();
65
    for root in roots {
66
        let replies = match store
67
            .find_event_relations(room.room_id(), &root, Some(&[RelationType::Thread][..]))
68
            .await
69
        {
70
            Ok(replies) => replies,
71
            Err(error) => {
72
                tracing::debug!(room_id = %room.room_id(), %root, %error, "reading a thread failed");
73
                if let Some(unread) = known.get(&root) {
74
                    counted.insert(root, *unread);
75
                }
76
                continue;
77
            }
78
        };
79
        let mut read = unthreaded.max(read_through(room, &store, ReceiptThread::Thread(root.clone())).await);
80
        for (reply, _) in &replies {
81
            if reply.sender().as_deref() == Some(own_user_id) {
82
                read = read.max(timestamp_ms(reply));
83
            }
84
        }
85
        let mut unread = ThreadUnread::default();
86
        for (reply, _) in &replies {
87
            let Some(at) = timestamp_ms(reply) else {
88
                continue;
89
            };
90
            if read.is_some_and(|read| at <= read)
91
                || reply.sender().as_deref() == Some(own_user_id)
92
                || !counts_as_unread(reply)
93
            {
94
                continue;
95
            }
96
            let actions = reply.push_actions().unwrap_or_default();
97
            unread.messages += 1;
98
            unread.notifications += u64::from(actions.iter().any(|action| action.should_notify()));
99
            unread.mentions += u64::from(actions.iter().any(|action| action.is_highlight()));
100
        }
101
        if unread != ThreadUnread::default() {
102
            counted.insert(root, unread);
103
        }
104
    }
105
    if counted == known {
106
        return None;
107
    }
108
    let stored = room
109
        .client()
110
        .state_store()
111
        .set_custom_value(&key(room), encode(&counted))
112
        .await;
113
    if let Err(error) = stored {
114
        tracing::debug!(room_id = %room.room_id(), %error, "saving thread unreads failed");
115
    }
116
    Some(counted)
117
}
118
119
/// receipts sit on events, and stream order between a thread and the rest of the room is
120
/// gone once it's in the store, so the event's timestamp stands for its place. a receipt
121
/// on an event the store never had falls back to when it was sent
122
async fn read_through(
123
    room: &Room,
124
    store: &EventCacheStoreLockGuard,
125
    thread: ReceiptThread,
126
) -> Option<u64> {
127
    let mut latest = None;
128
    for receipt_type in [ReceiptType::Read, ReceiptType::ReadPrivate] {
129
        let (event_id, receipt) = match room
130
            .load_user_receipt(receipt_type, thread.clone(), room.own_user_id())
131
            .await
132
        {
133
            Ok(Some(found)) => found,
134
            Ok(None) => continue,
135
            Err(error) => {
136
                tracing::debug!(room_id = %room.room_id(), %error, "reading a receipt failed");
137
                continue;
138
            }
139
        };
140
        let at = match store.find_event(room.room_id(), &event_id).await {
141
            Ok(Some(event)) => timestamp_ms(&event),
142
            _ => None,
143
        }
144
        .or_else(|| receipt.ts.map(|ts| u64::from(ts.get())));
145
        latest = latest.max(at);
146
    }
147
    latest
148
}
149
150
fn timestamp_ms(event: &TimelineEvent) -> Option<u64> {
151
    event.timestamp().map(|ts| u64::from(ts.get()))
152
}
153
154
/// the sdk's `marks_as_unread` for its room counts, which is private to it
155
fn counts_as_unread(event: &TimelineEvent) -> bool {
156
    let raw = event.raw();
157
    let redacted = raw
158
        .get_field::<Value>("unsigned")
159
        .ok()
160
        .flatten()
161
        .is_some_and(|unsigned| unsigned.get("redacted_because").is_some());
162
    let edit = raw
163
        .get_field::<Value>("content")
164
        .ok()
165
        .flatten()
166
        .and_then(|content| {
167
            content
168
                .pointer("/m.relates_to/rel_type")
169
                .and_then(Value::as_str)
170
                .map(|rel_type| rel_type == "m.replace")
171
        })
172
        .unwrap_or(false);
173
    let message_like = matches!(
174
        raw.get_field::<String>("type").ok().flatten().as_deref(),
175
        Some(
176
            "m.message"
177
                | "m.room.message"
178
                | "m.room.encrypted"
179
                | "m.sticker"
180
                | "m.poll.start"
181
                | "org.matrix.msc3381.poll.start"
182
                | "m.poll.end"
183
                | "org.matrix.msc3381.poll.end"
184
        )
185
    );
186
    message_like && !edit && !redacted
187
}
188
189
fn encode(unreads: &BTreeMap<OwnedEventId, ThreadUnread>) -> Vec<u8> {
190
    let value: serde_json::Map<String, Value> = unreads
191
        .iter()
192
        .map(|(root, unread)| {
193
            (
194
                root.to_string(),
195
                json!([unread.messages, unread.mentions, unread.notifications]),
196
            )
197
        })
198
        .collect();
199
    serde_json::to_vec(&value).unwrap_or_default()
200
}
201
202
fn decode(bytes: &[u8]) -> BTreeMap<OwnedEventId, ThreadUnread> {
203
    let Ok(value) = serde_json::from_slice::<BTreeMap<OwnedEventId, [u64; 3]>>(bytes) else {
204
        return BTreeMap::new();
205
    };
206
    value
207
        .into_iter()
208
        .map(|(root, [messages, mentions, notifications])| {
209
            (
210
                root,
211
                ThreadUnread {
212
                    messages,
213
                    mentions,
214
                    notifications,
215
                },
216
            )
217
        })
218
        .collect()
219
}