services interface: svsnick, svsjoin, svspart, svsmode (user +r/channel); dry force_part shared with sapart
This commit is contained in:
parent
bcfed8a621
commit
57e45f22a9
3 changed files with 195 additions and 15 deletions
|
|
@ -324,6 +324,26 @@ impl Server {
|
||||||
.unwrap_or(false)
|
.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),
|
/// 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.
|
/// then broadcast JOIN and send TOPIC + NAMES. Queues the join hook.
|
||||||
pub fn join(&mut self, uid: Uid, name: &str, key_arg: Option<&str>) {
|
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);
|
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;
|
struct Mode;
|
||||||
impl Command for Mode {
|
impl Command for Mode {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
use crate::channels::Topic;
|
use crate::channels::Topic;
|
||||||
use crate::command::{CmdResult, Command};
|
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::module::Hook;
|
||||||
use crate::numeric::*;
|
use crate::numeric::*;
|
||||||
use crate::server::{now, Server};
|
use crate::server::{now, Server};
|
||||||
|
|
@ -18,6 +18,10 @@ pub fn commands() -> Vec<Box<dyn Command>> {
|
||||||
Box::new(Wallops),
|
Box::new(Wallops),
|
||||||
Box::new(SvsLogin),
|
Box::new(SvsLogin),
|
||||||
Box::new(SvsLogout),
|
Box::new(SvsLogout),
|
||||||
|
Box::new(SvsNick),
|
||||||
|
Box::new(SvsJoin),
|
||||||
|
Box::new(SvsPart),
|
||||||
|
Box::new(SvsMode),
|
||||||
Box::new(GlobOps),
|
Box::new(GlobOps),
|
||||||
Box::new(SaJoin),
|
Box::new(SaJoin),
|
||||||
Box::new(SaPart),
|
Box::new(SaPart),
|
||||||
|
|
@ -296,24 +300,11 @@ impl Command for SaPart {
|
||||||
);
|
);
|
||||||
return CmdResult::Fail;
|
return CmdResult::Fail;
|
||||||
};
|
};
|
||||||
let chan = ¶ms[1];
|
|
||||||
let key = chan.to_ascii_lowercase();
|
|
||||||
let reason = params
|
let reason = params
|
||||||
.get(2)
|
.get(2)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| "Removed".to_string());
|
.unwrap_or_else(|| "Removed".to_string());
|
||||||
if s.is_member(tuid, &key) {
|
s.force_part(tuid, ¶ms[1], &reason);
|
||||||
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
|
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).
|
/// DIE — shut the server down (requires the server name as confirmation).
|
||||||
struct Die;
|
struct Die;
|
||||||
impl Command for Die {
|
impl Command for Die {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue