From fa9eaece45bb48a35401b7c87a86a1884e1212ed Mon Sep 17 00:00:00 2001 From: Jean Date: Sun, 19 Jul 2026 17:45:04 +0000 Subject: [PATCH] Cap and freeze-gate GroupServ registration --- api/src/lib.rs | 1 + modules/groupserv/src/register.rs | 13 +++++++++++++ src/engine/db/network.rs | 5 +++++ src/engine/db/store.rs | 3 +++ 4 files changed, 22 insertions(+) diff --git a/api/src/lib.rs b/api/src/lib.rs index 6a42714..bd14216 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -2079,6 +2079,7 @@ pub trait Store { fn group(&self, name: &str) -> Option; fn groups(&self) -> Vec; fn groups_of(&self, account: &str) -> Vec; + fn groups_founded(&self, account: &str) -> usize; // A user's effective channel capabilities (direct access unioned with groups). fn channel_caps(&self, channel: &str, account: &str) -> Caps; // Runtime operator grants (OperServ OPER), merged with config opers. `expires` diff --git a/modules/groupserv/src/register.rs b/modules/groupserv/src/register.rs index 3351eda..29be1fc 100644 --- a/modules/groupserv/src/register.rs +++ b/modules/groupserv/src/register.rs @@ -11,6 +11,12 @@ pub fn handle(me: &str, from: &Sender, name: Option<&str>, ctx: &mut ServiceCtx, ctx.notice(me, from.uid, "A group name starts with \x02!\x02, e.g. \x02!staff\x02."); return; } + // Honour the registration freeze staff raise during a spam wave, like + // NickServ/ChanServ REGISTER — otherwise the group namespace stays open. + if db.registrations_frozen() { + ctx.notice(me, from.uid, "Group registration is temporarily disabled. Please try again later."); + return; + } // Guard the group namespace like NickServ/ChanServ REGISTER: a look-alike group // name (!аdmin) could impersonate a real one that opers grant channel access to. if db.confusable_check_enabled() { @@ -19,6 +25,13 @@ pub fn handle(me: &str, from: &Sender, name: Option<&str>, ctx: &mut ServiceCtx, return; } } + // Cap groups per founder so the append-only log can't be grown without bound + // (mirrors NickServ's grouped-nick and AJOIN caps). + const MAX_GROUPS: usize = 25; + if db.groups_founded(acc) >= MAX_GROUPS { + ctx.notice(me, from.uid, format!("You've reached the maximum of {MAX_GROUPS} registered groups.")); + return; + } let acc = acc.to_string(); match db.group_register(name, &acc) { Ok(()) => ctx.notice(me, from.uid, format!("Group \x02{name}\x02 registered — you're the founder.")), diff --git a/src/engine/db/network.rs b/src/engine/db/network.rs index fe3725d..339b5dc 100644 --- a/src/engine/db/network.rs +++ b/src/engine/db/network.rs @@ -657,6 +657,11 @@ impl Db { names } + /// How many groups `account` is the founder of (bounds group registration). + pub fn groups_founded(&self, account: &str) -> usize { + self.net.groups.iter().filter(|g| g.founder.eq_ignore_ascii_case(account)).count() + } + /// The groups an account belongs to (founder or member). pub fn groups_of(&self, account: &str) -> Vec { self.net diff --git a/src/engine/db/store.rs b/src/engine/db/store.rs index e57233c..7c4881d 100644 --- a/src/engine/db/store.rs +++ b/src/engine/db/store.rs @@ -401,6 +401,9 @@ impl Store for Db { fn groups_of(&self, account: &str) -> Vec { Db::groups_of(self, account) } + fn groups_founded(&self, account: &str) -> usize { + Db::groups_founded(self, account) + } fn channel_caps(&self, channel: &str, account: &str) -> Caps { Db::channel_caps(self, channel, account) }