diff --git a/apps/discord-bot/package.json b/apps/discord-bot/package.json index 7626e9f..2c4d568 100644 --- a/apps/discord-bot/package.json +++ b/apps/discord-bot/package.json @@ -17,7 +17,7 @@ "dependencies": { "@lbf-bot/database": "workspace:*", "@lbf-bot/utils": "workspace:*", - "discord.js": "^14.21.0", + "discord.js": "^14.26.4", "sharp": "^0.34.5", "zod": "4.1.11" } diff --git a/apps/discord-bot/src/commands/index.ts b/apps/discord-bot/src/commands/index.ts index f41baec..56c793b 100644 --- a/apps/discord-bot/src/commands/index.ts +++ b/apps/discord-bot/src/commands/index.ts @@ -7,6 +7,8 @@ import { gemmesCommand } from "./gemmes"; import { resultCommand } from "./result"; import { queteCommand } from "./quete"; import { tgCommand } from "./tg"; +import { reportmsgCommand } from "./reportmsg"; +import { reportsCommand } from "./reports"; export type CommandHandler = ( message: OmitPartialGroupDMChannel>, @@ -26,5 +28,7 @@ export const commands: Record = { gemmes: gemmesCommand, quete: queteCommand, result: resultCommand, - tg: tgCommand, + tg: tgCommand, + reportmsg: reportmsgCommand, + reports: reportsCommand, }; diff --git a/apps/discord-bot/src/commands/reportmsg.ts b/apps/discord-bot/src/commands/reportmsg.ts new file mode 100644 index 0000000..39e12ac --- /dev/null +++ b/apps/discord-bot/src/commands/reportmsg.ts @@ -0,0 +1,23 @@ +import { ChannelType } from "discord.js"; +import { sendReportEmbed } from "~/reporting"; +import { replyError } from "~/discord"; +import type { Command } from "./index"; +import { env } from "~/env"; + +export const reportmsgCommand: Command = { + help: "Envoie le message de signalement dans ce salon", + handler: async (message) => { + if (!message.member?.roles.cache.has(env.DISCORD_STAFF_ROLE_ID)) { + await replyError(message, "Tu t'es cru chez mémé ou quoi faut être staff"); + return; + } + + if (message.channel.type !== ChannelType.GuildText) { + await replyError(message, "Cette commande doit être utilisée dans un salon texte."); + return; + } + + await message.delete(); + await sendReportEmbed(message.channel); + }, +}; diff --git a/apps/discord-bot/src/commands/reports.ts b/apps/discord-bot/src/commands/reports.ts new file mode 100644 index 0000000..784d327 --- /dev/null +++ b/apps/discord-bot/src/commands/reports.ts @@ -0,0 +1,50 @@ +import { EmbedBuilder } from "discord.js"; +import { db, tables, eq } from "@lbf-bot/database"; +import { noMention, replyError } from "~/discord"; +import { searchPlayer } from "~/wov"; +import type { Command } from "./index"; + +export const reportsCommand: Command = { + help: "Liste tous les signalements d'un joueur", + handler: async (message, args) => { + const username = args.join(" "); + if (!username) { + await replyError(message, "Usage: `lbf reports `"); + return; + } + + const player = await searchPlayer(username); + if (!player) { + await replyError(message, `Aucun joueur avec le pseudo **${username}** n'a été trouvé.`); + return; + } + + const reports = await db + .select() + .from(tables.reports) + .where(eq(tables.reports.playerId, player.id)) + .orderBy(tables.reports.createdAt); + + if (reports.length === 0) { + await message.reply({ ...noMention, content: `Aucun signalement trouvé pour **${username}**.` }); + return; + } + + const lines = reports.map((r, i) => { + const date = r.createdAt.toLocaleDateString("fr-FR"); + const reason = r.reason.length > 100 ? `${r.reason.slice(0, 100)}…` : r.reason; + const link = r.messageLink ? ` - [voir](${r.messageLink})` : ""; + return `**${i + 1}.** ${date} - \`${reason}\`${link}`; + }); + + await message.reply({ + ...noMention, + embeds: [ + new EmbedBuilder() + .setTitle(`Signalements de ${username}`) + .setDescription(lines.join("\n")) + .setFooter({ text: `${reports.length} signalement${reports.length > 1 ? "s" : ""}` }), + ], + }); + }, +}; diff --git a/apps/discord-bot/src/env.ts b/apps/discord-bot/src/env.ts index a1656a0..9369330 100644 --- a/apps/discord-bot/src/env.ts +++ b/apps/discord-bot/src/env.ts @@ -13,6 +13,7 @@ export const env = parseEnv({ // TODO: rename to reward ask channel or smth DISCORD_ADMIN_CHANNEL: z.string(), DISCORD_TRACKING_CHANNEL: z.string(), + DISCORD_REPORT_CHANNEL: z.string(), DISCORD_STAFF_ROLE_ID: z.string(), COMMIT_SHA: z.string().default("dev"), WOV_API_KEY: z.string(), diff --git a/apps/discord-bot/src/modes/bot.ts b/apps/discord-bot/src/modes/bot.ts index e0b988d..328210b 100644 --- a/apps/discord-bot/src/modes/bot.ts +++ b/apps/discord-bot/src/modes/bot.ts @@ -4,6 +4,11 @@ import { env } from "~/env"; import { questCheckCron } from "~/quests"; import { trackingCron } from "~/tracking"; import { commands } from "~/commands"; +import { + handleReportButton, handleReportModal, handleEditButton, handleDeleteButton, handleEditModal, + 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) => { logger.info(`Client ready`); @@ -37,7 +42,30 @@ const onMessage = async (message: OmitPartialGroupDMChannel) => { } }; -export const setupBotMode = (client: Client) => { - client.on("clientReady", (client) => { void onReady(client); }); - client.on("messageCreate", (message) => { void onMessage(message); }); +const onInteraction = async (interaction: Parameters[1]>[0], client: Client) => { + if (interaction.isButton()) { + if (interaction.customId === REPORT_BUTTON_ID) { + await handleReportButton(interaction); + } else if (interaction.customId.startsWith(`${REPORT_EDIT_BUTTON_PREFIX}:`)) { + const reportId = interaction.customId.slice(REPORT_EDIT_BUTTON_PREFIX.length + 1); + await handleEditButton(interaction, reportId); + } else if (interaction.customId.startsWith(`${REPORT_DELETE_BUTTON_PREFIX}:`)) { + const reportId = interaction.customId.slice(REPORT_DELETE_BUTTON_PREFIX.length + 1); + await handleDeleteButton(interaction, reportId); + } + } else if (interaction.isModalSubmit()) { + if (interaction.customId === REPORT_MODAL_ID) { + await handleReportModal(interaction, client); + } else if (interaction.customId.startsWith(`${REPORT_EDIT_MODAL_PREFIX}:`)) { + const rest = interaction.customId.slice(REPORT_EDIT_MODAL_PREFIX.length + 1); + const [reportId, channelId, messageId] = rest.split(":"); + await handleEditModal(interaction, client, reportId, channelId, messageId); + } + } +}; + +export const setupBotMode = (client: Client) => { + client.on("clientReady", (client) => { void onReady(client); }); + client.on("messageCreate", (message) => { void onMessage(message); }); + client.on("interactionCreate", (interaction) => { void onInteraction(interaction, client); }); }; diff --git a/apps/discord-bot/src/reporting.ts b/apps/discord-bot/src/reporting.ts new file mode 100644 index 0000000..01fd5ec --- /dev/null +++ b/apps/discord-bot/src/reporting.ts @@ -0,0 +1,270 @@ +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 buildReportEmbed = (report: { playerName: string; playerId: string; reason: string; reporterId: string }) => + new EmbedBuilder() + .setDescription([ + `**Pseudo**: \`${report.playerName}\``, + `**ID**: \`${report.playerId}\``, + `**Raison**: \`\`\`${report.reason}\`\`\``, + `-# Signalé par <@${report.reporterId}>` + ].join("\n")); + +const reportActionRow = (reportId: string) => + new ActionRowBuilder().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().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().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, + reporterUsername: interaction.user.username, + 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({ + embeds: [buildReportEmbed({ playerName, playerId: player.id, reason, reporterId: interaction.user.id })], + components: [reportActionRow(inserted.id)], + }); + messageLink = `https://discord.com/channels/${reportChannel.guild.id}/${reportChannel.id}/${reportMessage.id}`; + await db.update(tables.reports).set({ messageLink }).where(eq(tables.reports.id, inserted.id)); + if (attachments) { + await reportChannel.send({ files: attachments.map(a => a.url) }); + } + } 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)); + 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 updateData: { reason: string; screenshots?: string | null } = { reason }; + if (screenshotUrls !== null) updateData.screenshots = screenshotUrls; + + await db.update(tables.reports).set(updateData).where(eq(tables.reports.id, reportId)); + + 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 (attachments) { + await channel.send({ files: attachments.map(a => a.url) }); + } + } + + await interaction.editReply({ content: "Signalement modifié." }); +}; diff --git a/packages/database/drizzle/0001_broken_dorian_gray.sql b/packages/database/drizzle/0001_broken_dorian_gray.sql new file mode 100644 index 0000000..b7eb4e9 --- /dev/null +++ b/packages/database/drizzle/0001_broken_dorian_gray.sql @@ -0,0 +1,10 @@ +CREATE TABLE "reports" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "reporter_id" text NOT NULL, + "reporter_username" text NOT NULL, + "player_name" text NOT NULL, + "player_id" text NOT NULL, + "reason" text NOT NULL, + "screenshots" text, + "created_at" timestamp DEFAULT now() NOT NULL +); diff --git a/packages/database/drizzle/0002_lucky_praxagora.sql b/packages/database/drizzle/0002_lucky_praxagora.sql new file mode 100644 index 0000000..c563006 --- /dev/null +++ b/packages/database/drizzle/0002_lucky_praxagora.sql @@ -0,0 +1 @@ +ALTER TABLE "reports" ADD COLUMN "message_link" text; \ No newline at end of file diff --git a/packages/database/drizzle/meta/0001_snapshot.json b/packages/database/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..10ba1d9 --- /dev/null +++ b/packages/database/drizzle/meta/0001_snapshot.json @@ -0,0 +1,208 @@ +{ + "id": "4e4b7960-a3a0-4ee7-8bec-4f9379eae836", + "prevId": "80475ee2-c581-462e-8075-13b1ae696df5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "player_id": { + "name": "player_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "reporter_id": { + "name": "reporter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reporter_username": { + "name": "reporter_username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "player_name": { + "name": "player_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "player_id": { + "name": "player_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "screenshots": { + "name": "screenshots", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tracked_players": { + "name": "tracked_players", + "schema": "", + "columns": { + "player_id": { + "name": "player_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.username_history": { + "name": "username_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "player_id": { + "name": "player_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "username_history_player_id_tracked_players_player_id_fk": { + "name": "username_history_player_id_tracked_players_player_id_fk", + "tableFrom": "username_history", + "tableTo": "tracked_players", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "player_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/database/drizzle/meta/0002_snapshot.json b/packages/database/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..dbe3c33 --- /dev/null +++ b/packages/database/drizzle/meta/0002_snapshot.json @@ -0,0 +1,214 @@ +{ + "id": "4643e6bd-db18-44c9-893d-8f57a94e8c0a", + "prevId": "4e4b7960-a3a0-4ee7-8bec-4f9379eae836", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "player_id": { + "name": "player_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reports": { + "name": "reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "reporter_id": { + "name": "reporter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reporter_username": { + "name": "reporter_username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "player_name": { + "name": "player_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "player_id": { + "name": "player_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "screenshots": { + "name": "screenshots", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message_link": { + "name": "message_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tracked_players": { + "name": "tracked_players", + "schema": "", + "columns": { + "player_id": { + "name": "player_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.username_history": { + "name": "username_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "player_id": { + "name": "player_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "username_history_player_id_tracked_players_player_id_fk": { + "name": "username_history_player_id_tracked_players_player_id_fk", + "tableFrom": "username_history", + "tableTo": "tracked_players", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "player_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/database/drizzle/meta/_journal.json b/packages/database/drizzle/meta/_journal.json index d3d4571..9b79311 100644 --- a/packages/database/drizzle/meta/_journal.json +++ b/packages/database/drizzle/meta/_journal.json @@ -8,6 +8,20 @@ "when": 1764882945878, "tag": "0000_tan_justin_hammer", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1778601650406, + "tag": "0001_broken_dorian_gray", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1778605638295, + "tag": "0002_lucky_praxagora", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/database/src/schema/tables.ts b/packages/database/src/schema/tables.ts index 7aec490..b7f5f95 100644 --- a/packages/database/src/schema/tables.ts +++ b/packages/database/src/schema/tables.ts @@ -1,5 +1,20 @@ import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; +/** + * REPORTING SYSTEM + */ +export const reports = pgTable("reports", { + id: uuid("id").primaryKey().defaultRandom(), + reporterId: text("reporter_id").notNull(), + reporterUsername: text("reporter_username").notNull(), + playerName: text("player_name").notNull(), + playerId: text("player_id").notNull(), + reason: text("reason").notNull(), + screenshots: text("screenshots"), + messageLink: text("message_link"), + createdAt: timestamp("created_at").notNull().defaultNow(), +}); + /** * ECONOMY SYSTEM */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d915a31..446cb97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: workspace:* version: link:../../packages/utils discord.js: - specifier: ^14.21.0 - version: 14.25.1 + specifier: ^14.26.4 + version: 14.26.4 sharp: specifier: ^0.34.5 version: 0.34.5 @@ -95,8 +95,8 @@ importers: packages: - '@discordjs/builders@1.13.1': - resolution: {integrity: sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==} + '@discordjs/builders@1.14.1': + resolution: {integrity: sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==} engines: {node: '>=16.11.0'} '@discordjs/collection@1.5.3': @@ -111,8 +111,8 @@ packages: resolution: {integrity: sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==} engines: {node: '>=16.11.0'} - '@discordjs/rest@2.6.0': - resolution: {integrity: sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==} + '@discordjs/rest@2.6.1': + resolution: {integrity: sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==} engines: {node: '>=18'} '@discordjs/util@1.2.0': @@ -795,6 +795,10 @@ packages: resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + '@sapphire/snowflake@3.5.5': + resolution: {integrity: sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -953,11 +957,11 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - discord-api-types@0.38.36: - resolution: {integrity: sha512-qrbUbjjwtyeBg5HsAlm1C859epfOyiLjPqAOzkdWlCNsZCWJrertnETF/NwM8H+waMFU58xGSc5eXUfXah+WTQ==} + discord-api-types@0.38.47: + resolution: {integrity: sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==} - discord.js@14.25.1: - resolution: {integrity: sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==} + discord.js@14.26.4: + resolution: {integrity: sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA==} engines: {node: '>=18'} dotenv@17.2.3: @@ -1254,11 +1258,11 @@ packages: lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - magic-bytes.js@1.12.1: - resolution: {integrity: sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==} + magic-bytes.js@1.13.0: + resolution: {integrity: sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==} merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} @@ -1497,8 +1501,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@6.21.3: - resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} + undici@6.24.1: + resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==} engines: {node: '>=18.17'} uri-js@4.4.1: @@ -1538,12 +1542,12 @@ packages: snapshots: - '@discordjs/builders@1.13.1': + '@discordjs/builders@1.14.1': dependencies: '@discordjs/formatters': 0.6.2 '@discordjs/util': 1.2.0 '@sapphire/shapeshift': 4.0.0 - discord-api-types: 0.38.36 + discord-api-types: 0.38.47 fast-deep-equal: 3.1.3 ts-mixer: 6.0.4 tslib: 2.8.1 @@ -1554,33 +1558,33 @@ snapshots: '@discordjs/formatters@0.6.2': dependencies: - discord-api-types: 0.38.36 + discord-api-types: 0.38.47 - '@discordjs/rest@2.6.0': + '@discordjs/rest@2.6.1': dependencies: '@discordjs/collection': 2.1.1 '@discordjs/util': 1.2.0 '@sapphire/async-queue': 1.5.5 - '@sapphire/snowflake': 3.5.3 + '@sapphire/snowflake': 3.5.5 '@vladfrangu/async_event_emitter': 2.4.7 - discord-api-types: 0.38.36 - magic-bytes.js: 1.12.1 + discord-api-types: 0.38.47 + magic-bytes.js: 1.13.0 tslib: 2.8.1 - undici: 6.21.3 + undici: 6.24.1 '@discordjs/util@1.2.0': dependencies: - discord-api-types: 0.38.36 + discord-api-types: 0.38.47 '@discordjs/ws@1.2.3': dependencies: '@discordjs/collection': 2.1.1 - '@discordjs/rest': 2.6.0 + '@discordjs/rest': 2.6.1 '@discordjs/util': 1.2.0 '@sapphire/async-queue': 1.5.5 '@types/ws': 8.18.1 '@vladfrangu/async_event_emitter': 2.4.7 - discord-api-types: 0.38.36 + discord-api-types: 0.38.47 tslib: 2.8.1 ws: 8.18.3 transitivePeerDependencies: @@ -1987,10 +1991,12 @@ snapshots: '@sapphire/shapeshift@4.0.0': dependencies: fast-deep-equal: 3.1.3 - lodash: 4.17.21 + lodash: 4.18.1 '@sapphire/snowflake@3.5.3': {} + '@sapphire/snowflake@3.5.5': {} + '@types/esrecurse@4.3.1': {} '@types/estree@1.0.9': {} @@ -2168,23 +2174,23 @@ snapshots: dependencies: path-type: 4.0.0 - discord-api-types@0.38.36: {} + discord-api-types@0.38.47: {} - discord.js@14.25.1: + discord.js@14.26.4: dependencies: - '@discordjs/builders': 1.13.1 + '@discordjs/builders': 1.14.1 '@discordjs/collection': 1.5.3 '@discordjs/formatters': 0.6.2 - '@discordjs/rest': 2.6.0 + '@discordjs/rest': 2.6.1 '@discordjs/util': 1.2.0 '@discordjs/ws': 1.2.3 '@sapphire/snowflake': 3.5.3 - discord-api-types: 0.38.36 + discord-api-types: 0.38.47 fast-deep-equal: 3.1.3 lodash.snakecase: 4.1.1 - magic-bytes.js: 1.12.1 + magic-bytes.js: 1.13.0 tslib: 2.8.1 - undici: 6.21.3 + undici: 6.24.1 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -2486,9 +2492,9 @@ snapshots: lodash.snakecase@4.1.1: {} - lodash@4.17.21: {} + lodash@4.18.1: {} - magic-bytes.js@1.12.1: {} + magic-bytes.js@1.13.0: {} merge2@1.4.1: {} @@ -2719,7 +2725,7 @@ snapshots: undici-types@6.21.0: {} - undici@6.21.3: {} + undici@6.24.1: {} uri-js@4.4.1: dependencies: