solvemsg: arithmetic challenge before an un-vouched user's PMs deliver (anti-spam)
This commit is contained in:
parent
0bf6992d8a
commit
349200d695
3 changed files with 116 additions and 0 deletions
|
|
@ -117,6 +117,10 @@ amu_target = both
|
||||||
# hidewhois_hide_server = yes # hide 312
|
# hidewhois_hide_server = yes # hide 312
|
||||||
# hidewhois_hide_idle = yes # hide 317
|
# hidewhois_hide_idle = yes # hide 317
|
||||||
# hidewhois_hide_secure = yes # hide 671
|
# hidewhois_hide_secure = yes # hide 671
|
||||||
|
# --- solvemsg (m_solvemsg): an un-vouched user must answer one arithmetic
|
||||||
|
# question before their private messages are delivered (opers & logged-in
|
||||||
|
# accounts are exempt). Cheap anti-spam-bot gate.
|
||||||
|
# solvemsg = yes
|
||||||
# --- dccallow (m_dccallow): block unwanted DCC transfers unless the recipient
|
# --- dccallow (m_dccallow): block unwanted DCC transfers unless the recipient
|
||||||
# ran /DCCALLOW +<nick>. Blocked file globs are repeatable; blockchat also
|
# ran /DCCALLOW +<nick>. Blocked file globs are repeatable; blockchat also
|
||||||
# gates DCC CHAT. Recipients manage their allow-list with DCCALLOW +/-/LIST.
|
# gates DCC CHAT. Recipients manage their allow-list with DCCALLOW +/-/LIST.
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ pub mod securelist;
|
||||||
pub mod securitygroups;
|
pub mod securitygroups;
|
||||||
pub mod serverban;
|
pub mod serverban;
|
||||||
pub mod showfile;
|
pub mod showfile;
|
||||||
|
pub mod solvemsg;
|
||||||
pub mod snoop;
|
pub mod snoop;
|
||||||
pub mod tline;
|
pub mod tline;
|
||||||
pub mod whoisport;
|
pub mod whoisport;
|
||||||
|
|
@ -86,6 +87,7 @@ pub fn default_modules() -> Vec<Box<dyn Module>> {
|
||||||
Box::new(disable::Disable),
|
Box::new(disable::Disable),
|
||||||
Box::new(maphide::MapHide),
|
Box::new(maphide::MapHide),
|
||||||
Box::new(dccallow::DccAllow),
|
Box::new(dccallow::DccAllow),
|
||||||
|
Box::new(solvemsg::SolveMsg),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
110
src/modules/solvemsg.rs
Normal file
110
src/modules/solvemsg.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
//! solvemsg — a lightweight anti-spam gate: before an un-vouched user's *private*
|
||||||
|
//! messages are delivered, they must answer one small arithmetic question. Opers
|
||||||
|
//! and users logged into an account are exempt. Off unless `solvemsg = yes`.
|
||||||
|
//!
|
||||||
|
//! Flow: the first PM is held and a question is posed; the user replies with the
|
||||||
|
//! number (that reply is consumed), and once correct every later message passes.
|
||||||
|
//! Reference: InspIRCd's `m_solvemsg`. Original native Rust.
|
||||||
|
|
||||||
|
use crate::module::{ModResult, Module};
|
||||||
|
use crate::server::Server;
|
||||||
|
use crate::Uid;
|
||||||
|
|
||||||
|
/// Per-user challenge state, on `User.ext`.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct SolveState {
|
||||||
|
solved: bool,
|
||||||
|
answer: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A uniform-ish random byte in `0..max` via openssl (no `rand` crate).
|
||||||
|
fn rnd(max: u8) -> u8 {
|
||||||
|
let mut b = [0u8; 1];
|
||||||
|
let _ = openssl::rand::rand_bytes(&mut b);
|
||||||
|
b[0] % max
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SolveMsg;
|
||||||
|
|
||||||
|
impl Module for SolveMsg {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"solvemsg"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_pre_message(
|
||||||
|
&mut self,
|
||||||
|
s: &mut Server,
|
||||||
|
uid: Uid,
|
||||||
|
target: &str,
|
||||||
|
text: &str,
|
||||||
|
) -> ModResult {
|
||||||
|
if !s.conf_bool("solvemsg", false) {
|
||||||
|
return ModResult::Passthru;
|
||||||
|
}
|
||||||
|
// trusted: opers and logged-in accounts never see a challenge
|
||||||
|
let exempt = s
|
||||||
|
.users
|
||||||
|
.get(&uid)
|
||||||
|
.map(|u| u.flags.oper || u.account.is_some())
|
||||||
|
.unwrap_or(true);
|
||||||
|
if exempt {
|
||||||
|
return ModResult::Passthru;
|
||||||
|
}
|
||||||
|
// only gate PMs to another user (channels have their own controls)
|
||||||
|
let Some(tuid) = s.find_nick(target) else {
|
||||||
|
return ModResult::Passthru;
|
||||||
|
};
|
||||||
|
if tuid == uid {
|
||||||
|
return ModResult::Passthru;
|
||||||
|
}
|
||||||
|
let st = s.users.get(&uid).and_then(|u| u.ext.get::<SolveState>());
|
||||||
|
if st.map(|st| st.solved).unwrap_or(false) {
|
||||||
|
return ModResult::Passthru; // already solved
|
||||||
|
}
|
||||||
|
let pending = st.and_then(|st| st.answer);
|
||||||
|
let nick = s.users.get(&uid).map(|u| u.nick.clone()).unwrap_or_default();
|
||||||
|
|
||||||
|
// is this message the answer to an outstanding challenge?
|
||||||
|
if let Some(ans) = pending {
|
||||||
|
if text.trim().parse::<i64>().ok() == Some(ans) {
|
||||||
|
if let Some(u) = s.users.get_mut(&uid) {
|
||||||
|
u.ext.set(SolveState {
|
||||||
|
solved: true,
|
||||||
|
answer: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
s.send(
|
||||||
|
uid,
|
||||||
|
format!(
|
||||||
|
":{} NOTICE {nick} :*** Correct — you may now message freely; please resend your message.",
|
||||||
|
s.name
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return ModResult::Deny; // consume the answer itself
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pose a fresh question
|
||||||
|
let a = (rnd(9) + 1) as i64;
|
||||||
|
let b = (rnd(9) + 1) as i64;
|
||||||
|
let (sym, ans) = match rnd(3) {
|
||||||
|
0 => ("+", a + b),
|
||||||
|
1 => ("-", a - b),
|
||||||
|
_ => ("*", a * b),
|
||||||
|
};
|
||||||
|
if let Some(u) = s.users.get_mut(&uid) {
|
||||||
|
u.ext.set(SolveState {
|
||||||
|
solved: false,
|
||||||
|
answer: Some(ans),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
s.send(
|
||||||
|
uid,
|
||||||
|
format!(
|
||||||
|
":{} NOTICE {nick} :*** To cut spam, answer this to send your message — what is {a} {sym} {b} ?",
|
||||||
|
s.name
|
||||||
|
),
|
||||||
|
);
|
||||||
|
ModResult::Deny
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue