Resurface hidden DMs for forum message kinds

The relay accepted forum posts/comments (45001/45003) as channel-scoped
writes but only routed kinds 9/40002 through the atomic DM insert/resurface
path. Both clients classify all four as human-visible channel messages, so a
custom or stale client could post a forum event into a hidden DM: the event
rendered but the recipient's hidden_at stayed set and their clients stayed
unsubscribed — the original bug through another supported kind.

Centralize the resurface trigger on a shared is_human_visible_message_kind
predicate (kinds 9/40002/45001/45003, matching the clients'
CHANNEL_MESSAGE_EVENT_KINDS) and parameterize the live regression over all
four kinds so dropping either forum kind fails the test.

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Mongo <9cfd347903944d5b85aa6c93d2ab67381b978a92a31914bca69998968752a1d7@buzz.block.builderlab.xyz>
This commit is contained in:
kenny lopez
2026-08-21 15:56:37 +01:00
parent b0a324357f
commit 7265c10a29
3 changed files with 63 additions and 2 deletions

View File

@@ -811,6 +811,22 @@ pub const fn is_identity_archive_request_kind(kind: u32) -> bool {
matches!(kind, KIND_IA_ARCHIVE_REQUEST | KIND_IA_UNARCHIVE_REQUEST)
}
/// Returns `true` if `kind` is a human-visible channel message.
///
/// This is the set clients render as timeline content and count toward unreads
/// (`CHANNEL_MESSAGE_EVENT_KINDS` in Desktop, the equivalent Mobile set). It is
/// the authoritative trigger for DM resurfacing: a hidden DM must reappear when
/// any human-visible message arrives, regardless of which supported kind a
/// client uses. Reactions, edits, diffs, pins, deletions, and system messages
/// are deliberately excluded — they can land after the last real message and
/// must not resurface a conversation the recipient hid.
pub const fn is_human_visible_message_kind(kind: u32) -> bool {
matches!(
kind,
KIND_STREAM_MESSAGE | KIND_STREAM_MESSAGE_V2 | KIND_FORUM_POST | KIND_FORUM_COMMENT
)
}
/// Returns `true` if `kind` is a Buzz command kind that requires transactional execution.
pub const fn is_command_kind(kind: u32) -> bool {
matches!(
@@ -913,6 +929,39 @@ mod tests {
assert!(!is_relay_only_kind(KIND_NIP43_LEAVE_REQUEST));
}
#[test]
fn human_visible_message_kinds_cover_every_rendered_message() {
// Exactly the client `CHANNEL_MESSAGE_EVENT_KINDS` set — the set that
// must resurface a hidden DM. Forum posts/comments count: the relay
// accepts them as channel writes and both clients render them.
for kind in [
KIND_STREAM_MESSAGE,
KIND_STREAM_MESSAGE_V2,
KIND_FORUM_POST,
KIND_FORUM_COMMENT,
] {
assert!(
is_human_visible_message_kind(kind),
"kind {kind} must be human-visible"
);
}
// Non-message activity in a channel must not resurface a hidden DM.
for kind in [
KIND_REACTION,
KIND_STREAM_MESSAGE_EDIT,
KIND_STREAM_MESSAGE_DIFF,
KIND_STREAM_MESSAGE_PINNED,
KIND_SYSTEM_MESSAGE,
KIND_DELETION,
KIND_FORUM_VOTE,
] {
assert!(
!is_human_visible_message_kind(kind),
"kind {kind} must not be human-visible"
);
}
}
#[test]
fn parameterized_replaceable_range() {
assert!(!is_parameterized_replaceable(29999));

View File

@@ -3036,7 +3036,13 @@ async fn ingest_event_inner(
});
}
let dm_message_sender = if matches!(kind_u32, KIND_STREAM_MESSAGE | KIND_STREAM_MESSAGE_V2)
// Resurface a hidden DM for every human-visible message kind, not just
// kinds 9/40002. The relay also accepts forum posts/comments (45001/45003)
// as channel-scoped writes, and both clients render them as timeline
// messages, so a custom or stale client posting one into a hidden DM must
// reopen it too. Reactions, edits, and system kinds stay excluded via the
// shared predicate.
let dm_message_sender = if buzz_core::kind::is_human_visible_message_kind(kind_u32)
&& channel_row
.as_ref()
.is_some_and(|channel| channel.channel_type == "dm")

View File

@@ -1331,10 +1331,16 @@ async fn test_nipdv_hide_then_reopen_updates_snapshot() {
/// the recipient's hidden set. The sender remains hidden if they independently
/// closed the row: visibility is per viewer and delivery must not rewrite the
/// author's own preference.
///
/// Parameterized over every human-visible message kind the relay accepts as a
/// channel write (`is_human_visible_message_kind`): kinds 9/40002 plus forum
/// post/comment 45001/45003. A stale or custom client can post a forum event
/// into a DM, and both clients render it, so all four must resurface a hidden
/// DM. Dropping either forum kind from the ingest predicate fails this test.
#[tokio::test]
#[ignore]
async fn test_nipdv_supported_message_kinds_resurface_hidden_dm_for_recipient() {
for kind in [9, 40002] {
for kind in [9u16, 40002, 45001, 45003] {
let keys_a = Keys::generate();
let keys_b = Keys::generate();
let keys_c = Keys::generate();