hive-matrix-mcp: vary the UTD sentinel by event id so it re-arms the wake
This commit is contained in:
parent
be61059f15
commit
6ce01ff38d
1 changed files with 77 additions and 18 deletions
|
|
@ -281,22 +281,6 @@ async fn room_unread_state(
|
||||||
.map(|(id, _)| id);
|
.map(|(id, _)| id);
|
||||||
|
|
||||||
let is_encrypted = room.encryption_state().is_encrypted();
|
let is_encrypted = room.encryption_state().is_encrypted();
|
||||||
// UTD-gated body, same rule `to_timeline_event` uses for `read_room`:
|
|
||||||
// an encrypted room's genuinely-undecryptable event gets the sentinel;
|
|
||||||
// everything else (plaintext rooms, and successfully-decrypted events
|
|
||||||
// in an encrypted one) gets its real extracted body. Without this an
|
|
||||||
// encrypted room's raw ciphertext extracted as "" for every event, so
|
|
||||||
// two different unread messages produced the identical summary and
|
|
||||||
// never re-armed the wake.
|
|
||||||
let body_of = |raw: &matrix_sdk::deserialized_responses::TimelineEvent,
|
|
||||||
parsed: &matrix_sdk::ruma::events::AnySyncTimelineEvent| {
|
|
||||||
if is_encrypted && raw.kind.is_utd() {
|
|
||||||
"[unable to decrypt]".to_owned()
|
|
||||||
} else {
|
|
||||||
extract_body_sync(parsed)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let events = recent_events(room).await;
|
let events = recent_events(room).await;
|
||||||
for raw in &events {
|
for raw in &events {
|
||||||
let Ok(event) = raw.raw().deserialize() else {
|
let Ok(event) = raw.raw().deserialize() else {
|
||||||
|
|
@ -318,7 +302,7 @@ async fn room_unread_state(
|
||||||
if event.sender().as_str() == own_user_id.as_str() {
|
if event.sender().as_str() == own_user_id.as_str() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let body = body_of(raw, &event);
|
let body = utd_gated_body(is_encrypted, raw.kind.is_utd(), &event);
|
||||||
return Some((event.event_id().to_owned(), event.sender().to_owned(), body));
|
return Some((event.event_id().to_owned(), event.sender().to_owned(), body));
|
||||||
}
|
}
|
||||||
// Exhausted the lookback window without hitting the receipt or an
|
// Exhausted the lookback window without hitting the receipt or an
|
||||||
|
|
@ -334,7 +318,7 @@ async fn room_unread_state(
|
||||||
if newest.sender().as_str() == own_user_id.as_str() {
|
if newest.sender().as_str() == own_user_id.as_str() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let body = body_of(newest_raw, &newest);
|
let body = utd_gated_body(is_encrypted, newest_raw.kind.is_utd(), &newest);
|
||||||
Some((
|
Some((
|
||||||
newest.event_id().to_owned(),
|
newest.event_id().to_owned(),
|
||||||
newest.sender().to_owned(),
|
newest.sender().to_owned(),
|
||||||
|
|
@ -342,6 +326,32 @@ async fn room_unread_state(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// UTD-gated body, same rule `to_timeline_event` uses for `read_room`: an
|
||||||
|
/// encrypted room's genuinely-undecryptable event gets a sentinel that
|
||||||
|
/// still varies per event; everything else (plaintext rooms, and
|
||||||
|
/// successfully-decrypted events in an encrypted one) gets its real
|
||||||
|
/// extracted body.
|
||||||
|
///
|
||||||
|
/// The sentinel can't be a constant string. Two different UTD events
|
||||||
|
/// (the normal e2ee failure mode — a new device, a missing megolm
|
||||||
|
/// session, late-arriving keys) would otherwise produce byte-identical
|
||||||
|
/// summaries, and the harness's content-diff upsert would never re-wake
|
||||||
|
/// on the second one — the same silent-deafness failure as an undecrypted
|
||||||
|
/// room reporting an empty body for every event, just with a different
|
||||||
|
/// unchanging text. Folding the event id into the sentinel is cheap and
|
||||||
|
/// needs no new dedup-key plumbing.
|
||||||
|
fn utd_gated_body(
|
||||||
|
is_encrypted: bool,
|
||||||
|
is_utd: bool,
|
||||||
|
parsed: &matrix_sdk::ruma::events::AnySyncTimelineEvent,
|
||||||
|
) -> String {
|
||||||
|
if is_encrypted && is_utd {
|
||||||
|
format!("[unable to decrypt] ({})", parsed.event_id())
|
||||||
|
} else {
|
||||||
|
extract_body_sync(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn send_message(client: &Client, room_ref: &str, body: &str) -> DaemonResponse {
|
pub async fn send_message(client: &Client, room_ref: &str, body: &str) -> DaemonResponse {
|
||||||
let room = match resolve_room(client, room_ref).await {
|
let room = match resolve_room(client, room_ref).await {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
|
|
@ -1042,3 +1052,52 @@ pub async fn unread_summary(client: &Client) -> DaemonResponse {
|
||||||
let rooms = collect_unread(client).await;
|
let rooms = collect_unread(client).await;
|
||||||
DaemonResponse::ok(&rooms)
|
DaemonResponse::ok(&rooms)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod utd_body_tests {
|
||||||
|
use super::utd_gated_body;
|
||||||
|
|
||||||
|
/// Parses a minimal `m.room.message` sync event with the given event
|
||||||
|
/// id and body, enough for `utd_gated_body`'s plaintext path
|
||||||
|
/// (`extract_body_sync`) and the UTD path (which only reads the event
|
||||||
|
/// id) alike.
|
||||||
|
fn text_event(event_id: &str, body: &str) -> matrix_sdk::ruma::events::AnySyncTimelineEvent {
|
||||||
|
serde_json::from_value(serde_json::json!({
|
||||||
|
"type": "m.room.message",
|
||||||
|
"event_id": event_id,
|
||||||
|
"sender": "@alice:example.org",
|
||||||
|
"origin_server_ts": 1,
|
||||||
|
"content": {"msgtype": "m.text", "body": body},
|
||||||
|
}))
|
||||||
|
.expect("valid minimal m.room.message")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_utd_events_get_different_bodies() {
|
||||||
|
let a = utd_gated_body(true, true, &text_event("$a:example.org", "ciphertext a"));
|
||||||
|
let b = utd_gated_body(true, true, &text_event("$b:example.org", "ciphertext b"));
|
||||||
|
assert_ne!(
|
||||||
|
a, b,
|
||||||
|
"two different undecryptable events must not collapse to the \
|
||||||
|
same summary text, or the harness's content-diff upsert never \
|
||||||
|
re-wakes on the second one"
|
||||||
|
);
|
||||||
|
assert!(a.contains("$a:example.org"));
|
||||||
|
assert!(b.contains("$b:example.org"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decrypted_event_in_an_encrypted_room_uses_its_real_body() {
|
||||||
|
let got = utd_gated_body(true, false, &text_event("$a:example.org", "hello"));
|
||||||
|
assert_eq!(got, "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plaintext_room_ignores_the_utd_flag() {
|
||||||
|
// `is_utd` can transiently fire in a non-encrypted room while the
|
||||||
|
// SDK finishes reclassifying a freshly-synced cache row (see
|
||||||
|
// `to_timeline_event`'s doc comment) — `is_encrypted` gates it.
|
||||||
|
let got = utd_gated_body(false, true, &text_event("$a:example.org", "hello"));
|
||||||
|
assert_eq!(got, "hello");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue