reputation: count remote/services channel members toward the minchanmembers gate (score was frozen)

This commit is contained in:
Jean Chevronnet 2026-08-17 13:27:58 +00:00
parent 62c0141362
commit 3d7ee6b586

View file

@ -101,7 +101,10 @@ fn key_of(s: &Server, uid: Uid) -> Option<IpAddr> {
}
/// Whether `uid` is in at least one channel with `min` or more members (the
/// `minchanmembers` gate — stops idle bots farming score alone).
/// `minchanmembers` gate — stops idle bots farming score alone). Counts every
/// member, local AND remote (services bots, users on other servers), matching
/// the network-wide channel population — otherwise a user sharing a channel with
/// only remote/services members never bumps and their score freezes.
fn in_active_channel(s: &Server, uid: Uid, min: usize) -> bool {
if min <= 1 {
return true;
@ -112,7 +115,7 @@ fn in_active_channel(s: &Server, uid: Uid, min: usize) -> bool {
u.channels.iter().any(|k| {
s.channels
.get(k)
.map(|c| c.members.len() >= min)
.map(|c| c.members.len() + c.rmembers.len() >= min)
.unwrap_or(false)
})
})
@ -334,3 +337,95 @@ pub fn load(s: &mut Server) {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::channels::{Channel, Member};
use crate::config::Config;
use crate::extensible::Extensible;
use crate::socketengine::OutSink;
use crate::users::{Caps, User, UserFlags};
use std::collections::HashSet;
use std::sync::atomic::AtomicU64;
use std::sync::{mpsc, Arc};
// The minchanmembers bump gate must count ALL members — local plus remote
// (services bots, users on other servers) — like InspIRCd's GetUsers().size().
// Regression: counting only local members froze the score of anyone sharing a
// channel with remote/services members (e.g. reverse + a bot in #echoircd).
#[test]
fn active_channel_counts_remote_members() {
let (tx, _rx) = mpsc::channel();
let mut s = Server::new(Config::default(), tx, Arc::new(AtomicU64::new(1)));
let (utx, _urx) = mpsc::channel();
let mut chans = HashSet::new();
chans.insert("#echoircd".to_string());
s.users.insert(
1,
User {
uid: 1,
uuid: "0AAAAAAAB".into(),
nick: "reverse".into(),
ident: "r".into(),
realname: "r".into(),
host: "h".into(),
cloak: String::new(),
vhost: None,
secure: false,
certfp: None,
account: Some("reverse".into()),
signon: 0,
nick_ts: 0,
addr: "127.0.0.1:1".parse().unwrap(),
port: 6667,
registered: true,
dns_pending: false,
ident_pending: false,
auth_pending: false,
waitpong: None,
class: None,
pass: None,
deferred: Vec::new(),
cap: false,
cap_302: false,
caps: Caps::default(),
sasl_mech: None,
channels: chans,
watch: Vec::new(),
monitor: Vec::new(),
silence: Vec::new(),
accept: Vec::new(),
quitting: None,
flags: UserFlags::default(),
last_active: 0,
ping_sent: false,
ext: Extensible::default(),
out: OutSink::Thread(utx),
sock: None,
},
);
// #echoircd: 1 local (reverse) + 2 remote (a user + a services bot) = 3 total
let mut c = Channel::new("#echoircd");
c.members.insert(1, Member::default());
c.rmembers.insert("42SBOT0001".into(), Member::default());
c.rmembers.insert("10HUSER002".into(), Member::default());
s.channels.insert("#echoircd".into(), c);
assert!(
in_active_channel(&s, 1, 3),
"3 total members (1 local + 2 remote) must satisfy minchanmembers=3"
);
// drop one remote member -> 2 total -> below the gate
s.channels
.get_mut("#echoircd")
.unwrap()
.rmembers
.remove("10HUSER002");
assert!(
!in_active_channel(&s, 1, 3),
"2 total members must not satisfy minchanmembers=3"
);
}
}