diff --git a/api/src/lib.rs b/api/src/lib.rs index d916bd4..630aa95 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -403,6 +403,19 @@ pub enum ChanSetting { BotGreet, } +// A BotServ kicker: the assigned bot kicks a message that trips an enabled one. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Kicker { + Caps, + Bolds, + Colors, + Underlines, + Reverses, + Italics, + // Exemption, not a rule: never kick channel operators. + DontKickOps, +} + impl ChannelView { // The channel mode this account is entitled to on join (+o founder/op, +v // voice), or None if it holds no access. @@ -534,6 +547,8 @@ pub trait Store { fn set_mlock(&mut self, name: &str, on: &str, off: &str) -> Result<(), ChanError>; fn set_desc(&mut self, channel: &str, desc: &str) -> Result<(), ChanError>; fn set_channel_setting(&mut self, channel: &str, setting: ChanSetting, on: bool) -> Result<(), ChanError>; + fn set_kicker(&mut self, channel: &str, kicker: Kicker, on: bool) -> Result<(), ChanError>; + fn set_caps_kicker(&mut self, channel: &str, caps_min: u16, caps_percent: u16) -> Result<(), ChanError>; fn set_channel_topic(&mut self, channel: &str, topic: &str) -> Result<(), ChanError>; fn suspend_channel(&mut self, channel: &str, by: &str, reason: &str, expires: Option) -> Result<(), ChanError>; fn unsuspend_channel(&mut self, channel: &str) -> Result; diff --git a/botserv/src/assign.rs b/botserv/src/assign.rs index 157df4c..5da96da 100644 --- a/botserv/src/assign.rs +++ b/botserv/src/assign.rs @@ -1,4 +1,4 @@ -use fedserv_api::{Priv, Sender, ServiceCtx, Store}; +use fedserv_api::{Sender, ServiceCtx, Store}; // ASSIGN <#channel> / UNASSIGN <#channel>: put a bot in a channel (or take // it out). Channel founder only (or a services admin). @@ -8,12 +8,7 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: ctx.notice(me, from.uid, syntax); return; }; - let Some(founder) = db.channel(chan).map(|c| c.founder) else { - ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); - return; - }; - if from.account != Some(founder.as_str()) && !from.privs.has(Priv::Admin) { - ctx.notice(me, from.uid, format!("Only \x02{chan}\x02's founder can assign a bot to it.")); + if !super::require_channel_admin(me, from, chan, ctx, db) { return; } if !assigning { diff --git a/botserv/src/kick.rs b/botserv/src/kick.rs new file mode 100644 index 0000000..2f2edc7 --- /dev/null +++ b/botserv/src/kick.rs @@ -0,0 +1,77 @@ +use fedserv_api::{Kicker, Sender, ServiceCtx, Store}; + +// KICK <#channel> {ON|OFF} [params]: configure the bot's kickers. +// Types: CAPS [min [percent]], BOLDS, COLORS, UNDERLINES, REVERSES, ITALICS, +// and the exemption DONTKICKOPS. Founder-or-admin. +pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db: &mut dyn Store) { + let (Some(&chan), Some(kind)) = (args.get(1), args.get(2)) else { + ctx.notice(me, from.uid, "Syntax: KICK <#channel> [params]"); + return; + }; + if !super::require_channel_admin(me, from, chan, ctx, db) { + return; + } + let on = match args.get(3).map(|s| s.to_ascii_uppercase()).as_deref() { + Some("ON") | Some("TRUE") => true, + Some("OFF") | Some("FALSE") => false, + _ => { + ctx.notice(me, from.uid, "Say \x02ON\x02 or \x02OFF\x02, e.g. KICK #channel CAPS ON."); + return; + } + }; + + // CAPS carries optional length/percentage thresholds when turned on. + if kind.eq_ignore_ascii_case("CAPS") { + if on { + let min = args.get(4).and_then(|s| s.parse::().ok()).unwrap_or(0); + let percent = args.get(5).and_then(|s| s.parse::().ok()).filter(|p| (1..=100).contains(p)).unwrap_or(0); + match db.set_caps_kicker(chan, min, percent) { + Ok(()) => ctx.notice(me, from.uid, format!("The bot will now kick for \x02caps\x02 in \x02{chan}\x02.")), + Err(_) => reg_error(me, from, chan, ctx), + } + } else { + toggle(me, from, chan, Kicker::Caps, false, ctx, db); + } + return; + } + + let kicker = match kind.to_ascii_uppercase().as_str() { + "BOLDS" => Kicker::Bolds, + "COLORS" | "COLOURS" => Kicker::Colors, + "UNDERLINES" => Kicker::Underlines, + "REVERSES" => Kicker::Reverses, + "ITALICS" => Kicker::Italics, + "DONTKICKOPS" => Kicker::DontKickOps, + other => { + ctx.notice(me, from.uid, format!("Unknown kicker \x02{other}\x02. Try CAPS, BOLDS, COLORS, UNDERLINES, REVERSES, ITALICS or DONTKICKOPS.")); + return; + } + }; + toggle(me, from, chan, kicker, on, ctx, db); +} + +fn label(kicker: Kicker) -> &'static str { + match kicker { + Kicker::Caps => "caps", + Kicker::Bolds => "bolds", + Kicker::Colors => "colours", + Kicker::Underlines => "underlines", + Kicker::Reverses => "reverses", + Kicker::Italics => "italics", + Kicker::DontKickOps => "dontkickops", + } +} + +fn toggle(me: &str, from: &Sender, chan: &str, kicker: Kicker, on: bool, ctx: &mut ServiceCtx, db: &mut dyn Store) { + match db.set_kicker(chan, kicker, on) { + Ok(()) if kicker == Kicker::DontKickOps && on => ctx.notice(me, from.uid, format!("The bot will no longer kick channel operators in \x02{chan}\x02.")), + Ok(()) if kicker == Kicker::DontKickOps => ctx.notice(me, from.uid, format!("The bot may again kick channel operators in \x02{chan}\x02.")), + Ok(()) if on => ctx.notice(me, from.uid, format!("The \x02{}\x02 kicker is now on in \x02{chan}\x02.", label(kicker))), + Ok(()) => ctx.notice(me, from.uid, format!("The \x02{}\x02 kicker is now off in \x02{chan}\x02.", label(kicker))), + Err(_) => reg_error(me, from, chan, ctx), + } +} + +fn reg_error(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx) { + ctx.notice(me, from.uid, format!("Couldn't update \x02{chan}\x02 — please try again in a moment.")); +} diff --git a/botserv/src/lib.rs b/botserv/src/lib.rs index 5e30804..d1ffde9 100644 --- a/botserv/src/lib.rs +++ b/botserv/src/lib.rs @@ -2,7 +2,7 @@ //! and (in later slices) run fantasy commands. `lib.rs` holds the dispatcher; //! each command lives in its own file, matching NickServ/ChanServ. -use fedserv_api::{NetView, Sender, Service, ServiceCtx, Store}; +use fedserv_api::{NetView, Priv, Sender, Service, ServiceCtx, Store}; #[path = "bot.rs"] mod bot; @@ -14,6 +14,22 @@ mod info; mod say; #[path = "set.rs"] mod set; +#[path = "kick.rs"] +mod kick; + +// Shared gate: the sender may administer 's bot options only as its +// founder or a services admin. Notices and returns false on failure. +fn require_channel_admin(me: &str, from: &Sender, chan: &str, ctx: &mut ServiceCtx, db: &dyn Store) -> bool { + let Some(founder) = db.channel(chan).map(|c| c.founder) else { + ctx.notice(me, from.uid, format!("\x02{chan}\x02 isn't registered.")); + return false; + }; + if from.account != Some(founder.as_str()) && !from.privs.has(Priv::Admin) { + ctx.notice(me, from.uid, format!("Only \x02{chan}\x02's founder can change that.")); + return false; + } + true +} pub struct BotServ { pub uid: String, @@ -40,7 +56,8 @@ impl Service for BotServ { Some("SAY") => say::handle(me, from, args, ctx, net, db, false), Some("ACT") => say::handle(me, from, args, ctx, net, db, true), Some("SET") => set::handle(me, from, args, ctx, db), - Some("HELP") | None => ctx.notice(me, from.uid, "BotServ keeps service bots for your channels: \x02ASSIGN\x02 <#channel> puts a bot in your channel, \x02UNASSIGN\x02 <#channel> removes it, \x02INFO\x02 shows details, \x02SAY\x02/\x02ACT\x02 <#channel> speaks through the bot, \x02SET\x02 <#channel> GREET toggles greets. Operators also have \x02BOT\x02 ADD|DEL|LIST."), + Some("KICK") => kick::handle(me, from, args, ctx, db), + Some("HELP") | None => ctx.notice(me, from.uid, "BotServ keeps service bots for your channels: \x02ASSIGN\x02 <#channel> puts a bot in your channel, \x02UNASSIGN\x02 <#channel> removes it, \x02INFO\x02 shows details, \x02SAY\x02/\x02ACT\x02 <#channel> speaks through the bot, \x02SET\x02 <#channel> GREET toggles greets, \x02KICK\x02 <#channel> configures kickers. Operators also have \x02BOT\x02 ADD|DEL|LIST."), Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")), } } diff --git a/botserv/src/set.rs b/botserv/src/set.rs index e92977d..db85205 100644 --- a/botserv/src/set.rs +++ b/botserv/src/set.rs @@ -1,4 +1,4 @@ -use fedserv_api::{ChanSetting, Priv, Sender, ServiceCtx, Store}; +use fedserv_api::{ChanSetting, Sender, ServiceCtx, Store}; // SET <#channel>