feat(discord-bot): REPORTS_JSON for reports migration
This commit is contained in:
@@ -33,4 +33,5 @@ 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(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ 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, 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, importReports, 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);
|
||||||
|
|
||||||
await questCheckCron(client);
|
await questCheckCron(client);
|
||||||
setInterval(() => void questCheckCron(client), env.WOV_FETCH_INTERVAL);
|
setInterval(() => void questCheckCron(client), env.WOV_FETCH_INTERVAL);
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ 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 } from "@lbf-bot/database";
|
import { db, tables, eq, and } from "@lbf-bot/database";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import { searchPlayer } from "~/wov";
|
import { searchPlayer } from "~/wov";
|
||||||
|
|
||||||
@@ -168,7 +169,6 @@ export const handleReportModal = async (interaction: ModalSubmitInteraction, cli
|
|||||||
.insert(tables.reports)
|
.insert(tables.reports)
|
||||||
.values({
|
.values({
|
||||||
reporterId: interaction.user.id,
|
reporterId: interaction.user.id,
|
||||||
reporterUsername: interaction.user.username,
|
|
||||||
playerName,
|
playerName,
|
||||||
playerId: player.id,
|
playerId: player.id,
|
||||||
reason,
|
reason,
|
||||||
@@ -242,6 +242,104 @@ export const handleDeleteButton = async (interaction: ButtonInteraction, reportI
|
|||||||
await interaction.editReply({ content: "Signalement supprimé." });
|
await interaction.editReply({ content: "Signalement supprimé." });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const importEntrySchema = z.object({
|
||||||
|
username: z.string(),
|
||||||
|
author_id: z.string(),
|
||||||
|
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;
|
||||||
|
|
||||||
|
for (const entry of parsed.data) {
|
||||||
|
const player = await searchPlayer(entry.username);
|
||||||
|
if (!player) {
|
||||||
|
logger.warn(`Import: player not found, skipping — ${entry.username}`);
|
||||||
|
failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await db.query.reports.findFirst({
|
||||||
|
columns: { id: true },
|
||||||
|
where: and(eq(tables.reports.playerId, player.id), eq(tables.reports.reason, entry.reason)),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [inserted] = await db
|
||||||
|
.insert(tables.reports)
|
||||||
|
.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({
|
||||||
|
content: "─────────────────────────────────",
|
||||||
|
embeds: [buildReportEmbed({ playerName: entry.username, playerId: player.id, reason: entry.reason, reporterId: entry.author_id })],
|
||||||
|
components: [reportActionRow(inserted.id)],
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageLink = `https://discord.com/channels/${reportChannel.guild.id}/${reportChannel.id}/${reportMessage.id}`;
|
||||||
|
const screenshotsMessage = await reportChannel.send({ content: "-# Pas de screenshots" });
|
||||||
|
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// avoid spamming too much the APIs
|
||||||
|
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`);
|
||||||
|
};
|
||||||
|
|
||||||
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) => {
|
||||||
const reason = interaction.fields.getTextInputValue("reason");
|
const reason = interaction.fields.getTextInputValue("reason");
|
||||||
const { attachments, screenshotUrls } = extractScreenshots(interaction.fields);
|
const { attachments, screenshotUrls } = extractScreenshots(interaction.fields);
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ 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:
|
||||||
|
|||||||
1
packages/database/drizzle/0004_curved_imperial_guard.sql
Normal file
1
packages/database/drizzle/0004_curved_imperial_guard.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "reports" DROP COLUMN "reporter_username";
|
||||||
214
packages/database/drizzle/meta/0004_snapshot.json
Normal file
214
packages/database/drizzle/meta/0004_snapshot.json
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
{
|
||||||
|
"id": "11a9aa46-c570-4372-aa71-c5a576a930f4",
|
||||||
|
"prevId": "a19383ff-8e7a-4da4-8c29-888e8ea9f670",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"screenshots_message_id": {
|
||||||
|
"name": "screenshots_message_id",
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,13 @@
|
|||||||
"when": 1778607030657,
|
"when": 1778607030657,
|
||||||
"tag": "0003_uneven_mephistopheles",
|
"tag": "0003_uneven_mephistopheles",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780166865311,
|
||||||
|
"tag": "0004_curved_imperial_guard",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,6 @@ import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|||||||
export const reports = pgTable("reports", {
|
export const reports = pgTable("reports", {
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
reporterId: text("reporter_id").notNull(),
|
reporterId: text("reporter_id").notNull(),
|
||||||
reporterUsername: text("reporter_username").notNull(),
|
|
||||||
playerName: text("player_name").notNull(),
|
playerName: text("player_name").notNull(),
|
||||||
playerId: text("player_id").notNull(),
|
playerId: text("player_id").notNull(),
|
||||||
reason: text("reason").notNull(),
|
reason: text("reason").notNull(),
|
||||||
|
|||||||
Reference in New Issue
Block a user