From cc6336d9851983ca341d40cf2332052e9394a4f2 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Mon, 19 Oct 2026 16:29:32 -0400 Subject: [PATCH] feat(relay): tag every membership notice with its channel type (#8406) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ€– ## Summary - When someone is added to or removed from a system channel, the relay now sends them the usual "you were added / removed" notice. Before, it sent nothing, so a client could learn about a new system channel only by rereading every channel. - **A client sees a notice only if it knows the channel's type.** Every membership notice now carries its channel type. Clients that do not send `#t` (every client today) see only the types that existed before system channels, so they see no change. A type that Buzz adds later is hidden from them by default. - The notices are stored like every other membership notice, so a client that was offline can catch up with `since` instead of rereading all its channels. ## Details - Every kind:44100 (added) and kind:44101 (removed) notice now carries `["t", ]`, for example `["t","stream"]` or `["t","system"]`. - **One place reads the channel type:** `emit_membership_notification` in `side_effects.rs`, where the relay signs the notice. A channel's type is set when it is created and no code path changes it, so the tag stays correct. If the channel row is missing, the relay sends no notice rather than an untagged one. - **The rule (NIP-CL):** - a filter with `#t` matches a notice only when the notice's type is one of its `#t` values; - a filter without `#t` matches only the **legacy types**: `stream`, `forum`, `dm`, `workflow`. This list is frozen (`LEGACY_NOTICE_CHANNEL_TYPES`), so a new type never joins it; - a notice without a `t` tag was stored before this change. It is always a legacy type, so the relay treats it as one, and replay across the upgrade stays complete. - **Fails closed.** The rule lives in two shared places, and both default to the legacy types. A read path that is not updated behaves as it does today: - `filters_match` / `notice_visible` (buzz-core). Live fan-out, the REQ history post-filter, COUNT, the HTTP bridge and push all match through it. - `EventQuery::notice_channel_types` (buzz-db). Its SQL keeps only visible notices before `LIMIT`, so page limits and COUNT stay exact. `None` means legacy types. Only `filter_to_query_params` sets it, from the filter's `#t`. - A client that knows every current type sends one filter: `{"kinds":[44100,44101],"#p":[],"#t":["stream","forum","dm","workflow","system"]}`. NIP-CL and NOSTR.md describe this. - **Only the target receives a notice.** A notice names the target, the channel and who made the change, and the relay signs it, so knowing its id is not permission to read it. 44100/44101 are now in `RESULT_GATED_KINDS`, the one list used by the `ids` exemption check, the per-result reader check (WS and HTTP history, push), live fan-out (same pod and Redis) and COUNT. This also closes the same id lookup for ordinary channels' notices. - No migration: 44100/44101 are already excluded from search indexing. ## Testing - `buzz-core`: a table test covers untagged, `stream`, `system` and a future type against filters without `#t`, with `#t` for one type, and with `#t` for every type. Other kinds keep the plain `#t` match. - Postgres tests (`system_channel_postgres_tests.rs`): live fan-out, REQ history, COUNT and the HTTP bridge, for a filter without `#t`, a `#t:["system"]` filter and an all-types filter; an untagged legacy notice; a non-target with an `ids` filter on each path; and a raw `EventQuery` that does not set the field. - I broke each part on purpose and the tests failed: no type tag on the notice, no rule in `filters_match`, no SQL rule, and no untagged clause in the SQL. - End to end: I built a relay container from f24f8e7ba and ran buzz-app's `container-relay.e2e.test.ts` (block/buzz-app#1290) against it. The live system channel test passed 5 of 5 runs. Paired client change: block/buzz-app#1290. It works unchanged against this relay. --------- Signed-off-by: Logan Johnson Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> --- NOSTR.md | 10 +- crates/buzz-core/src/filter.rs | 163 +++++++- crates/buzz-core/src/kind.rs | 17 +- crates/buzz-db/src/store/channel_members.rs | 34 +- crates/buzz-db/src/store/event.rs | 58 +++ crates/buzz-relay/src/handlers/count.rs | 2 +- crates/buzz-relay/src/handlers/event.rs | 13 +- crates/buzz-relay/src/handlers/req.rs | 48 ++- .../buzz-relay/src/handlers/side_effects.rs | 26 +- .../handlers/system_channel_postgres_tests.rs | 395 ++++++++++++++++-- docs/nips/NIP-CL.md | 49 ++- 11 files changed, 736 insertions(+), 79 deletions(-) diff --git a/NOSTR.md b/NOSTR.md index 62e438d30d..96ccb0edc0 100644 --- a/NOSTR.md +++ b/NOSTR.md @@ -152,7 +152,10 @@ out unless they ask for it: combine with OR, so `#t:["system",