import type { ChatInputCommandInteraction, GuildBasedChannel, Message } from "discord.js"; import type { CommandArgValue, CommandArgument, TranslationVars } from "../types/command.js"; export interface ArgumentParseError { key: string; vars?: TranslationVars; } export interface ParsedArgumentsResult { values: Record; errors: ArgumentParseError[]; } const USER_MENTION_PATTERN = /^<@!?(\d{16,22})>$/; const CHANNEL_MENTION_PATTERN = /^<#(\d{16,22})>$/; const ROLE_MENTION_PATTERN = /^<@&(\d{16,22})>$/; const SNOWFLAKE_ID_PATTERN = /^(\d{16,22})$/; const BOOLEAN_TRUE = new Set(["true", "1", "yes", "y", "on"]); const BOOLEAN_FALSE = new Set(["false", "0", "no", "n", "off"]); const isGuildBasedChannel = (value: unknown): value is GuildBasedChannel => { return Boolean(value) && typeof value === "object" && "guild" in (value as Record); }; export const tokenizePrefixInput = (raw: string): string[] => { const tokens: string[] = []; const regex = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|(\S+)/g; let match: RegExpExecArray | null; while ((match = regex.exec(raw)) !== null) { tokens.push((match[1] ?? match[2] ?? match[3] ?? "").replace(/\\(["'])/g, "$1")); } return tokens; }; export const parsePrefixArgs = async ( message: Message, definitions: CommandArgument[], rawInput: string, ): Promise => { const tokens = tokenizePrefixInput(rawInput); const values: Record = {}; const errors: ArgumentParseError[] = []; for (const definition of definitions) { const token = tokens.shift(); if (!token) { if (definition.required) { errors.push({ key: "errors.args.missing", vars: { arg: definition.name } }); } values[definition.name] = undefined; continue; } const parsed = await parseByTypeFromPrefix(message, definition.type, token); if (parsed.ok) { values[definition.name] = parsed.value; continue; } errors.push(parsed.error); } if (tokens.length > 0) { errors.push({ key: "errors.args.tooMany", vars: { count: tokens.length, extras: tokens.join(" "), }, }); } return { values, errors }; }; export const parseSlashArgs = ( interaction: ChatInputCommandInteraction, definitions: CommandArgument[], ): ParsedArgumentsResult => { const values: Record = {}; const errors: ArgumentParseError[] = []; for (const definition of definitions) { try { values[definition.name] = parseByTypeFromSlash(interaction, definition); } catch { if (definition.required) { errors.push({ key: "errors.args.missing", vars: { arg: definition.name } }); } values[definition.name] = undefined; } } return { values, errors }; }; const parseByTypeFromPrefix = async ( message: Message, type: CommandArgument["type"], token: string, ): Promise<{ ok: true; value: CommandArgValue } | { ok: false; error: ArgumentParseError }> => { switch (type) { case "string": return { ok: true, value: token }; case "int": { if (!/^-?\d+$/.test(token)) { return { ok: false, error: { key: "errors.args.invalidInt", vars: { value: token } } }; } const value = Number.parseInt(token, 10); return { ok: true, value }; } case "number": { const value = Number(token); if (!Number.isFinite(value)) { return { ok: false, error: { key: "errors.args.invalidNumber", vars: { value: token } } }; } return { ok: true, value }; } case "boolean": { const normalized = token.toLowerCase(); if (BOOLEAN_TRUE.has(normalized)) { return { ok: true, value: true }; } if (BOOLEAN_FALSE.has(normalized)) { return { ok: true, value: false }; } return { ok: false, error: { key: "errors.args.invalidBoolean", vars: { value: token } } }; } case "user": { const mentionMatch = token.match(USER_MENTION_PATTERN); const idMatch = token.match(SNOWFLAKE_ID_PATTERN); const userId = mentionMatch?.[1] ?? idMatch?.[1]; if (!userId) { return { ok: false, error: { key: "errors.args.invalidUser", vars: { value: token } } }; } const fromMention = message.mentions.users.get(userId); if (fromMention) { return { ok: true, value: fromMention }; } try { const fetched = await message.client.users.fetch(userId); return { ok: true, value: fetched }; } catch { return { ok: false, error: { key: "errors.args.invalidUser", vars: { value: token } } }; } } case "channel": { const mentionMatch = token.match(CHANNEL_MENTION_PATTERN); const idMatch = token.match(SNOWFLAKE_ID_PATTERN); const channelId = mentionMatch?.[1] ?? idMatch?.[1]; if (!channelId) { return { ok: false, error: { key: "errors.args.invalidChannel", vars: { value: token } } }; } const fromMention = message.mentions.channels.get(channelId); if (fromMention && isGuildBasedChannel(fromMention)) { return { ok: true, value: fromMention }; } const fromGuildCache = message.guild?.channels.cache.get(channelId); if (fromGuildCache) { return { ok: true, value: fromGuildCache }; } try { const fetchedFromGuild = await message.guild?.channels.fetch(channelId); if (fetchedFromGuild) { return { ok: true, value: fetchedFromGuild }; } } catch { // Try the global cache/fetch fallback below. } try { const fetched = await message.client.channels.fetch(channelId); if (fetched && isGuildBasedChannel(fetched)) { return { ok: true, value: fetched }; } } catch { // Falls through to invalid channel error. } return { ok: false, error: { key: "errors.args.invalidChannel", vars: { value: token } } }; } case "role": { const mentionMatch = token.match(ROLE_MENTION_PATTERN); const idMatch = token.match(SNOWFLAKE_ID_PATTERN); const roleId = mentionMatch?.[1] ?? idMatch?.[1]; if (!roleId || !message.guild) { return { ok: false, error: { key: "errors.args.invalidRole", vars: { value: token } } }; } const fromMention = message.mentions.roles.get(roleId); if (fromMention) { return { ok: true, value: fromMention }; } const fromCache = message.guild.roles.cache.get(roleId); if (fromCache) { return { ok: true, value: fromCache }; } try { const fetched = await message.guild.roles.fetch(roleId); if (fetched) { return { ok: true, value: fetched }; } } catch { // Falls through to invalid role error. } return { ok: false, error: { key: "errors.args.invalidRole", vars: { value: token } } }; } default: return { ok: true, value: token }; } }; const parseByTypeFromSlash = ( interaction: ChatInputCommandInteraction, definition: CommandArgument, ): CommandArgValue => { switch (definition.type) { case "string": return interaction.options.getString(definition.name, definition.required) ?? undefined; case "int": return interaction.options.getInteger(definition.name, definition.required) ?? undefined; case "number": return interaction.options.getNumber(definition.name, definition.required) ?? undefined; case "boolean": return interaction.options.getBoolean(definition.name, definition.required) ?? undefined; case "user": return interaction.options.getUser(definition.name, definition.required) ?? undefined; case "channel": return (interaction.options.getChannel(definition.name, definition.required) ?? undefined) as CommandArgValue; case "role": return (interaction.options.getRole(definition.name, definition.required) ?? undefined) as CommandArgValue; default: return interaction.options.getString(definition.name, definition.required) ?? undefined; } };