anticaps channel mode (+B <percent>)
This commit is contained in:
parent
68fdd91eaa
commit
050ce37612
4 changed files with 70 additions and 3 deletions
|
|
@ -148,6 +148,7 @@ pub struct ChanModes {
|
||||||
pub nickflood: Option<Rate>, // +F
|
pub nickflood: Option<Rate>, // +F
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChanModes {
|
impl ChanModes {
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,18 @@ fn apply_censor(body: &str, censor: &[(String, String)]) -> Option<String> {
|
||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Percentage of the ASCII letters in `t` that are uppercase, or `None` when there
|
||||||
|
/// are too few letters to judge (so short shouts like "OK" aren't blocked). Used
|
||||||
|
/// by the +B anticaps channel mode.
|
||||||
|
fn caps_percent(t: &str) -> Option<u8> {
|
||||||
|
let letters = t.chars().filter(|c| c.is_ascii_alphabetic()).count();
|
||||||
|
if letters < 8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let upper = t.chars().filter(|c| c.is_ascii_uppercase()).count();
|
||||||
|
Some(((upper * 100) / letters) as u8)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn commands() -> Vec<Box<dyn Command>> {
|
pub fn commands() -> Vec<Box<dyn Command>> {
|
||||||
vec![Box::new(PrivMsg), Box::new(Notice), Box::new(TagMsg)]
|
vec![Box::new(PrivMsg), Box::new(Notice), Box::new(TagMsg)]
|
||||||
}
|
}
|
||||||
|
|
@ -305,6 +317,21 @@ pub(crate) fn deliver(s: &mut Server, uid: Uid, params: &[String], notice: bool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// +B anticaps — reject a mostly-uppercase message (ops/opers exempt)
|
||||||
|
if !flood_exempt {
|
||||||
|
if let Some(pct) = s.channels.get(&key).and_then(|c| c.modes.anticaps) {
|
||||||
|
if caps_percent(&body).map(|p| p >= pct).unwrap_or(false) {
|
||||||
|
if !notice {
|
||||||
|
s.numeric(
|
||||||
|
uid,
|
||||||
|
ERR_CANNOTSENDTOCHAN,
|
||||||
|
&format!("{target} :Cannot send to channel (+B: too many capitals)"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return CmdResult::Fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// deliver to every member except the sender and +D (deaf) users, tagging
|
// deliver to every member except the sender and +D (deaf) users, tagging
|
||||||
// per-recipient (server-time + any client-only tags on the line)
|
// per-recipient (server-time + any client-only tags on the line)
|
||||||
let line = format!(":{prefix} {cmd} {target} :{body}");
|
let line = format!(":{prefix} {cmd} {target} :{body}");
|
||||||
|
|
|
||||||
41
src/mode.rs
41
src/mode.rs
|
|
@ -84,6 +84,7 @@ static CHAN_MODES: &[&(dyn ChanMode + Sync)] = &[
|
||||||
&NICKFLOOD,
|
&NICKFLOOD,
|
||||||
&REDIRECT,
|
&REDIRECT,
|
||||||
&CHANHISTORY,
|
&CHANHISTORY,
|
||||||
|
&ANTICAPS,
|
||||||
];
|
];
|
||||||
|
|
||||||
// --- 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 ----------
|
||||||
|
|
@ -796,6 +797,44 @@ impl ChanMode for RedirectMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// +B `<percent>` — reject channel messages that are at least `<percent>` uppercase
|
||||||
|
/// (InspIRCd `m_anticaps`). Enforced in the message path; ops are exempt.
|
||||||
|
struct AntiCapsMode;
|
||||||
|
static ANTICAPS: AntiCapsMode = AntiCapsMode;
|
||||||
|
impl ChanMode for AntiCapsMode {
|
||||||
|
fn letter(&self) -> char {
|
||||||
|
'B'
|
||||||
|
}
|
||||||
|
fn wants_param(&self, adding: bool) -> bool {
|
||||||
|
adding
|
||||||
|
}
|
||||||
|
fn apply(
|
||||||
|
&self,
|
||||||
|
s: &mut Server,
|
||||||
|
_chan: &str,
|
||||||
|
key: &str,
|
||||||
|
_uid: Uid,
|
||||||
|
adding: bool,
|
||||||
|
param: Option<&str>,
|
||||||
|
) -> Applied {
|
||||||
|
if adding {
|
||||||
|
let pct = param.and_then(|p| p.parse::<u8>().ok()).filter(|&p| p >= 1);
|
||||||
|
let Some(pct) = pct.map(|p| p.min(100)) else {
|
||||||
|
return Applied::No; // needs a 1..=100 percentage
|
||||||
|
};
|
||||||
|
if let Some(c) = s.channels.get_mut(key) {
|
||||||
|
c.modes.anticaps = Some(pct);
|
||||||
|
}
|
||||||
|
Applied::Yes(Some(pct.to_string()))
|
||||||
|
} else {
|
||||||
|
if let Some(c) = s.channels.get_mut(key) {
|
||||||
|
c.modes.anticaps = None;
|
||||||
|
}
|
||||||
|
Applied::Yes(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === user modes ============================================================
|
// === user modes ============================================================
|
||||||
|
|
||||||
/// A user mode (+i/+w/+o) — same handler-object shape as [`ChanMode`], and the
|
/// A user mode (+i/+w/+o) — same handler-object shape as [`ChanMode`], and the
|
||||||
|
|
@ -1020,7 +1059,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn registry_covers_all_channel_modes() {
|
fn registry_covers_all_channel_modes() {
|
||||||
for c in "qaohvbeIklmntiszpONCTcSRMfjFLgGu".chars() {
|
for c in "qaohvbeIklmntiszpONCTcSRMfjFLgGuB".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());
|
||||||
|
|
|
||||||
|
|
@ -409,7 +409,7 @@ impl Server {
|
||||||
uid,
|
uid,
|
||||||
RPL_MYINFO,
|
RPL_MYINFO,
|
||||||
&format!(
|
&format!(
|
||||||
"{} echoircd-{VERSION} iowxsgBDIHrRz qaohvbeIklimnpstzCTcSNORMfjFLgGu",
|
"{} echoircd-{VERSION} iowxsgBDIHrRz qaohvbeIklimnpstzCTcSNORMfjFLgGuB",
|
||||||
self.name
|
self.name
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -417,7 +417,7 @@ impl Server {
|
||||||
uid,
|
uid,
|
||||||
RPL_ISUPPORT,
|
RPL_ISUPPORT,
|
||||||
&format!(
|
&format!(
|
||||||
"CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beIg,k,lfjFLH,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,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",
|
||||||
self.network
|
self.network
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue