atagen

macltok

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