From fc6278e4cbe00acba0c8bdbe645df85e607af667 Mon Sep 17 00:00:00 2001 From: Fuwn Date: Mon, 27 Oct 2025 22:20:07 -0700 Subject: feat(gateway:commands): Add timeout command --- packages/gateway/src/commands/commandHandler.ts | 2 + packages/gateway/src/commands/timeout.ts | 120 ++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 packages/gateway/src/commands/timeout.ts diff --git a/packages/gateway/src/commands/commandHandler.ts b/packages/gateway/src/commands/commandHandler.ts index 163568e..7442c47 100644 --- a/packages/gateway/src/commands/commandHandler.ts +++ b/packages/gateway/src/commands/commandHandler.ts @@ -11,6 +11,7 @@ import { handleVerbalGatesCommand } from "./verbalGates"; import { handleWebhookCommand } from "./webhook"; import { handleDeleteWebhookCommand } from "./deleteWebhook"; import { handleCharacterClaimUsageCommand } from "./characterClaimUsage"; +import { handleTimeoutCommand } from "./timeout"; export const handleCommandHandler = (client: Client) => { client.on(Events.MessageCreate, async (message: Message) => { @@ -33,6 +34,7 @@ export const handleCommandHandler = (client: Client) => { handleWebhookCommand(message), handleDeleteWebhookCommand(message), handleCharacterClaimUsageCommand(message), + handleTimeoutCommand(message), ]); }); }; diff --git a/packages/gateway/src/commands/timeout.ts b/packages/gateway/src/commands/timeout.ts new file mode 100644 index 0000000..d43d6bd --- /dev/null +++ b/packages/gateway/src/commands/timeout.ts @@ -0,0 +1,120 @@ +import { Message } from "discord.js"; +import { CENTRAL_GUILD_ID, ROLEPLAY_GUILD_ID } from "../constants"; +import { logUnexpectedDiscordAPIError, replyWithCleanup } from "../utilities"; + +const parseDuration = (duration: string): number | null => { + const match = duration.match(/^(\d+)([smhd])$/); + + if (!match) return null; + + const value = parseInt(match[1]); + const unit = match[2]; + + switch (unit) { + case "s": + return value * 1000; + case "m": + return value * 60 * 1000; + case "h": + return value * 60 * 60 * 1000; + case "d": + return value * 24 * 60 * 60 * 1000; + default: + return null; + } +}; + +const getUserId = (input: string): string | null => { + const mentionMatch = input.match(/^<@(\d+)>$/); + + if (mentionMatch) return mentionMatch[1]; + + const userIdMatch = input.match(/^\d+$/); + + if (userIdMatch) return input; + + return null; +}; + +const getGuild = (client: any, serverId: string) => { + if (serverId === "central" || serverId === CENTRAL_GUILD_ID) + return client.guilds.cache.get(CENTRAL_GUILD_ID); + + if (serverId === "roleplay" || serverId === ROLEPLAY_GUILD_ID) + return client.guilds.cache.get(ROLEPLAY_GUILD_ID); + + return null; +}; + +export const handleTimeoutCommand = async (message: Message) => { + if (message.author.bot) return; + + if (message.content.toLowerCase().startsWith("uma!timeout")) { + const application = await message.client.application?.fetch(); + const ownerId = application?.owner?.id; + + if (message.author.id !== ownerId) return; + + const parameters = message.content.split(" ").slice(1); + + if (parameters.length < 3) { + await replyWithCleanup( + message, + "❌ Usage: `uma!timeout [reason]`\n\n**Examples:**\n- `uma!timeout central @user 1h Being toxic`\n- `uma!timeout roleplay 1234567890123456789 6h`\n- `uma!timeout 1406422617724026901 @user 1d Spam`\n\n**Duration format:** Use suffix: `s` (seconds), `m` (minutes), `h` (hours), `d` (days)", + ); + + return; + } + + const [serverInput, userInput, durationInput, ...reasonParts] = parameters; + const reason = reasonParts.join(" ") || undefined; + const durationMs = parseDuration(durationInput); + + if (!durationMs) { + await replyWithCleanup( + message, + "❌ Invalid duration format. Use: ``\nExamples: `1m`, `6h`, `1d`", + ); + + return; + } + + const guild = getGuild(message.client, serverInput); + + if (!guild) { + await replyWithCleanup( + message, + "❌ Invalid server. Use `central`, `roleplay`, or a guild ID.", + ); + + return; + } + + const userId = getUserId(userInput); + + if (!userId) { + await replyWithCleanup( + message, + "❌ Invalid user. Use a user ID or mention: `<@userId>`", + ); + + return; + } + + try { + const member = await guild.members.fetch(userId); + + await member.timeout(durationMs, reason); + await replyWithCleanup( + message, + `✅ Successfully timed out ${member.user.tag} in ${guild.name} for ${durationInput}${reason ? ` - ${reason}` : ""}`, + ); + } catch (error) { + logUnexpectedDiscordAPIError(error); + await replyWithCleanup( + message, + `❌ Failed to timeout user. ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } + } +}; -- cgit v1.2.3