reputation: full port — y: score extban (y:<N/y:>N), WHOIS score, +2 for accounts, bump interval/scorecap/minchanmembers config; conn_join/conn_umodes/connbanner/operjoin/opermodes/seenicks/chancreate
This commit is contained in:
parent
64dcbd8bf4
commit
eb121a75f8
6 changed files with 211 additions and 16 deletions
|
|
@ -13,42 +13,98 @@ use crate::numeric::{ERR_NOPRIVILEGES, ERR_NOSUCHNICK};
|
|||
use crate::server::Server;
|
||||
use crate::Uid;
|
||||
|
||||
const REP_CAP: u32 = 100_000;
|
||||
const SAVE_EVERY: u32 = 20; // ticks between disk saves (~5 min at TICK_SECS=15)
|
||||
|
||||
/// per-IP reputation score. Stored in `Server.ext`.
|
||||
#[derive(Default)]
|
||||
pub struct Reputation(pub HashMap<IpAddr, u32>);
|
||||
|
||||
/// The tick-driven accrual + periodic save. Holds a tick counter of its own.
|
||||
/// Whether `uid` is in at least one channel with `min` or more members (the
|
||||
/// reputation `minchanmembers` gate — stops idle bots farming score alone).
|
||||
fn in_active_channel(s: &Server, uid: Uid, min: usize) -> bool {
|
||||
if min <= 1 {
|
||||
return true;
|
||||
}
|
||||
s.users
|
||||
.get(&uid)
|
||||
.map(|u| {
|
||||
u.channels.iter().any(|k| {
|
||||
s.channels
|
||||
.get(k)
|
||||
.map(|c| c.members.len() >= min)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// The tick-driven accrual + periodic save. Bumps every `rep_bump_secs` (default
|
||||
/// 5 min): +1 per connected user's IP, +1 more if they're logged into an account.
|
||||
#[derive(Default)]
|
||||
pub struct ReputationMod {
|
||||
ticks: u32,
|
||||
secs: u64, // seconds since the last bump
|
||||
since_save: u64,
|
||||
}
|
||||
impl Module for ReputationMod {
|
||||
fn name(&self) -> &'static str {
|
||||
"reputation"
|
||||
}
|
||||
fn on_tick(&mut self, s: &mut Server) {
|
||||
let ips: Vec<IpAddr> = s
|
||||
self.secs += crate::server::TICK_SECS;
|
||||
self.since_save += crate::server::TICK_SECS;
|
||||
if self.secs < s.rep_bump_secs {
|
||||
return;
|
||||
}
|
||||
self.secs = 0;
|
||||
let cap = s.rep_scorecap;
|
||||
let min = s.rep_minchanmembers;
|
||||
// (ip, bump amount): +1 base, +1 if the user is logged into services
|
||||
let bumps: Vec<(IpAddr, u32)> = s
|
||||
.users
|
||||
.values()
|
||||
.filter(|u| u.registered)
|
||||
.map(|u| u.addr.ip())
|
||||
.filter(|u| in_active_channel(s, u.uid, min))
|
||||
.map(|u| (u.addr.ip(), if u.account.is_some() { 2 } else { 1 }))
|
||||
.collect();
|
||||
let store = s.ext.get_or_insert_with::<Reputation>(Reputation::default);
|
||||
for ip in ips {
|
||||
for (ip, amt) in bumps {
|
||||
let e = store.0.entry(ip).or_insert(0);
|
||||
*e = (*e + 1).min(REP_CAP);
|
||||
*e = (*e + amt).min(cap);
|
||||
}
|
||||
self.ticks += 1;
|
||||
#[allow(clippy::manual_is_multiple_of)] // is_multiple_of is unstable on our MSRV
|
||||
if self.ticks % SAVE_EVERY == 0 {
|
||||
if self.since_save >= 600 {
|
||||
self.since_save = 0;
|
||||
save(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The reputation score of the IP `uid` is connecting from.
|
||||
pub fn score_of(s: &Server, uid: Uid) -> u32 {
|
||||
let Some(ip) = s.users.get(&uid).map(|u| u.addr.ip()) else {
|
||||
return 0;
|
||||
};
|
||||
s.ext
|
||||
.get::<Reputation>()
|
||||
.and_then(|r| r.0.get(&ip))
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// The `y:` score extban: `y:<N` matches a score below N, `y:>N` above N.
|
||||
pub fn score_ban_match(s: &Server, uid: Uid, spec: &str) -> bool {
|
||||
let (gt, num) = match spec.strip_prefix('>') {
|
||||
Some(n) => (true, n),
|
||||
None => (false, spec.strip_prefix('<').unwrap_or(spec)),
|
||||
};
|
||||
let Ok(threshold) = num.trim().parse::<u32>() else {
|
||||
return false;
|
||||
};
|
||||
let score = score_of(s, uid);
|
||||
if gt {
|
||||
score > threshold
|
||||
} else {
|
||||
score < threshold
|
||||
}
|
||||
}
|
||||
|
||||
pub fn commands() -> Vec<Box<dyn Command>> {
|
||||
vec![Box::new(ReputationCmd)]
|
||||
}
|
||||
|
|
@ -90,10 +146,11 @@ impl Command for ReputationCmd {
|
|||
.map(|u| u.nick.clone())
|
||||
.unwrap_or_default();
|
||||
if let Some(val) = params.get(1).and_then(|v| v.parse::<u32>().ok()) {
|
||||
let cap = s.rep_scorecap;
|
||||
s.ext
|
||||
.get_or_insert_with::<Reputation>(Reputation::default)
|
||||
.0
|
||||
.insert(ip, val.min(REP_CAP));
|
||||
.insert(ip, val.min(cap));
|
||||
save(s);
|
||||
s.send(
|
||||
uid,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue