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 redirect: Option<String>, // +L <#target> — when full, send there
|
||||||
pub history: Option<(u32, u64)>, // +H <lines>:<secs> — replay recent messages to joiners
|
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 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 {
|
impl ChanModes {
|
||||||
|
|
@ -172,6 +175,9 @@ impl ChanModes {
|
||||||
'M' => self.reg_moderated = on,
|
'M' => self.reg_moderated = on,
|
||||||
'G' => self.censor = on,
|
'G' => self.censor = on,
|
||||||
'u' => self.auditorium = 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 {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.members.is_empty() && self.rmembers.is_empty()
|
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 {
|
impl Server {
|
||||||
|
|
@ -350,7 +362,7 @@ impl Server {
|
||||||
if let Some(u) = self.users.get_mut(&uid) {
|
if let Some(u) = self.users.get_mut(&uid) {
|
||||||
u.channels.remove(&key);
|
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),
|
/// 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) {
|
if let Some(u) = self.users.get_mut(&uid) {
|
||||||
u.channels.remove(key);
|
u.channels.remove(key);
|
||||||
}
|
}
|
||||||
self.channels.retain(|_, c| !c.is_empty());
|
self.channels.retain(|_, c| c.keep_alive());
|
||||||
self.events
|
self.events
|
||||||
.push_back(Hook::Part(uid, key.to_string(), "flood".to_string()));
|
.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) {
|
if let Some(u) = s.users.get_mut(&uid) {
|
||||||
u.channels.remove(&key);
|
u.channels.remove(&key);
|
||||||
}
|
}
|
||||||
s.channels.retain(|_, c| !c.is_empty());
|
s.channels.retain(|_, c| c.keep_alive());
|
||||||
s.join(uid, chan, None);
|
s.join(uid, chan, None);
|
||||||
CmdResult::Ok
|
CmdResult::Ok
|
||||||
}
|
}
|
||||||
|
|
@ -167,7 +167,7 @@ impl Command for Remove {
|
||||||
if let Some(u) = s.users.get_mut(&tuid) {
|
if let Some(u) = s.users.get_mut(&tuid) {
|
||||||
u.channels.remove(&key);
|
u.channels.remove(&key);
|
||||||
}
|
}
|
||||||
s.channels.retain(|_, c| !c.is_empty());
|
s.channels.retain(|_, c| c.keep_alive());
|
||||||
CmdResult::Ok
|
CmdResult::Ok
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -195,8 +195,11 @@ impl Command for Invite {
|
||||||
);
|
);
|
||||||
return CmdResult::Fail;
|
return CmdResult::Fail;
|
||||||
}
|
}
|
||||||
// only ops may invite into an +i channel
|
// only ops may invite into an +i channel — unless +A (allow anyone to invite)
|
||||||
if s.channels[&key].modes.invite_only && !s.is_op(uid, &key) {
|
if s.channels[&key].modes.invite_only
|
||||||
|
&& !s.channels[&key].modes.allowinvite
|
||||||
|
&& !s.is_op(uid, &key)
|
||||||
|
{
|
||||||
s.numeric(
|
s.numeric(
|
||||||
uid,
|
uid,
|
||||||
ERR_CHANOPRIVSNEEDED,
|
ERR_CHANOPRIVSNEEDED,
|
||||||
|
|
@ -373,7 +376,7 @@ impl Command for Part {
|
||||||
if let Some(u) = s.users.get_mut(&uid) {
|
if let Some(u) = s.users.get_mut(&uid) {
|
||||||
u.channels.remove(&key);
|
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()));
|
s.events.push_back(Hook::Part(uid, key, reason.clone()));
|
||||||
}
|
}
|
||||||
CmdResult::Ok
|
CmdResult::Ok
|
||||||
|
|
@ -428,6 +431,15 @@ impl Command for Kick {
|
||||||
);
|
);
|
||||||
return CmdResult::Fail;
|
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 kicker = s.users[&uid].nick.clone();
|
||||||
let reason = params.get(2).cloned().unwrap_or(kicker);
|
let reason = params.get(2).cloned().unwrap_or(kicker);
|
||||||
let prefix = s.users[&uid].prefix();
|
let prefix = s.users[&uid].prefix();
|
||||||
|
|
@ -443,7 +455,7 @@ impl Command for Kick {
|
||||||
if let Some(u) = s.users.get_mut(&tuid) {
|
if let Some(u) = s.users.get_mut(&tuid) {
|
||||||
u.channels.remove(&key);
|
u.channels.remove(&key);
|
||||||
}
|
}
|
||||||
s.channels.retain(|_, c| !c.is_empty());
|
s.channels.retain(|_, c| c.keep_alive());
|
||||||
s.events
|
s.events
|
||||||
.push_back(Hook::Part(tuid, key, "kicked".to_string()));
|
.push_back(Hook::Part(tuid, key, "kicked".to_string()));
|
||||||
CmdResult::Ok
|
CmdResult::Ok
|
||||||
|
|
|
||||||
|
|
@ -175,9 +175,10 @@ impl Command for Whois {
|
||||||
&format!("{nick} :is an IRC operator"),
|
&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 {
|
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
|
// opers can see through the cloak to the real host/ip
|
||||||
if asker_oper && disp != realhost {
|
if asker_oper && disp != realhost {
|
||||||
|
|
|
||||||
|
|
@ -975,7 +975,7 @@ impl Command for SaKick {
|
||||||
if let Some(u) = s.users.get_mut(&tuid) {
|
if let Some(u) = s.users.get_mut(&tuid) {
|
||||||
u.channels.remove(&key);
|
u.channels.remove(&key);
|
||||||
}
|
}
|
||||||
s.channels.retain(|_, c| !c.is_empty());
|
s.channels.retain(|_, c| c.keep_alive());
|
||||||
s.events
|
s.events
|
||||||
.push_back(Hook::Part(tuid, key, "kicked".to_string()));
|
.push_back(Hook::Part(tuid, key, "kicked".to_string()));
|
||||||
let by = oper_nick(s, uid);
|
let by = oper_nick(s, uid);
|
||||||
|
|
@ -1099,7 +1099,7 @@ impl Command for ClearChan {
|
||||||
s.events
|
s.events
|
||||||
.push_back(Hook::Part(tuid, key.clone(), "cleared".to_string()));
|
.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);
|
let by = oper_nick(s, uid);
|
||||||
s.snotice(&format!("{by} used CLEARCHAN on {chan}"));
|
s.snotice(&format!("{by} used CLEARCHAN on {chan}"));
|
||||||
CmdResult::Ok
|
CmdResult::Ok
|
||||||
|
|
@ -1208,7 +1208,8 @@ impl Command for SwhoisCmd {
|
||||||
let Some(t) = oper_target(s, uid, ¶ms[0]) else {
|
let Some(t) = oper_target(s, uid, ¶ms[0]) else {
|
||||||
return CmdResult::Fail;
|
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 let Some(u) = s.users.get_mut(&t) {
|
||||||
if text.is_empty() {
|
if text.is_empty() {
|
||||||
u.ext.take::<Swhois>();
|
u.ext.take::<Swhois>();
|
||||||
|
|
|
||||||
|
|
@ -915,7 +915,7 @@ impl Server {
|
||||||
format!(":{prefix} PART {chan} :{reason}")
|
format!(":{prefix} PART {chan} :{reason}")
|
||||||
};
|
};
|
||||||
self.to_channel(&key, &line, None);
|
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() {
|
let fwd = if reason.is_empty() {
|
||||||
format!(":{uuid} PART {chan}")
|
format!(":{uuid} PART {chan}")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -950,7 +950,7 @@ impl Server {
|
||||||
for m in notify {
|
for m in notify {
|
||||||
self.send(m, line.clone());
|
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) {
|
if let Some(ru) = self.remote_users.remove(uuid) {
|
||||||
self.remote_nick.remove(&ru.nick.to_ascii_lowercase());
|
self.remote_nick.remove(&ru.nick.to_ascii_lowercase());
|
||||||
}
|
}
|
||||||
|
|
@ -1056,7 +1056,7 @@ impl Server {
|
||||||
&format!(":{prefix} KICK {chan} {victim} :{reason}"),
|
&format!(":{prefix} KICK {chan} {victim} :{reason}"),
|
||||||
None,
|
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));
|
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,
|
&REDIRECT,
|
||||||
&CHANHISTORY,
|
&CHANHISTORY,
|
||||||
&ANTICAPS,
|
&ANTICAPS,
|
||||||
|
&NOKICKS,
|
||||||
|
&ALLOWINVITE,
|
||||||
|
&PERMANENT,
|
||||||
];
|
];
|
||||||
|
|
||||||
// --- prefix modes (+q/+a/+o/+h/+v): a per-member rank, needs a nick ----------
|
// --- prefix modes (+q/+a/+o/+h/+v): a per-member rank, needs a nick ----------
|
||||||
|
|
@ -294,6 +297,27 @@ static AUDITORIUM: Flag = Flag {
|
||||||
ch: 'u',
|
ch: 'u',
|
||||||
set: set_auditorium,
|
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 {
|
impl ChanMode for Flag {
|
||||||
fn letter(&self) -> char {
|
fn letter(&self) -> char {
|
||||||
|
|
@ -1059,7 +1083,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn registry_covers_all_channel_modes() {
|
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(c).is_some(), "missing handler for +{c}");
|
||||||
}
|
}
|
||||||
assert!(chan_mode('y').is_none());
|
assert!(chan_mode('y').is_none());
|
||||||
|
|
|
||||||
|
|
@ -423,7 +423,7 @@ impl Server {
|
||||||
for m in seen {
|
for m in seen {
|
||||||
self.send(m, line.clone());
|
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
|
self.watch_notify_offline(&user.nick); // tell WATCH/MONITOR watchers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -409,7 +409,7 @@ impl Server {
|
||||||
uid,
|
uid,
|
||||||
RPL_MYINFO,
|
RPL_MYINFO,
|
||||||
&format!(
|
&format!(
|
||||||
"{} echoircd-{VERSION} iowxsgBDIHrRz qaohvbeIklimnpstzCTcSNORMfjFLgGuB",
|
"{} echoircd-{VERSION} iowxsgBDIHrRz qaohvbeIklimnpstzCTcSNORMfjFLgGuBQAP",
|
||||||
self.name
|
self.name
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -417,7 +417,7 @@ impl Server {
|
||||||
uid,
|
uid,
|
||||||
RPL_ISUPPORT,
|
RPL_ISUPPORT,
|
||||||
&format!(
|
&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
|
self.network
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue