services interface: svsnick, svsjoin, svspart, svsmode (user +r/channel); dry force_part shared with sapart
This commit is contained in:
parent
6d19b6771b
commit
2d08ca58ea
3 changed files with 195 additions and 15 deletions
|
|
@ -324,6 +324,26 @@ impl Server {
|
|||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Force `uid` out of `chan` (SAPART / SVSPART enforcement): announce the PART
|
||||
/// to the channel and to links, drop the membership, reap the channel if empty.
|
||||
/// No-op if the user isn't a member.
|
||||
pub fn force_part(&mut self, uid: Uid, chan: &str, reason: &str) {
|
||||
let key = chan.to_ascii_lowercase();
|
||||
if !self.is_member(uid, &key) {
|
||||
return;
|
||||
}
|
||||
let prefix = self.users[&uid].prefix();
|
||||
self.to_channel(&key, &format!(":{prefix} PART {chan} :{reason}"), None);
|
||||
self.propagate_part(uid, chan, reason);
|
||||
if let Some(ch) = self.channels.get_mut(&key) {
|
||||
ch.members.remove(&uid);
|
||||
}
|
||||
if let Some(u) = self.users.get_mut(&uid) {
|
||||
u.channels.remove(&key);
|
||||
}
|
||||
self.channels.retain(|_, c| !c.is_empty());
|
||||
}
|
||||
|
||||
/// Join a user to a channel (creating it if new, giving the creator +o),
|
||||
/// then broadcast JOIN and send TOPIC + NAMES. Queues the join hook.
|
||||
pub fn join(&mut self, uid: Uid, name: &str, key_arg: Option<&str>) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,35 @@ fn emit(applied: &mut String, last: &mut char, sign: char, c: char) {
|
|||
applied.push(c);
|
||||
}
|
||||
|
||||
/// Apply user modes to `tuid` with services authority (SVSMODE): no "only your
|
||||
/// own modes" restriction — that's the whole point. Reuses the per-mode handlers
|
||||
/// and broadcasts the result to the target as `:nick MODE nick :<changes>`.
|
||||
pub fn svs_set_user_modes(s: &mut Server, tuid: Uid, modestring: &str) {
|
||||
let mut sign = '+';
|
||||
let mut applied = String::new();
|
||||
let mut last = ' ';
|
||||
for c in modestring.chars() {
|
||||
if c == '+' || c == '-' {
|
||||
sign = c;
|
||||
continue;
|
||||
}
|
||||
let adding = sign == '+';
|
||||
if let Some(handler) = user_mode(c) {
|
||||
if handler.apply(s, tuid, adding) {
|
||||
emit(&mut applied, &mut last, sign, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !applied.is_empty() {
|
||||
let nick = s
|
||||
.users
|
||||
.get(&tuid)
|
||||
.map(|u| u.nick.clone())
|
||||
.unwrap_or_default();
|
||||
s.send(tuid, format!(":{nick} MODE {nick} :{applied}"));
|
||||
}
|
||||
}
|
||||
|
||||
struct Mode;
|
||||
impl Command for Mode {
|
||||
fn name(&self) -> &'static str {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
use crate::channels::Topic;
|
||||
use crate::command::{CmdResult, Command};
|
||||
use crate::coremods::core_mode::apply_mode;
|
||||
use crate::coremods::core_mode::{apply_mode, svs_set_user_modes};
|
||||
use crate::module::Hook;
|
||||
use crate::numeric::*;
|
||||
use crate::server::{now, Server};
|
||||
|
|
@ -18,6 +18,10 @@ pub fn commands() -> Vec<Box<dyn Command>> {
|
|||
Box::new(Wallops),
|
||||
Box::new(SvsLogin),
|
||||
Box::new(SvsLogout),
|
||||
Box::new(SvsNick),
|
||||
Box::new(SvsJoin),
|
||||
Box::new(SvsPart),
|
||||
Box::new(SvsMode),
|
||||
Box::new(GlobOps),
|
||||
Box::new(SaJoin),
|
||||
Box::new(SaPart),
|
||||
|
|
@ -296,24 +300,11 @@ impl Command for SaPart {
|
|||
);
|
||||
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());
|
||||
}
|
||||
s.force_part(tuid, ¶ms[1], &reason);
|
||||
CmdResult::Ok
|
||||
}
|
||||
}
|
||||
|
|
@ -363,6 +354,146 @@ impl Command for SaNick {
|
|||
}
|
||||
}
|
||||
|
||||
// --- SVS* : the services interface. Same enforcement as the SA* oper commands,
|
||||
// under the names a services package speaks (like SVSLOGIN). Gated to opers/
|
||||
// services; a linked services pseudoserver drives these once S2S routes them.
|
||||
|
||||
/// SVSNICK — force a nick change (nick-registration enforcement). An optional
|
||||
/// third param is the new-nick TS, accepted and ignored (single-TS model).
|
||||
struct SvsNick;
|
||||
impl Command for SvsNick {
|
||||
fn name(&self) -> &'static str {
|
||||
"SVSNICK"
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// SVSJOIN — force a user into a channel, bypassing +i/+k/+l/+b.
|
||||
struct SvsJoin;
|
||||
impl Command for SvsJoin {
|
||||
fn name(&self) -> &'static str {
|
||||
"SVSJOIN"
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// SVSPART — force a user out of a channel.
|
||||
struct SvsPart;
|
||||
impl Command for SvsPart {
|
||||
fn name(&self) -> &'static str {
|
||||
"SVSPART"
|
||||
}
|
||||
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 reason = params
|
||||
.get(2)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "Services forced part".to_string());
|
||||
s.force_part(tuid, ¶ms[1], &reason);
|
||||
CmdResult::Ok
|
||||
}
|
||||
}
|
||||
|
||||
/// SVSMODE — set modes on a user (e.g. `+r` registered) or a channel with services
|
||||
/// authority, bypassing the "own modes only" / rank checks a normal MODE enforces.
|
||||
struct SvsMode;
|
||||
impl Command for SvsMode {
|
||||
fn name(&self) -> &'static str {
|
||||
"SVSMODE"
|
||||
}
|
||||
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 params[0].starts_with('#') {
|
||||
// channel modes with services authority — same path as SAMODE
|
||||
s.mode_sudo = true;
|
||||
let r = apply_mode(s, uid, params);
|
||||
s.mode_sudo = false;
|
||||
return r;
|
||||
}
|
||||
let Some(tuid) = s.find_nick(¶ms[0]) else {
|
||||
s.numeric(
|
||||
uid,
|
||||
ERR_NOSUCHNICK,
|
||||
&format!("{} :No such nick/channel", params[0]),
|
||||
);
|
||||
return CmdResult::Fail;
|
||||
};
|
||||
svs_set_user_modes(s, tuid, ¶ms[1]);
|
||||
CmdResult::Ok
|
||||
}
|
||||
}
|
||||
|
||||
/// DIE — shut the server down (requires the server name as confirmation).
|
||||
struct Die;
|
||||
impl Command for Die {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue