mirror of
https://github.com/arthur-pbty/flint.git
synced 2026-08-01 20:29:03 +02:00
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
378 lines
12 KiB
TypeScript
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();
|
|
});
|
|
};
|
|
}; |