discord bot

This commit is contained in:
legop3
2026-04-29 13:01:21 -04:00
parent ea3c030a21
commit c20c22eafc
21 changed files with 1179 additions and 1895 deletions
@@ -0,0 +1,72 @@
// Discord Bridge Command
// Purpose: Handles chat bridge configuration/status commands per guild.
// Scope: Manages bridge channel, mode, and webhook provisioning.
const { PermissionsBitField } = require('discord.js');
function createBridgeCommand({ getGuildConfig, setGuildConfig, removeGuildConfig, normalizeMode, VALID_MODES, isAdminUser }) {
function canManageBridge(message) {
if (isAdminUser(message.author.id)) return true;
if (!message.guild || !message.member) return false;
const perms = message.member.permissions;
if (!perms) return false;
return perms.has(PermissionsBitField.Flags.ManageGuild) || perms.has(PermissionsBitField.Flags.Administrator);
}
function canManageWebhooksInChannel(channel) {
if (!channel?.guild) return false;
const botMember = channel.guild.members?.me;
const perms = channel.permissionsFor(botMember);
if (!perms) return false;
return perms.has(PermissionsBitField.Flags.ManageWebhooks);
}
async function ensureBridgeWebhook(channel, guildId) {
if (!channel?.id || !guildId) return null;
if (!canManageWebhooksInChannel(channel)) throw new Error('Missing Manage Webhooks permission in this channel.');
const existing = getGuildConfig(guildId);
if (existing?.channelId && String(existing.channelId) === String(channel.id) && existing?.webhookId && existing?.webhookToken) return existing;
const webhook = await channel.createWebhook({ name: 'Rover Chat Bridge', reason: 'Rover chat bridge webhook' });
if (!webhook?.id || !webhook?.token) throw new Error('Failed to create webhook.');
return setGuildConfig(guildId, { channelId: channel.id, mode: existing?.mode || 'global', webhookId: webhook.id, webhookToken: webhook.token });
}
function status(entry) {
if (!entry) return 'Chat bridge is not configured for this server.';
return `Chat bridge is **${entry.mode}** in <#${entry.channelId}>.`;
}
return async function handleBridgeCommand(message, tokens) {
if (!message.guild) return message.reply({ content: 'Chat bridge must be configured in a server channel.', allowedMentions: { parse: [], repliedUser: false } });
const guildId = message.guild.id;
let action = (tokens.shift() || 'status').toLowerCase();
let mode = null;
if (action === 'global' || action === 'private') { mode = action; action = 'here'; }
else if (action === 'here' || action === 'mode') { mode = (tokens.shift() || '').toLowerCase(); }
if (mode && !VALID_MODES.has(mode)) return message.reply({ content: 'Invalid mode. Use `global` or `private`.', allowedMentions: { parse: [], repliedUser: false } });
if (action === 'status') return message.reply({ content: status(getGuildConfig(guildId)), allowedMentions: { parse: [], repliedUser: false } });
if (action === 'off') { removeGuildConfig(guildId); return message.reply({ content: 'Chat bridge disabled for this server.', allowedMentions: { parse: [], repliedUser: false } }); }
if (!canManageBridge(message)) return message.reply({ content: 'You need Manage Server permissions to change the chat bridge.', allowedMentions: { parse: [], repliedUser: false } });
if (action === 'here') {
try {
const entry = await ensureBridgeWebhook(message.channel, guildId);
if (mode) setGuildConfig(guildId, { channelId: entry.channelId, mode, webhookId: entry.webhookId, webhookToken: entry.webhookToken });
const updated = getGuildConfig(guildId);
return message.reply({ content: `Chat bridge set to **${updated.mode}** in <#${updated.channelId}>.`, allowedMentions: { parse: [], repliedUser: false } });
} catch (err) {
return message.reply({ content: `Failed to set chat bridge: ${err.message}`, allowedMentions: { parse: [], repliedUser: false } });
}
}
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 } });
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 } });
};
}
module.exports = { createBridgeCommand };
@@ -0,0 +1,49 @@
// Discord Deter Command
// Purpose: Handles deterrence moderation commands for lockdown admins.
// Scope: Supports list, ban, and unban subcommands.
function createDeterCommand({ listDeterredUsers, deterUser, undeterUser, isLockdownAdminUser, sanitizeMentions }) {
function mask(v) {
const key = String(v || '').trim();
if (!key) return 'n/a';
if (key.length <= 10) return `${key.slice(0, 2)}***${key.slice(-2)}`;
return `${key.slice(0, 6)}...${key.slice(-6)}`;
}
return async function handleDeterCommand(message, tokens) {
if (!isLockdownAdminUser(message.author?.id)) {
await message.reply({ content: 'Only lockdown admins can manage deterred users.', allowedMentions: { parse: [], repliedUser: false } });
return;
}
const action = (tokens.shift() || 'list').toLowerCase();
if (action === 'list') {
const users = listDeterredUsers();
if (!users.length) return message.reply({ content: 'No deterred users.', allowedMentions: { parse: [], repliedUser: false } });
const lines = users.map((entry, idx) => `${idx + 1}. ${entry.id} | ${entry.nickname || 'unknown'} | ${mask(entry.cookieUserId)}`);
return message.reply({ content: ['Deterred users:', ...lines].join('\n').slice(0, 1900), allowedMentions: { parse: [], repliedUser: false } });
}
if (action === 'ban') {
const selector = String(tokens.shift() || '').trim();
const reason = tokens.join(' ').trim();
if (!selector) return message.reply({ content: 'Usage: `rs deter ban <cookieUserId|nickname|ip> [reason]`', allowedMentions: { parse: [], repliedUser: false } });
try {
const deterred = deterUser(selector, { reason, actor: message.author?.id || null });
return message.reply({ content: sanitizeMentions(`${deterred.created ? 'Deterred' : 'Updated deterrence for'} ${deterred.nickname || 'unknown'} (${mask(deterred.cookieUserId)}).`), allowedMentions: { parse: [], repliedUser: false } });
} catch (err) {
return message.reply({ content: sanitizeMentions(`Failed to deter user: ${err.message}`), allowedMentions: { parse: [], repliedUser: false } });
}
}
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 } });
try {
const removed = undeterUser(selector, message.author?.id || null);
return message.reply({ content: sanitizeMentions(`Removed deterrence for ${removed.nickname || 'unknown'} (${mask(removed.cookieUserId)}).`), allowedMentions: { parse: [], repliedUser: false } });
} catch (err) {
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> [reason]`, or `rs deter unban <selector>`.', allowedMentions: { parse: [], repliedUser: false } });
};
}
module.exports = { createDeterCommand };
@@ -0,0 +1,31 @@
// Discord Goal Command
// Purpose: Handles community goal view/update/clear operations.
// Scope: Allows read by all and write by admins.
function createGoalCommand({ getCommunityGoal, setCommunityGoal, clearCommunityGoal, isAdminUser, sanitizeMentions }) {
return async function handleGoalCommand(message, tokens) {
const query = tokens.join(' ').trim();
const lower = query.toLowerCase();
if (!query) {
const goal = getCommunityGoal();
await message.reply({ content: goal?.text ? `Community goal: ${sanitizeMentions(goal.text)}` : 'No community goal set.', allowedMentions: { parse: [], repliedUser: false } });
return;
}
if (!isAdminUser(message.author.id)) {
await message.reply({ content: 'Only admins can update the community goal.', allowedMentions: { parse: [], repliedUser: false } });
return;
}
try {
if (lower === 'clear') {
clearCommunityGoal({ by: message.author?.id || null });
await message.reply({ content: 'Community goal cleared.', allowedMentions: { parse: [], repliedUser: false } });
} else {
setCommunityGoal(query, { by: message.author?.id || null });
await message.reply({ content: sanitizeMentions(`Community goal set: ${query}`), allowedMentions: { parse: [], repliedUser: false } });
}
} catch (err) {
await message.reply({ content: sanitizeMentions(`Failed to update goal: ${err.message}`), allowedMentions: { parse: [], repliedUser: false } });
}
};
}
module.exports = { createGoalCommand };
@@ -0,0 +1,25 @@
// Discord Help Command
// Purpose: Provides help text for rover bot Discord commands.
// Scope: Returns static command usage text.
function formatHelp() {
return [
'**Rover Bot Commands**',
'`rs help` — show this help',
'`rs status [id]` — show rover status (all or one)',
'`rs replay [sources]` — send instant replay (room/rover)',
'`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 lock <id>` — lock a rover',
'`rs unlock <id>` — unlock a rover',
'`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 community goal',
'`rs verify list|remove ...` — manage verified users',
'`rs deter list|ban|unban ...` — manage deterred users',
'`ts` — show time status',
].join('\n');
}
module.exports = { formatHelp };
@@ -0,0 +1,91 @@
// Discord Commands Router
// Purpose: Routes incoming Discord command messages to one-file-per-command handlers.
// Scope: Central command dispatcher and permission gate orchestration.
const { formatHelp } = require('./help');
const { createStatusCommand } = require('./status');
const { createReplayCommand } = require('./replay');
const { createLockCommand } = require('./lock');
const { createModeCommand } = require('./mode');
const { createReasonCommand } = require('./reason');
const { createGoalCommand } = require('./goal');
const { createVerifyCommand } = require('./verify');
const { createDeterCommand } = require('./deter');
const { createBridgeCommand } = require('./bridge');
const { createTimeStatusCommand } = require('./timeStatus');
function createCommandHandlers(deps) {
const {
getMode,
MODES,
isAdminUser,
isLockdownAdminUser,
} = deps;
const handleStatusCommand = createStatusCommand(deps);
const handleReplayCommand = createReplayCommand(deps);
const handleLockCommand = createLockCommand(deps);
const handleModeCommand = createModeCommand(deps);
const handleReasonCommand = createReasonCommand(deps);
const handleGoalCommand = createGoalCommand(deps);
const handleVerifyCommand = createVerifyCommand(deps);
const handleDeterCommand = createDeterCommand(deps);
const handleBridgeCommand = createBridgeCommand(deps);
const handleTimeStatusCommand = createTimeStatusCommand(deps);
async function handleCommand(message) {
if (message.author.bot) return;
const content = (message.content || '').trim();
const lower = content.toLowerCase();
if (lower === 'ts' || lower.startsWith('ts')) return handleTimeStatusCommand(message);
if (!lower.startsWith('rs')) return;
const tokens = content.split(/\s+/);
tokens.shift();
const action = (tokens.shift() || '').toLowerCase();
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']);
if (!isAdmin && action !== '' && action !== 'status' && action !== 'help' && action !== 'replay' && action !== 'bridge' && action !== 'goal' && action !== 'reason' && action !== 'verify' && action !== 'deter') {
return;
}
if (mode === MODES.LOCKDOWN && moderationActions.has(action) && !isLockdownAdmin) {
await message.reply({ content: 'Lockdown mode: only lockdown admins can run that command.', allowedMentions: { parse: [], repliedUser: false } });
return;
}
switch (action) {
case '':
case 'status':
return handleStatusCommand(message, tokens[0]);
case 'help':
return message.reply(formatHelp());
case 'replay':
return handleReplayCommand(message, tokens.join(' '));
case 'bridge':
return handleBridgeCommand(message, tokens);
case 'lock':
return handleLockCommand(message, tokens[0], true);
case 'unlock':
return handleLockCommand(message, tokens[0], false);
case 'mode':
return handleModeCommand(message, tokens);
case 'goal':
return handleGoalCommand(message, tokens);
case 'reason':
return handleReasonCommand(message, tokens);
case 'verify':
return handleVerifyCommand(message, tokens);
case 'deter':
return handleDeterCommand(message, tokens);
default:
return message.reply(formatHelp());
}
}
return { handleCommand };
}
module.exports = { createCommandHandlers };
@@ -0,0 +1,19 @@
// Discord Lock Command
// Purpose: Handles `rs lock` and `rs unlock` operations for rover availability control.
// Scope: Applies lock state updates for a single rover ID.
function createLockCommand({ lockRover, sanitizeMentions }) {
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 } });
return;
}
try {
lockRover(roverId, locked, { reason: 'discord' });
await message.reply({ content: sanitizeMentions(`${locked ? 'Locked' : 'Unlocked'} ${roverId}.`), allowedMentions: { parse: [], repliedUser: false } });
} catch (err) {
await message.reply({ content: sanitizeMentions(`Failed: ${err.message}`), allowedMentions: { parse: [], repliedUser: false } });
}
};
}
module.exports = { createLockCommand };
@@ -0,0 +1,23 @@
// Discord Mode Command
// Purpose: Handles `rs mode` updates from Discord admins.
// 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 = []) {
const next = String(tokens.shift() || '').toLowerCase();
const reasonText = tokens.join(' ').trim();
if (!Object.values(MODES).includes(next)) {
await message.reply({ content: 'Invalid mode. Use one of: open, turns, admin, lockdown.', allowedMentions: { parse: [], repliedUser: false } });
return;
}
try {
const role = isLockdownAdminUser(message.author?.id) ? 'lockdown' : 'admin';
setMode(next, { data: { role, user: { username: `discord:${message.author?.username || 'unknown'}` } } });
if (reasonText) setAdminReason(reasonText, { by: message.author?.id || null });
await message.reply({ content: sanitizeMentions(`Mode set to ${next}.`), allowedMentions: { parse: [], repliedUser: false } });
} catch (err) {
await message.reply({ content: sanitizeMentions(`Failed to set mode: ${err.message}`), allowedMentions: { parse: [], repliedUser: false } });
}
};
}
module.exports = { createModeCommand };
@@ -0,0 +1,31 @@
// Discord Reason Command
// Purpose: Handles admin-mode reason view/update/clear operations.
// Scope: Allows read by all and write by admins.
function createReasonCommand({ getAdminReason, setAdminReason, clearAdminReason, isAdminUser, sanitizeMentions }) {
return async function handleReasonCommand(message, tokens) {
const query = tokens.join(' ').trim();
const lower = query.toLowerCase();
if (!query) {
const reason = getAdminReason();
await message.reply({ content: reason?.text ? `Admin mode reason: ${sanitizeMentions(reason.text)}` : 'No admin mode reason set.', allowedMentions: { parse: [], repliedUser: false } });
return;
}
if (!isAdminUser(message.author.id)) {
await message.reply({ content: 'Only admins can update the admin mode reason.', allowedMentions: { parse: [], repliedUser: false } });
return;
}
try {
if (lower === 'clear') {
clearAdminReason({ by: message.author?.id || null });
await message.reply({ content: 'Admin mode reason cleared.', allowedMentions: { parse: [], repliedUser: false } });
} else {
setAdminReason(query, { by: message.author?.id || null });
await message.reply({ content: sanitizeMentions(`Admin mode reason set: ${query}`), allowedMentions: { parse: [], repliedUser: false } });
}
} catch (err) {
await message.reply({ content: sanitizeMentions(`Failed to update reason: ${err.message}`), allowedMentions: { parse: [], repliedUser: false } });
}
};
}
module.exports = { createReasonCommand };
@@ -0,0 +1,58 @@
// Discord Replay Command
// Purpose: Handles replay capture requests from Discord.
// Scope: Resolves source selectors, enforces cooldowns, and replies with replay video attachment.
const { AttachmentBuilder } = require('discord.js');
function createReplayCommand({ getMode, MODES, tryTriggerReplay, getReplaySources, getDefaultDiscordSources, validateSources, buildReplayVideo, sanitizeMentions }) {
function normalizeReplayQuery(input) { return String(input || '').trim().toLowerCase(); }
function sanitizeReplayTitleForFilename(title) {
const cleaned = String(title || '').replace(/[\\/:*?"<>|]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 96);
return cleaned || 'replay';
}
function buildDefaultReplayTitle(requester, sources = []) {
const roverSource = sources.find((entry) => entry?.type === 'rover');
return `${String(requester || 'Someone').trim() || 'Someone'} driving ${roverSource?.label || roverSource?.id || 'a rover'}`;
}
function resolveReplaySources(query) {
const cleaned = normalizeReplayQuery(query);
if (!cleaned || cleaned === 'all' || cleaned === '*') return { sources: getDefaultDiscordSources() };
const tokens = cleaned.split(',').map((token) => token.trim()).filter(Boolean);
const all = getReplaySources();
const matches = [];
tokens.forEach((token) => {
const [prefix, rest] = token.includes(':') ? token.split(':', 2) : [null, token];
const candidate = all.find((entry) => (String(entry.id).toLowerCase() === rest || String(entry.label || '').toLowerCase() === rest) && (!prefix || entry.type === prefix));
if (candidate) matches.push({ type: candidate.type, id: candidate.id, label: candidate.label });
});
const sources = validateSources(matches);
return sources.length ? { sources } : { error: 'No matching sources found', matches: [] };
}
return async function handleReplayCommand(message, query) {
if (getMode() === MODES.LOCKDOWN) {
await message.reply({ content: 'Replay is disabled while the server is in lockdown.', allowedMentions: { parse: [], repliedUser: false } });
return;
}
const attempt = tryTriggerReplay({ by: message.author?.id || null, source: 'discord' });
if (!attempt.ok) {
await message.reply({ content: `Replay cooldown active. Try again in ${Math.ceil(attempt.remainingMs / 1000)}s.`, allowedMentions: { parse: [], repliedUser: false } });
return;
}
const resolved = resolveReplaySources(query);
if (resolved?.error) {
await message.reply({ content: sanitizeMentions(resolved.error), allowedMentions: { parse: [], repliedUser: false } });
return;
}
const requester = message.member?.nickname || message.author?.globalName || message.author?.username || 'Discord';
const title = buildDefaultReplayTitle(requester, resolved.sources || []);
try {
const { buffer } = await buildReplayVideo({ sources: resolved.sources || [], title, requester });
const attachment = new AttachmentBuilder(buffer, { name: `${sanitizeReplayTitleForFilename(title)}.mp4` });
await message.reply({ content: sanitizeMentions(`**${title}**`), files: [attachment], allowedMentions: { parse: [], repliedUser: false } });
} catch (err) {
await message.reply({ content: sanitizeMentions(`Replay failed: ${err.message}`), allowedMentions: { parse: [], repliedUser: false } });
}
};
}
module.exports = { createReplayCommand };
@@ -0,0 +1,64 @@
// Discord Status Command
// Purpose: Handles rover status display command with battery and lock details.
// Scope: Builds and sends rover status embed for one rover or all visible rovers.
const { EmbedBuilder } = require('discord.js');
function createStatusCommand({ rovers, roverManager }) {
function formatVoltage(voltageMv) { return voltageMv == null ? 'n/a' : `${(voltageMv / 1000).toFixed(2)}V`; }
function formatCurrent(currentMa) { return currentMa == null ? 'n/a' : `${currentMa}mA`; }
function formatChargeState(batteryState) {
if (!batteryState) return 'n/a';
const chargeText = batteryState.charge != null && batteryState.capacity != null ? `${batteryState.charge}/${batteryState.capacity}mAh` : 'n/a';
const percentText = batteryState.percentDisplay != null ? `${batteryState.percentDisplay}%` : 'n/a';
return `${chargeText} (${percentText})`;
}
function findRoverRecord(id) {
if (!id) return null;
for (const record of rovers.values()) {
if (String(record.id) === String(id) || String(record.meta?.name) === String(id)) return record;
}
return null;
}
function buildSnapshot(record) {
if (!record) return null;
const sensors = record.lastSensor?.decoded || record.lastSensor?.sensors || null;
return {
name: record.meta?.name || record.id,
locked: record.locked,
lockReason: record.lockReason,
docked: Boolean(sensors?.chargingSources?.homeBase),
charging: Boolean([2,3,4].includes(sensors?.chargingState?.code)),
chargingLabel: sensors?.chargingState?.label || 'unknown',
voltageMv: sensors?.voltageMv ?? null,
currentMa: sensors?.currentMa ?? null,
batteryState: record.batteryState,
oiMode: sensors?.oiMode?.label || 'unknown',
};
}
function buildEmbed(records) {
const embed = new EmbedBuilder().setTitle('Rover Battery Status').setColor(0x2196f3).setTimestamp(new Date());
const snapshots = records.map(buildSnapshot).filter(Boolean);
if (!snapshots.length) {
embed.setDescription('No rovers online.');
return embed;
}
snapshots.forEach((s) => {
const lockLabel = s.locked ? `locked${s.lockReason ? ` (${s.lockReason})` : ''}` : 'unlocked';
embed.addFields({ name: s.name, value: [`Dock: ${s.docked ? 'docked' : 'undocked'}`, `Charging: ${s.charging ? `charging (${s.chargingLabel})` : 'not charging'}`, `Battery: ${formatChargeState(s.batteryState)}`, `Voltage: ${formatVoltage(s.voltageMv)}`, `Current: ${formatCurrent(s.currentMa)}`, `OI: ${s.oiMode}`, `Lock: ${lockLabel}`].join('\n'), inline: true });
});
return embed;
}
return async function handleStatusCommand(message, roverId) {
const single = roverId ? findRoverRecord(roverId) : null;
if (roverId && !single) {
const embed = new EmbedBuilder().setTitle('Rover Status').setDescription('Unknown rover.').setColor(0x2196f3).setTimestamp(new Date());
await message.reply({ embeds: [embed], allowedMentions: { parse: [], repliedUser: false } });
return;
}
const records = roverId ? [single] : Array.from(rovers.values()).filter((entry) => roverManager.canReplayRoverId(entry?.id));
await message.reply({ embeds: [buildEmbed(records)], allowedMentions: { parse: [], repliedUser: false } });
};
}
module.exports = { createStatusCommand };
@@ -0,0 +1,36 @@
// Discord Time Status Command
// Purpose: Handles `ts` command to show timezone snapshots.
// Scope: Builds a concise time embed for common zones and server local zone.
const { EmbedBuilder } = require('discord.js');
function createTimeStatusCommand({ config, discordConfig }) {
function buildEmbed({ title, description, color, includeSiteUrl = true }) {
const embed = new EmbedBuilder().setTitle(title || 'Update').setColor(color || 0x2196f3);
const siteUrl = includeSiteUrl && discordConfig.siteUrl ? String(discordConfig.siteUrl) : '';
if (description) embed.setDescription(siteUrl ? `${description}\n\n${siteUrl}` : description);
else if (siteUrl) embed.setDescription(siteUrl);
embed.setTimestamp(new Date());
return embed;
}
function getServerTimezone() {
return config.timezone || config.server?.timezone || process.env.TZ || 'America/New_York';
}
function formatTimeInZone(date, timeZone) {
try {
return new Intl.DateTimeFormat('en-US', { timeZone, hour: '2-digit', minute: '2-digit', hour12: false }).format(date);
} catch {
return 'n/a';
}
}
return async function handleTimeStatusCommand(message) {
const serverTimezone = getServerTimezone();
const now = new Date();
const zones = ['UTC', 'America/Los_Angeles', 'America/Denver', 'America/Chicago', 'America/New_York'];
const lines = zones.map((zone) => `${zone}${formatTimeInZone(now, zone)}${zone === serverTimezone ? ' **(server local timezone)**' : ''}`);
const embed = buildEmbed({ title: 'Time Status', description: lines.join('\n'), color: 0x2196f3 });
embed.setFooter({ text: `Server local timezone: ${serverTimezone}` });
await message.reply({ embeds: [embed], allowedMentions: { parse: [], repliedUser: false } });
};
}
module.exports = { createTimeStatusCommand };
@@ -0,0 +1,38 @@
// Discord Verify Command
// Purpose: Handles verified-user moderation commands for lockdown admins.
// Scope: Supports list and remove subcommands.
function createVerifyCommand({ listVerifiedUsers, removeVerifiedUser, isLockdownAdminUser, sanitizeMentions }) {
function mask(v) {
const key = String(v || '').trim();
if (!key) return 'n/a';
if (key.length <= 10) return `${key.slice(0, 2)}***${key.slice(-2)}`;
return `${key.slice(0, 6)}...${key.slice(-6)}`;
}
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 } });
return;
}
const action = (tokens.shift() || 'list').toLowerCase();
if (action === 'list') {
const users = listVerifiedUsers();
if (!users.length) return message.reply({ content: 'No verified users.', allowedMentions: { parse: [], repliedUser: false } });
const lines = users.map((entry, idx) => `${idx + 1}. ${entry.nickname || 'unknown'} | ${mask(entry.cookieUserId)}`);
return message.reply({ content: ['Verified users:', ...lines].join('\n').slice(0, 1900), allowedMentions: { parse: [], repliedUser: false } });
}
if (action === 'remove') {
const selector = tokens.join(' ').trim();
if (!selector) return message.reply({ content: 'Usage: `rs verify remove <cookieUserId|nickname>`', allowedMentions: { parse: [], repliedUser: false } });
try {
const removed = removeVerifiedUser(selector, message.author?.id || null);
return message.reply({ content: `Removed verified user ${sanitizeMentions(removed.nickname || 'unknown')} (${mask(removed.cookieUserId)}).`, allowedMentions: { parse: [], repliedUser: false } });
} catch (err) {
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 } });
};
}
module.exports = { createVerifyCommand };