From fbeafebdef3837b382051c968f26980546f4f439 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Fri, 30 Oct 2026 17:16:08 -0400 Subject: [PATCH] fix(relay): let an operator rename an archived channel The relay refused every edit of an archived channel unless it also unarchived it, so the rename script could not repair an archived duplicate such as the old square-ios copy, and the unique index could not be built. A relay operator's name-only rename now applies to an archived channel and leaves it archived. Other edits of an archived channel are still refused. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 Signed-off-by: Logan Johnson --- crates/buzz-db/src/store/channel.rs | 5 +++ .../handlers/group_state_postgres_tests.rs | 31 +++++++++++++++++++ .../buzz-relay/src/handlers/side_effects.rs | 4 +++ 3 files changed, 40 insertions(+) diff --git a/crates/buzz-db/src/store/channel.rs b/crates/buzz-db/src/store/channel.rs index 4cde838cf1..81c7a92b65 100644 --- a/crates/buzz-db/src/store/channel.rs +++ b/crates/buzz-db/src/store/channel.rs @@ -1393,6 +1393,9 @@ pub struct ChannelMetadataEdit { pub purpose: Option, /// `Some(true)` archives the channel; `Some(false)` restores it. pub archived: Option, + /// Lets this edit change an archived channel, which stays archived. Only + /// a relay operator's name-only rename sets it. + pub keep_archived: bool, } /// The result of [`Db::apply_channel_metadata_event`]. @@ -1450,6 +1453,7 @@ pub(crate) async fn apply_channel_metadata_event( // channel already in the requested state. match (locked.archived(), edit.archived) { (true, Some(false)) | (false, None | Some(true)) => {} + (true, None) if edit.keep_archived => {} (true, _) => return Err(DbError::AccessDenied("channel is archived".to_string())), (false, Some(false)) => { return Err(DbError::AccessDenied("channel is not archived".to_string())); @@ -1462,6 +1466,7 @@ pub(crate) async fn apply_channel_metadata_event( topic, purpose, archived, + keep_archived: _, } = edit; let actor = event.pubkey.to_bytes(); if !columns.is_empty() { diff --git a/crates/buzz-relay/src/handlers/group_state_postgres_tests.rs b/crates/buzz-relay/src/handlers/group_state_postgres_tests.rs index 587979c3e3..aa9d35487b 100644 --- a/crates/buzz-relay/src/handlers/group_state_postgres_tests.rs +++ b/crates/buzz-relay/src/handlers/group_state_postgres_tests.rs @@ -1202,6 +1202,27 @@ async fn a_relay_operator_may_only_rename_a_channel() { assert_eq!(name(state.clone()).await, before); } + // The rename repairs archived duplicates too, so archive the channel. An + // owner still may not edit it while it is archived. + state + .db + .archive_channel(community_id, channel) + .await + .expect("archive"); + match ingest_event( + &state, + &tenant, + edit(&owner, &[&["name", "by-owner"]]), + http_auth(&owner), + ) + .await + { + Err(IngestError::Rejected(message)) => assert!(message.contains("archived"), "{message}"), + Err(other) => panic!("expected a refusal, got {other:?}"), + Ok(result) => panic!("expected a refusal, got {:?}", result.message), + } + assert_eq!(name(state.clone()).await, before); + let renamed = ingest_event( &state, &tenant, @@ -1212,6 +1233,16 @@ async fn a_relay_operator_may_only_rename_a_channel() { .unwrap_or_else(|error| panic!("operator rename: {error:?}")); assert!(renamed.accepted, "{}", renamed.message); assert_eq!(name(state.clone()).await, "renamed-by-operator"); + assert!( + state + .db + .get_channel(community_id, channel) + .await + .expect("channel") + .archived_at + .is_some(), + "the rename keeps the channel archived" + ); let tags = group_state_tags(&state, &tenant, channel, 39000).await; assert!( tags.iter() diff --git a/crates/buzz-relay/src/handlers/side_effects.rs b/crates/buzz-relay/src/handlers/side_effects.rs index c97e95f1f2..e052eddd69 100644 --- a/crates/buzz-relay/src/handlers/side_effects.rs +++ b/crates/buzz-relay/src/handlers/side_effects.rs @@ -2144,6 +2144,10 @@ pub(crate) async fn persist_edit_metadata( if let Some(labels) = channel_labels_from_tags(event) { edit.columns.labels = Some(labels.map_err(|e| anyhow::anyhow!("{e}"))?); } + // An operator's rename may repair an archived duplicate name, so it + // applies to an archived channel and leaves it archived. + edit.keep_archived = + is_rename_only(event) && actor_is_relay_operator(state, event.pubkey.to_bytes()).await?; let now = chrono::Utc::now(); let mut notices = Vec::new();