Files
flint/apps/bot/src/legacy/features/logs/commandPanel.ts
T
Puechberty Arthur a3c0b0b9b4 ajoute du support de toute ces langues :
fr → Français
en → Anglais
es → Espagnol
de → Allemand
ja → Japonais
pt → Portugais
ru → Russe
it → Italien
nl → Néerlandais
pl → Polonais
zh → Chinois
hi → Hindi
ar → Arabe
bn → Bengali
id → Indonésien
tr → Turc
2026-04-21 19:22:33 +02:00

378 lines
12 KiB
TypeScript

import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ContainerBuilder,
MessageFlags,
TextDisplayBuilder,
} from "discord.js";
import { resolveReplyMessage } from "../../core/discord/replyMessageResolver.js";
import { createScopedLogger } from "../../core/logging/logger.js";
import type { CommandExecutionContext } from "../../types/command.js";
import type {
LogEventDefinition,
LogEventKey,
LogEventStateByKey,
LogPanelCustomIds,
} from "../../types/logs.js";
import {
LOG_EVENT_DEFINITIONS,
LOG_EVENT_KEYS,
LOGS_PANEL_EVENTS_PER_PAGE,
} from "./catalog.js";
import type { LogEventService } from "./service.js";
const logger = createScopedLogger("command:logs");
interface LogPanelUiState {
pageIndex: number;
feedback: string | null;
}
const chunk = <T>(items: readonly T[], size: number): T[][] => {
const output: T[][] = [];
for (let index = 0; index < items.length; index += size) {
output.push(items.slice(index, index + size));
}
return output;
};
const createCustomIds = (): LogPanelCustomIds => {
const nonce = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`;
const toggleButtonsByEvent = {} as Record<LogEventKey, string>;
for (const eventKey of LOG_EVENT_KEYS) {
toggleButtonsByEvent[eventKey] = `logs:toggle:${eventKey}:${nonce}`;
}
return {
enableAllButton: `logs:enable-all:${nonce}`,
disableAllButton: `logs:disable-all:${nonce}`,
createChannelsButton: `logs:create-channels:${nonce}`,
previousPageButton: `logs:page-prev:${nonce}`,
nextPageButton: `logs:page-next:${nonce}`,
toggleButtonsByEvent,
};
};
const clampPageIndex = (index: number, pageCount: number): number => {
if (index < 0) {
return 0;
}
if (index >= pageCount) {
return pageCount - 1;
}
return index;
};
const pageCount = (): number => Math.max(1, Math.ceil(LOG_EVENT_DEFINITIONS.length / LOGS_PANEL_EVENTS_PER_PAGE));
const pageEvents = (uiState: LogPanelUiState): LogEventDefinition[] => {
const totalPages = pageCount();
uiState.pageIndex = clampPageIndex(uiState.pageIndex, totalPages);
const start = uiState.pageIndex * LOGS_PANEL_EVENTS_PER_PAGE;
return LOG_EVENT_DEFINITIONS.slice(start, start + LOGS_PANEL_EVENTS_PER_PAGE);
};
const eventStatusLabel = (ctx: CommandExecutionContext, enabled: boolean): string => {
return enabled ? ctx.ct("ui.status.enabled") : ctx.ct("ui.status.disabled");
};
const eventLabel = (ctx: CommandExecutionContext, eventKey: LogEventKey): string => {
return ctx.ct(`ui.events.${eventKey}`);
};
const panelContent = (
ctx: CommandExecutionContext,
state: LogEventStateByKey,
uiState: LogPanelUiState,
): string => {
const totalPages = pageCount();
const currentEvents = pageEvents(uiState);
const enabledCount = LOG_EVENT_KEYS.filter((eventKey) => state[eventKey].enabled).length;
const lines = [
`## ${ctx.ct("ui.embed.title")}`,
ctx.ct("ui.embed.description"),
"",
ctx.ct("ui.pageLabel", { page: uiState.pageIndex + 1, pageCount: totalPages }),
ctx.ct("ui.enabledCountLabel", { enabledCount, totalCount: LOG_EVENT_KEYS.length }),
"",
`${ctx.ct("ui.eventsHeader")}:`,
];
for (const definition of currentEvents) {
const eventConfig = state[definition.key];
const channelDisplay = eventConfig.channelId ? `<#${eventConfig.channelId}>` : ctx.ct("ui.channelNotConfigured");
lines.push(`- ${eventLabel(ctx, definition.key)} | ${eventStatusLabel(ctx, eventConfig.enabled)} | ${ctx.ct("ui.channelLabel")}: ${channelDisplay}`);
}
if (uiState.feedback) {
lines.push("", `${ctx.ct("ui.feedbackLabel")}: ${uiState.feedback}`);
}
return lines.join("\n");
};
const buildContainer = (
ctx: CommandExecutionContext,
state: LogEventStateByKey,
customIds: LogPanelCustomIds,
uiState: LogPanelUiState,
disabled = false,
): ContainerBuilder => {
const totalPages = pageCount();
const currentEvents = pageEvents(uiState);
const enableAllButton = new ButtonBuilder()
.setCustomId(customIds.enableAllButton)
.setLabel(ctx.ct("ui.buttons.enableAll"))
.setStyle(ButtonStyle.Success)
.setDisabled(disabled);
const disableAllButton = new ButtonBuilder()
.setCustomId(customIds.disableAllButton)
.setLabel(ctx.ct("ui.buttons.disableAll"))
.setStyle(ButtonStyle.Danger)
.setDisabled(disabled);
const createChannelsButton = new ButtonBuilder()
.setCustomId(customIds.createChannelsButton)
.setLabel(ctx.ct("ui.buttons.createChannels"))
.setStyle(ButtonStyle.Primary)
.setDisabled(disabled);
const previousPageButton = new ButtonBuilder()
.setCustomId(customIds.previousPageButton)
.setLabel(ctx.ct("ui.buttons.previousPage"))
.setStyle(ButtonStyle.Secondary)
.setDisabled(disabled || uiState.pageIndex <= 0);
const nextPageButton = new ButtonBuilder()
.setCustomId(customIds.nextPageButton)
.setLabel(ctx.ct("ui.buttons.nextPage"))
.setStyle(ButtonStyle.Secondary)
.setDisabled(disabled || uiState.pageIndex >= totalPages - 1);
const container = new ContainerBuilder();
container.addTextDisplayComponents(new TextDisplayBuilder().setContent(panelContent(ctx, state, uiState)));
container.addActionRowComponents(
new ActionRowBuilder<ButtonBuilder>().addComponents(enableAllButton, disableAllButton, createChannelsButton),
);
for (const definitionChunk of chunk(currentEvents, 5)) {
const rowButtons = definitionChunk.map((definition) => {
const eventConfig = state[definition.key];
const localizedEvent = eventLabel(ctx, definition.key);
const label = eventConfig.enabled
? ctx.ct("ui.buttons.disableEvent", { event: localizedEvent })
: ctx.ct("ui.buttons.enableEvent", { event: localizedEvent });
return new ButtonBuilder()
.setCustomId(customIds.toggleButtonsByEvent[definition.key])
.setLabel(label)
.setStyle(eventConfig.enabled ? ButtonStyle.Danger : ButtonStyle.Success)
.setDisabled(disabled);
});
container.addActionRowComponents(new ActionRowBuilder<ButtonBuilder>().addComponents(...rowButtons));
}
container.addActionRowComponents(
new ActionRowBuilder<ButtonBuilder>().addComponents(previousPageButton, nextPageButton),
);
return container;
};
const applyAllEnabled = (state: LogEventStateByKey, enabled: boolean): void => {
for (const eventKey of LOG_EVENT_KEYS) {
state[eventKey].enabled = enabled;
}
};
const toToggleLookup = (customIds: LogPanelCustomIds): Map<string, LogEventKey> => {
return new Map(
LOG_EVENT_KEYS.map((eventKey) => [customIds.toggleButtonsByEvent[eventKey], eventKey]),
);
};
export const createLogsCommandExecute = (logEventService: LogEventService) => {
return async (ctx: CommandExecutionContext): Promise<void> => {
if (!ctx.guild) {
await ctx.reply(ctx.ct("responses.guildOnly"));
return;
}
const guild = ctx.guild;
const state = await logEventService.loadGuildState(ctx.client, guild.id);
const customIds = createCustomIds();
const toggleLookup = toToggleLookup(customIds);
const uiState: LogPanelUiState = {
pageIndex: 0,
feedback: null,
};
const replyResult = await ctx.reply({
flags: MessageFlags.IsComponentsV2,
components: [buildContainer(ctx, state, customIds, uiState)],
withResponse: true,
});
const replyMessage = resolveReplyMessage(replyResult);
if (!replyMessage) {
return;
}
const ownerId = ctx.user.id;
const sessionKey = logEventService.panelSessionKey(ctx.client, guild.id, ownerId);
const disablePanel = async (): Promise<void> => {
await replyMessage
.edit({
flags: MessageFlags.IsComponentsV2,
components: [buildContainer(ctx, state, customIds, uiState, true)],
})
.catch(() => undefined);
};
const collector = replyMessage.createMessageComponentCollector({ time: 15 * 60_000 });
await logEventService.replacePanelSession(sessionKey, { collector, disable: disablePanel });
collector.on("collect", async (interaction) => {
if (interaction.user.id !== ownerId) {
await interaction.reply({
content: ctx.ct("responses.notOwner"),
flags: [MessageFlags.Ephemeral],
});
return;
}
try {
if (!interaction.isButton()) {
await interaction.reply({
content: ctx.ct("responses.invalidSelection"),
flags: [MessageFlags.Ephemeral],
});
return;
}
if (interaction.customId === customIds.enableAllButton) {
applyAllEnabled(state, true);
await logEventService.persistGuildState(ctx.client, guild.id, state);
uiState.feedback = ctx.ct("responses.allEnabled");
await interaction.update({
flags: MessageFlags.IsComponentsV2,
components: [buildContainer(ctx, state, customIds, uiState)],
});
return;
}
if (interaction.customId === customIds.disableAllButton) {
applyAllEnabled(state, false);
await logEventService.persistGuildState(ctx.client, guild.id, state);
uiState.feedback = ctx.ct("responses.allDisabled");
await interaction.update({
flags: MessageFlags.IsComponentsV2,
components: [buildContainer(ctx, state, customIds, uiState)],
});
return;
}
if (interaction.customId === customIds.createChannelsButton) {
await interaction.deferUpdate();
const result = await logEventService.createCategoryChannels(ctx.client, guild, state);
if (result.failedCategories.length === 0) {
uiState.feedback = ctx.ct("responses.channelsCreated", {
created: result.createdCount,
reused: result.reusedCount,
});
} else if (result.createdCount + result.reusedCount > 0) {
uiState.feedback = ctx.ct("responses.channelsPartial", {
created: result.createdCount,
reused: result.reusedCount,
failed: result.failedCategories.join(", "),
});
} else {
uiState.feedback = ctx.ct("responses.channelsFailed", {
failed: result.failedCategories.join(", "),
});
}
await replyMessage.edit({
flags: MessageFlags.IsComponentsV2,
components: [buildContainer(ctx, state, customIds, uiState)],
});
return;
}
if (interaction.customId === customIds.previousPageButton) {
uiState.pageIndex = Math.max(0, uiState.pageIndex - 1);
await interaction.update({
flags: MessageFlags.IsComponentsV2,
components: [buildContainer(ctx, state, customIds, uiState)],
});
return;
}
if (interaction.customId === customIds.nextPageButton) {
uiState.pageIndex = Math.min(pageCount() - 1, uiState.pageIndex + 1);
await interaction.update({
flags: MessageFlags.IsComponentsV2,
components: [buildContainer(ctx, state, customIds, uiState)],
});
return;
}
const eventKey = toggleLookup.get(interaction.customId);
if (eventKey) {
state[eventKey].enabled = !state[eventKey].enabled;
await logEventService.persistGuildState(ctx.client, guild.id, state);
const localizedEvent = eventLabel(ctx, eventKey);
uiState.feedback = state[eventKey].enabled
? ctx.ct("responses.eventEnabled", { event: localizedEvent })
: ctx.ct("responses.eventDisabled", { event: localizedEvent });
await interaction.update({
flags: MessageFlags.IsComponentsV2,
components: [buildContainer(ctx, state, customIds, uiState)],
});
return;
}
await interaction.reply({
content: ctx.ct("responses.invalidSelection"),
flags: [MessageFlags.Ephemeral],
});
} catch (error) {
logger.error({ err: error }, "interaction failed");
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({
content: ctx.t("errors.execution"),
flags: [MessageFlags.Ephemeral],
}).catch(() => undefined);
return;
}
await interaction.followUp({
content: ctx.t("errors.execution"),
flags: [MessageFlags.Ephemeral],
}).catch(() => undefined);
}
});
collector.on("end", async () => {
logEventService.deletePanelSessionIfCollectorMatch(sessionKey, collector);
await disablePanel();
});
};
};