300 lines
12 KiB
TypeScript
300 lines
12 KiB
TypeScript
import {
|
|
ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, ComponentType,
|
|
EmbedBuilder, FileUploadBuilder, LabelBuilder, MessageFlags, ModalBuilder, TextInputBuilder, TextInputStyle,
|
|
type ButtonInteraction, type Client, type FileUploadModalData, type ModalSubmitInteraction, type TextChannel,
|
|
} from "discord.js";
|
|
import { createLogger } from "@lbf-bot/utils";
|
|
import { db, tables, eq } from "@lbf-bot/database";
|
|
import { env } from "~/env";
|
|
import { searchPlayer } from "~/wov";
|
|
|
|
const logger = createLogger({ prefix: "reporting" });
|
|
|
|
export const REPORT_BUTTON_ID = "report:open";
|
|
export const REPORT_MODAL_ID = "report:modal";
|
|
export const REPORT_EDIT_BUTTON_PREFIX = "report:edit";
|
|
export const REPORT_DELETE_BUTTON_PREFIX = "report:delete";
|
|
export const REPORT_EDIT_MODAL_PREFIX = "report:edit:modal";
|
|
|
|
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()
|
|
.setDescription([
|
|
`**Pseudo**: \`${report.playerName}\``,
|
|
`**ID**: \`${report.playerId}\``,
|
|
`**Date**: \`${formatDate(report.createdAt)}\``,
|
|
`**Raison**: \`\`\`${report.reason}\`\`\``,
|
|
`-# Signalé par <@${report.reporterId}>`
|
|
].join("\n"));
|
|
|
|
const reportActionRow = (reportId: string) =>
|
|
new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
new ButtonBuilder()
|
|
.setCustomId(`${REPORT_EDIT_BUTTON_PREFIX}:${reportId}`)
|
|
.setLabel("Modifier")
|
|
.setStyle(ButtonStyle.Secondary),
|
|
new ButtonBuilder()
|
|
.setCustomId(`${REPORT_DELETE_BUTTON_PREFIX}:${reportId}`)
|
|
.setLabel("Supprimer")
|
|
.setStyle(ButtonStyle.Danger),
|
|
);
|
|
|
|
const isAuthorized = (interaction: ButtonInteraction | ModalSubmitInteraction, reporterId: string) => {
|
|
if (interaction.user.id === reporterId) return true;
|
|
const { member } = interaction;
|
|
if (!member) return false;
|
|
const roles = member.roles;
|
|
if (Array.isArray(roles)) return roles.includes(env.DISCORD_STAFF_ROLE_ID);
|
|
return "cache" in roles && roles.cache.has(env.DISCORD_STAFF_ROLE_ID);
|
|
};
|
|
|
|
const extractScreenshots = (fields: ModalSubmitInteraction["fields"]) => {
|
|
const fileField = fields.fields.get("screenshots") as FileUploadModalData | undefined;
|
|
const attachments = fileField?.type === ComponentType.FileUpload && fileField.attachments?.size > 0
|
|
? [...fileField.attachments.values()]
|
|
: null;
|
|
const screenshotUrls = attachments ? attachments.map(a => a.url).join("\n") : null;
|
|
return { attachments, screenshotUrls };
|
|
};
|
|
|
|
const buildModal = () =>
|
|
new ModalBuilder()
|
|
.setCustomId(REPORT_MODAL_ID)
|
|
.setTitle("Signaler un joueur")
|
|
.addLabelComponents(
|
|
new LabelBuilder()
|
|
.setLabel("Pseudo du joueur (les majuscules comptent !)")
|
|
.setTextInputComponent(
|
|
new TextInputBuilder()
|
|
.setCustomId("player_name")
|
|
.setStyle(TextInputStyle.Short)
|
|
.setRequired(true),
|
|
),
|
|
new LabelBuilder()
|
|
.setLabel("Raison du signalement")
|
|
.setTextInputComponent(
|
|
new TextInputBuilder()
|
|
.setCustomId("reason")
|
|
.setStyle(TextInputStyle.Paragraph)
|
|
.setRequired(true),
|
|
),
|
|
new LabelBuilder()
|
|
.setLabel("Screenshots (optionnel)")
|
|
.setFileUploadComponent(
|
|
new FileUploadBuilder()
|
|
.setCustomId("screenshots")
|
|
.setMaxValues(10)
|
|
.setRequired(false),
|
|
),
|
|
);
|
|
|
|
const buildEditModal = (reportId: string, channelId: string, messageId: string, currentReason: string) =>
|
|
new ModalBuilder()
|
|
.setCustomId(`${REPORT_EDIT_MODAL_PREFIX}:${reportId}:${channelId}:${messageId}`)
|
|
.setTitle("Modifier le signalement")
|
|
.addLabelComponents(
|
|
new LabelBuilder()
|
|
.setLabel("Raison du signalement")
|
|
.setTextInputComponent(
|
|
new TextInputBuilder()
|
|
.setCustomId("reason")
|
|
.setStyle(TextInputStyle.Paragraph)
|
|
.setValue(currentReason)
|
|
.setRequired(true),
|
|
),
|
|
new LabelBuilder()
|
|
.setLabel("Screenshots (optionnel)")
|
|
.setFileUploadComponent(
|
|
new FileUploadBuilder()
|
|
.setCustomId("screenshots")
|
|
.setMaxValues(10)
|
|
.setRequired(false),
|
|
),
|
|
);
|
|
|
|
const retryRow = () =>
|
|
new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
new ButtonBuilder()
|
|
.setCustomId(REPORT_BUTTON_ID)
|
|
.setLabel("Réessayer")
|
|
.setStyle(ButtonStyle.Danger),
|
|
);
|
|
|
|
export const sendReportEmbed = async (channel: TextChannel) => {
|
|
await channel.send({
|
|
embeds: [
|
|
new EmbedBuilder()
|
|
.setTitle("🚨 Signaler un joueur")
|
|
.setDescription([
|
|
"Tu as observé un comportement toxique ou de l'anti jeu ?",
|
|
"",
|
|
"Clique sur le bouton ci-dessous pour signaler un joueur.",
|
|
"Pense à fournir le plus de détails dans 'raison', tu peux aussi ajouter des screenshots.",
|
|
"",
|
|
`-# Les signalements sont envoyés dans <#${env.DISCORD_REPORT_CHANNEL}>.`,
|
|
].join("\n"))
|
|
.setColor(0xe74c3c),
|
|
],
|
|
components: [
|
|
new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
new ButtonBuilder()
|
|
.setCustomId(REPORT_BUTTON_ID)
|
|
.setLabel("Signaler un joueur")
|
|
.setStyle(ButtonStyle.Danger),
|
|
),
|
|
],
|
|
});
|
|
};
|
|
|
|
export const handleReportButton = async (interaction: ButtonInteraction) => {
|
|
await interaction.showModal(buildModal());
|
|
};
|
|
|
|
export const handleReportModal = async (interaction: ModalSubmitInteraction, client: Client) => {
|
|
const playerName = interaction.fields.getTextInputValue("player_name");
|
|
const reason = interaction.fields.getTextInputValue("reason");
|
|
const { attachments, screenshotUrls } = extractScreenshots(interaction.fields);
|
|
|
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
|
|
const player = await searchPlayer(playerName);
|
|
if (!player) {
|
|
await interaction.editReply({
|
|
content: `Aucun joueur avec le pseudo **${playerName}** n'a été trouvé. Vérifie les majuscules et réessaie.`,
|
|
components: [retryRow()],
|
|
});
|
|
return;
|
|
}
|
|
|
|
const [inserted] = await db
|
|
.insert(tables.reports)
|
|
.values({
|
|
reporterId: interaction.user.id,
|
|
playerName,
|
|
playerId: player.id,
|
|
reason,
|
|
screenshots: screenshotUrls,
|
|
})
|
|
.returning({ id: tables.reports.id });
|
|
|
|
let messageLink = "";
|
|
const reportChannel = await client.channels.fetch(env.DISCORD_REPORT_CHANNEL);
|
|
if (reportChannel?.type === ChannelType.GuildText) {
|
|
const reportMessage = await reportChannel.send({
|
|
content: "─────────────────────────────────",
|
|
embeds: [buildReportEmbed({ playerName, playerId: player.id, reason, reporterId: interaction.user.id, createdAt: new Date() })],
|
|
components: [reportActionRow(inserted.id)],
|
|
});
|
|
messageLink = `https://discord.com/channels/${reportChannel.guild.id}/${reportChannel.id}/${reportMessage.id}`;
|
|
const screenshotsMessage = attachments
|
|
? await reportChannel.send({ files: attachments.map(a => a.url) })
|
|
: await reportChannel.send({ content: "-# Pas de screenshots" });
|
|
await db.update(tables.reports)
|
|
.set({ messageLink, screenshotsMessageId: screenshotsMessage.id })
|
|
.where(eq(tables.reports.id, inserted.id));
|
|
} else {
|
|
logger.error("Invalid 'DISCORD_REPORT_CHANNEL'");
|
|
}
|
|
|
|
await interaction.editReply({
|
|
content: `Le joueur **${playerName}** a bien été signalé. Merci !${messageLink ? ` ${messageLink}` : ""}`,
|
|
});
|
|
};
|
|
|
|
export const handleEditButton = async (interaction: ButtonInteraction, reportId: string) => {
|
|
const [report] = await db.select().from(tables.reports).where(eq(tables.reports.id, reportId));
|
|
if (!report) {
|
|
await interaction.reply({ content: "Signalement introuvable.", flags: MessageFlags.Ephemeral });
|
|
return;
|
|
}
|
|
if (!isAuthorized(interaction, report.reporterId)) {
|
|
await interaction.reply({ content: "Tu n'as pas la permission de modifier ce signalement.", flags: MessageFlags.Ephemeral });
|
|
return;
|
|
}
|
|
|
|
await interaction.showModal(buildEditModal(reportId, interaction.channelId, interaction.message.id, report.reason));
|
|
};
|
|
|
|
export const handleDeleteButton = async (interaction: ButtonInteraction, reportId: string) => {
|
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
|
|
const [report] = await db.select().from(tables.reports).where(eq(tables.reports.id, reportId));
|
|
if (!report) {
|
|
await interaction.editReply({ content: "Signalement introuvable." });
|
|
return;
|
|
}
|
|
if (!isAuthorized(interaction, report.reporterId)) {
|
|
await interaction.editReply({ content: "Tu n'as pas la permission de supprimer ce signalement." });
|
|
return;
|
|
}
|
|
|
|
await db.delete(tables.reports).where(eq(tables.reports.id, reportId));
|
|
|
|
if (report.screenshotsMessageId) {
|
|
try {
|
|
const screenshotsMsg = await interaction.message.channel.messages.fetch(report.screenshotsMessageId);
|
|
await screenshotsMsg.delete();
|
|
} catch {
|
|
// already deleted or not found
|
|
}
|
|
}
|
|
|
|
await interaction.message.delete();
|
|
await interaction.editReply({ content: "Signalement supprimé." });
|
|
};
|
|
|
|
|
|
export const handleEditModal = async (interaction: ModalSubmitInteraction, client: Client, reportId: string, channelId: string, messageId: string) => {
|
|
const reason = interaction.fields.getTextInputValue("reason");
|
|
const { attachments, screenshotUrls } = extractScreenshots(interaction.fields);
|
|
|
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
|
|
const [report] = await db.select().from(tables.reports).where(eq(tables.reports.id, reportId));
|
|
if (!report) {
|
|
await interaction.editReply({ content: "Signalement introuvable." });
|
|
return;
|
|
}
|
|
if (!isAuthorized(interaction, report.reporterId)) {
|
|
await interaction.editReply({ content: "Tu n'as pas la permission de modifier ce signalement." });
|
|
return;
|
|
}
|
|
|
|
const channel = await client.channels.fetch(channelId);
|
|
if (channel?.type === ChannelType.GuildText) {
|
|
try {
|
|
const message = await channel.messages.fetch(messageId);
|
|
await message.edit({
|
|
embeds: [buildReportEmbed({ ...report, reason })],
|
|
components: [reportActionRow(reportId)],
|
|
});
|
|
} catch {
|
|
logger.error("Failed to fetch/edit report message");
|
|
}
|
|
|
|
if (report.screenshotsMessageId) {
|
|
try {
|
|
const screenshotsMsg = await channel.messages.fetch(report.screenshotsMessageId);
|
|
if (attachments) {
|
|
await screenshotsMsg.edit({ content: "", files: attachments.map(a => a.url), attachments: [] });
|
|
} else {
|
|
await screenshotsMsg.edit({ content: "-# Pas de screenshots", attachments: [] });
|
|
}
|
|
} catch {
|
|
logger.error("Failed to edit screenshots message");
|
|
}
|
|
}
|
|
}
|
|
|
|
await db.update(tables.reports)
|
|
.set({ reason, screenshots: screenshotUrls })
|
|
.where(eq(tables.reports.id, reportId));
|
|
|
|
await interaction.editReply({ content: "Signalement modifié." });
|
|
};
|