Merge pull request #19 from Saul5662/feat/fun-commands

feat(commands): add a fun command category with 18 public `rs` commands
This commit is contained in:
legop3
2026-07-29 01:55:28 -04:00
committed by GitHub
21 changed files with 2753 additions and 4 deletions
@@ -13,9 +13,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,
@@ -56,6 +69,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();
@@ -101,6 +128,8 @@ function createCommandHandlers(deps) {
// lockdown admin while the entire server is in lockdown.
const moderationActions = new Set(['lock', 'unlock', 'mode', 'goal', 'reason', 'verify', 'deter', 'gain', '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
@@ -112,12 +141,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;
}
@@ -158,6 +192,7 @@ function createCommandHandlers(deps) {
case 'gain':
return handleGainCommand(request, tokens);
default:
if (funHandlers[action]) return funHandlers[action](request, tokens);
return request.reply(formatHelp({ commandPrefix, timeStatusCommand, includeDiscord: request.transport === 'discord', isFeatureEnabled: deps.isFeatureEnabled }));
}
}