oper toolbox: SAQUIT, CHGNAME, CLEARCHAN, CHECK, ALLTIME
This commit is contained in:
parent
d65e17b6b5
commit
2aec23c5f9
1 changed files with 230 additions and 1 deletions
|
|
@ -6,7 +6,7 @@ use crate::command::{CmdResult, Command};
|
||||||
use crate::coremods::core_mode::{apply_mode, svs_set_user_modes};
|
use crate::coremods::core_mode::{apply_mode, svs_set_user_modes};
|
||||||
use crate::module::Hook;
|
use crate::module::Hook;
|
||||||
use crate::numeric::*;
|
use crate::numeric::*;
|
||||||
use crate::server::{now, Server};
|
use crate::server::{iso_time, now, Server};
|
||||||
use crate::users::{valid_host, valid_ident, valid_nick};
|
use crate::users::{valid_host, valid_ident, valid_nick};
|
||||||
use crate::xline::{parse_duration, XKind};
|
use crate::xline::{parse_duration, XKind};
|
||||||
use crate::Uid;
|
use crate::Uid;
|
||||||
|
|
@ -42,6 +42,11 @@ pub fn commands() -> Vec<Box<dyn Command>> {
|
||||||
Box::new(SaMode),
|
Box::new(SaMode),
|
||||||
Box::new(SaTopic),
|
Box::new(SaTopic),
|
||||||
Box::new(SaKick),
|
Box::new(SaKick),
|
||||||
|
Box::new(SaQuit),
|
||||||
|
Box::new(ChgName),
|
||||||
|
Box::new(ClearChan),
|
||||||
|
Box::new(Check),
|
||||||
|
Box::new(AllTime),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -972,3 +977,227 @@ impl Command for SaKick {
|
||||||
CmdResult::Ok
|
CmdResult::Ok
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// SAQUIT — force a user to quit the network (InspIRCd `m_saquit`). Looks to
|
||||||
|
/// everyone like a normal client QUIT.
|
||||||
|
struct SaQuit;
|
||||||
|
impl Command for SaQuit {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"SAQUIT"
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
let reason = params
|
||||||
|
.get(1)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "Services forced quit".to_string());
|
||||||
|
s.send(tuid, format!("ERROR :Closing link: (SAQUIT: {reason})"));
|
||||||
|
s.remove_user(tuid, &format!("Quit: {reason}"));
|
||||||
|
let by = oper_nick(s, uid);
|
||||||
|
s.snotice(&format!("{by} used SAQUIT on {}: {reason}", params[0]));
|
||||||
|
CmdResult::Ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CHGNAME — change another user's real name (InspIRCd `m_chgname`). The oper-driven
|
||||||
|
/// counterpart to SETNAME; broadcast to `setname`-capable peers so clients update live.
|
||||||
|
struct ChgName;
|
||||||
|
impl Command for ChgName {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"CHGNAME"
|
||||||
|
}
|
||||||
|
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(t) = oper_target(s, uid, ¶ms[0]) else {
|
||||||
|
return CmdResult::Fail;
|
||||||
|
};
|
||||||
|
let realname = params[1].clone();
|
||||||
|
let prefix = match s.users.get_mut(&t) {
|
||||||
|
Some(u) => {
|
||||||
|
u.realname = realname.clone();
|
||||||
|
u.prefix()
|
||||||
|
}
|
||||||
|
None => return CmdResult::Fail,
|
||||||
|
};
|
||||||
|
let line = format!(":{prefix} SETNAME :{realname}");
|
||||||
|
if s.users.get(&t).map(|u| u.caps.setname).unwrap_or(false) {
|
||||||
|
s.send(t, line.clone());
|
||||||
|
}
|
||||||
|
s.notify_peers(t, &line, |c| c.setname);
|
||||||
|
let by = oper_nick(s, uid);
|
||||||
|
s.snotice(&format!("{by} used CHGNAME on {}: {realname}", params[0]));
|
||||||
|
CmdResult::Ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CLEARCHAN — kick every user out of a channel (InspIRCd `m_clearchan`). Each
|
||||||
|
/// removal is a normal KICK, propagated like SAKICK.
|
||||||
|
struct ClearChan;
|
||||||
|
impl Command for ClearChan {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"CLEARCHAN"
|
||||||
|
}
|
||||||
|
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 chan = params[0].clone();
|
||||||
|
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 reason = params
|
||||||
|
.get(1)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "Channel cleared by services".to_string());
|
||||||
|
let prefix = s.users[&uid].prefix();
|
||||||
|
let members: Vec<Uid> = s.channels[&key].members.keys().copied().collect();
|
||||||
|
for tuid in members {
|
||||||
|
let victim = s
|
||||||
|
.users
|
||||||
|
.get(&tuid)
|
||||||
|
.map(|u| u.nick.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
if victim.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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.events
|
||||||
|
.push_back(Hook::Part(tuid, key.clone(), "cleared".to_string()));
|
||||||
|
}
|
||||||
|
s.channels.retain(|_, c| !c.is_empty());
|
||||||
|
let by = oper_nick(s, uid);
|
||||||
|
s.snotice(&format!("{by} used CLEARCHAN on {chan}"));
|
||||||
|
CmdResult::Ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CHECK — oper diagnostic dump for a nick or channel (InspIRCd `m_check`).
|
||||||
|
struct Check;
|
||||||
|
impl Command for Check {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"CHECK"
|
||||||
|
}
|
||||||
|
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 target = params[0].clone();
|
||||||
|
if target.starts_with('#') {
|
||||||
|
let key = target.to_ascii_lowercase();
|
||||||
|
let (count, topic, members) = match s.channels.get(&key) {
|
||||||
|
Some(ch) => (
|
||||||
|
ch.members.len(),
|
||||||
|
ch.topic
|
||||||
|
.as_ref()
|
||||||
|
.map(|t| t.text.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
ch.members
|
||||||
|
.keys()
|
||||||
|
.filter_map(|m| s.users.get(m).map(|u| u.nick.clone()))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
),
|
||||||
|
None => {
|
||||||
|
s.numeric(
|
||||||
|
uid,
|
||||||
|
ERR_NOSUCHCHANNEL,
|
||||||
|
&format!("{target} :No such channel"),
|
||||||
|
);
|
||||||
|
return CmdResult::Fail;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
onotice(
|
||||||
|
s,
|
||||||
|
uid,
|
||||||
|
&format!("*** CHECK {target}: channel, {count} members"),
|
||||||
|
);
|
||||||
|
if !topic.is_empty() {
|
||||||
|
onotice(s, uid, &format!("*** topic: {topic}"));
|
||||||
|
}
|
||||||
|
onotice(s, uid, &format!("*** members: {}", members.join(" ")));
|
||||||
|
} else if let Some(t) = s.find_nick(&target) {
|
||||||
|
let lines = {
|
||||||
|
let u = &s.users[&t];
|
||||||
|
vec![
|
||||||
|
format!("*** CHECK {target}: {}", u.prefix()),
|
||||||
|
format!("*** realhost {} ip {}", u.host, u.addr.ip()),
|
||||||
|
format!("*** realname: {}", u.realname),
|
||||||
|
format!(
|
||||||
|
"*** account: {} secure: {}",
|
||||||
|
u.account.clone().unwrap_or_else(|| "*".to_string()),
|
||||||
|
u.secure
|
||||||
|
),
|
||||||
|
format!("*** umodes: +{}", u.flags.umodes()),
|
||||||
|
format!(
|
||||||
|
"*** signon {} idle {}s",
|
||||||
|
iso_time(u.signon),
|
||||||
|
now().saturating_sub(u.last_active)
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"*** channels: {}",
|
||||||
|
u.channels.iter().cloned().collect::<Vec<_>>().join(" ")
|
||||||
|
),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
for l in lines {
|
||||||
|
onotice(s, uid, &l);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.numeric(
|
||||||
|
uid,
|
||||||
|
ERR_NOSUCHNICK,
|
||||||
|
&format!("{target} :No such nick/channel"),
|
||||||
|
);
|
||||||
|
return CmdResult::Fail;
|
||||||
|
}
|
||||||
|
CmdResult::Ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ALLTIME — show the current server time to the requesting oper (InspIRCd
|
||||||
|
/// `m_alltime`; on a single server there's just the one time to report).
|
||||||
|
struct AllTime;
|
||||||
|
impl Command for AllTime {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"ALLTIME"
|
||||||
|
}
|
||||||
|
fn handle(&self, s: &mut Server, uid: Uid, _params: &[String]) -> CmdResult {
|
||||||
|
if !require_oper(s, uid) {
|
||||||
|
return CmdResult::Fail;
|
||||||
|
}
|
||||||
|
let msg = format!("ALLTIME: {} {}", s.name, iso_time(now()));
|
||||||
|
onotice(s, uid, &msg);
|
||||||
|
CmdResult::Ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue