mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
feat(commands): add a fun command category with 18 public rs commands
Adds a `fun` category to the operator command registry, reachable identically from site chat and Discord: - text: bonk, hug, slap, 8ball, roll, coin, ship, rate, uwu, wanted - counters: bonkboard, pet, snitch - hardware: honk, boo, spin, disco, vibecheck Supporting pieces: - `permission: 'public'` in the registry. The dispatcher previously decided non-admin access with a hardcoded chain of `action !== '...'` comparisons, so every new public command needed a dispatcher edit. That chain is replaced by a registry lookup plus SELF_GATED_ACTIONS, which names the commands that enforce their own permissions internally (goal/reason are read-public write-admin; verify/deter reject non-lockdown-admins themselves). Existing behavior for every pre-existing command is unchanged. - `cooldowns.js`, a per-actor per-command in-memory gate. Site chat's own rate limit is per-socket-per-message and does not bound a specific command, so without this one person could turn `rs honk` into a siren. Site chat rebuilds its router per message, so the gate is created at module scope there and injected. - `funStatsService`, a small JSON store for the persistent tallies. Counters are keyed by an actor key spanning transports (`user:<id>` / `discord:<id>`), and a Discord id has no row in `users`, so `user_feature_state` could not hold them without violating its foreign key. Safety notes: - `issueCommand` is the raw rover transport and performs none of the ownership, deterrence, or private-safety checks the socket `command` handler applies, so honk and spin re-check `canDrive` themselves and spin re-applies `applyPrivateDriveSafety`. Both are therefore site-chat only: a Discord message has no socket and can never satisfy those checks. - `boo` speaks a canned taunt rather than caller-supplied text, so it cannot become an unmoderated TTS channel aimed at whoever is nearest a rover. - `disco` obeys the existing room-light lock and the homeAssistant feature gate. - The whole fun category is suspended in lockdown mode. - Mute and deterrence already stop command-shaped chat before the router runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d000b8f4f8
commit
a2fbbc100e
@@ -12,9 +12,22 @@ const { createLightsCommand } = require('./commands/lights');
|
||||
const { createKickCommand } = require('./commands/kick');
|
||||
const { createLiftCommand } = require('./commands/lift');
|
||||
const { createNeatoCommand } = require('./commands/neato');
|
||||
const { createFunTextCommands } = require('./commands/funText');
|
||||
const { createFunStatsCommands } = require('./commands/funStats');
|
||||
const { createFunRoverCommands } = require('./commands/funRover');
|
||||
const { createCooldownGate } = require('./cooldowns');
|
||||
const { getCommandConfig } = require('./config');
|
||||
const { buildCommandRegistry } = require('./registry');
|
||||
|
||||
/*
|
||||
Commands that enforce their own permissions inside their handler rather than at
|
||||
the dispatcher. `goal` and `reason` are readable by anyone but only writable by
|
||||
an admin; `verify` and `deter` reject non-lockdown-admins themselves so they can
|
||||
explain which role is missing. Listing them here preserves that behavior now
|
||||
that the general non-admin gate is driven by registry metadata.
|
||||
*/
|
||||
const SELF_GATED_ACTIONS = new Set(['', 'status', 'help', 'replay', 'bridge', 'goal', 'reason', 'verify', 'deter']);
|
||||
|
||||
function createCommandHandlers(deps) {
|
||||
const {
|
||||
getMode,
|
||||
@@ -54,6 +67,20 @@ function createCommandHandlers(deps) {
|
||||
const handleLiftCommand = createLiftCommand(deps);
|
||||
const handleNeatoCommand = createNeatoCommand(deps);
|
||||
|
||||
/*
|
||||
Fun commands share one cooldown gate. Web chat rebuilds this router per
|
||||
message, so an injected gate is what makes the cooldowns actually shared
|
||||
across a user's messages; a gate created here would be discarded every time
|
||||
and rate limit nothing. Falling back to a local gate keeps the router usable
|
||||
on its own (and in tests) without making every caller supply one.
|
||||
*/
|
||||
const cooldowns = deps.commandCooldowns || createCooldownGate();
|
||||
const funHandlers = {
|
||||
...createFunTextCommands({ ...deps, cooldowns }),
|
||||
...createFunStatsCommands({ ...deps, cooldowns }),
|
||||
...createFunRoverCommands({ ...deps, cooldowns }),
|
||||
};
|
||||
|
||||
function stripCommandPrefix(content) {
|
||||
const trimmed = String(content || '').trim();
|
||||
const lower = trimmed.toLowerCase();
|
||||
@@ -99,6 +126,8 @@ function createCommandHandlers(deps) {
|
||||
// lockdown admin while the entire server is in lockdown.
|
||||
const moderationActions = new Set(['lock', 'unlock', 'mode', 'goal', 'reason', 'verify', 'deter', 'lights', 'kick', 'lift', 'neato']);
|
||||
const isAccessModeCommand = commandDefinition?.permission === 'access-mode';
|
||||
const isPublicCommand = commandDefinition?.permission === 'public';
|
||||
const isFunCommand = commandDefinition?.category === 'fun';
|
||||
|
||||
// Feature commands are public activities while access is open or managed
|
||||
// by turns. In admin mode they follow the same admin-only boundary as rover
|
||||
@@ -110,12 +139,17 @@ function createCommandHandlers(deps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAccessModeCommand && !isAdmin && action !== '' && action !== 'status' && action !== 'help' && action !== 'replay' && action !== 'bridge' && action !== 'goal' && action !== 'reason' && action !== 'verify' && action !== 'deter') {
|
||||
if (!isAccessModeCommand && !isPublicCommand && !isAdmin && !SELF_GATED_ACTIONS.has(action)) {
|
||||
await request.reply({ content: 'Only admins can run that command.', allowedMentions: { parse: [], repliedUser: false } });
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === MODES.LOCKDOWN && moderationActions.has(action) && !isLockdownAdmin) {
|
||||
/*
|
||||
Lockdown exists to make the server quiet and controlled, so the whole fun
|
||||
category is suspended alongside the moderation-sensitive actions rather than
|
||||
leaving a horn command reachable by anyone while the fleet is locked down.
|
||||
*/
|
||||
if (mode === MODES.LOCKDOWN && (moderationActions.has(action) || isFunCommand) && !isLockdownAdmin) {
|
||||
await request.reply({ content: 'Lockdown mode: only lockdown admins can run that command.', allowedMentions: { parse: [], repliedUser: false } });
|
||||
return;
|
||||
}
|
||||
@@ -154,6 +188,7 @@ function createCommandHandlers(deps) {
|
||||
case 'deter':
|
||||
return handleDeterCommand(request, tokens);
|
||||
default:
|
||||
if (funHandlers[action]) return funHandlers[action](request, tokens);
|
||||
return request.reply(formatHelp({ commandPrefix, timeStatusCommand, includeDiscord: request.transport === 'discord', isFeatureEnabled: deps.isFeatureEnabled }));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user