channel-rename: implement IRCv3 draft/channel-rename — RENAME command + cap, in-place rename (RENAME for cap clients, PART/JOIN fallback for the rest), and S2S propagation

This commit is contained in:
Jean Chevronnet 2026-08-17 23:45:02 +00:00
parent 755e835baf
commit c9bd8e7492
6 changed files with 400 additions and 1 deletions

View file

@ -1,6 +1,6 @@
//! core_channel — channel membership commands: JOIN, PART, KICK, TOPIC, NAMES.
use crate::channels::{normalize_ban_mask, Ban, Topic, RANK_HALFOP};
use crate::channels::{normalize_ban_mask, valid_chan, Ban, Topic, RANK_HALFOP, RANK_OP};
use crate::command::{CmdResult, Command};
use crate::module::Hook;
use crate::numeric::*;
@ -12,6 +12,7 @@ pub fn commands() -> Vec<Box<dyn Command>> {
vec![
Box::new(Join),
Box::new(Part),
Box::new(Rename),
Box::new(Kick),
Box::new(TopicCmd),
Box::new(Names),
@ -479,6 +480,112 @@ impl Command for Part {
}
}
/// RENAME `<old-channel> <new-channel> [<reason>]` — IRCv3 `draft/channel-rename`.
/// A channel operator renames a channel in place, keeping its membership, modes
/// and topic. Members that negotiated the cap see a `RENAME`; the rest are moved
/// with a PART/JOIN. Registered (+r) channels are managed by services, so a
/// client can't rename them — ChanServ does that over S2S.
struct Rename;
impl Command for Rename {
fn name(&self) -> &'static str {
"RENAME"
}
fn min_params(&self) -> usize {
2
}
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
let (old, new) = (&params[0], &params[1]);
let reason = params.get(2).cloned().unwrap_or_default();
let oldkey = old.to_ascii_lowercase();
let newkey = new.to_ascii_lowercase();
let case_only = oldkey == newkey;
if !s.channels.contains_key(&oldkey) {
s.numeric(uid, ERR_NOSUCHCHANNEL, &format!("{old} :No such channel"));
return CmdResult::Fail;
}
if !s.is_member(uid, &oldkey) {
s.numeric(
uid,
ERR_NOTONCHANNEL,
&format!("{old} :You're not on that channel"),
);
return CmdResult::Fail;
}
let is_oper = s.is_oper(uid);
if s.rank(uid, &oldkey) < RANK_OP && !is_oper {
s.numeric(
uid,
ERR_CHANOPRIVSNEEDED,
&format!("{old} :You're not a channel operator"),
);
return CmdResult::Fail;
}
// A registered channel's name is owned by services; renaming it moves the
// registration, which only ChanServ (founder-authorised) may do.
if s.channels[&oldkey].modes.registered && !is_oper {
s.fail(
uid,
"RENAME",
"CANNOT_RENAME",
"This channel is registered — ask ChanServ to rename it.",
);
return CmdResult::Fail;
}
if !valid_chan(new, s.conf_num("maxchannel", 50usize)) {
s.fail(
uid,
"RENAME",
"CANNOT_RENAME",
&format!("{new} is not a valid channel name."),
);
return CmdResult::Fail;
}
// A pure prefix-type change (e.g. # -> &) isn't a rename we support.
if new.chars().next() != old.chars().next() {
s.fail(
uid,
"RENAME",
"CANNOT_RENAME",
"The channel prefix can't be changed.",
);
return CmdResult::Fail;
}
if !case_only && s.channels.contains_key(&newkey) {
s.fail(
uid,
"RENAME",
"CHANNEL_NAME_IN_USE",
&format!("{new} already exists."),
);
return CmdResult::Fail;
}
if !is_oper {
if let Some(reason) = s.matched_cban(&newkey) {
s.fail(
uid,
"RENAME",
"CANNOT_RENAME",
&format!("{new} is CBAN'd: {reason}"),
);
return CmdResult::Fail;
}
}
// Capture the identity/TS before the move, then rename + propagate.
let (prefix, uuid) = {
let u = &s.users[&uid];
(u.prefix(), u.uuid.clone())
};
let oldname = s.channels[&oldkey].name.clone();
if s.rename_channel(&oldkey, new, &prefix, &reason).is_none() {
s.fail(uid, "RENAME", "CANNOT_RENAME", "The channel cannot be renamed.");
return CmdResult::Fail;
}
s.snotice_c('a', &format!("{oldname} renamed to {new} by {prefix}"));
s.propagate_rename(&uuid, &oldname, new, &reason, None);
CmdResult::Ok
}
}
struct Kick;
impl Command for Kick {
fn name(&self) -> &'static str {