//! 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. 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, } /// A uniform-ish random byte in `0..max` via the OpenSSL CSPRNG. 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::()); 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::().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 } }