feat(discord-bot): remove REPORTS_JSON and implement embed migration
All checks were successful
Build and Deploy / typecheck (push) Successful in 29s
Build and Deploy / build (push) Successful in 25s
Build and Deploy / deploy (push) Successful in 4s

This commit is contained in:
2026-05-30 21:58:47 +02:00
parent 943d69472c
commit 82eb239f5e
4 changed files with 31 additions and 91 deletions

View File

@@ -33,5 +33,4 @@ export const env = parseEnv({
.transform((x) => x.split(",").map((x) => x.trim())) .transform((x) => x.split(",").map((x) => x.trim()))
.optional() .optional()
.default([]), .default([]),
REPORTS_JSON: z.string().optional(),
}); });

View File

@@ -4,13 +4,13 @@ import { env } from "~/env";
import { questCheckCron } from "~/quests"; import { questCheckCron } from "~/quests";
import { trackingCron } from "~/tracking"; import { trackingCron } from "~/tracking";
import { commands } from "~/commands"; import { commands } from "~/commands";
import { handleReportButton, handleReportModal, handleEditButton, handleDeleteButton, handleEditModal, importReports, REPORT_BUTTON_ID, REPORT_MODAL_ID, REPORT_EDIT_BUTTON_PREFIX, REPORT_DELETE_BUTTON_PREFIX, REPORT_EDIT_MODAL_PREFIX } from "~/reporting"; import { handleReportButton, handleReportModal, handleEditButton, handleDeleteButton, handleEditModal, migrateReportEmbeds, REPORT_BUTTON_ID, REPORT_MODAL_ID, REPORT_EDIT_BUTTON_PREFIX, REPORT_DELETE_BUTTON_PREFIX, REPORT_EDIT_MODAL_PREFIX } from "~/reporting";
const onReady = async (client: Client<true>) => { const onReady = async (client: Client<true>) => {
logger.info(`Client ready`); logger.info(`Client ready`);
logger.info(`Connected as @${client.user.username}`); logger.info(`Connected as @${client.user.username}`);
await importReports(client); void migrateReportEmbeds(client);
await questCheckCron(client); await questCheckCron(client);
setInterval(() => void questCheckCron(client), env.WOV_FETCH_INTERVAL); setInterval(() => void questCheckCron(client), env.WOV_FETCH_INTERVAL);

View File

@@ -3,9 +3,8 @@ import {
EmbedBuilder, FileUploadBuilder, LabelBuilder, MessageFlags, ModalBuilder, TextInputBuilder, TextInputStyle, EmbedBuilder, FileUploadBuilder, LabelBuilder, MessageFlags, ModalBuilder, TextInputBuilder, TextInputStyle,
type ButtonInteraction, type Client, type FileUploadModalData, type ModalSubmitInteraction, type TextChannel, type ButtonInteraction, type Client, type FileUploadModalData, type ModalSubmitInteraction, type TextChannel,
} from "discord.js"; } from "discord.js";
import { z } from "zod";
import { createLogger } from "@lbf-bot/utils"; import { createLogger } from "@lbf-bot/utils";
import { db, tables, eq, and } from "@lbf-bot/database"; import { db, tables, eq } from "@lbf-bot/database";
import { env } from "~/env"; import { env } from "~/env";
import { searchPlayer } from "~/wov"; import { searchPlayer } from "~/wov";
@@ -17,11 +16,18 @@ export const REPORT_EDIT_BUTTON_PREFIX = "report:edit";
export const REPORT_DELETE_BUTTON_PREFIX = "report:delete"; export const REPORT_DELETE_BUTTON_PREFIX = "report:delete";
export const REPORT_EDIT_MODAL_PREFIX = "report:edit:modal"; export const REPORT_EDIT_MODAL_PREFIX = "report:edit:modal";
const buildReportEmbed = (report: { playerName: string; playerId: string; reason: string; reporterId: string }) => const formatDate = (date: Date): string => {
const d = String(date.getDate()).padStart(2, "0");
const m = String(date.getMonth() + 1).padStart(2, "0");
return `${d}/${m}/${date.getFullYear()}`;
};
const buildReportEmbed = (report: { playerName: string; playerId: string; reason: string; reporterId: string; createdAt: Date }) =>
new EmbedBuilder() new EmbedBuilder()
.setDescription([ .setDescription([
`**Pseudo**: \`${report.playerName}\``, `**Pseudo**: \`${report.playerName}\``,
`**ID**: \`${report.playerId}\``, `**ID**: \`${report.playerId}\``,
`**Date**: \`${formatDate(report.createdAt)}\``,
`**Raison**: \`\`\`${report.reason}\`\`\``, `**Raison**: \`\`\`${report.reason}\`\`\``,
`-# Signalé par <@${report.reporterId}>` `-# Signalé par <@${report.reporterId}>`
].join("\n")); ].join("\n"));
@@ -181,7 +187,7 @@ export const handleReportModal = async (interaction: ModalSubmitInteraction, cli
if (reportChannel?.type === ChannelType.GuildText) { if (reportChannel?.type === ChannelType.GuildText) {
const reportMessage = await reportChannel.send({ const reportMessage = await reportChannel.send({
content: "─────────────────────────────────", content: "─────────────────────────────────",
embeds: [buildReportEmbed({ playerName, playerId: player.id, reason, reporterId: interaction.user.id })], embeds: [buildReportEmbed({ playerName, playerId: player.id, reason, reporterId: interaction.user.id, createdAt: new Date() })],
components: [reportActionRow(inserted.id)], components: [reportActionRow(inserted.id)],
}); });
messageLink = `https://discord.com/channels/${reportChannel.guild.id}/${reportChannel.id}/${reportMessage.id}`; messageLink = `https://discord.com/channels/${reportChannel.guild.id}/${reportChannel.id}/${reportMessage.id}`;
@@ -242,102 +248,38 @@ export const handleDeleteButton = async (interaction: ButtonInteraction, reportI
await interaction.editReply({ content: "Signalement supprimé." }); await interaction.editReply({ content: "Signalement supprimé." });
}; };
const importEntrySchema = z.object({ export const migrateReportEmbeds = async (client: Client<true>) => {
username: z.string(), const reports = await db.select().from(tables.reports);
author_id: z.string(), let migrated = 0;
reason: z.string(),
date: z.string(),
});
const parseImportDate = (date: string): Date => {
const [month, day, year] = date.split("/").map(Number);
return new Date(2000 + year, month - 1, day);
};
export const importReports = async (client: Client<true>) => {
if (!env.REPORTS_JSON) return;
let rawJson: unknown;
try {
rawJson = JSON.parse(env.REPORTS_JSON);
} catch {
logger.error("REPORTS_JSON is not valid JSON");
return;
}
const parsed = z.array(importEntrySchema).safeParse(rawJson);
if (!parsed.success) {
logger.error("Invalid REPORTS_JSON:", parsed.error.issues);
return;
}
const reportChannel = await client.channels.fetch(env.DISCORD_REPORT_CHANNEL);
if (reportChannel?.type !== ChannelType.GuildText) {
logger.error("Invalid 'DISCORD_REPORT_CHANNEL' for import");
return;
}
let imported = 0;
let skipped = 0;
let failed = 0; let failed = 0;
for (const entry of parsed.data) { for (const report of reports) {
const player = await searchPlayer(entry.username); if (!report.messageLink) continue;
if (!player) {
logger.warn(`Import: player not found, skipping — ${entry.username}`);
failed++;
continue;
}
const existing = await db.query.reports.findFirst({ const parts = report.messageLink.split("/");
columns: { id: true }, const channelId = parts.at(-2);
where: and(eq(tables.reports.playerId, player.id), eq(tables.reports.reason, entry.reason)), const messageId = parts.at(-1);
}); if (!channelId || !messageId) continue;
if (existing) {
skipped++;
continue;
}
try { try {
const [inserted] = await db const channel = await client.channels.fetch(channelId);
.insert(tables.reports) if (channel?.type !== ChannelType.GuildText) continue;
.values({
reporterId: entry.author_id,
playerName: entry.username,
playerId: player.id,
reason: entry.reason,
createdAt: parseImportDate(entry.date),
})
.returning({ id: tables.reports.id });
const reportMessage = await reportChannel.send({ const message = await channel.messages.fetch(messageId);
content: "─────────────────────────────────", await message.edit({
embeds: [buildReportEmbed({ playerName: entry.username, playerId: player.id, reason: entry.reason, reporterId: entry.author_id })], embeds: [buildReportEmbed(report)],
components: [reportActionRow(inserted.id)], components: [reportActionRow(report.id)],
}); });
migrated++;
const messageLink = `https://discord.com/channels/${reportChannel.guild.id}/${reportChannel.id}/${reportMessage.id}`; } catch {
const screenshotsMessage = await reportChannel.send({ content: "-# Pas de screenshots" }); logger.warn(`Embed migration failed for report ${report.id}`);
await db.update(tables.reports)
.set({ messageLink, screenshotsMessageId: screenshotsMessage.id })
.where(eq(tables.reports.id, inserted.id));
imported++;
} catch (error) {
logger.error(`Import: failed for ${entry.username}:`, error);
failed++; failed++;
} }
// avoid spamming too much the APIs
await new Promise(resolve => setTimeout(resolve, 500)); await new Promise(resolve => setTimeout(resolve, 500));
if (imported > 0 && imported % 50 === 0) {
await new Promise(resolve => setTimeout(resolve, 60000));
}
} }
logger.info(`Import complete: ${imported} imported, ${skipped} skipped, ${failed} not found`); logger.info(`Embed migration complete: ${migrated} updated, ${failed} failed`);
}; };
export const handleEditModal = async (interaction: ModalSubmitInteraction, client: Client, reportId: string, channelId: string, messageId: string) => { export const handleEditModal = async (interaction: ModalSubmitInteraction, client: Client, reportId: string, channelId: string, messageId: string) => {

View File

@@ -58,7 +58,6 @@ services:
- QUEST_REWARDS - QUEST_REWARDS
- QUEST_REWARDS_ARE_GEMS - QUEST_REWARDS_ARE_GEMS
- QUEST_EXCLUDE - QUEST_EXCLUDE
- REPORTS_JSON
networks: networks:
lbf-network: lbf-network: