mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 17:40:46 -04:00
configurable discord prefix! yay
This commit is contained in:
@@ -3,7 +3,10 @@
|
||||
// Scope: Manages bridge channel, mode, and webhook provisioning.
|
||||
const { PermissionsBitField } = require('discord.js');
|
||||
|
||||
function createBridgeCommand({ getGuildConfig, setGuildConfig, removeGuildConfig, normalizeMode, VALID_MODES, isAdminUser }) {
|
||||
function createBridgeCommand({ getGuildConfig, setGuildConfig, removeGuildConfig, normalizeMode, VALID_MODES, isAdminUser, discordConfig }) {
|
||||
// Error text should name the active prefix because bridge setup is one of the
|
||||
// first commands an admin runs when a bot instance joins a shared Discord.
|
||||
const commandPrefix = String(discordConfig?.commandPrefix || 'rs').trim() || 'rs';
|
||||
function canManageBridge(message) {
|
||||
if (isAdminUser(message.author.id)) return true;
|
||||
if (!message.guild || !message.member) return false;
|
||||
@@ -45,7 +48,7 @@ function createBridgeCommand({ getGuildConfig, setGuildConfig, removeGuildConfig
|
||||
|
||||
// Every command below this point mutates the guild bridge configuration.
|
||||
// Keeping the authorization check in one shared gate prevents destructive
|
||||
// actions, especially `rs bridge off`, from accidentally bypassing the same
|
||||
// actions, especially bridge disable, from accidentally bypassing the same
|
||||
// Manage Server/admin requirement used by `here` and `mode`.
|
||||
if (!canManageBridge(message)) return message.reply({ content: 'You need Manage Server permissions to change the chat bridge.', allowedMentions: { parse: [], repliedUser: false } });
|
||||
|
||||
@@ -64,14 +67,14 @@ function createBridgeCommand({ getGuildConfig, setGuildConfig, removeGuildConfig
|
||||
|
||||
if (action === 'mode') {
|
||||
const current = getGuildConfig(guildId);
|
||||
if (!current?.channelId) return message.reply({ content: 'No chat bridge channel set. Use `rs bridge here <global|private>` first.', allowedMentions: { parse: [], repliedUser: false } });
|
||||
if (!current?.channelId) return message.reply({ content: `No chat bridge channel set. Use \`${commandPrefix} bridge here <global|private>\` first.`, allowedMentions: { parse: [], repliedUser: false } });
|
||||
const nextMode = normalizeMode(mode, null);
|
||||
if (!VALID_MODES.has(nextMode)) return message.reply({ content: 'Invalid mode. Use `global` or `private`.', allowedMentions: { parse: [], repliedUser: false } });
|
||||
const entry = setGuildConfig(guildId, { channelId: current.channelId, mode: nextMode, webhookId: current.webhookId, webhookToken: current.webhookToken });
|
||||
return message.reply({ content: `Chat bridge mode updated to **${entry.mode}** in <#${entry.channelId}>.`, allowedMentions: { parse: [], repliedUser: false } });
|
||||
}
|
||||
|
||||
return message.reply({ content: 'Unknown bridge command. Try `rs bridge`.', allowedMentions: { parse: [], repliedUser: false } });
|
||||
return message.reply({ content: `Unknown bridge command. Try \`${commandPrefix} bridge\`.`, allowedMentions: { parse: [], repliedUser: false } });
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
// Scope: Supports list, ban, and unban subcommands.
|
||||
const { mask, resolveIdentitySelector } = require('./resolvers');
|
||||
|
||||
function createDeterCommand({ listDeterredUsers, listVerifiedUsers, deterUser, undeterUser, isLockdownAdminUser, sanitizeMentions }) {
|
||||
function createDeterCommand({ listDeterredUsers, listVerifiedUsers, deterUser, undeterUser, isLockdownAdminUser, sanitizeMentions, discordConfig }) {
|
||||
// Moderation errors often get copied into Discord chat, so they should show
|
||||
// the configured bot prefix instead of the legacy default when several bots
|
||||
// are present in the same server.
|
||||
const commandPrefix = String(discordConfig?.commandPrefix || 'rs').trim() || 'rs';
|
||||
|
||||
return async function handleDeterCommand(message, tokens) {
|
||||
if (!isLockdownAdminUser(message.author?.id)) {
|
||||
@@ -19,7 +23,7 @@ function createDeterCommand({ listDeterredUsers, listVerifiedUsers, deterUser, u
|
||||
}
|
||||
if (action === 'ban') {
|
||||
const selector = tokens.join(' ').trim();
|
||||
if (!selector) return message.reply({ content: 'Usage: `rs deter ban <cookieUserId|nickname|ip>`', allowedMentions: { parse: [], repliedUser: false } });
|
||||
if (!selector) return message.reply({ content: `Usage: \`${commandPrefix} deter ban <cookieUserId|nickname|ip>\``, allowedMentions: { parse: [], repliedUser: false } });
|
||||
try {
|
||||
const verifiedMatch = resolveIdentitySelector(selector, listVerifiedUsers(), { includeId: false });
|
||||
if (verifiedMatch.error && !/not found/i.test(verifiedMatch.error)) {
|
||||
@@ -37,7 +41,7 @@ function createDeterCommand({ listDeterredUsers, listVerifiedUsers, deterUser, u
|
||||
}
|
||||
if (action === 'unban') {
|
||||
const selector = tokens.join(' ').trim();
|
||||
if (!selector) return message.reply({ content: 'Usage: `rs deter unban <id|cookieUserId|nickname|ip>`', allowedMentions: { parse: [], repliedUser: false } });
|
||||
if (!selector) return message.reply({ content: `Usage: \`${commandPrefix} deter unban <id|cookieUserId|nickname|ip>\``, allowedMentions: { parse: [], repliedUser: false } });
|
||||
try {
|
||||
const resolved = resolveIdentitySelector(selector, listDeterredUsers(), { includeId: true });
|
||||
if (resolved.error) return message.reply({ content: sanitizeMentions(resolved.error), allowedMentions: { parse: [], repliedUser: false } });
|
||||
@@ -47,7 +51,7 @@ function createDeterCommand({ listDeterredUsers, listVerifiedUsers, deterUser, u
|
||||
return message.reply({ content: sanitizeMentions(`Failed to remove deterrence: ${err.message}`), allowedMentions: { parse: [], repliedUser: false } });
|
||||
}
|
||||
}
|
||||
return message.reply({ content: 'Unknown deter command. Use `rs deter list`, `rs deter ban <selector>`, or `rs deter unban <selector>`.', allowedMentions: { parse: [], repliedUser: false } });
|
||||
return message.reply({ content: `Unknown deter command. Use \`${commandPrefix} deter list\`, \`${commandPrefix} deter ban <selector>\`, or \`${commandPrefix} deter unban <selector>\`.`, allowedMentions: { parse: [], repliedUser: false } });
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
// Discord Help Command
|
||||
// Purpose: Provides help text for rover bot Discord commands.
|
||||
// Scope: Returns static command usage text.
|
||||
function formatHelp() {
|
||||
// Scope: Returns usage text with the configured command names for this bot instance.
|
||||
function formatHelp({ commandPrefix = 'rs', timeStatusCommand = 'ts' } = {}) {
|
||||
const prefix = String(commandPrefix || 'rs').trim() || 'rs';
|
||||
const timeCommand = timeStatusCommand ? String(timeStatusCommand).trim() : '';
|
||||
return [
|
||||
'**Rover Bot Commands**',
|
||||
'`rs help` — show this help',
|
||||
'`rs status [rover]` — show rover status; rover names can be fuzzy',
|
||||
'`rs replay [sources]` — send instant replay; source names can be fuzzy',
|
||||
'`rs bridge` — show chat bridge status for this server',
|
||||
'`rs bridge here <global|private>` — set chat bridge to this channel',
|
||||
'`rs bridge mode <global|private>` — change chat bridge mode',
|
||||
'`rs bridge off` — disable chat bridge for this server',
|
||||
'`rs lights <status|lock|unlock>` — show or change room light lock state',
|
||||
'`rs kick <user> [reason]` — remove a user from their current rover; use `user | reason` for multi-word names',
|
||||
'`rs lock <rover>` — lock a rover; rover names can be fuzzy',
|
||||
'`rs unlock <rover>` — unlock a rover; rover names can be fuzzy',
|
||||
'`rs mode <open|turns|admin|lockdown>` — change server mode',
|
||||
'`rs reason [text|clear]` — show or set admin mode reason',
|
||||
'`rs goal [text|clear]` — show or set global objective',
|
||||
'`rs verify list` — list verified users (lockdown admins)',
|
||||
'`rs verify remove <cookieUserId|nickname>` — remove verified user; nicknames can be fuzzy or multi-word (lockdown admins)',
|
||||
'`rs deter list` — list deterred users (lockdown admins)',
|
||||
'`rs deter ban <cookieUserId|nickname|ip>` — deter a user; nicknames can be fuzzy or multi-word (lockdown admins)',
|
||||
'`rs deter unban <id|cookieUserId|nickname|ip>` — remove deterrence; nicknames can be fuzzy or multi-word (lockdown admins)',
|
||||
'`ts` — show time status',
|
||||
].join('\n');
|
||||
`\`${prefix} help\` — show this help`,
|
||||
`\`${prefix} status [rover]\` — show rover status; rover names can be fuzzy`,
|
||||
`\`${prefix} replay [sources]\` — send instant replay; source names can be fuzzy`,
|
||||
`\`${prefix} bridge\` — show chat bridge status for this server`,
|
||||
`\`${prefix} bridge here <global|private>\` — set chat bridge to this channel`,
|
||||
`\`${prefix} bridge mode <global|private>\` — change chat bridge mode`,
|
||||
`\`${prefix} bridge off\` — disable chat bridge for this server`,
|
||||
`\`${prefix} lights <status|lock|unlock>\` — show or change room light lock state`,
|
||||
`\`${prefix} kick <user> [reason]\` — remove a user from their current rover; use \`user | reason\` for multi-word names`,
|
||||
`\`${prefix} lock <rover>\` — lock a rover; rover names can be fuzzy`,
|
||||
`\`${prefix} unlock <rover>\` — unlock a rover; rover names can be fuzzy`,
|
||||
`\`${prefix} mode <open|turns|admin|lockdown>\` — change server mode`,
|
||||
`\`${prefix} reason [text|clear]\` — show or set admin mode reason`,
|
||||
`\`${prefix} goal [text|clear]\` — show or set global objective`,
|
||||
`\`${prefix} verify list\` — list verified users (lockdown admins)`,
|
||||
`\`${prefix} verify remove <cookieUserId|nickname>\` — remove verified user; nicknames can be fuzzy or multi-word (lockdown admins)`,
|
||||
`\`${prefix} deter list\` — list deterred users (lockdown admins)`,
|
||||
`\`${prefix} deter ban <cookieUserId|nickname|ip>\` — deter a user; nicknames can be fuzzy or multi-word (lockdown admins)`,
|
||||
`\`${prefix} deter unban <id|cookieUserId|nickname|ip>\` — remove deterrence; nicknames can be fuzzy or multi-word (lockdown admins)`,
|
||||
timeCommand ? `\`${timeCommand}\` — show time status` : '',
|
||||
].filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
module.exports = { formatHelp };
|
||||
|
||||
@@ -22,6 +22,21 @@ function createCommandHandlers(deps) {
|
||||
isAdminUser,
|
||||
isLockdownAdminUser,
|
||||
} = deps;
|
||||
// Each running rover server can bring its own Discord bot into the same
|
||||
// guild, so the primary command prefix must come from config instead of
|
||||
// being hard-coded globally. The fallback preserves existing installs.
|
||||
const commandPrefix = String(deps.discordConfig?.commandPrefix || 'rs').trim() || 'rs';
|
||||
// The legacy time command is a bare word rather than a prefixed command. It
|
||||
// therefore needs its own configurable value, and `null` intentionally
|
||||
// disables it so multiple bots do not all answer `ts` in the same channel.
|
||||
const timeStatusCommand = deps.discordConfig?.timeStatusCommand === null
|
||||
? ''
|
||||
: String(deps.discordConfig?.timeStatusCommand || 'ts').trim();
|
||||
// Lowercase cached copies avoid re-normalizing every message and keep command
|
||||
// matching case-insensitive without changing the original configured text
|
||||
// that is shown in help output.
|
||||
const normalizedCommandPrefix = commandPrefix.toLowerCase();
|
||||
const normalizedTimeStatusCommand = timeStatusCommand.toLowerCase();
|
||||
|
||||
const handleStatusCommand = createStatusCommand(deps);
|
||||
const handleReplayCommand = deps.createReplayTextCommand
|
||||
@@ -38,6 +53,20 @@ function createCommandHandlers(deps) {
|
||||
const handleLightsCommand = createLightsCommand(deps);
|
||||
const handleKickCommand = createKickCommand(deps);
|
||||
|
||||
function stripCommandPrefix(content) {
|
||||
const trimmed = String(content || '').trim();
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (!lower.startsWith(normalizedCommandPrefix)) return null;
|
||||
|
||||
const nextCharacter = trimmed.charAt(commandPrefix.length);
|
||||
// Prefixes are matched as whole command tokens so an instance using `rs`
|
||||
// still ignores ordinary words such as `rsvp`. This mirrors the old regex
|
||||
// behavior while letting each Discord bot instance use its own prefix.
|
||||
if (nextCharacter && !/\s/.test(nextCharacter)) return null;
|
||||
|
||||
return trimmed.slice(commandPrefix.length).trim();
|
||||
}
|
||||
|
||||
async function handleCommand(message) {
|
||||
if (message.author.bot) return;
|
||||
const content = (message.content || '').trim();
|
||||
@@ -46,11 +75,12 @@ function createCommandHandlers(deps) {
|
||||
// startsWith checks made ordinary messages such as "rsvp" or "tshirt" look
|
||||
// like commands, which is especially bad now that web chat will run the
|
||||
// same server-side dispatcher before broadcasting user text.
|
||||
if (lower === 'ts') return handleTimeStatusCommand(message);
|
||||
if (!/^rs(?:\s|$)/i.test(content)) return;
|
||||
if (normalizedTimeStatusCommand && lower === normalizedTimeStatusCommand) return handleTimeStatusCommand(message);
|
||||
|
||||
const tokens = content.split(/\s+/);
|
||||
tokens.shift();
|
||||
const commandBody = stripCommandPrefix(content);
|
||||
if (commandBody === null) return;
|
||||
|
||||
const tokens = commandBody ? commandBody.split(/\s+/) : [];
|
||||
const action = (tokens.shift() || '').toLowerCase();
|
||||
const rest = tokens.join(' ').trim();
|
||||
const isAdmin = isAdminUser(message.author.id);
|
||||
@@ -77,7 +107,7 @@ function createCommandHandlers(deps) {
|
||||
case 'status':
|
||||
return handleStatusCommand(message, rest);
|
||||
case 'help':
|
||||
return message.reply(formatHelp());
|
||||
return message.reply(formatHelp({ commandPrefix, timeStatusCommand }));
|
||||
case 'replay':
|
||||
return handleReplayCommand(message, tokens.join(' '));
|
||||
case 'bridge':
|
||||
@@ -101,7 +131,7 @@ function createCommandHandlers(deps) {
|
||||
case 'deter':
|
||||
return handleDeterCommand(message, tokens);
|
||||
default:
|
||||
return message.reply(formatHelp());
|
||||
return message.reply(formatHelp({ commandPrefix, timeStatusCommand }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ function splitSelectorAndReason(rawText) {
|
||||
/*
|
||||
A pipe delimiter is the escape hatch for multi-word nicknames. Without a
|
||||
delimiter the command intentionally treats the first token as the selector
|
||||
so quick admin commands stay short: `rs kick bob being reckless`.
|
||||
so quick admin commands stay short, for example:
|
||||
`<configured-prefix> kick bob being reckless`.
|
||||
*/
|
||||
return {
|
||||
selector: normalizeText(text.slice(0, pipeIndex)),
|
||||
@@ -61,9 +62,9 @@ function buildKickCandidates({ io, roverManager, assignmentService, getNickname
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function resolveKickTarget(selector, candidates) {
|
||||
function resolveKickTarget(selector, candidates, commandPrefix = 'rs') {
|
||||
const query = normalizeSearchText(selector);
|
||||
if (!query) return { error: 'Specify a user to kick. Example: `rs kick nickname reason`' };
|
||||
if (!query) return { error: `Specify a user to kick. Example: \`${commandPrefix} kick nickname reason\`` };
|
||||
const exact = candidates.filter((entry) => (
|
||||
entry.searchSocketId === query ||
|
||||
entry.searchShortSocketId === query ||
|
||||
@@ -96,7 +97,11 @@ function resolveKickTarget(selector, candidates) {
|
||||
return { target: first.item };
|
||||
}
|
||||
|
||||
function createKickCommand({ io, roverManager, getNickname, sanitizeMentions }) {
|
||||
function createKickCommand({ io, roverManager, getNickname, sanitizeMentions, discordConfig }) {
|
||||
// The kick parser itself does not need the prefix, but its validation message
|
||||
// does. Keeping this local avoids passing display-only config through the
|
||||
// lower-level fuzzy target resolver except when an error string is needed.
|
||||
const commandPrefix = String(discordConfig?.commandPrefix || 'rs').trim() || 'rs';
|
||||
return async function handleKickCommand(message, rawText) {
|
||||
const { selector, reason } = splitSelectorAndReason(rawText);
|
||||
const assignmentService = require('../../assignmentService');
|
||||
@@ -106,7 +111,7 @@ function createKickCommand({ io, roverManager, getNickname, sanitizeMentions })
|
||||
assignmentService,
|
||||
getNickname,
|
||||
});
|
||||
const resolved = resolveKickTarget(selector, candidates);
|
||||
const resolved = resolveKickTarget(selector, candidates, commandPrefix);
|
||||
if (resolved.error) {
|
||||
await message.reply({ content: sanitizeMentions(resolved.error), allowedMentions: { parse: [], repliedUser: false } });
|
||||
return;
|
||||
|
||||
@@ -12,10 +12,15 @@ function describeLightPolicy(lightPolicy = {}) {
|
||||
return 'Room lights are unlocked.';
|
||||
}
|
||||
|
||||
function createLightsCommand({ homeAssistantService, sanitizeMentions }) {
|
||||
function createLightsCommand({ homeAssistantService, sanitizeMentions, discordConfig }) {
|
||||
// The HA policy behavior is prefix-agnostic; this value is only used so
|
||||
// invalid-command guidance points admins at this bot instance's namespace.
|
||||
const commandPrefix = String(discordConfig?.commandPrefix || 'rs').trim() || 'rs';
|
||||
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`.
|
||||
// Defaulting to status makes the bare lights command safe to type while
|
||||
// still exposing explicit mutating forms under the configured prefix. This
|
||||
// matters when several bot instances share a Discord server and each one
|
||||
// needs its own command namespace.
|
||||
const action = String(tokens.shift() || 'status').trim().toLowerCase();
|
||||
|
||||
if (!homeAssistantService) {
|
||||
@@ -36,7 +41,7 @@ function createLightsCommand({ homeAssistantService, sanitizeMentions }) {
|
||||
|
||||
if (action !== 'lock' && action !== 'unlock') {
|
||||
await message.reply({
|
||||
content: 'Invalid lights command. Use `rs lights lock`, `rs lights unlock`, or `rs lights status`.',
|
||||
content: `Invalid lights command. Use \`${commandPrefix} lights lock\`, \`${commandPrefix} lights unlock\`, or \`${commandPrefix} lights status\`.`,
|
||||
allowedMentions: { parse: [], repliedUser: false },
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// Discord Lock Command
|
||||
// Purpose: Handles `rs lock` and `rs unlock` operations for rover availability control.
|
||||
// Purpose: Handles lock and unlock operations for rover availability control.
|
||||
// Scope: Applies lock state updates for a single rover ID.
|
||||
const { resolveRoverSelector } = require('./resolvers');
|
||||
|
||||
function createLockCommand({ lockRover, sanitizeMentions, rovers }) {
|
||||
function createLockCommand({ lockRover, sanitizeMentions, rovers, discordConfig }) {
|
||||
// Only the user-facing example depends on the prefix. The actual lock logic
|
||||
// still receives the already-parsed rover selector from the shared router.
|
||||
const commandPrefix = String(discordConfig?.commandPrefix || 'rs').trim() || 'rs';
|
||||
return async function handleLockCommand(message, roverId, locked) {
|
||||
if (!roverId) {
|
||||
await message.reply({ content: 'Specify a rover ID. Example: `rs lock alpha`', allowedMentions: { parse: [], repliedUser: false } });
|
||||
await message.reply({ content: `Specify a rover ID. Example: \`${commandPrefix} lock alpha\``, allowedMentions: { parse: [], repliedUser: false } });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Discord Mode Command
|
||||
// Purpose: Handles `rs mode` updates from Discord admins.
|
||||
// Purpose: Handles mode updates from Discord admins through the configured bot prefix.
|
||||
// Scope: Validates mode values and applies mode changes with optional reason text.
|
||||
function createModeCommand({ MODES, setMode, setAdminReason, isLockdownAdminUser, sanitizeMentions }) {
|
||||
return async function handleModeCommand(message, tokens = []) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Discord Time Status Command
|
||||
// Purpose: Handles `ts` command to show timezone snapshots.
|
||||
// Purpose: Handles the configured time-status shortcut to show timezone snapshots.
|
||||
// Scope: Builds a concise time embed for common zones and server local zone.
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
// Scope: Supports list and remove subcommands.
|
||||
const { mask, resolveIdentitySelector } = require('./resolvers');
|
||||
|
||||
function createVerifyCommand({ listVerifiedUsers, removeVerifiedUser, isLockdownAdminUser, sanitizeMentions }) {
|
||||
function createVerifyCommand({ listVerifiedUsers, removeVerifiedUser, isLockdownAdminUser, sanitizeMentions, discordConfig }) {
|
||||
// Verification usage text follows the configured prefix for the same reason
|
||||
// as the command router: each bot instance needs its own command namespace.
|
||||
const commandPrefix = String(discordConfig?.commandPrefix || 'rs').trim() || 'rs';
|
||||
return async function handleVerifyCommand(message, tokens) {
|
||||
if (!isLockdownAdminUser(message.author?.id)) {
|
||||
await message.reply({ content: 'Only lockdown admins can manage verified users.', allowedMentions: { parse: [], repliedUser: false } });
|
||||
@@ -18,7 +21,7 @@ function createVerifyCommand({ listVerifiedUsers, removeVerifiedUser, isLockdown
|
||||
}
|
||||
if (action === 'remove') {
|
||||
const selector = tokens.join(' ').trim();
|
||||
if (!selector) return message.reply({ content: 'Usage: `rs verify remove <cookieUserId|nickname>`', allowedMentions: { parse: [], repliedUser: false } });
|
||||
if (!selector) return message.reply({ content: `Usage: \`${commandPrefix} verify remove <cookieUserId|nickname>\``, allowedMentions: { parse: [], repliedUser: false } });
|
||||
try {
|
||||
const resolved = resolveIdentitySelector(selector, listVerifiedUsers(), { includeId: false });
|
||||
if (resolved.error) return message.reply({ content: sanitizeMentions(resolved.error), allowedMentions: { parse: [], repliedUser: false } });
|
||||
@@ -32,7 +35,7 @@ function createVerifyCommand({ listVerifiedUsers, removeVerifiedUser, isLockdown
|
||||
return message.reply({ content: sanitizeMentions(`Failed to remove verified user: ${err.message}`), allowedMentions: { parse: [], repliedUser: false } });
|
||||
}
|
||||
}
|
||||
return message.reply({ content: 'Unknown verify command. Use `rs verify list` or `rs verify remove <cookieUserId|nickname>`.', allowedMentions: { parse: [], repliedUser: false } });
|
||||
return message.reply({ content: `Unknown verify command. Use \`${commandPrefix} verify list\` or \`${commandPrefix} verify remove <cookieUserId|nickname>\`.`, allowedMentions: { parse: [], repliedUser: false } });
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,15 @@ const { createIntegrations } = require('./integrations');
|
||||
const config = loadConfig();
|
||||
const discordConfig = config.discord || {};
|
||||
const enabled = Boolean(discordConfig.token);
|
||||
// These normalized command names mirror the command router. Bridge-channel
|
||||
// command replies are mirrored into web chat, so this entrypoint needs to know
|
||||
// the configured command names before it wraps message.reply.
|
||||
const commandPrefix = String(discordConfig.commandPrefix || 'rs').trim() || 'rs';
|
||||
const timeStatusCommand = discordConfig.timeStatusCommand === null
|
||||
? ''
|
||||
: String(discordConfig.timeStatusCommand || 'ts').trim();
|
||||
const normalizedCommandPrefix = commandPrefix.toLowerCase();
|
||||
const normalizedTimeStatusCommand = timeStatusCommand.toLowerCase();
|
||||
const adminIds = new Set((config.admins || []).map((a) => String(a.discord_id || '').trim()).filter(Boolean));
|
||||
const lockdownAdminIds = new Set((config.admins || []).filter((admin) => admin.lockdown).map((admin) => String(admin.discord_id || '').trim()).filter(Boolean));
|
||||
|
||||
@@ -201,7 +210,15 @@ const integrationHandlers = integrations.register();
|
||||
|
||||
function isTextCommand(content) {
|
||||
const clean = String(content || '').trim();
|
||||
return clean.toLowerCase() === 'ts' || /^rs(?:\s|$)/i.test(clean);
|
||||
const lower = clean.toLowerCase();
|
||||
if (normalizedTimeStatusCommand && lower === normalizedTimeStatusCommand) return true;
|
||||
if (!lower.startsWith(normalizedCommandPrefix)) return false;
|
||||
|
||||
const nextCharacter = clean.charAt(commandPrefix.length);
|
||||
// Mirrored bridge commands must use the exact same whole-token prefix rule
|
||||
// as the command router. If this check is looser than the router, normal
|
||||
// bridge chat can be wrapped as a command reply even though no command runs.
|
||||
return !nextCharacter || /\s/.test(nextCharacter);
|
||||
}
|
||||
|
||||
function isBridgeChannelMessage(message) {
|
||||
|
||||
Reference in New Issue
Block a user