780 lines
22 KiB
Rust
780 lines
22 KiB
Rust
//! core_oper — IRC operator commands: OPER, KILL, WALLOPS. Mirrors InspIRCd's
|
|
//! `coremods/core_oper/`. Oper blocks are configured with `oper = name pass`.
|
|
|
|
use crate::channels::Topic;
|
|
use crate::command::{CmdResult, Command};
|
|
use crate::config::Config;
|
|
use crate::coremods::core_mode::apply_mode;
|
|
use crate::module::Hook;
|
|
use crate::numeric::*;
|
|
use crate::server::{now, Server};
|
|
use crate::users::{valid_host, valid_ident, valid_nick};
|
|
use crate::xline::{parse_duration, XKind};
|
|
use crate::Uid;
|
|
|
|
pub fn commands() -> Vec<Box<dyn Command>> {
|
|
vec![
|
|
Box::new(Oper),
|
|
Box::new(Kill),
|
|
Box::new(Wallops),
|
|
Box::new(SvsLogin),
|
|
Box::new(SvsLogout),
|
|
Box::new(Rehash),
|
|
Box::new(GlobOps),
|
|
Box::new(SaJoin),
|
|
Box::new(SaPart),
|
|
Box::new(SaNick),
|
|
Box::new(Die),
|
|
Box::new(Restart),
|
|
Box::new(Kline),
|
|
Box::new(Gline),
|
|
Box::new(Zline),
|
|
Box::new(ChgHost),
|
|
Box::new(ChgIdent),
|
|
Box::new(SetHost),
|
|
Box::new(SetIdent),
|
|
Box::new(SaMode),
|
|
Box::new(SaTopic),
|
|
Box::new(SaKick),
|
|
]
|
|
}
|
|
|
|
/// Reject non-opers with 481; returns whether the caller is an oper.
|
|
fn require_oper(s: &mut Server, uid: Uid) -> bool {
|
|
if s.is_oper(uid) {
|
|
return true;
|
|
}
|
|
s.numeric(
|
|
uid,
|
|
ERR_NOPRIVILEGES,
|
|
":Permission Denied- You're not an IRC operator",
|
|
);
|
|
false
|
|
}
|
|
|
|
struct Oper;
|
|
impl Command for Oper {
|
|
fn name(&self) -> &'static str {
|
|
"OPER"
|
|
}
|
|
fn min_params(&self) -> usize {
|
|
2
|
|
}
|
|
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
|
let (name, pass) = (¶ms[0], ¶ms[1]);
|
|
if s.opers.iter().any(|(n, p)| n == name && p == pass) {
|
|
s.oper_up(uid);
|
|
CmdResult::Ok
|
|
} else {
|
|
s.numeric(uid, ERR_PASSWDMISMATCH, ":Password incorrect");
|
|
CmdResult::Fail
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Kill;
|
|
impl Command for Kill {
|
|
fn name(&self) -> &'static str {
|
|
"KILL"
|
|
}
|
|
fn min_params(&self) -> usize {
|
|
1
|
|
}
|
|
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
|
if !s.is_oper(uid) {
|
|
s.numeric(
|
|
uid,
|
|
ERR_NOPRIVILEGES,
|
|
":Permission Denied- You're not an IRC operator",
|
|
);
|
|
return CmdResult::Fail;
|
|
}
|
|
let target = ¶ms[0];
|
|
let reason = params
|
|
.get(1)
|
|
.cloned()
|
|
.unwrap_or_else(|| "Killed".to_string());
|
|
let Some(tuid) = s.find_nick(target) else {
|
|
s.numeric(
|
|
uid,
|
|
ERR_NOSUCHNICK,
|
|
&format!("{target} :No such nick/channel"),
|
|
);
|
|
return CmdResult::Fail;
|
|
};
|
|
let killer = s
|
|
.users
|
|
.get(&uid)
|
|
.map(|u| u.nick.clone())
|
|
.unwrap_or_default();
|
|
s.send(
|
|
tuid,
|
|
format!(":{} KILL {target} :{killer} ({reason})", s.name),
|
|
);
|
|
s.remove_user(tuid, &format!("Killed by {killer}: {reason}"));
|
|
CmdResult::Ok
|
|
}
|
|
}
|
|
|
|
/// SVSLOGIN / SVSLOGOUT — the **services interface** to the account layer
|
|
/// ([`crate::accounts`]). Over S2S these arrive from a services pseudoserver
|
|
/// (Anope/Atheme); until S2S exists an oper may invoke them to drive `+r` and the
|
|
/// account-gated channel modes. `SVSLOGIN <nick> <account>` logs a user in
|
|
/// (`account` of `*`/`0` logs out); `SVSLOGOUT <nick>` logs them out.
|
|
struct SvsLogin;
|
|
impl Command for SvsLogin {
|
|
fn name(&self) -> &'static str {
|
|
"SVSLOGIN"
|
|
}
|
|
fn min_params(&self) -> usize {
|
|
2
|
|
}
|
|
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
|
if !s.is_oper(uid) {
|
|
s.numeric(
|
|
uid,
|
|
ERR_NOPRIVILEGES,
|
|
":Permission Denied- SVSLOGIN is a services command",
|
|
);
|
|
return CmdResult::Fail;
|
|
}
|
|
let (target, account) = (¶ms[0], ¶ms[1]);
|
|
let Some(tuid) = s.find_nick(target) else {
|
|
s.numeric(
|
|
uid,
|
|
ERR_NOSUCHNICK,
|
|
&format!("{target} :No such nick/channel"),
|
|
);
|
|
return CmdResult::Fail;
|
|
};
|
|
if account == "*" || account == "0" {
|
|
s.logout(tuid);
|
|
} else {
|
|
s.set_login(tuid, account);
|
|
}
|
|
CmdResult::Ok
|
|
}
|
|
}
|
|
|
|
struct SvsLogout;
|
|
impl Command for SvsLogout {
|
|
fn name(&self) -> &'static str {
|
|
"SVSLOGOUT"
|
|
}
|
|
fn min_params(&self) -> usize {
|
|
1
|
|
}
|
|
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
|
if !s.is_oper(uid) {
|
|
s.numeric(
|
|
uid,
|
|
ERR_NOPRIVILEGES,
|
|
":Permission Denied- SVSLOGOUT is a services command",
|
|
);
|
|
return CmdResult::Fail;
|
|
}
|
|
let Some(tuid) = s.find_nick(¶ms[0]) else {
|
|
s.numeric(
|
|
uid,
|
|
ERR_NOSUCHNICK,
|
|
&format!("{} :No such nick/channel", params[0]),
|
|
);
|
|
return CmdResult::Fail;
|
|
};
|
|
s.logout(tuid);
|
|
CmdResult::Ok
|
|
}
|
|
}
|
|
|
|
struct Wallops;
|
|
impl Command for Wallops {
|
|
fn name(&self) -> &'static str {
|
|
"WALLOPS"
|
|
}
|
|
fn min_params(&self) -> usize {
|
|
1
|
|
}
|
|
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
|
if !s.is_oper(uid) {
|
|
s.numeric(
|
|
uid,
|
|
ERR_NOPRIVILEGES,
|
|
":Permission Denied- You're not an IRC operator",
|
|
);
|
|
return CmdResult::Fail;
|
|
}
|
|
let from = s
|
|
.users
|
|
.get(&uid)
|
|
.map(|u| u.nick.clone())
|
|
.unwrap_or_default();
|
|
s.wallops(&from, ¶ms[0]);
|
|
CmdResult::Ok
|
|
}
|
|
}
|
|
|
|
/// REHASH — reload the config file (MOTD, oper blocks, cloak key).
|
|
struct Rehash;
|
|
impl Command for Rehash {
|
|
fn name(&self) -> &'static str {
|
|
"REHASH"
|
|
}
|
|
fn handle(&self, s: &mut Server, uid: Uid, _params: &[String]) -> CmdResult {
|
|
if !require_oper(s, uid) {
|
|
return CmdResult::Fail;
|
|
}
|
|
let fresh = Config::load(&s.conf_path);
|
|
s.motd = fresh.motd;
|
|
s.opers = fresh.opers;
|
|
s.cloak_key = fresh.cloak_key;
|
|
s.censor = fresh.censor;
|
|
s.amu = fresh.amu;
|
|
s.resolve_hosts = fresh.resolve_hosts;
|
|
s.use_resolved_host = fresh.use_resolved_host;
|
|
s.numeric(uid, RPL_REHASHING, &format!("{} :Rehashing", s.conf_path));
|
|
CmdResult::Ok
|
|
}
|
|
}
|
|
|
|
/// GLOBOPS — a message to every IRC operator.
|
|
struct GlobOps;
|
|
impl Command for GlobOps {
|
|
fn name(&self) -> &'static str {
|
|
"GLOBOPS"
|
|
}
|
|
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 from = s
|
|
.users
|
|
.get(&uid)
|
|
.map(|u| u.nick.clone())
|
|
.unwrap_or_default();
|
|
let opers: Vec<Uid> = s
|
|
.users
|
|
.iter()
|
|
.filter(|(_, u)| u.flags.oper)
|
|
.map(|(&u, _)| u)
|
|
.collect();
|
|
for o in opers {
|
|
let nick = s.users.get(&o).map(|u| u.nick.clone()).unwrap_or_default();
|
|
s.send(
|
|
o,
|
|
format!(
|
|
":{} NOTICE {nick} :*** GLOBOPS from {from}: {}",
|
|
s.name, params[0]
|
|
),
|
|
);
|
|
}
|
|
CmdResult::Ok
|
|
}
|
|
}
|
|
|
|
/// SAJOIN — force a user into a channel.
|
|
struct SaJoin;
|
|
impl Command for SaJoin {
|
|
fn name(&self) -> &'static str {
|
|
"SAJOIN"
|
|
}
|
|
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) = s.find_nick(¶ms[0]) else {
|
|
s.numeric(
|
|
uid,
|
|
ERR_NOSUCHNICK,
|
|
&format!("{} :No such nick/channel", params[0]),
|
|
);
|
|
return CmdResult::Fail;
|
|
};
|
|
s.join(tuid, ¶ms[1], None);
|
|
CmdResult::Ok
|
|
}
|
|
}
|
|
|
|
/// SAPART — force a user out of a channel.
|
|
struct SaPart;
|
|
impl Command for SaPart {
|
|
fn name(&self) -> &'static str {
|
|
"SAPART"
|
|
}
|
|
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) = s.find_nick(¶ms[0]) else {
|
|
s.numeric(
|
|
uid,
|
|
ERR_NOSUCHNICK,
|
|
&format!("{} :No such nick/channel", params[0]),
|
|
);
|
|
return CmdResult::Fail;
|
|
};
|
|
let chan = ¶ms[1];
|
|
let key = chan.to_ascii_lowercase();
|
|
let reason = params
|
|
.get(2)
|
|
.cloned()
|
|
.unwrap_or_else(|| "Removed".to_string());
|
|
if s.is_member(tuid, &key) {
|
|
let prefix = s.users[&tuid].prefix();
|
|
s.to_channel(&key, &format!(":{prefix} PART {chan} :{reason}"), None);
|
|
s.propagate_part(tuid, chan, &reason);
|
|
if let Some(ch) = s.channels.get_mut(&key) {
|
|
ch.members.remove(&tuid);
|
|
}
|
|
if let Some(u) = s.users.get_mut(&tuid) {
|
|
u.channels.remove(&key);
|
|
}
|
|
s.channels.retain(|_, c| !c.is_empty());
|
|
}
|
|
CmdResult::Ok
|
|
}
|
|
}
|
|
|
|
/// SANICK — force a user's nickname.
|
|
struct SaNick;
|
|
impl Command for SaNick {
|
|
fn name(&self) -> &'static str {
|
|
"SANICK"
|
|
}
|
|
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) = s.find_nick(¶ms[0]) else {
|
|
s.numeric(
|
|
uid,
|
|
ERR_NOSUCHNICK,
|
|
&format!("{} :No such nick/channel", params[0]),
|
|
);
|
|
return CmdResult::Fail;
|
|
};
|
|
let newnick = ¶ms[1];
|
|
if !valid_nick(newnick) {
|
|
s.numeric(
|
|
uid,
|
|
ERR_ERRONEUSNICKNAME,
|
|
&format!("{newnick} :Erroneous nickname"),
|
|
);
|
|
return CmdResult::Fail;
|
|
}
|
|
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);
|
|
CmdResult::Ok
|
|
}
|
|
}
|
|
|
|
/// DIE — shut the server down (requires the server name as confirmation).
|
|
struct Die;
|
|
impl Command for Die {
|
|
fn name(&self) -> &'static str {
|
|
"DIE"
|
|
}
|
|
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;
|
|
}
|
|
if params[0] != s.name {
|
|
s.numeric(uid, ERR_NOPRIVILEGES, ":DIE requires the server name");
|
|
return CmdResult::Fail;
|
|
}
|
|
let by = s
|
|
.users
|
|
.get(&uid)
|
|
.map(|u| u.nick.clone())
|
|
.unwrap_or_default();
|
|
eprintln!("[oper] DIE by {by}");
|
|
std::process::exit(0);
|
|
}
|
|
}
|
|
|
|
/// RESTART — like DIE (a supervisor is expected to relaunch us).
|
|
struct Restart;
|
|
impl Command for Restart {
|
|
fn name(&self) -> &'static str {
|
|
"RESTART"
|
|
}
|
|
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;
|
|
}
|
|
if params[0] != s.name {
|
|
s.numeric(uid, ERR_NOPRIVILEGES, ":RESTART requires the server name");
|
|
return CmdResult::Fail;
|
|
}
|
|
let by = s
|
|
.users
|
|
.get(&uid)
|
|
.map(|u| u.nick.clone())
|
|
.unwrap_or_default();
|
|
eprintln!("[oper] RESTART by {by}");
|
|
std::process::exit(0);
|
|
}
|
|
}
|
|
|
|
/// Shared KLINE/GLINE/ZLINE handling: the mask alone removes, mask+duration adds.
|
|
fn do_xline(s: &mut Server, uid: Uid, params: &[String], kind: XKind) -> CmdResult {
|
|
if !require_oper(s, uid) {
|
|
return CmdResult::Fail;
|
|
}
|
|
let mask = params[0].clone();
|
|
let nick = s
|
|
.users
|
|
.get(&uid)
|
|
.map(|u| u.nick.clone())
|
|
.unwrap_or_default();
|
|
if params.len() < 2 {
|
|
let word = if s.remove_xline(kind, &mask) {
|
|
"removed"
|
|
} else {
|
|
"not found"
|
|
};
|
|
s.send(
|
|
uid,
|
|
format!(
|
|
":{} NOTICE {nick} :{}-line {word}: {mask}",
|
|
s.name,
|
|
kind.tag()
|
|
),
|
|
);
|
|
return CmdResult::Ok;
|
|
}
|
|
let dur = parse_duration(¶ms[1]).unwrap_or(0);
|
|
let reason = params
|
|
.get(2)
|
|
.cloned()
|
|
.unwrap_or_else(|| "No reason given".to_string());
|
|
s.add_xline(kind, &mask, dur, &nick, &reason);
|
|
CmdResult::Ok
|
|
}
|
|
|
|
/// KLINE — ban a `user@host` mask on this server.
|
|
struct Kline;
|
|
impl Command for Kline {
|
|
fn name(&self) -> &'static str {
|
|
"KLINE"
|
|
}
|
|
fn min_params(&self) -> usize {
|
|
1
|
|
}
|
|
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
|
do_xline(s, uid, params, XKind::Kline)
|
|
}
|
|
}
|
|
|
|
/// GLINE — a network-wide `user@host` ban.
|
|
struct Gline;
|
|
impl Command for Gline {
|
|
fn name(&self) -> &'static str {
|
|
"GLINE"
|
|
}
|
|
fn min_params(&self) -> usize {
|
|
1
|
|
}
|
|
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
|
do_xline(s, uid, params, XKind::Gline)
|
|
}
|
|
}
|
|
|
|
/// ZLINE — ban an IP address (glob).
|
|
struct Zline;
|
|
impl Command for Zline {
|
|
fn name(&self) -> &'static str {
|
|
"ZLINE"
|
|
}
|
|
fn min_params(&self) -> usize {
|
|
1
|
|
}
|
|
fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult {
|
|
do_xline(s, uid, params, XKind::Zline)
|
|
}
|
|
}
|
|
|
|
/// Resolve a nick to a uid, sending ERR_NOSUCHNICK if it's unknown.
|
|
fn oper_target(s: &mut Server, uid: Uid, nick: &str) -> Option<Uid> {
|
|
match s.find_nick(nick) {
|
|
Some(t) => Some(t),
|
|
None => {
|
|
s.numeric(
|
|
uid,
|
|
ERR_NOSUCHNICK,
|
|
&format!("{nick} :No such nick/channel"),
|
|
);
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A server NOTICE to the invoking oper (soft errors for the CHG*/SA* set).
|
|
fn onotice(s: &mut Server, uid: Uid, msg: &str) {
|
|
let nick = s
|
|
.users
|
|
.get(&uid)
|
|
.map(|u| u.nick.clone())
|
|
.unwrap_or_else(|| "*".to_string());
|
|
s.send(uid, format!(":{} NOTICE {nick} :{msg}", s.name));
|
|
}
|
|
|
|
/// The oper's nick, for audit snotices.
|
|
fn oper_nick(s: &Server, uid: Uid) -> String {
|
|
s.users
|
|
.get(&uid)
|
|
.map(|u| u.nick.clone())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// CHGHOST — change another user's displayed host.
|
|
struct ChgHost;
|
|
impl Command for ChgHost {
|
|
fn name(&self) -> &'static str {
|
|
"CHGHOST"
|
|
}
|
|
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;
|
|
}
|
|
if !valid_host(¶ms[1]) {
|
|
onotice(s, uid, "*** CHGHOST: invalid characters in hostname");
|
|
return CmdResult::Fail;
|
|
}
|
|
let Some(t) = oper_target(s, uid, ¶ms[0]) else {
|
|
return CmdResult::Fail;
|
|
};
|
|
s.change_host_ident(t, None, Some(¶ms[1]));
|
|
let by = oper_nick(s, uid);
|
|
s.snotice(&format!(
|
|
"{by} used CHGHOST on {}: {}",
|
|
params[0], params[1]
|
|
));
|
|
CmdResult::Ok
|
|
}
|
|
}
|
|
|
|
/// SETHOST — change your own displayed host.
|
|
struct SetHost;
|
|
impl Command for SetHost {
|
|
fn name(&self) -> &'static str {
|
|
"SETHOST"
|
|
}
|
|
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;
|
|
}
|
|
if !valid_host(¶ms[0]) {
|
|
onotice(s, uid, "*** SETHOST: invalid characters in hostname");
|
|
return CmdResult::Fail;
|
|
}
|
|
s.change_host_ident(uid, None, Some(¶ms[0]));
|
|
CmdResult::Ok
|
|
}
|
|
}
|
|
|
|
/// CHGIDENT — change another user's ident/username.
|
|
struct ChgIdent;
|
|
impl Command for ChgIdent {
|
|
fn name(&self) -> &'static str {
|
|
"CHGIDENT"
|
|
}
|
|
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;
|
|
}
|
|
if !valid_ident(¶ms[1]) {
|
|
onotice(s, uid, "*** CHGIDENT: invalid characters in ident");
|
|
return CmdResult::Fail;
|
|
}
|
|
let Some(t) = oper_target(s, uid, ¶ms[0]) else {
|
|
return CmdResult::Fail;
|
|
};
|
|
s.change_host_ident(t, Some(¶ms[1]), None);
|
|
let by = oper_nick(s, uid);
|
|
s.snotice(&format!(
|
|
"{by} used CHGIDENT on {}: {}",
|
|
params[0], params[1]
|
|
));
|
|
CmdResult::Ok
|
|
}
|
|
}
|
|
|
|
/// SETIDENT — change your own ident/username.
|
|
struct SetIdent;
|
|
impl Command for SetIdent {
|
|
fn name(&self) -> &'static str {
|
|
"SETIDENT"
|
|
}
|
|
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;
|
|
}
|
|
if !valid_ident(¶ms[0]) {
|
|
onotice(s, uid, "*** SETIDENT: invalid characters in ident");
|
|
return CmdResult::Fail;
|
|
}
|
|
s.change_host_ident(uid, Some(¶ms[0]), None);
|
|
CmdResult::Ok
|
|
}
|
|
}
|
|
|
|
/// SAMODE — apply a channel MODE as the server, bypassing the rank ladder.
|
|
struct SaMode;
|
|
impl Command for SaMode {
|
|
fn name(&self) -> &'static str {
|
|
"SAMODE"
|
|
}
|
|
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;
|
|
}
|
|
s.mode_sudo = true;
|
|
let r = apply_mode(s, uid, params);
|
|
s.mode_sudo = false;
|
|
let by = oper_nick(s, uid);
|
|
s.snotice(&format!("{by} used SAMODE: {}", params.join(" ")));
|
|
r
|
|
}
|
|
}
|
|
|
|
/// SATOPIC — set a channel topic as the server, bypassing +t / op checks.
|
|
struct SaTopic;
|
|
impl Command for SaTopic {
|
|
fn name(&self) -> &'static str {
|
|
"SATOPIC"
|
|
}
|
|
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 chan = ¶ms[0];
|
|
let key = chan.to_ascii_lowercase();
|
|
if !s.channels.contains_key(&key) {
|
|
s.numeric(uid, ERR_NOSUCHCHANNEL, &format!("{chan} :No such channel"));
|
|
return CmdResult::Fail;
|
|
}
|
|
let text = params[1].clone();
|
|
let (prefix, setter) = {
|
|
let u = &s.users[&uid];
|
|
(u.prefix(), u.nick.clone())
|
|
};
|
|
if let Some(ch) = s.channels.get_mut(&key) {
|
|
ch.topic = Some(Topic {
|
|
text: text.clone(),
|
|
setter,
|
|
ts: now(),
|
|
});
|
|
}
|
|
s.to_channel(&key, &format!(":{prefix} TOPIC {chan} :{text}"), None);
|
|
s.propagate_from_user(uid, &format!("TOPIC {chan} :{text}"));
|
|
let by = oper_nick(s, uid);
|
|
s.snotice(&format!("{by} used SATOPIC on {chan}"));
|
|
CmdResult::Ok
|
|
}
|
|
}
|
|
|
|
/// SAKICK — kick a user as the server, bypassing rank checks.
|
|
struct SaKick;
|
|
impl Command for SaKick {
|
|
fn name(&self) -> &'static str {
|
|
"SAKICK"
|
|
}
|
|
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 (chan, victim) = (¶ms[0], ¶ms[1]);
|
|
let key = chan.to_ascii_lowercase();
|
|
if !s.channels.contains_key(&key) {
|
|
s.numeric(uid, ERR_NOSUCHCHANNEL, &format!("{chan} :No such channel"));
|
|
return CmdResult::Fail;
|
|
}
|
|
let Some(tuid) = s.find_nick(victim) else {
|
|
s.numeric(
|
|
uid,
|
|
ERR_NOSUCHNICK,
|
|
&format!("{victim} :No such nick/channel"),
|
|
);
|
|
return CmdResult::Fail;
|
|
};
|
|
if !s.channels[&key].members.contains_key(&tuid) {
|
|
s.numeric(
|
|
uid,
|
|
ERR_USERNOTINCHANNEL,
|
|
&format!("{victim} {chan} :They aren't on that channel"),
|
|
);
|
|
return CmdResult::Fail;
|
|
}
|
|
let reason = params
|
|
.get(2)
|
|
.cloned()
|
|
.unwrap_or_else(|| "Kicked by services".to_string());
|
|
let prefix = s.users[&uid].prefix();
|
|
s.to_channel(
|
|
&key,
|
|
&format!(":{prefix} KICK {chan} {victim} :{reason}"),
|
|
None,
|
|
);
|
|
s.propagate_from_user(uid, &format!("KICK {chan} {victim} :{reason}"));
|
|
if let Some(ch) = s.channels.get_mut(&key) {
|
|
ch.members.remove(&tuid);
|
|
}
|
|
if let Some(u) = s.users.get_mut(&tuid) {
|
|
u.channels.remove(&key);
|
|
}
|
|
s.channels.retain(|_, c| !c.is_empty());
|
|
s.events
|
|
.push_back(Hook::Part(tuid, key, "kicked".to_string()));
|
|
let by = oper_nick(s, uid);
|
|
s.snotice(&format!("{by} used SAKICK on {victim} in {chan}"));
|
|
CmdResult::Ok
|
|
}
|
|
}
|