diff --git a/src/coremods/core_oper.rs b/src/coremods/core_oper.rs index d6fc482..c6e9f01 100644 --- a/src/coremods/core_oper.rs +++ b/src/coremods/core_oper.rs @@ -50,6 +50,8 @@ pub fn commands() -> Vec> { 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 `; 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, ¶ms[0]) else { + return CmdResult::Fail; + }; + let newnick = ¶ms[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, ¶ms[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 `. struct Connect; impl Command for Connect { diff --git a/src/coremods/core_user.rs b/src/coremods/core_user.rs index 6c7e6ef..c45b12c 100644 --- a/src/coremods/core_user.rs +++ b/src/coremods/core_user.rs @@ -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) { diff --git a/src/users.rs b/src/users.rs index 6fa32c6..1eb5159 100644 --- a/src/users.rs +++ b/src/users.rs @@ -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, // AWAY message, if set }