NICKLOCK / NICKUNLOCK (services hold a user's nick)

This commit is contained in:
Jean Chevronnet 2026-08-09 01:09:46 +00:00
parent 4d6525cac2
commit e2a4a3e248
3 changed files with 95 additions and 0 deletions

View file

@ -50,6 +50,8 @@ pub fn commands() -> Vec<Box<dyn Command>> {
Box::new(AllTime),
Box::new(SwhoisCmd),
Box::new(SetIdle),
Box::new(NickLock),
Box::new(NickUnlock),
]
}
@ -699,6 +701,84 @@ impl Command for Cban {
}
}
/// NICKLOCK — force a user's nick and lock it so they can't change it (InspIRCd
/// `m_nicklock`). `NICKLOCK <nick> <newnick>`; opers/services still can.
struct NickLock;
impl Command for NickLock {
fn name(&self) -> &'static str {
"NICKLOCK"
}
fn min_params(&self) -> usize {
2
}
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
if !require_oper(s, uid) {
return CmdResult::Fail;
}
let Some(tuid) = oper_target(s, uid, &params[0]) else {
return CmdResult::Fail;
};
let newnick = &params[1];
if !valid_nick(newnick) {
s.numeric(
uid,
ERR_ERRONEUSNICKNAME,
&format!("{newnick} :Erroneous nickname"),
);
return CmdResult::Fail;
}
let cur = s
.users
.get(&tuid)
.map(|u| u.nick.clone())
.unwrap_or_default();
if !newnick.eq_ignore_ascii_case(&cur) {
if s.find_nick(newnick).is_some()
|| s.remote_nick.contains_key(&newnick.to_ascii_lowercase())
{
s.numeric(
uid,
ERR_NICKNAMEINUSE,
&format!("{newnick} :Nickname is already in use"),
);
return CmdResult::Fail;
}
s.set_nick(tuid, newnick);
}
if let Some(u) = s.users.get_mut(&tuid) {
u.flags.nick_locked = true;
}
let by = oper_nick(s, uid);
s.snotice(&format!("{by} used NICKLOCK on {newnick}"));
CmdResult::Ok
}
}
/// NICKUNLOCK — release a NICKLOCK so the user may change nick again.
struct NickUnlock;
impl Command for NickUnlock {
fn name(&self) -> &'static str {
"NICKUNLOCK"
}
fn min_params(&self) -> usize {
1
}
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
if !require_oper(s, uid) {
return CmdResult::Fail;
}
let Some(tuid) = oper_target(s, uid, &params[0]) else {
return CmdResult::Fail;
};
if let Some(u) = s.users.get_mut(&tuid) {
u.flags.nick_locked = false;
}
let by = oper_nick(s, uid);
s.snotice(&format!("{by} used NICKUNLOCK on {}", params[0]));
CmdResult::Ok
}
}
/// CONNECT — dial a configured server link on demand. `CONNECT <servername>`.
struct Connect;
impl Command for Connect {

View file

@ -353,6 +353,20 @@ impl Command for Nick {
);
return CmdResult::Fail;
}
// NICKLOCK: a services-held nick can't be changed by the user (opers bypass)
if s.users
.get(&uid)
.map(|u| u.flags.nick_locked)
.unwrap_or(false)
&& !s.is_oper(uid)
{
s.numeric(
uid,
ERR_CANTCHANGENICK,
":Your nickname is locked and cannot be changed",
);
return CmdResult::Fail;
}
// Q-line: a reserved nick is refused (opers and services bypass)
if !s.is_oper(uid) {
if let Some(reason) = s.matched_qline(newnick) {

View file

@ -32,6 +32,7 @@ pub struct UserFlags {
pub callerid: bool, // +g (only accept PMs from users on the ACCEPT list)
pub showwhois: bool, // +W (get a notice when someone WHOISes you)
pub deny_uncommon: bool, // +c (only users sharing a channel may PM you)
pub nick_locked: bool, // NICKLOCK: services/oper holds this nick (no self-change)
pub away: Option<String>, // AWAY message, if set
}