mirror of
https://github.com/arthur-pbty/flint.git
synced 2026-08-01 20:29:03 +02:00
feat: ajouter la gestion des réponses pour les commandes de préfixe et de slash
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { MessageFlags } from "discord.js";
|
||||
import { defineCommand } from "../../framework/commands/defineCommand.js";
|
||||
|
||||
export const pingCommand = defineCommand({
|
||||
@@ -14,10 +15,11 @@ export const pingCommand = defineCommand({
|
||||
const responses = (ctx.commandText.responses as Record<string, unknown> | undefined) ?? {};
|
||||
const template = typeof responses.pong === "string" ? responses.pong : "Pong {{latency}}ms";
|
||||
|
||||
await ctx.reply(
|
||||
ctx.format(template, {
|
||||
await ctx.reply({
|
||||
content: ctx.format(template, {
|
||||
latency: ctx.client.ws.ping,
|
||||
}),
|
||||
);
|
||||
flags: [MessageFlags.Ephemeral],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { CommandRegistry } from "../commands/registry.js";
|
||||
import type { I18nService } from "../i18n/I18nService.js";
|
||||
import type {
|
||||
BotCommand,
|
||||
CommandExecutionContext,
|
||||
CommandSource,
|
||||
SupportedLang,
|
||||
TranslationVars,
|
||||
} from "../types/command.js";
|
||||
|
||||
export interface HandlerExecutionDeps {
|
||||
registry: CommandRegistry;
|
||||
i18n: I18nService;
|
||||
prefix: string;
|
||||
defaultLang: SupportedLang;
|
||||
}
|
||||
|
||||
interface BuildExecutionContextInput {
|
||||
command: BotCommand;
|
||||
source: CommandSource;
|
||||
lang: SupportedLang;
|
||||
args: CommandExecutionContext["args"];
|
||||
client: CommandExecutionContext["client"];
|
||||
user: CommandExecutionContext["user"];
|
||||
guild: CommandExecutionContext["guild"];
|
||||
channel: CommandExecutionContext["channel"];
|
||||
raw: CommandExecutionContext["raw"];
|
||||
reply: CommandExecutionContext["reply"];
|
||||
}
|
||||
|
||||
export const createTranslator = (
|
||||
i18n: I18nService,
|
||||
lang: SupportedLang,
|
||||
): ((key: string, vars?: TranslationVars) => string) => {
|
||||
return (key: string, vars?: TranslationVars): string => i18n.t(lang, key, vars);
|
||||
};
|
||||
|
||||
export const buildCommandExecutionContext = (
|
||||
deps: HandlerExecutionDeps,
|
||||
input: BuildExecutionContextInput,
|
||||
): CommandExecutionContext => {
|
||||
const t = createTranslator(deps.i18n, input.lang);
|
||||
const ct = (relativeKey: string, vars?: TranslationVars): string =>
|
||||
deps.i18n.commandT(input.lang, input.command.meta.name, relativeKey, vars);
|
||||
|
||||
return {
|
||||
client: input.client,
|
||||
user: input.user,
|
||||
guild: input.guild,
|
||||
channel: input.channel,
|
||||
lang: input.lang,
|
||||
args: input.args,
|
||||
command: input.command,
|
||||
source: input.source,
|
||||
t,
|
||||
ct,
|
||||
commandText: deps.i18n.commandObject(input.lang, input.command.meta.name),
|
||||
format: (template, vars) => deps.i18n.format(template, vars),
|
||||
i18n: deps.i18n,
|
||||
raw: input.raw,
|
||||
reply: input.reply,
|
||||
registry: deps.registry,
|
||||
prefix: deps.prefix,
|
||||
defaultLang: deps.defaultLang,
|
||||
};
|
||||
};
|
||||
@@ -1,31 +1,19 @@
|
||||
import type { InteractionReplyOptions, Message, MessageReplyOptions } from "discord.js";
|
||||
import type { Message } from "discord.js";
|
||||
|
||||
import { parsePrefixArgs } from "../commands/argParser.js";
|
||||
import { buildPrefixUsage } from "../commands/usage.js";
|
||||
import type { CommandRegistry } from "../commands/registry.js";
|
||||
import { CommandExecutor } from "../execution/CommandExecutor.js";
|
||||
import type { I18nService } from "../i18n/I18nService.js";
|
||||
import type { ReplyPayload, SupportedLang } from "../types/command.js";
|
||||
import {
|
||||
buildCommandExecutionContext,
|
||||
createTranslator,
|
||||
type HandlerExecutionDeps,
|
||||
} from "./commandExecutionContext.js";
|
||||
import { createPrefixReply } from "./replyAdapter.js";
|
||||
|
||||
interface PrefixHandlerDeps {
|
||||
registry: CommandRegistry;
|
||||
i18n: I18nService;
|
||||
interface PrefixHandlerDeps extends HandlerExecutionDeps {
|
||||
executor: CommandExecutor;
|
||||
prefix: string;
|
||||
defaultLang: SupportedLang;
|
||||
}
|
||||
|
||||
const toMessageReplyOptions = (payload: Exclude<ReplyPayload, string>): MessageReplyOptions => {
|
||||
const {
|
||||
ephemeral: _ephemeral,
|
||||
fetchReply: _fetchReply,
|
||||
withResponse: _withResponse,
|
||||
...rest
|
||||
} = payload as InteractionReplyOptions & MessageReplyOptions;
|
||||
|
||||
return rest as MessageReplyOptions;
|
||||
};
|
||||
|
||||
export const createPrefixHandler = (deps: PrefixHandlerDeps) => {
|
||||
return async (message: Message): Promise<void> => {
|
||||
if (message.author.bot || !message.content.startsWith(deps.prefix)) {
|
||||
@@ -46,10 +34,11 @@ export const createPrefixHandler = (deps: PrefixHandlerDeps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const reply = createPrefixReply(message);
|
||||
|
||||
const command = match.command;
|
||||
const lang = match.lang;
|
||||
const t = (key: string, vars?: Record<string, string | number | boolean | null | undefined>): string =>
|
||||
deps.i18n.t(lang, key, vars);
|
||||
const t = createTranslator(deps.i18n, lang);
|
||||
|
||||
const parsed = await parsePrefixArgs(message, command.args, rawArgs);
|
||||
if (parsed.errors.length > 0) {
|
||||
@@ -59,35 +48,24 @@ export const createPrefixHandler = (deps: PrefixHandlerDeps) => {
|
||||
}
|
||||
|
||||
const usage = buildPrefixUsage(command, deps.prefix, lang, deps.defaultLang, deps.i18n);
|
||||
await message.reply(t(firstError.key, { ...(firstError.vars ?? {}), usage }));
|
||||
await reply(t(firstError.key, { ...(firstError.vars ?? {}), usage }));
|
||||
return;
|
||||
}
|
||||
|
||||
const ct = (relativeKey: string, vars?: Record<string, string | number | boolean | null | undefined>): string =>
|
||||
deps.i18n.commandT(lang, command.meta.name, relativeKey, vars);
|
||||
|
||||
await deps.executor.run(command, {
|
||||
client: message.client,
|
||||
user: message.author,
|
||||
guild: message.guild,
|
||||
channel: message.channel,
|
||||
lang,
|
||||
args: parsed.values,
|
||||
await deps.executor.run(
|
||||
command,
|
||||
source: "prefix",
|
||||
t,
|
||||
ct,
|
||||
commandText: deps.i18n.commandObject(lang, command.meta.name),
|
||||
format: (template, vars) => deps.i18n.format(template, vars),
|
||||
i18n: deps.i18n,
|
||||
raw: message,
|
||||
reply: (payload: ReplyPayload) =>
|
||||
typeof payload === "string"
|
||||
? message.reply(payload)
|
||||
: message.reply(toMessageReplyOptions(payload)),
|
||||
registry: deps.registry,
|
||||
prefix: deps.prefix,
|
||||
defaultLang: deps.defaultLang,
|
||||
});
|
||||
buildCommandExecutionContext(deps, {
|
||||
command,
|
||||
source: "prefix",
|
||||
lang,
|
||||
args: parsed.values,
|
||||
client: message.client,
|
||||
user: message.author,
|
||||
guild: message.guild,
|
||||
channel: message.channel,
|
||||
raw: message,
|
||||
reply,
|
||||
}),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import type {
|
||||
ChatInputCommandInteraction,
|
||||
InteractionReplyOptions,
|
||||
Message,
|
||||
MessageReplyOptions,
|
||||
} from "discord.js";
|
||||
|
||||
import type { ReplyPayload } from "../types/command.js";
|
||||
|
||||
const PREFIX_EPHEMERAL_DELETE_DELAY_MS = 10_000;
|
||||
|
||||
const PREFIX_ALLOWED_MENTIONS_DEFAULT: NonNullable<MessageReplyOptions["allowedMentions"]> = {
|
||||
repliedUser: false,
|
||||
};
|
||||
|
||||
const SLASH_ALLOWED_MENTIONS_DEFAULT: NonNullable<InteractionReplyOptions["allowedMentions"]> = {};
|
||||
|
||||
type PrefixReplyObject = Exclude<ReplyPayload, string>;
|
||||
|
||||
const EPHEMERAL_FLAG = 64n;
|
||||
|
||||
const hasBitfieldLike = (value: unknown): value is { bitfield: bigint | number } => {
|
||||
return Boolean(value) && typeof value === "object" && "bitfield" in (value as Record<string, unknown>);
|
||||
};
|
||||
|
||||
const hasEphemeralInFlags = (flags: unknown): boolean => {
|
||||
if (flags === undefined || flags === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof flags === "number" || typeof flags === "bigint") {
|
||||
return (BigInt(flags) & EPHEMERAL_FLAG) !== 0n;
|
||||
}
|
||||
|
||||
if (typeof flags === "string") {
|
||||
return flags === "Ephemeral" || flags === "64";
|
||||
}
|
||||
|
||||
if (Array.isArray(flags)) {
|
||||
return flags.some((entry) => hasEphemeralInFlags(entry));
|
||||
}
|
||||
|
||||
if (hasBitfieldLike(flags)) {
|
||||
return (BigInt(flags.bitfield) & EPHEMERAL_FLAG) !== 0n;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const hasEphemeral = (payload: PrefixReplyObject): boolean => {
|
||||
const slashPayload = payload as InteractionReplyOptions;
|
||||
return hasEphemeralInFlags(slashPayload.flags);
|
||||
};
|
||||
|
||||
const scheduleDelete = (message: Message): void => {
|
||||
setTimeout(() => {
|
||||
void message.delete().catch(() => undefined);
|
||||
}, PREFIX_EPHEMERAL_DELETE_DELAY_MS);
|
||||
};
|
||||
|
||||
const withPrefixAllowedMentions = (options: MessageReplyOptions): MessageReplyOptions => {
|
||||
return {
|
||||
...options,
|
||||
allowedMentions: {
|
||||
...PREFIX_ALLOWED_MENTIONS_DEFAULT,
|
||||
...(options.allowedMentions ?? {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const withSlashAllowedMentions = (options: InteractionReplyOptions): InteractionReplyOptions => {
|
||||
return {
|
||||
...options,
|
||||
allowedMentions: {
|
||||
...SLASH_ALLOWED_MENTIONS_DEFAULT,
|
||||
...(options.allowedMentions ?? {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const toMessageReplyOptions = (payload: Exclude<ReplyPayload, string>): MessageReplyOptions => {
|
||||
const rest = { ...(payload as Record<string, unknown>) };
|
||||
|
||||
// Drop interaction-only fields so prefix replies stay valid message payloads.
|
||||
delete rest.fetchReply;
|
||||
delete rest.withResponse;
|
||||
delete rest.ephemeral;
|
||||
delete rest.flags;
|
||||
|
||||
return rest as MessageReplyOptions;
|
||||
};
|
||||
|
||||
export const createPrefixReply = (message: Message): ((payload: ReplyPayload) => Promise<unknown>) => {
|
||||
return async (payload: ReplyPayload): Promise<unknown> => {
|
||||
if (typeof payload === "string") {
|
||||
return message.reply(withPrefixAllowedMentions({ content: payload }));
|
||||
}
|
||||
|
||||
const shouldDeleteAfterDelay = hasEphemeral(payload);
|
||||
const sent = await message.reply(withPrefixAllowedMentions(toMessageReplyOptions(payload)));
|
||||
if (shouldDeleteAfterDelay) {
|
||||
scheduleDelete(sent);
|
||||
}
|
||||
|
||||
return sent;
|
||||
};
|
||||
};
|
||||
|
||||
export const createSlashReply = (
|
||||
interaction: ChatInputCommandInteraction,
|
||||
): ((payload: ReplyPayload) => Promise<unknown>) => {
|
||||
return async (payload: ReplyPayload): Promise<unknown> => {
|
||||
if (typeof payload === "string") {
|
||||
const options = withSlashAllowedMentions({ content: payload });
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
return interaction.followUp(options);
|
||||
}
|
||||
|
||||
return interaction.reply(options);
|
||||
}
|
||||
|
||||
const options = withSlashAllowedMentions(payload as InteractionReplyOptions);
|
||||
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
return interaction.followUp(options);
|
||||
}
|
||||
|
||||
return interaction.reply(options);
|
||||
};
|
||||
};
|
||||
@@ -1,36 +1,19 @@
|
||||
import type { ChatInputCommandInteraction } from "discord.js";
|
||||
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
|
||||
|
||||
import { parseSlashArgs } from "../commands/argParser.js";
|
||||
import { buildSlashUsage } from "../commands/usage.js";
|
||||
import type { CommandRegistry } from "../commands/registry.js";
|
||||
import { CommandExecutor } from "../execution/CommandExecutor.js";
|
||||
import type { I18nService } from "../i18n/I18nService.js";
|
||||
import type { ReplyPayload, SupportedLang } from "../types/command.js";
|
||||
import {
|
||||
buildCommandExecutionContext,
|
||||
createTranslator,
|
||||
type HandlerExecutionDeps,
|
||||
} from "./commandExecutionContext.js";
|
||||
import { createSlashReply } from "./replyAdapter.js";
|
||||
|
||||
interface SlashHandlerDeps {
|
||||
registry: CommandRegistry;
|
||||
i18n: I18nService;
|
||||
interface SlashHandlerDeps extends HandlerExecutionDeps {
|
||||
executor: CommandExecutor;
|
||||
prefix: string;
|
||||
defaultLang: SupportedLang;
|
||||
}
|
||||
|
||||
const resolveReply = async (interaction: ChatInputCommandInteraction, payload: ReplyPayload): Promise<unknown> => {
|
||||
if (typeof payload === "string") {
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
return interaction.followUp({ content: payload });
|
||||
}
|
||||
|
||||
return interaction.reply({ content: payload });
|
||||
}
|
||||
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
return interaction.followUp(payload as never);
|
||||
}
|
||||
|
||||
return interaction.reply(payload as never);
|
||||
};
|
||||
|
||||
export const createSlashHandler = (deps: SlashHandlerDeps) => {
|
||||
return async (interaction: ChatInputCommandInteraction): Promise<void> => {
|
||||
const command = deps.registry.findBySlashTrigger(interaction.commandName);
|
||||
@@ -39,8 +22,8 @@ export const createSlashHandler = (deps: SlashHandlerDeps) => {
|
||||
}
|
||||
|
||||
const lang = deps.i18n.resolveLang(interaction.locale ?? interaction.guildLocale);
|
||||
const t = (key: string, vars?: Record<string, string | number | boolean | null | undefined>): string =>
|
||||
deps.i18n.t(lang, key, vars);
|
||||
const reply = createSlashReply(interaction);
|
||||
const t = createTranslator(deps.i18n, lang);
|
||||
|
||||
const parsed = parseSlashArgs(interaction, command.args);
|
||||
if (parsed.errors.length > 0) {
|
||||
@@ -50,35 +33,27 @@ export const createSlashHandler = (deps: SlashHandlerDeps) => {
|
||||
}
|
||||
|
||||
const usage = buildSlashUsage(command, lang, deps.i18n);
|
||||
await resolveReply(interaction, {
|
||||
await reply({
|
||||
content: t(firstError.key, { ...(firstError.vars ?? {}), usage }),
|
||||
ephemeral: true,
|
||||
flags: [MessageFlags.Ephemeral],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const ct = (relativeKey: string, vars?: Record<string, string | number | boolean | null | undefined>): string =>
|
||||
deps.i18n.commandT(lang, command.meta.name, relativeKey, vars);
|
||||
|
||||
await deps.executor.run(command, {
|
||||
client: interaction.client,
|
||||
user: interaction.user,
|
||||
guild: interaction.guild,
|
||||
channel: interaction.channel,
|
||||
lang,
|
||||
args: parsed.values,
|
||||
await deps.executor.run(
|
||||
command,
|
||||
source: "slash",
|
||||
t,
|
||||
ct,
|
||||
commandText: deps.i18n.commandObject(lang, command.meta.name),
|
||||
format: (template, vars) => deps.i18n.format(template, vars),
|
||||
i18n: deps.i18n,
|
||||
raw: interaction,
|
||||
reply: (payload: ReplyPayload) => resolveReply(interaction, payload),
|
||||
registry: deps.registry,
|
||||
prefix: deps.prefix,
|
||||
defaultLang: deps.defaultLang,
|
||||
});
|
||||
buildCommandExecutionContext(deps, {
|
||||
command,
|
||||
source: "slash",
|
||||
lang,
|
||||
args: parsed.values,
|
||||
client: interaction.client,
|
||||
user: interaction.user,
|
||||
guild: interaction.guild,
|
||||
channel: interaction.channel,
|
||||
raw: interaction,
|
||||
reply,
|
||||
}),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user