Files
flint/src/framework/commands/argParser.ts
T
Puechberty Arthur f6a2e2b970 feat: update environment configuration and enhance presence management
- Updated .env.example to use more secure database credentials.
- Modified Dockerfile to remove unnecessary data copy.
- Enhanced README with additional setup instructions and security notes.
- Updated docker-compose files to improve database connection handling and security.
- Added error messages for invalid channel and role resolutions in locales.
- Introduced new scripts for code quality checks in package.json.
- Refactored presence management logic to improve state handling and timer management.
- Added tests for argument tokenizer, command definition, presence types, and template variables.
2026-04-12 16:07:27 +02:00

267 lines
8.0 KiB
TypeScript

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<string, CommandArgValue>;
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<string, unknown>);
};
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<ParsedArgumentsResult> => {
const tokens = tokenizePrefixInput(rawInput);
const values: Record<string, CommandArgValue> = {};
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<string, CommandArgValue> = {};
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;
}
};