From b08393833846e99bca4f4563022bca51c52784db Mon Sep 17 00:00:00 2001 From: legop3 Date: Sat, 4 Jul 2026 21:07:13 -0400 Subject: [PATCH] light lock rs command --- .../src/services/chatService/textCommands.js | 6 ++ .../discordBotService/commands/help.js | 1 + .../discordBotService/commands/index.js | 10 ++- .../discordBotService/commands/lights.js | 71 +++++++++++++++++++ .../src/services/discordBotService/index.js | 6 ++ to-do.md | 4 +- 6 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 server/src/services/discordBotService/commands/lights.js diff --git a/server/src/services/chatService/textCommands.js b/server/src/services/chatService/textCommands.js index ce972622..1e89a6e4 100644 --- a/server/src/services/chatService/textCommands.js +++ b/server/src/services/chatService/textCommands.js @@ -9,6 +9,7 @@ const { getActiveDrivers } = require('../turnService'); const { getNickname } = require('../nicknameService'); const { getGlobalObjective, setGlobalObjective, clearGlobalObjective } = require('../globalObjectiveService'); const { getAdminReason, setAdminReason, clearAdminReason } = require('../adminReasonService'); +const homeAssistantService = require('../homeAssistantService'); const { listVerifiedUsers, removeVerifiedUser, @@ -162,6 +163,11 @@ async function runChatTextCommand({ text, socket, sendSystemMessage }) { getAdminReason, setAdminReason, clearAdminReason, + // Web chat builds its own command-router instance for the sending socket. + // Supplying the same Home Assistant service used by Discord keeps `rs + // lights lock/unlock` from becoming transport-specific, and it preserves + // the existing session update path for all connected browsers. + homeAssistantService, getGuildConfig: () => null, setGuildConfig: () => null, removeGuildConfig: () => null, diff --git a/server/src/services/discordBotService/commands/help.js b/server/src/services/discordBotService/commands/help.js index fd1b0cb9..1380f66b 100644 --- a/server/src/services/discordBotService/commands/help.js +++ b/server/src/services/discordBotService/commands/help.js @@ -11,6 +11,7 @@ function formatHelp() { '`rs bridge here ` — set chat bridge to this channel', '`rs bridge mode ` — change chat bridge mode', '`rs bridge off` — disable chat bridge for this server', + '`rs lights ` — show or change room light lock state', '`rs lock ` — lock a rover; rover names can be fuzzy', '`rs unlock ` — unlock a rover; rover names can be fuzzy', '`rs mode ` — change server mode', diff --git a/server/src/services/discordBotService/commands/index.js b/server/src/services/discordBotService/commands/index.js index 711314c7..4e551fa1 100644 --- a/server/src/services/discordBotService/commands/index.js +++ b/server/src/services/discordBotService/commands/index.js @@ -12,6 +12,7 @@ const { createVerifyCommand } = require('./verify'); const { createDeterCommand } = require('./deter'); const { createBridgeCommand } = require('./bridge'); const { createTimeStatusCommand } = require('./timeStatus'); +const { createLightsCommand } = require('./lights'); function createCommandHandlers(deps) { const { @@ -33,6 +34,7 @@ function createCommandHandlers(deps) { const handleDeterCommand = createDeterCommand(deps); const handleBridgeCommand = createBridgeCommand(deps); const handleTimeStatusCommand = createTimeStatusCommand(deps); + const handleLightsCommand = createLightsCommand(deps); async function handleCommand(message) { if (message.author.bot) return; @@ -52,7 +54,11 @@ function createCommandHandlers(deps) { const isAdmin = isAdminUser(message.author.id); const isLockdownAdmin = isLockdownAdminUser(message.author.id); const mode = getMode(); - const moderationActions = new Set(['lock', 'unlock', 'mode', 'goal', 'reason', 'verify', 'deter']); + // Actions in this set can change operational safety or access policy, so + // lockdown mode narrows them from normal admins to lockdown admins. Room + // light locking belongs here because it can force the physical room lights + // on and disables ordinary Home Assistant room controls for everyone else. + const moderationActions = new Set(['lock', 'unlock', 'mode', 'goal', 'reason', 'verify', 'deter', 'lights']); if (!isAdmin && action !== '' && action !== 'status' && action !== 'help' && action !== 'replay' && action !== 'bridge' && action !== 'goal' && action !== 'reason' && action !== 'verify' && action !== 'deter') { await message.reply({ content: 'Only admins can run that command.', allowedMentions: { parse: [], repliedUser: false } }); @@ -74,6 +80,8 @@ function createCommandHandlers(deps) { return handleReplayCommand(message, tokens.join(' ')); case 'bridge': return handleBridgeCommand(message, tokens); + case 'lights': + return handleLightsCommand(message, tokens); case 'lock': return handleLockCommand(message, rest, true); case 'unlock': diff --git a/server/src/services/discordBotService/commands/lights.js b/server/src/services/discordBotService/commands/lights.js new file mode 100644 index 00000000..5eec4d1d --- /dev/null +++ b/server/src/services/discordBotService/commands/lights.js @@ -0,0 +1,71 @@ +// Discord Lights Command +// Purpose: Handles admin room-light lock policy commands from Discord and web chat. +// Scope: Delegates all actual Home Assistant policy behavior to homeAssistantService. +function describeLightPolicy(lightPolicy = {}) { + // The HA service exposes both the newer explicit lockState and the older + // lockedOn boolean. Prefer lockState because it can distinguish locked-on + // from locked-off, but keep lockedOn as a defensive fallback for any caller + // that passes an older or partial policy object. + const lockState = lightPolicy?.lockState || (lightPolicy?.lockedOn ? 'on' : null); + if (lockState === 'on') return 'Room lights are locked on.'; + if (lockState === 'off') return 'Room lights are locked off.'; + return 'Room lights are unlocked.'; +} + +function createLightsCommand({ homeAssistantService, sanitizeMentions }) { + return async function handleLightsCommand(message, tokens = []) { + // Defaulting to status makes `rs lights` safe to type while still exposing + // the explicit mutating forms as `rs lights lock` and `rs lights unlock`. + const action = String(tokens.shift() || 'status').trim().toLowerCase(); + + if (!homeAssistantService) { + await message.reply({ + content: 'Room light controls are unavailable.', + allowedMentions: { parse: [], repliedUser: false }, + }); + return; + } + + if (action === 'status') { + await message.reply({ + content: describeLightPolicy(homeAssistantService.getLightPolicyState?.() || {}), + allowedMentions: { parse: [], repliedUser: false }, + }); + return; + } + + if (action !== 'lock' && action !== 'unlock') { + await message.reply({ + content: 'Invalid lights command. Use `rs lights lock`, `rs lights unlock`, or `rs lights status`.', + allowedMentions: { parse: [], repliedUser: false }, + }); + return; + } + + try { + const locked = action === 'lock'; + // The bot command intentionally calls the shared policy setter instead of + // issuing direct Home Assistant entity commands. That keeps all secondary + // behavior centralized: web UI controls become disabled through the + // session lightPolicy update, lock-on still forces configured lights to + // white where possible, and commandService sees the same update event that + // forces rover lasers off while the room is locked on. + await homeAssistantService.setLightsLockedOn(locked, { + source: `bot-command:lights:${action}`, + forceApply: true, + }); + + await message.reply({ + content: sanitizeMentions(locked ? 'Room lights locked on.' : 'Room lights unlocked.'), + allowedMentions: { parse: [], repliedUser: false }, + }); + } catch (err) { + await message.reply({ + content: sanitizeMentions(`Failed to update room lights: ${err.message}`), + allowedMentions: { parse: [], repliedUser: false }, + }); + } + }; +} + +module.exports = { createLightsCommand }; diff --git a/server/src/services/discordBotService/index.js b/server/src/services/discordBotService/index.js index 8cc4e780..319d29d1 100644 --- a/server/src/services/discordBotService/index.js +++ b/server/src/services/discordBotService/index.js @@ -19,6 +19,7 @@ const { getActiveDrivers } = require('../turnService'); const { getNickname } = require('../nicknameService'); const { getGlobalObjective, setGlobalObjective, clearGlobalObjective } = require('../globalObjectiveService'); const { getAdminReason, setAdminReason, clearAdminReason } = require('../adminReasonService'); +const homeAssistantService = require('../homeAssistantService'); const { getGuildConfig, listGuildConfigs, @@ -136,6 +137,11 @@ const commands = createCommandHandlers({ getAdminReason, setAdminReason, clearAdminReason, + // Room-light lock commands must use the same Home Assistant service instance + // as sockets, HA button triggers, and idle/darkness policies. Passing the + // service into the shared command router keeps Discord and mirrored web-chat + // command behavior aligned without duplicating Home Assistant calls here. + homeAssistantService, getGuildConfig, setGuildConfig, removeGuildConfig, diff --git a/to-do.md b/to-do.md index 4a73c93b..02749302 100644 --- a/to-do.md +++ b/to-do.md @@ -2,8 +2,8 @@ 2. pagewide system for "why was i removed from a rover", instead of the link to spectator page thing 3. admin command for kicking people off of rovers. just a kick, nothing persistent 4. kick people off rover after 3 consecurtive bump-off attempts -5. add rs admin command for controlling light lock -6. make signaling more clear for idle skips and idle skip kicks +5. make signaling more clear for idle skips and idle skip kicks +6. add a kick command that disconnects sockets with a message 7. fix this: `Jun 18 15:14:18 roombaserver.local node[216731]: /home/daniel/MultiRoombaRover/server/src/services/roverManager/socketHandlers.js:92 Jun 18 15:14:18 roombaserver.local node[216731]: cb({ error: err.message });