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();