From 5bc41f2e60c15480e4489a73e8690877930077b9 Mon Sep 17 00:00:00 2001 From: Jean Date: Mon, 13 Jul 2026 17:36:34 +0000 Subject: [PATCH] BotServ: NOBOT and PRIVATE assignment protections SET <#channel> NOBOT reserves (un)assigning a bot for services operators (the founder is refused). SET PRIVATE (admin only) marks a bot operators-only to assign; it's flagged in BOT LIST and the private flag survives BOT CHANGE. Both gates live in the ASSIGN path. --- api/src/lib.rs | 8 +++++++ botserv/src/assign.rs | 15 +++++++++++-- botserv/src/bot.rs | 3 ++- botserv/src/set.rs | 49 ++++++++++++++++++++++++++++++++++++------- src/engine/db.rs | 30 +++++++++++++++++++++++--- 5 files changed, 92 insertions(+), 13 deletions(-) diff --git a/api/src/lib.rs b/api/src/lib.rs index 34819f7..fc2ac5a 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -342,6 +342,9 @@ pub struct BotView { pub user: String, pub host: String, pub gecos: String, + // A private bot may only be assigned by a services admin and is hidden from + // BOT LIST for everyone else. + pub private: bool, } // A services suspension on an account: who, why, when, and an optional expiry @@ -388,6 +391,8 @@ pub struct ChannelView { pub assigned_bot: Option, // BotServ: whether members' personal greets are shown on join. pub bot_greet: bool, + // BotServ: whether the founder is barred from (un)assigning a bot. + pub nobot: bool, } // A single ChanServ SET option, named for the typed `set_channel_setting` call. @@ -401,6 +406,8 @@ pub enum ChanSetting { TopicLock, // BotServ: show members' personal greets on join. BotGreet, + // BotServ: forbid the founder from (un)assigning a bot (admin override only). + NoBot, } // A BotServ kicker: the assigned bot kicks a message that trips an enabled one. @@ -571,6 +578,7 @@ pub trait Store { // BotServ registry (oper-only at the command layer). fn bot_add(&mut self, nick: &str, user: &str, host: &str, gecos: &str) -> Result<(), ChanError>; fn bot_change(&mut self, old: &str, new_nick: &str, user: &str, host: &str, gecos: &str) -> Result<(), ChanError>; + fn bot_set_private(&mut self, nick: &str, private: bool) -> Result; fn bot_del(&mut self, nick: &str) -> Result; fn bots(&self) -> Vec; fn assign_bot(&mut self, channel: &str, bot: &str) -> Result<(), ChanError>; diff --git a/botserv/src/assign.rs b/botserv/src/assign.rs index 5da96da..34e0ac2 100644 --- a/botserv/src/assign.rs +++ b/botserv/src/assign.rs @@ -1,4 +1,4 @@ -use fedserv_api::{Sender, ServiceCtx, Store}; +use fedserv_api::{Priv, Sender, ServiceCtx, Store}; // ASSIGN <#channel> / UNASSIGN <#channel>: put a bot in a channel (or take // it out). Channel founder only (or a services admin). @@ -11,6 +11,12 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: if !super::require_channel_admin(me, from, chan, ctx, db) { return; } + // NOBOT reserves (un)assignment for services operators. + let is_admin = from.privs.has(Priv::Admin); + if !is_admin && db.channel(chan).is_some_and(|c| c.nobot) { + ctx.notice(me, from.uid, format!("\x02{chan}\x02 is set \x02NOBOT\x02 — only a services operator can change its bot.")); + return; + } if !assigning { match db.unassign_bot(chan) { Ok(true) => ctx.notice(me, from.uid, format!("The bot has left \x02{chan}\x02.")), @@ -23,10 +29,15 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: ctx.notice(me, from.uid, "Syntax: ASSIGN <#channel> "); return; }; - let Some(botnick) = db.bots().into_iter().find(|b| b.nick.eq_ignore_ascii_case(bot)).map(|b| b.nick) else { + let Some(target) = db.bots().into_iter().find(|b| b.nick.eq_ignore_ascii_case(bot)) else { ctx.notice(me, from.uid, format!("There's no bot named \x02{bot}\x02. See \x02BOT LIST\x02.")); return; }; + if target.private && !is_admin { + ctx.notice(me, from.uid, format!("Bot \x02{}\x02 is private — only a services operator can assign it.", target.nick)); + return; + } + let botnick = target.nick; match db.assign_bot(chan, &botnick) { Ok(()) => ctx.notice(me, from.uid, format!("Bot \x02{botnick}\x02 is now assigned to \x02{chan}\x02.")), Err(_) => ctx.notice(me, from.uid, "Sorry, that didn't work. Please try again in a moment."), diff --git a/botserv/src/bot.rs b/botserv/src/bot.rs index 56f7bc7..7da2d46 100644 --- a/botserv/src/bot.rs +++ b/botserv/src/bot.rs @@ -57,7 +57,8 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: } ctx.notice(me, from.uid, format!("Bots ({}):", bots.len())); for b in &bots { - ctx.notice(me, from.uid, format!(" \x02{}\x02 ({}@{}) — {}", b.nick, b.user, b.host, b.gecos)); + let flag = if b.private { " \x02[private]\x02" } else { "" }; + ctx.notice(me, from.uid, format!(" \x02{}\x02 ({}@{}) — {}{flag}", b.nick, b.user, b.host, b.gecos)); } } Some(other) => ctx.notice(me, from.uid, format!("Unknown BOT command \x02{other}\x02. Use \x02ADD\x02, \x02CHANGE\x02, \x02DEL\x02 or \x02LIST\x02.")), diff --git a/botserv/src/set.rs b/botserv/src/set.rs index 084f5a2..82f6aff 100644 --- a/botserv/src/set.rs +++ b/botserv/src/set.rs @@ -1,13 +1,43 @@ -use fedserv_api::{parse_duration, ChanSetting, Sender, ServiceCtx, Store}; +use fedserv_api::{parse_duration, ChanSetting, Priv, Sender, ServiceCtx, Store}; -// SET <#channel>