atagen

macltok

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

Blame: src/normalise/thread.rs

21b9227729a4atagen2026-10-25 11:03 UTC1
use std::collections::{BTreeMap, HashSet};
e5c6427adb35atagen2026-10-25 10:23 UTC2
e5c6427adb35atagen2026-10-25 10:23 UTC3
use matrix_sdk::{
21b9227729a4atagen2026-10-25 11:03 UTC4
    Room, RoomState, cross_process_lock::MappedCrossProcessLockState,
21b9227729a4atagen2026-10-25 11:03 UTC5
    deserialized_responses::TimelineEvent,
e5c6427adb35atagen2026-10-25 10:23 UTC6
};
21b9227729a4atagen2026-10-25 11:03 UTC7
use matrix_sdk_base::event_cache::store::EventCacheStoreLockGuard;
e5c6427adb35atagen2026-10-25 10:23 UTC8
use ruma::{
21b9227729a4atagen2026-10-25 11:03 UTC9
    OwnedEventId,
21b9227729a4atagen2026-10-25 11:03 UTC10
    events::{
21b9227729a4atagen2026-10-25 11:03 UTC11
        receipt::{ReceiptThread, ReceiptType},
21b9227729a4atagen2026-10-25 11:03 UTC12
        relation::RelationType,
21b9227729a4atagen2026-10-25 11:03 UTC13
    },
e5c6427adb35atagen2026-10-25 10:23 UTC14
};
21b9227729a4atagen2026-10-25 11:03 UTC15
use serde_json::{Value, json};
e5c6427adb35atagen2026-10-25 10:23 UTC16
e5c6427adb35atagen2026-10-25 10:23 UTC17
use crate::ThreadUnread;
e5c6427adb35atagen2026-10-25 10:23 UTC18
21b9227729a4atagen2026-10-25 11:03 UTC19
fn key(room: &Room) -> Vec<u8> {
21b9227729a4atagen2026-10-25 11:03 UTC20
    format!("macltok.thread_unreads.{}", room.room_id()).into_bytes()
e5c6427adb35atagen2026-10-25 10:23 UTC21
}
e5c6427adb35atagen2026-10-25 10:23 UTC22
21b9227729a4atagen2026-10-25 11:03 UTC23
/// what the last recount left, so a room list never touches the event cache
21b9227729a4atagen2026-10-25 11:03 UTC24
pub(crate) async fn stored_thread_unreads(room: &Room) -> BTreeMap<OwnedEventId, ThreadUnread> {
21b9227729a4atagen2026-10-25 11:03 UTC25
    if room.state() != RoomState::Joined {
21b9227729a4atagen2026-10-25 11:03 UTC26
        return BTreeMap::new();
21b9227729a4atagen2026-10-25 11:03 UTC27
    }
60454002af5fatagen2026-10-25 11:13 UTC28
    match room
60454002af5fatagen2026-10-25 11:13 UTC29
        .client()
60454002af5fatagen2026-10-25 11:13 UTC30
        .state_store()
60454002af5fatagen2026-10-25 11:13 UTC31
        .get_custom_value(&key(room))
60454002af5fatagen2026-10-25 11:13 UTC32
        .await
60454002af5fatagen2026-10-25 11:13 UTC33
    {
21b9227729a4atagen2026-10-25 11:03 UTC34
        Ok(Some(bytes)) => decode(&bytes),
21b9227729a4atagen2026-10-25 11:03 UTC35
        Ok(None) => BTreeMap::new(),
21b9227729a4atagen2026-10-25 11:03 UTC36
        Err(error) => {
21b9227729a4atagen2026-10-25 11:03 UTC37
            tracing::debug!(room_id = %room.room_id(), %error, "reading thread unreads failed");
21b9227729a4atagen2026-10-25 11:03 UTC38
            BTreeMap::new()
e5c6427adb35atagen2026-10-25 10:23 UTC39
        }
e5c6427adb35atagen2026-10-25 10:23 UTC40
    }
e5c6427adb35atagen2026-10-25 10:23 UTC41
}
e5c6427adb35atagen2026-10-25 10:23 UTC42
e5c6427adb35atagen2026-10-25 10:23 UTC43
/// the sdk leaves thread events out of the room's counts once threads are on and keeps none
21b9227729a4atagen2026-10-25 11:03 UTC44
/// of its own for them. this counts `touched` and every thread that still had something
21b9227729a4atagen2026-10-25 11:03 UTC45
/// unread from the event cache store, which holds every reply it ever took in whatever is
21b9227729a4atagen2026-10-25 11:03 UTC46
/// in memory, against a receipt for that thread (msc3771), an unthreaded one or the user's
21b9227729a4atagen2026-10-25 11:03 UTC47
/// own reply. `None` when nothing moved
21b9227729a4atagen2026-10-25 11:03 UTC48
pub(crate) async fn recount_thread_unreads(
21b9227729a4atagen2026-10-25 11:03 UTC49
    room: &Room,
21b9227729a4atagen2026-10-25 11:03 UTC50
    touched: HashSet<OwnedEventId>,
21b9227729a4atagen2026-10-25 11:03 UTC51
) -> Option<BTreeMap<OwnedEventId, ThreadUnread>> {
21b9227729a4atagen2026-10-25 11:03 UTC52
    let known = stored_thread_unreads(room).await;
21b9227729a4atagen2026-10-25 11:03 UTC53
    let mut roots = touched;
21b9227729a4atagen2026-10-25 11:03 UTC54
    roots.extend(known.keys().cloned());
21b9227729a4atagen2026-10-25 11:03 UTC55
    if roots.is_empty() || room.state() != RoomState::Joined {
21b9227729a4atagen2026-10-25 11:03 UTC56
        return None;
e5c6427adb35atagen2026-10-25 10:23 UTC57
    }
21b9227729a4atagen2026-10-25 11:03 UTC58
    let store = match room.client().event_cache_store().lock().await {
60454002af5fatagen2026-10-25 11:13 UTC59
        Ok(
60454002af5fatagen2026-10-25 11:13 UTC60
            MappedCrossProcessLockState::Clean(store) | MappedCrossProcessLockState::Dirty(store),
60454002af5fatagen2026-10-25 11:13 UTC61
        ) => store,
21b9227729a4atagen2026-10-25 11:03 UTC62
        Err(error) => {
21b9227729a4atagen2026-10-25 11:03 UTC63
            tracing::debug!(room_id = %room.room_id(), %error, "locking the event cache store failed");
21b9227729a4atagen2026-10-25 11:03 UTC64
            return None;
21b9227729a4atagen2026-10-25 11:03 UTC65
        }
e5c6427adb35atagen2026-10-25 10:23 UTC66
    };
21b9227729a4atagen2026-10-25 11:03 UTC67
    let own_user_id = room.own_user_id();
21b9227729a4atagen2026-10-25 11:03 UTC68
    let unthreaded = read_through(room, &store, ReceiptThread::Unthreaded).await;
21b9227729a4atagen2026-10-25 11:03 UTC69
    let mut counted = BTreeMap::new();
21b9227729a4atagen2026-10-25 11:03 UTC70
    for root in roots {
21b9227729a4atagen2026-10-25 11:03 UTC71
        let replies = match store
21b9227729a4atagen2026-10-25 11:03 UTC72
            .find_event_relations(room.room_id(), &root, Some(&[RelationType::Thread][..]))
21b9227729a4atagen2026-10-25 11:03 UTC73
            .await
21b9227729a4atagen2026-10-25 11:03 UTC74
        {
21b9227729a4atagen2026-10-25 11:03 UTC75
            Ok(replies) => replies,
21b9227729a4atagen2026-10-25 11:03 UTC76
            Err(error) => {
21b9227729a4atagen2026-10-25 11:03 UTC77
                tracing::debug!(room_id = %room.room_id(), %root, %error, "reading a thread failed");
21b9227729a4atagen2026-10-25 11:03 UTC78
                if let Some(unread) = known.get(&root) {
21b9227729a4atagen2026-10-25 11:03 UTC79
                    counted.insert(root, *unread);
21b9227729a4atagen2026-10-25 11:03 UTC80
                }
21b9227729a4atagen2026-10-25 11:03 UTC81
                continue;
e5c6427adb35atagen2026-10-25 10:23 UTC82
            }
21b9227729a4atagen2026-10-25 11:03 UTC83
        };
60454002af5fatagen2026-10-25 11:13 UTC84
        let mut read =
60454002af5fatagen2026-10-25 11:13 UTC85
            unthreaded.max(read_through(room, &store, ReceiptThread::Thread(root.clone())).await);
21b9227729a4atagen2026-10-25 11:03 UTC86
        for (reply, _) in &replies {
21b9227729a4atagen2026-10-25 11:03 UTC87
            if reply.sender().as_deref() == Some(own_user_id) {
21b9227729a4atagen2026-10-25 11:03 UTC88
                read = read.max(timestamp_ms(reply));
e5c6427adb35atagen2026-10-25 10:23 UTC89
            }
21b9227729a4atagen2026-10-25 11:03 UTC90
        }
e5c6427adb35atagen2026-10-25 10:23 UTC91
        let mut unread = ThreadUnread::default();
21b9227729a4atagen2026-10-25 11:03 UTC92
        for (reply, _) in &replies {
21b9227729a4atagen2026-10-25 11:03 UTC93
            let Some(at) = timestamp_ms(reply) else {
21b9227729a4atagen2026-10-25 11:03 UTC94
                continue;
21b9227729a4atagen2026-10-25 11:03 UTC95
            };
21b9227729a4atagen2026-10-25 11:03 UTC96
            if read.is_some_and(|read| at <= read)
21b9227729a4atagen2026-10-25 11:03 UTC97
                || reply.sender().as_deref() == Some(own_user_id)
21b9227729a4atagen2026-10-25 11:03 UTC98
                || !counts_as_unread(reply)
e5c6427adb35atagen2026-10-25 10:23 UTC99
            {
21b9227729a4atagen2026-10-25 11:03 UTC100
                continue;
e5c6427adb35atagen2026-10-25 10:23 UTC101
            }
21b9227729a4atagen2026-10-25 11:03 UTC102
            let actions = reply.push_actions().unwrap_or_default();
21b9227729a4atagen2026-10-25 11:03 UTC103
            unread.messages += 1;
21b9227729a4atagen2026-10-25 11:03 UTC104
            unread.notifications += u64::from(actions.iter().any(|action| action.should_notify()));
21b9227729a4atagen2026-10-25 11:03 UTC105
            unread.mentions += u64::from(actions.iter().any(|action| action.is_highlight()));
e5c6427adb35atagen2026-10-25 10:23 UTC106
        }
e5c6427adb35atagen2026-10-25 10:23 UTC107
        if unread != ThreadUnread::default() {
21b9227729a4atagen2026-10-25 11:03 UTC108
            counted.insert(root, unread);
e5c6427adb35atagen2026-10-25 10:23 UTC109
        }
e5c6427adb35atagen2026-10-25 10:23 UTC110
    }
21b9227729a4atagen2026-10-25 11:03 UTC111
    if counted == known {
21b9227729a4atagen2026-10-25 11:03 UTC112
        return None;
21b9227729a4atagen2026-10-25 11:03 UTC113
    }
21b9227729a4atagen2026-10-25 11:03 UTC114
    let stored = room
21b9227729a4atagen2026-10-25 11:03 UTC115
        .client()
21b9227729a4atagen2026-10-25 11:03 UTC116
        .state_store()
21b9227729a4atagen2026-10-25 11:03 UTC117
        .set_custom_value(&key(room), encode(&counted))
21b9227729a4atagen2026-10-25 11:03 UTC118
        .await;
21b9227729a4atagen2026-10-25 11:03 UTC119
    if let Err(error) = stored {
21b9227729a4atagen2026-10-25 11:03 UTC120
        tracing::debug!(room_id = %room.room_id(), %error, "saving thread unreads failed");
21b9227729a4atagen2026-10-25 11:03 UTC121
    }
21b9227729a4atagen2026-10-25 11:03 UTC122
    Some(counted)
e5c6427adb35atagen2026-10-25 10:23 UTC123
}
e5c6427adb35atagen2026-10-25 10:23 UTC124
21b9227729a4atagen2026-10-25 11:03 UTC125
/// receipts sit on events, and stream order between a thread and the rest of the room is
21b9227729a4atagen2026-10-25 11:03 UTC126
/// gone once it's in the store, so the event's timestamp stands for its place. a receipt
21b9227729a4atagen2026-10-25 11:03 UTC127
/// on an event the store never had falls back to when it was sent
21b9227729a4atagen2026-10-25 11:03 UTC128
async fn read_through(
21b9227729a4atagen2026-10-25 11:03 UTC129
    room: &Room,
21b9227729a4atagen2026-10-25 11:03 UTC130
    store: &EventCacheStoreLockGuard,
21b9227729a4atagen2026-10-25 11:03 UTC131
    thread: ReceiptThread,
21b9227729a4atagen2026-10-25 11:03 UTC132
) -> Option<u64> {
21b9227729a4atagen2026-10-25 11:03 UTC133
    let mut latest = None;
e5c6427adb35atagen2026-10-25 10:23 UTC134
    for receipt_type in [ReceiptType::Read, ReceiptType::ReadPrivate] {
21b9227729a4atagen2026-10-25 11:03 UTC135
        let (event_id, receipt) = match room
e5c6427adb35atagen2026-10-25 10:23 UTC136
            .load_user_receipt(receipt_type, thread.clone(), room.own_user_id())
e5c6427adb35atagen2026-10-25 10:23 UTC137
            .await
e5c6427adb35atagen2026-10-25 10:23 UTC138
        {
21b9227729a4atagen2026-10-25 11:03 UTC139
            Ok(Some(found)) => found,
21b9227729a4atagen2026-10-25 11:03 UTC140
            Ok(None) => continue,
e5c6427adb35atagen2026-10-25 10:23 UTC141
            Err(error) => {
e5c6427adb35atagen2026-10-25 10:23 UTC142
                tracing::debug!(room_id = %room.room_id(), %error, "reading a receipt failed");
21b9227729a4atagen2026-10-25 11:03 UTC143
                continue;
e5c6427adb35atagen2026-10-25 10:23 UTC144
            }
21b9227729a4atagen2026-10-25 11:03 UTC145
        };
21b9227729a4atagen2026-10-25 11:03 UTC146
        let at = match store.find_event(room.room_id(), &event_id).await {
21b9227729a4atagen2026-10-25 11:03 UTC147
            Ok(Some(event)) => timestamp_ms(&event),
21b9227729a4atagen2026-10-25 11:03 UTC148
            _ => None,
e5c6427adb35atagen2026-10-25 10:23 UTC149
        }
21b9227729a4atagen2026-10-25 11:03 UTC150
        .or_else(|| receipt.ts.map(|ts| u64::from(ts.get())));
21b9227729a4atagen2026-10-25 11:03 UTC151
        latest = latest.max(at);
e5c6427adb35atagen2026-10-25 10:23 UTC152
    }
21b9227729a4atagen2026-10-25 11:03 UTC153
    latest
e5c6427adb35atagen2026-10-25 10:23 UTC154
}
e5c6427adb35atagen2026-10-25 10:23 UTC155
21b9227729a4atagen2026-10-25 11:03 UTC156
fn timestamp_ms(event: &TimelineEvent) -> Option<u64> {
21b9227729a4atagen2026-10-25 11:03 UTC157
    event.timestamp().map(|ts| u64::from(ts.get()))
21b9227729a4atagen2026-10-25 11:03 UTC158
}
21b9227729a4atagen2026-10-25 11:03 UTC159
21b9227729a4atagen2026-10-25 11:03 UTC160
/// the sdk's `marks_as_unread` for its room counts, which is private to it
e5c6427adb35atagen2026-10-25 10:23 UTC161
fn counts_as_unread(event: &TimelineEvent) -> bool {
e5c6427adb35atagen2026-10-25 10:23 UTC162
    let raw = event.raw();
e5c6427adb35atagen2026-10-25 10:23 UTC163
    let redacted = raw
e5c6427adb35atagen2026-10-25 10:23 UTC164
        .get_field::<Value>("unsigned")
e5c6427adb35atagen2026-10-25 10:23 UTC165
        .ok()
e5c6427adb35atagen2026-10-25 10:23 UTC166
        .flatten()
e5c6427adb35atagen2026-10-25 10:23 UTC167
        .is_some_and(|unsigned| unsigned.get("redacted_because").is_some());
21b9227729a4atagen2026-10-25 11:03 UTC168
    let edit = raw
21b9227729a4atagen2026-10-25 11:03 UTC169
        .get_field::<Value>("content")
21b9227729a4atagen2026-10-25 11:03 UTC170
        .ok()
21b9227729a4atagen2026-10-25 11:03 UTC171
        .flatten()
21b9227729a4atagen2026-10-25 11:03 UTC172
        .and_then(|content| {
21b9227729a4atagen2026-10-25 11:03 UTC173
            content
21b9227729a4atagen2026-10-25 11:03 UTC174
                .pointer("/m.relates_to/rel_type")
21b9227729a4atagen2026-10-25 11:03 UTC175
                .and_then(Value::as_str)
21b9227729a4atagen2026-10-25 11:03 UTC176
                .map(|rel_type| rel_type == "m.replace")
21b9227729a4atagen2026-10-25 11:03 UTC177
        })
21b9227729a4atagen2026-10-25 11:03 UTC178
        .unwrap_or(false);
e5c6427adb35atagen2026-10-25 10:23 UTC179
    let message_like = matches!(
e5c6427adb35atagen2026-10-25 10:23 UTC180
        raw.get_field::<String>("type").ok().flatten().as_deref(),
e5c6427adb35atagen2026-10-25 10:23 UTC181
        Some(
21b9227729a4atagen2026-10-25 11:03 UTC182
            "m.message"
21b9227729a4atagen2026-10-25 11:03 UTC183
                | "m.room.message"
e5c6427adb35atagen2026-10-25 10:23 UTC184
                | "m.room.encrypted"
e5c6427adb35atagen2026-10-25 10:23 UTC185
                | "m.sticker"
e5c6427adb35atagen2026-10-25 10:23 UTC186
                | "m.poll.start"
e5c6427adb35atagen2026-10-25 10:23 UTC187
                | "org.matrix.msc3381.poll.start"
e5c6427adb35atagen2026-10-25 10:23 UTC188
                | "m.poll.end"
e5c6427adb35atagen2026-10-25 10:23 UTC189
                | "org.matrix.msc3381.poll.end"
e5c6427adb35atagen2026-10-25 10:23 UTC190
        )
e5c6427adb35atagen2026-10-25 10:23 UTC191
    );
21b9227729a4atagen2026-10-25 11:03 UTC192
    message_like && !edit && !redacted
21b9227729a4atagen2026-10-25 11:03 UTC193
}
21b9227729a4atagen2026-10-25 11:03 UTC194
21b9227729a4atagen2026-10-25 11:03 UTC195
fn encode(unreads: &BTreeMap<OwnedEventId, ThreadUnread>) -> Vec<u8> {
21b9227729a4atagen2026-10-25 11:03 UTC196
    let value: serde_json::Map<String, Value> = unreads
21b9227729a4atagen2026-10-25 11:03 UTC197
        .iter()
21b9227729a4atagen2026-10-25 11:03 UTC198
        .map(|(root, unread)| {
21b9227729a4atagen2026-10-25 11:03 UTC199
            (
21b9227729a4atagen2026-10-25 11:03 UTC200
                root.to_string(),
21b9227729a4atagen2026-10-25 11:03 UTC201
                json!([unread.messages, unread.mentions, unread.notifications]),
21b9227729a4atagen2026-10-25 11:03 UTC202
            )
21b9227729a4atagen2026-10-25 11:03 UTC203
        })
21b9227729a4atagen2026-10-25 11:03 UTC204
        .collect();
21b9227729a4atagen2026-10-25 11:03 UTC205
    serde_json::to_vec(&value).unwrap_or_default()
21b9227729a4atagen2026-10-25 11:03 UTC206
}
21b9227729a4atagen2026-10-25 11:03 UTC207
21b9227729a4atagen2026-10-25 11:03 UTC208
fn decode(bytes: &[u8]) -> BTreeMap<OwnedEventId, ThreadUnread> {
21b9227729a4atagen2026-10-25 11:03 UTC209
    let Ok(value) = serde_json::from_slice::<BTreeMap<OwnedEventId, [u64; 3]>>(bytes) else {
21b9227729a4atagen2026-10-25 11:03 UTC210
        return BTreeMap::new();
21b9227729a4atagen2026-10-25 11:03 UTC211
    };
21b9227729a4atagen2026-10-25 11:03 UTC212
    value
21b9227729a4atagen2026-10-25 11:03 UTC213
        .into_iter()
21b9227729a4atagen2026-10-25 11:03 UTC214
        .map(|(root, [messages, mentions, notifications])| {
21b9227729a4atagen2026-10-25 11:03 UTC215
            (
21b9227729a4atagen2026-10-25 11:03 UTC216
                root,
21b9227729a4atagen2026-10-25 11:03 UTC217
                ThreadUnread {
21b9227729a4atagen2026-10-25 11:03 UTC218
                    messages,
21b9227729a4atagen2026-10-25 11:03 UTC219
                    mentions,
21b9227729a4atagen2026-10-25 11:03 UTC220
                    notifications,
21b9227729a4atagen2026-10-25 11:03 UTC221
                },
21b9227729a4atagen2026-10-25 11:03 UTC222
            )
21b9227729a4atagen2026-10-25 11:03 UTC223
        })
21b9227729a4atagen2026-10-25 11:03 UTC224
        .collect()
e5c6427adb35atagen2026-10-25 10:23 UTC225
}