channel modes +Q nokicks / +A allowinvite / +P permchannels; fix SWHOIS (full multi-word text, no doubled nick in WHOIS)
This commit is contained in:
parent
050ce37612
commit
8dc51a281b
8 changed files with 70 additions and 20 deletions
|
|
@ -149,6 +149,9 @@ pub struct ChanModes {
|
|||
pub redirect: Option<String>, // +L <#target> — when full, send there
|
||||
pub history: Option<(u32, u64)>, // +H <lines>:<secs> — replay recent messages to joiners
|
||||
pub anticaps: Option<u8>, // +B <percent> — block messages that are mostly CAPS
|
||||
pub nokicks: bool, // +Q — KICK is disabled on the channel
|
||||
pub allowinvite: bool, // +A — any member (not just ops) may INVITE
|
||||
pub permanent: bool, // +P — channel persists with zero members
|
||||
}
|
||||
|
||||
impl ChanModes {
|
||||
|
|
@ -172,6 +175,9 @@ impl ChanModes {
|
|||
'M' => self.reg_moderated = on,
|
||||
'G' => self.censor = on,
|
||||
'u' => self.auditorium = on,
|
||||
'Q' => self.nokicks = on,
|
||||
'A' => self.allowinvite = on,
|
||||
'P' => self.permanent = on,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -305,6 +311,12 @@ impl Channel {
|
|||
pub fn is_empty(&self) -> bool {
|
||||
self.members.is_empty() && self.rmembers.is_empty()
|
||||
}
|
||||
|
||||
/// Whether to keep this channel in the table: it has members, or it's +P
|
||||
/// (permanent). The predicate every `channels.retain` prune uses.
|
||||
pub fn keep_alive(&self) -> bool {
|
||||
!self.is_empty() || self.modes.permanent
|
||||
}
|
||||
}
|
||||
|
||||
impl Server {
|
||||
|
|
@ -350,7 +362,7 @@ impl Server {
|
|||
if let Some(u) = self.users.get_mut(&uid) {
|
||||
u.channels.remove(&key);
|
||||
}
|
||||
self.channels.retain(|_, c| !c.is_empty());
|
||||
self.channels.retain(|_, c| c.keep_alive());
|
||||
}
|
||||
|
||||
/// Join a user to a channel (creating it if new, giving the creator +o),
|
||||
|
|
@ -745,7 +757,7 @@ impl Server {
|
|||
if let Some(u) = self.users.get_mut(&uid) {
|
||||
u.channels.remove(key);
|
||||
}
|
||||
self.channels.retain(|_, c| !c.is_empty());
|
||||
self.channels.retain(|_, c| c.keep_alive());
|
||||
self.events
|
||||
.push_back(Hook::Part(uid, key.to_string(), "flood".to_string()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ impl Command for Cycle {
|
|||
if let Some(u) = s.users.get_mut(&uid) {
|
||||
u.channels.remove(&key);
|
||||
}
|
||||
s.channels.retain(|_, c| !c.is_empty());
|
||||
s.channels.retain(|_, c| c.keep_alive());
|
||||
s.join(uid, chan, None);
|
||||
CmdResult::Ok
|
||||
}
|
||||
|
|
@ -167,7 +167,7 @@ impl Command for Remove {
|
|||
if let Some(u) = s.users.get_mut(&tuid) {
|
||||
u.channels.remove(&key);
|
||||
}
|
||||
s.channels.retain(|_, c| !c.is_empty());
|
||||
s.channels.retain(|_, c| c.keep_alive());
|
||||
CmdResult::Ok
|
||||
}
|
||||
}
|
||||
|
|
@ -195,8 +195,11 @@ impl Command for Invite {
|
|||
);
|
||||
return CmdResult::Fail;
|
||||
}
|
||||
// only ops may invite into an +i channel
|
||||
if s.channels[&key].modes.invite_only && !s.is_op(uid, &key) {
|
||||
// only ops may invite into an +i channel — unless +A (allow anyone to invite)
|
||||
if s.channels[&key].modes.invite_only
|
||||
&& !s.channels[&key].modes.allowinvite
|
||||
&& !s.is_op(uid, &key)
|
||||
{
|
||||
s.numeric(
|
||||
uid,
|
||||
ERR_CHANOPRIVSNEEDED,
|
||||
|
|
@ -373,7 +376,7 @@ impl Command for Part {
|
|||
if let Some(u) = s.users.get_mut(&uid) {
|
||||
u.channels.remove(&key);
|
||||
}
|
||||
s.channels.retain(|_, c| !c.is_empty());
|
||||
s.channels.retain(|_, c| c.keep_alive());
|
||||
s.events.push_back(Hook::Part(uid, key, reason.clone()));
|
||||
}
|
||||
CmdResult::Ok
|
||||
|
|
@ -428,6 +431,15 @@ impl Command for Kick {
|
|||
);
|
||||
return CmdResult::Fail;
|
||||
}
|
||||
// +Q — kicks disabled (IRC operators bypass; SAKICK is a separate path)
|
||||
if s.channels[&key].modes.nokicks && !s.is_oper(uid) {
|
||||
s.numeric(
|
||||
uid,
|
||||
ERR_CHANOPRIVSNEEDED,
|
||||
&format!("{chan} :Kicks are disabled here (+Q)"),
|
||||
);
|
||||
return CmdResult::Fail;
|
||||
}
|
||||
let kicker = s.users[&uid].nick.clone();
|
||||
let reason = params.get(2).cloned().unwrap_or(kicker);
|
||||
let prefix = s.users[&uid].prefix();
|
||||
|
|
@ -443,7 +455,7 @@ impl Command for Kick {
|
|||
if let Some(u) = s.users.get_mut(&tuid) {
|
||||
u.channels.remove(&key);
|
||||
}
|
||||
s.channels.retain(|_, c| !c.is_empty());
|
||||
s.channels.retain(|_, c| c.keep_alive());
|
||||
s.events
|
||||
.push_back(Hook::Part(tuid, key, "kicked".to_string()));
|
||||
CmdResult::Ok
|
||||
|
|
|
|||
|
|
@ -175,9 +175,10 @@ impl Command for Whois {
|
|||
&format!("{nick} :is an IRC operator"),
|
||||
);
|
||||
}
|
||||
// 320: oper-set SWHOIS line
|
||||
// 320: oper-set SWHOIS line. No redundant target-nick param — just the
|
||||
// text — so clients that don't special-case 320 don't echo the nick.
|
||||
if let Some(line) = &swhois {
|
||||
s.numeric(uid, RPL_WHOISSPECIAL, &format!("{nick} :{line}"));
|
||||
s.numeric(uid, RPL_WHOISSPECIAL, &format!(":{line}"));
|
||||
}
|
||||
// opers can see through the cloak to the real host/ip
|
||||
if asker_oper && disp != realhost {
|
||||
|
|
|
|||
|
|
@ -975,7 +975,7 @@ impl Command for SaKick {
|
|||
if let Some(u) = s.users.get_mut(&tuid) {
|
||||
u.channels.remove(&key);
|
||||
}
|
||||
s.channels.retain(|_, c| !c.is_empty());
|
||||
s.channels.retain(|_, c| c.keep_alive());
|
||||
s.events
|
||||
.push_back(Hook::Part(tuid, key, "kicked".to_string()));
|
||||
let by = oper_nick(s, uid);
|
||||
|
|
@ -1099,7 +1099,7 @@ impl Command for ClearChan {
|
|||
s.events
|
||||
.push_back(Hook::Part(tuid, key.clone(), "cleared".to_string()));
|
||||
}
|
||||
s.channels.retain(|_, c| !c.is_empty());
|
||||
s.channels.retain(|_, c| c.keep_alive());
|
||||
let by = oper_nick(s, uid);
|
||||
s.snotice(&format!("{by} used CLEARCHAN on {chan}"));
|
||||
CmdResult::Ok
|
||||
|
|
@ -1208,7 +1208,8 @@ impl Command for SwhoisCmd {
|
|||
let Some(t) = oper_target(s, uid, ¶ms[0]) else {
|
||||
return CmdResult::Fail;
|
||||
};
|
||||
let text = params[1].clone();
|
||||
// everything after the nick is the line — works with or without a `:`
|
||||
let text = params[1..].join(" ");
|
||||
if let Some(u) = s.users.get_mut(&t) {
|
||||
if text.is_empty() {
|
||||
u.ext.take::<Swhois>();
|
||||
|
|
|
|||
|
|
@ -915,7 +915,7 @@ impl Server {
|
|||
format!(":{prefix} PART {chan} :{reason}")
|
||||
};
|
||||
self.to_channel(&key, &line, None);
|
||||
self.channels.retain(|_, c| !c.is_empty());
|
||||
self.channels.retain(|_, c| c.keep_alive());
|
||||
let fwd = if reason.is_empty() {
|
||||
format!(":{uuid} PART {chan}")
|
||||
} else {
|
||||
|
|
@ -950,7 +950,7 @@ impl Server {
|
|||
for m in notify {
|
||||
self.send(m, line.clone());
|
||||
}
|
||||
self.channels.retain(|_, c| !c.is_empty());
|
||||
self.channels.retain(|_, c| c.keep_alive());
|
||||
if let Some(ru) = self.remote_users.remove(uuid) {
|
||||
self.remote_nick.remove(&ru.nick.to_ascii_lowercase());
|
||||
}
|
||||
|
|
@ -1056,7 +1056,7 @@ impl Server {
|
|||
&format!(":{prefix} KICK {chan} {victim} :{reason}"),
|
||||
None,
|
||||
);
|
||||
self.channels.retain(|_, c| !c.is_empty());
|
||||
self.channels.retain(|_, c| c.keep_alive());
|
||||
self.propagate(&format!(":{src} KICK {chan} {victim} :{reason}"), Some(via));
|
||||
}
|
||||
|
||||
|
|
|
|||
26
src/mode.rs
26
src/mode.rs
|
|
@ -85,6 +85,9 @@ static CHAN_MODES: &[&(dyn ChanMode + Sync)] = &[
|
|||
&REDIRECT,
|
||||
&CHANHISTORY,
|
||||
&ANTICAPS,
|
||||
&NOKICKS,
|
||||
&ALLOWINVITE,
|
||||
&PERMANENT,
|
||||
];
|
||||
|
||||
// --- prefix modes (+q/+a/+o/+h/+v): a per-member rank, needs a nick ----------
|
||||
|
|
@ -294,6 +297,27 @@ static AUDITORIUM: Flag = Flag {
|
|||
ch: 'u',
|
||||
set: set_auditorium,
|
||||
};
|
||||
fn set_nokicks(m: &mut ChanModes, v: bool) {
|
||||
m.nokicks = v;
|
||||
}
|
||||
fn set_allowinvite(m: &mut ChanModes, v: bool) {
|
||||
m.allowinvite = v;
|
||||
}
|
||||
fn set_permanent(m: &mut ChanModes, v: bool) {
|
||||
m.permanent = v;
|
||||
}
|
||||
static NOKICKS: Flag = Flag {
|
||||
ch: 'Q',
|
||||
set: set_nokicks,
|
||||
};
|
||||
static ALLOWINVITE: Flag = Flag {
|
||||
ch: 'A',
|
||||
set: set_allowinvite,
|
||||
};
|
||||
static PERMANENT: Flag = Flag {
|
||||
ch: 'P',
|
||||
set: set_permanent,
|
||||
};
|
||||
|
||||
impl ChanMode for Flag {
|
||||
fn letter(&self) -> char {
|
||||
|
|
@ -1059,7 +1083,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn registry_covers_all_channel_modes() {
|
||||
for c in "qaohvbeIklmntiszpONCTcSRMfjFLgGuB".chars() {
|
||||
for c in "qaohvbeIklmntiszpONCTcSRMfjFLgGuBQAP".chars() {
|
||||
assert!(chan_mode(c).is_some(), "missing handler for +{c}");
|
||||
}
|
||||
assert!(chan_mode('y').is_none());
|
||||
|
|
|
|||
|
|
@ -423,7 +423,7 @@ impl Server {
|
|||
for m in seen {
|
||||
self.send(m, line.clone());
|
||||
}
|
||||
self.channels.retain(|_, c| !c.is_empty());
|
||||
self.channels.retain(|_, c| c.keep_alive());
|
||||
self.watch_notify_offline(&user.nick); // tell WATCH/MONITOR watchers
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@ impl Server {
|
|||
uid,
|
||||
RPL_MYINFO,
|
||||
&format!(
|
||||
"{} echoircd-{VERSION} iowxsgBDIHrRz qaohvbeIklimnpstzCTcSNORMfjFLgGuB",
|
||||
"{} echoircd-{VERSION} iowxsgBDIHrRz qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAP",
|
||||
self.name
|
||||
),
|
||||
);
|
||||
|
|
@ -417,7 +417,7 @@ impl Server {
|
|||
uid,
|
||||
RPL_ISUPPORT,
|
||||
&format!(
|
||||
"CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIg,k,lfjFLHB,CGMNORSTcimnpstuz EXTBAN=,cmn WATCH=128 MONITOR=128 SILENCE=32 CALLERID=g WHOX CHATHISTORY=256 MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN=30 CHANNELLEN=50 NETWORK={} :are supported by this server",
|
||||
"CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIg,k,lfjFLHB,ACGMNOPQRSTcimnpstuz EXTBAN=,cmn WATCH=128 MONITOR=128 SILENCE=32 CALLERID=g WHOX CHATHISTORY=256 MSGREFTYPES=timestamp,msgid UTF8ONLY CASEMAPPING=ascii NICKLEN=30 CHANNELLEN=50 NETWORK={} :are supported by this server",
|
||||
self.network
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue