mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(relay): scope push matches with the read path's channel rule (#8507)
🤖 ## Summary This is a consistency and forward-compatibility fix. It does not change what users of current clients see. Mobile push notifications now decide which channels can notify a device with the same rule that reads use, with one exception that the protocol requires: system channels never push. Before this change, push and reads used two separate rules: - **Reads** (subscriptions, counts, search and the bridge) show only the original channel types (stream, forum, DM, workflow) unless the filter names the channel. - **Push** skipped only `system` channels. Today `system` is the only non-original type, so the two rules give the same result. But a channel type added later would be hidden from reads and would still send push notifications to every member. This PR removes that gap: push now calls the same function that reads use, so a future type gets the correct push behavior with no extra work. ## What changes - A channel of an original type pushes as before. Membership is still required. - A system channel never pushes, even when a push filter names it. NIP-CL says: "The relay MUST NOT send push notifications for system channels." This is the same as before. - A channel of a type added later pushes only to a push filter that names that channel with `#h` (the channel tag), as reads do. - A push filter that names one channel does not get pushes from other named-only channels. - Events that are not in a channel (for example gift wraps) match as before. Push filters cannot carry the `#t` or `#P` opt-in that reads accept, so for push, naming the channel means `#h` only. ## Impact on current clients: none - System channels still never push, and no current channel type is affected by the later-type rule. - The mobile app names channels by `#h` only for joined, unarchived DM channels (`mobile/lib/shared/push/push_subscription_provider.dart`). DMs are an original type. - Naming a channel cannot get around membership. A push filter that names a channel still gets nothing unless its owner is a member of that channel. - Database cost is unchanged: one per-batch query replaces another, on the same channel list, with no extra round trip. - The push docs (NIP-PL and the push suppression doc) do not mention channel types, so they need no update. ## Details - The push matcher loads the named-only channels in each batch and checks each lease filter with `filter_channel_scope`, the function REQ, COUNT, search and the bridge already use. Before that check, it rejects system channels outright. - This removes `system_channel_ids_among`, which push was the last caller of. ### Related issue Part of the system channels spec (§2, rule 5). Builds on #8181 and #8469. No duplicate PR found. ### Testing - A `match_job` unit test covers: unnamed filters on an original-type channel, on a channel-less event, on a system channel and on a later-type channel; a later-type channel named by `#h` (1 wake); a system channel named by `#h` (0 wakes); an unrelated `#h` (no widening); and non-members. Removing either the shared scope check or the system exception fails it. - Before this PR, no test covered the push channel filter, not even for `system`. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -1321,31 +1321,6 @@ pub async fn channel_type_of(
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// The IDs in `channel_ids` that name system channels, deleted or not.
|
||||
/// Push matching uses it to skip them.
|
||||
pub async fn system_channel_ids_among(
|
||||
pool: &PgPool,
|
||||
community_id: CommunityId,
|
||||
channel_ids: &[Uuid],
|
||||
) -> Result<Vec<Uuid>> {
|
||||
if channel_ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut connection = crate::observability::acquire_writer(
|
||||
pool,
|
||||
crate::observability::WriterOperation::Authorization,
|
||||
)
|
||||
.await?;
|
||||
Ok(sqlx::query_scalar(
|
||||
"SELECT id FROM channels \
|
||||
WHERE community_id = $1 AND id = ANY($2) AND channel_type = 'system'",
|
||||
)
|
||||
.bind(community_id.as_uuid())
|
||||
.bind(channel_ids)
|
||||
.fetch_all(&mut *connection)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// The channels in `channel_ids` whose type is not a legacy type
|
||||
/// ([`buzz_core::channel::LEGACY_CHANNEL_TYPES`]), deleted or not, each with
|
||||
/// its type: system channels and any type added later. A filter reads them
|
||||
@@ -2029,16 +2004,6 @@ impl Db {
|
||||
channel_type_of(&self.pool, community_id, channel_id).await
|
||||
}
|
||||
|
||||
/// The IDs in `channel_ids` that name system channels, deleted or not.
|
||||
#[datastore_span(name = "system_channel_ids_among", system = "postgresql")]
|
||||
pub async fn system_channel_ids_among(
|
||||
&self,
|
||||
community_id: CommunityId,
|
||||
channel_ids: &[Uuid],
|
||||
) -> Result<Vec<Uuid>> {
|
||||
system_channel_ids_among(&self.pool, community_id, channel_ids).await
|
||||
}
|
||||
|
||||
/// The channels in `channel_ids` whose type is not a legacy type, deleted
|
||||
/// or not, each with its type.
|
||||
#[datastore_span(name = "named_only_channels_among", system = "postgresql")]
|
||||
|
||||
Reference in New Issue
Block a user