diff --git a/NOSTR.md b/NOSTR.md index 7b6641779f..856b6f414a 100644 --- a/NOSTR.md +++ b/NOSTR.md @@ -219,7 +219,10 @@ message in the channel. When someone becomes a community member (an invite claim or a kind:9030 add) the relay joins them to every effective default channel as a self-join: the body of kind:9021, so the channel's `join_role` applies and the channel gets -its `member_joined` kind:40099, kind:39002 and kind:44100. It does this once, +its `member_joined` kind:40099, kind:39002 and kind:44100. There is no cap on +how many channels that is: whatever carries the kind:39000 tag is joined, and +the work a new member costs grows with the number of channels an owner or +admin flags. It does this once, at that moment, and never again on login or reconnect, so a later kind:9022 leave stands. A channel that fails to join is logged and counted (`buzz_auto_join_failures_total`), not retried: the kind:39000 tag is the diff --git a/crates/buzz-db/src/store/channel.rs b/crates/buzz-db/src/store/channel.rs index 005dda072b..3487ce14c4 100644 --- a/crates/buzz-db/src/store/channel.rs +++ b/crates/buzz-db/src/store/channel.rs @@ -616,13 +616,15 @@ async fn list_channels_with_operation( rows.into_iter().map(row_to_channel_record).collect() } -/// The most channels the relay joins a new community member into. -pub const AUTO_JOIN_CHANNEL_LIMIT: i64 = 32; - /// Lists the channels whose `auto_join` is effective: set on a stream channel /// that is open, unarchived and not deleted. Oldest first, so the first -/// default channel a member sees is the one that was flagged first. Bounded by -/// [`AUTO_JOIN_CHANNEL_LIMIT`]. +/// default channel a member sees is the one that was flagged first. +/// +/// Every effective channel, not a page of them: kind:9002 accepts the flag on +/// any number of channels, and the kind:39000 `auto_join` tag promises the +/// relay joins new members there, so a cap here would silently break that +/// promise for the channels past it. The set is bounded by the community's +/// channels, each flagged by a relay owner or admin. pub async fn list_auto_join_channels( pool: &PgPool, community_id: CommunityId, @@ -645,20 +647,11 @@ pub async fn list_auto_join_channels( WHERE community_id = $1 AND auto_join AND channel_type = 'stream' AND visibility = 'open' AND archived_at IS NULL AND deleted_at IS NULL ORDER BY created_at ASC - LIMIT $2 "#, ) .bind(community_id.as_uuid()) - .bind(AUTO_JOIN_CHANNEL_LIMIT) .fetch_all(&mut *connection) .await?; - if rows.len() as i64 == AUTO_JOIN_CHANNEL_LIMIT { - tracing::warn!( - community = %community_id, - limit = AUTO_JOIN_CHANNEL_LIMIT, - "auto_join channel list reached its cap; any channel beyond it is not applied" - ); - } rows.into_iter().map(row_to_channel_record).collect() } diff --git a/crates/buzz-relay/src/handlers/edit_metadata_postgres_tests.rs b/crates/buzz-relay/src/handlers/edit_metadata_postgres_tests.rs index 89d7197755..f13a3c1a73 100644 --- a/crates/buzz-relay/src/handlers/edit_metadata_postgres_tests.rs +++ b/crates/buzz-relay/src/handlers/edit_metadata_postgres_tests.rs @@ -618,6 +618,55 @@ fn archive_event(owner: &Keys, channel_id: Uuid, value: &str) -> nostr::Event { .expect("sign 9002") } +/// The loader returns every effective default channel. It used to stop at 32 +/// while kind:9002 kept accepting the flag past that, so the kind:39000 tag +/// promised a join the relay never made for the newest channels. Mutation: +/// put a `LIMIT` back on `list_auto_join_channels` → RED. +#[tokio::test] +#[ignore = "requires Postgres"] +async fn every_default_channel_is_listed() { + let state = state().await; + let tenant = community(&state, "auto-join-all").await; + let admin = Keys::generate(); + state + .db + .add_relay_member( + tenant.community(), + &admin.public_key().to_hex(), + "admin", + None, + &Keys::generate(), + ) + .await + .map(|write| write.outcome) + .expect("seed relay admin"); + + let mut flagged = Vec::new(); + for _ in 0..33 { + let channel_id = create_channel(&state, &tenant, &admin, &[]).await; + edit_channel( + &state, + &tenant, + &admin, + channel_id, + &[&["auto_join", "true"]], + ) + .await + .expect("relay admin sets auto_join"); + flagged.push(channel_id); + } + + let listed: Vec = state + .db + .list_auto_join_channels(tenant.community()) + .await + .expect("list auto_join channels") + .iter() + .map(|channel| channel.id) + .collect(); + assert_eq!(listed, flagged, "every flagged channel, oldest first"); +} + /// A resend checked after its first copy committed is a duplicate. /// Validation runs on the new archive state and must not refuse the resend; /// the write must then report it as a duplicate, with no change and no