diff --git a/modules/chanserv/src/lib.rs b/modules/chanserv/src/lib.rs index 939f3e4..4f350f3 100644 --- a/modules/chanserv/src/lib.rs +++ b/modules/chanserv/src/lib.rs @@ -79,6 +79,23 @@ const TOPICS: &[HelpEntry] = &[ HelpEntry { cmd: "NOEXPIRE", summary: "pin against expiry (operator)", detail: "Syntax: \x02NOEXPIRE <#channel> {ON|OFF}\x02\nPins a channel so inactivity expiry never drops it. Operators only." }, ]; +// Every command the dispatcher below recognizes. The fantasy router consults +// this so an unknown in-channel `!word` — another bot's command sharing the `!` +// prefix — is silently ignored rather than drawing a "don't know that command" +// reply from the assigned bot. Keep in sync with the `on_command` match. +pub const COMMANDS: &[&str] = &[ + "REGISTER", "INFO", "DROP", "MLOCK", "MODE", "ACCESS", "FLAGS", "OP", "DEOP", "VOICE", + "DEVOICE", "UP", "DOWN", "OWNER", "DEOWNER", "PROTECT", "ADMIN", "DEPROTECT", "DEADMIN", + "HALFOP", "DEHALFOP", "KICK", "BAN", "UNBAN", "TOPIC", "INVITE", "AKICK", "LEVELS", + "STATUS", "SUSPEND", "UNSUSPEND", "NOEXPIRE", "LIST", "SET", "ENTRYMSG", "GETKEY", + "SEEN", "ENFORCE", "SYNC", "CLONE", "SOP", "AOP", "HOP", "VOP", "HELP", +]; + +/// True if `cmd` (any case) is a command ChanServ handles. +pub fn is_command(cmd: &str) -> bool { + COMMANDS.iter().any(|c| cmd.eq_ignore_ascii_case(c)) +} + pub struct ChanServ { pub uid: String, } @@ -315,6 +332,8 @@ impl Service for ChanServ { Some("HOP") => xop::handle(me, from, "HOP", "halfop", args, ctx, db), Some("VOP") => xop::handle(me, from, "VOP", "voice", args, ctx, db), Some("HELP") => echo_api::help(me, from, ctx, BLURB, TOPICS, args.get(1).copied()), + // Direct `/msg ChanServ FOO` earns this reply; the fantasy router + // gates on COMMANDS first so an in-channel `!foo` stays silent. Some(other) => ctx.notice(me, from.uid, format!("I don't know the command \x02{other}\x02. Try \x02HELP\x02.")), None => {} } diff --git a/src/engine/dispatch.rs b/src/engine/dispatch.rs index 0d81e37..0ce5488 100644 --- a/src/engine/dispatch.rs +++ b/src/engine/dispatch.rs @@ -199,6 +199,12 @@ impl Engine { return; } + // Only ChanServ's own verbs are fantasy commands. An unknown `!word` is + // some other bot's command sharing the `!` prefix, not ours, so ignore it + // silently instead of the bot barking "I don't know that command". + if !echo_chanserv::is_command(cmd) { + return; + } let Some(csuid) = self.service_uid("ChanServ") else { return }; // Rewrite `!cmd args…` into `CMD #channel args…` for ChanServ. let mark = ctx.actions.len(); diff --git a/src/engine/tests.rs b/src/engine/tests.rs index b35e2f4..d498baa 100644 --- a/src/engine/tests.rs +++ b/src/engine/tests.rs @@ -3469,6 +3469,11 @@ assert!(chan(&mut e, "000AAAAAB", "#c", "hello everyone").is_empty(), "chatter ignored"); // Fantasy in a channel with no bot does nothing. assert!(chan(&mut e, "000AAAAAB", "#d", "!op").is_empty(), "no bot, no fantasy"); + + // An unknown `!word` (another bot's command sharing the `!` prefix) draws + // no reply from the assigned bot — not even "I don't know that command". + let out = chan(&mut e, "000AAAAAB", "#c", "!cissue \"CERT LIST\" fix it"); + assert!(out.is_empty(), "unknown fantasy command stays silent: {out:?}"); } // MemoServ delivers a memo to an offline account and notifies them on login. @@ -3539,8 +3544,12 @@ assert!(notice(&cs(&mut e, "000AAAAAB", "SUSPEND #c"), "Access denied")); // The oper suspends it. assert!(notice(&cs(&mut e, "000AAAAAC", "SUSPEND #c raided"), "now suspended")); - // The founder can no longer manage it. + // The founder can no longer manage it — including MODE/ACCESS/FLAGS, which + // used to bypass the suspension freeze. assert!(notice(&cs(&mut e, "000AAAAAB", "SET #c SIGNKICK ON"), "suspended")); + assert!(notice(&cs(&mut e, "000AAAAAB", "MODE #c +m"), "suspended"), "MODE frozen while suspended"); + assert!(notice(&cs(&mut e, "000AAAAAB", "ACCESS #c ADD someone sop"), "suspended"), "ACCESS frozen while suspended"); + assert!(notice(&cs(&mut e, "000AAAAAB", "FLAGS #c someone +o"), "suspended"), "FLAGS frozen while suspended"); // UNSUSPEND restores management. assert!(notice(&cs(&mut e, "000AAAAAC", "UNSUSPEND #c"), "no longer suspended")); assert!(notice(&cs(&mut e, "000AAAAAB", "SET #c SIGNKICK ON"), "is now"));