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 <noreply@anthropic.com>
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-10-30 17:16:08 -04:00
parent c0b62bb1b3
commit fbeafebdef
3 changed files with 40 additions and 0 deletions

View File

@@ -1393,6 +1393,9 @@ pub struct ChannelMetadataEdit {
pub purpose: Option<String>,
/// `Some(true)` archives the channel; `Some(false)` restores it.
pub archived: Option<bool>,
/// 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() {

View File

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

View File

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