Compare commits
18 Commits
8abdfc3b2f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8420344519 | |||
| 1c2d09cafc | |||
| 4ce3e97727 | |||
| 4059ea1ddf | |||
| 82eb239f5e | |||
| 943d69472c | |||
| fcd85350ec | |||
| dab026de99 | |||
| d76ac73d84 | |||
| d679a63d3d | |||
| 2f7f8689b2 | |||
| ca13301263 | |||
| 00b3ade095 | |||
| f5a7dbf1e8 | |||
| 69e461b54b | |||
| 4b28b78800 | |||
| 8629cf246b | |||
| 528fff3a5b |
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[**/*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
44
.gitea/workflows/deploy.yml
Normal file
44
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Build and Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
typecheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
run_install: true
|
||||||
|
|
||||||
|
- name: Typecheck and Lint
|
||||||
|
run: pnpm check
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: typecheck
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: docker build -f apps/discord-bot/Dockerfile .
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Trigger Portainer webhook
|
||||||
|
run: curl -X POST "${{ secrets.PORTAINER_WEBHOOK_URL }}"
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
name: Build and Push Docker Image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Log in to DockerHub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: git.pihkaal.me
|
|
||||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
|
||||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
run: |
|
|
||||||
docker build -t git.pihkaal.me/pihkaal/lbf-bot:latest -f apps/discord-bot/Dockerfile .
|
|
||||||
docker push git.pihkaal.me/pihkaal/lbf-bot:latest
|
|
||||||
BIN
apps/discord-bot/assets/poop.png
Normal file
BIN
apps/discord-bot/assets/poop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
@@ -1,23 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "@lbf/discord-bot",
|
"name": "@lbf/discord-bot",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx src/index.ts",
|
"dev": "COMMIT_SHA=$(git rev-parse --short HEAD)-dev tsx src/index.ts",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"build": "rm -rf dist && tsc --project tsconfig.build.json && tsc-alias --project tsconfig.build.json",
|
"build": "rm -rf dist && tsc --project tsconfig.build.json && tsc-alias --project tsconfig.build.json",
|
||||||
"dev:user": "tsx src/index.ts -- --user"
|
"dev:user": "COMMIT_SHA=$(git rev-parse --short HEAD)-dev tsx src/index.ts -- --user",
|
||||||
},
|
"check": "tsc --noEmit && eslint src/"
|
||||||
"devDependencies": {
|
},
|
||||||
"@types/node": "^22.10.2",
|
"devDependencies": {
|
||||||
"tsc-alias": "^1.8.16",
|
"@types/node": "^22.10.2",
|
||||||
"tsx": "^4.19.2",
|
"tsc-alias": "^1.8.16",
|
||||||
"typescript": "^5.7.2"
|
"tsx": "^4.19.2",
|
||||||
},
|
"typescript": "^5.7.2"
|
||||||
"dependencies": {
|
},
|
||||||
"@lbf-bot/utils": "workspace:*",
|
"dependencies": {
|
||||||
"@lbf-bot/database": "workspace:*",
|
"@lbf-bot/database": "workspace:*",
|
||||||
"discord.js": "^14.21.0",
|
"@lbf-bot/utils": "workspace:*",
|
||||||
"dotenv": "^17.2.3",
|
"discord.js": "^14.26.4",
|
||||||
"zod": "^3.24.4"
|
"sharp": "^0.34.5",
|
||||||
}
|
"zod": "4.1.11"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
apps/discord-bot/src/commands/fdp.ts
Normal file
48
apps/discord-bot/src/commands/fdp.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { AttachmentBuilder } from "discord.js";
|
||||||
|
import sharp from "sharp";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { noMention } from "~/discord";
|
||||||
|
import type { Command } from "./index";
|
||||||
|
|
||||||
|
const POOP_PATH = join(dirname(fileURLToPath(import.meta.url)), "../../assets/poop.png");
|
||||||
|
|
||||||
|
export const fdpCommand: Command = {
|
||||||
|
help: "Essaie et tu verras",
|
||||||
|
handler: async (message) => {
|
||||||
|
const avatarUrl = message.author.displayAvatarURL({ size: 512, extension: "png" });
|
||||||
|
|
||||||
|
const avatarBuffer = Buffer.from(await (await fetch(avatarUrl)).arrayBuffer());
|
||||||
|
|
||||||
|
const { width: avatarWidth, height: avatarHeight } = await sharp(avatarBuffer).metadata();
|
||||||
|
if (!avatarWidth || !avatarHeight) return;
|
||||||
|
|
||||||
|
const poopWidth = Math.floor(avatarWidth / 2);
|
||||||
|
const poopBuffer = await sharp(POOP_PATH).resize({ width: poopWidth }).toBuffer();
|
||||||
|
const { height: poopHeight } = await sharp(poopBuffer).metadata();
|
||||||
|
if (!poopHeight) return;
|
||||||
|
|
||||||
|
const overlap = Math.floor(poopHeight / 4);
|
||||||
|
const poopLeft = Math.floor((avatarWidth - poopWidth) / 2);
|
||||||
|
|
||||||
|
const result = await sharp({
|
||||||
|
create: {
|
||||||
|
width: avatarWidth,
|
||||||
|
height: avatarHeight + poopHeight - overlap,
|
||||||
|
channels: 4,
|
||||||
|
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.composite([
|
||||||
|
{ input: avatarBuffer, top: poopHeight - overlap, left: 0 },
|
||||||
|
{ input: poopBuffer, top: 0, left: poopLeft },
|
||||||
|
])
|
||||||
|
.webp()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
await message.reply({
|
||||||
|
...noMention,
|
||||||
|
files: [new AttachmentBuilder(result, { name: "fdp.webp" })],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,67 +1,51 @@
|
|||||||
import { EmbedBuilder } from "discord.js";
|
import { EmbedBuilder } from "discord.js";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import type { Command } from "~/commands";
|
import { getClanMembers } from "~/wov";
|
||||||
import { getAccountBalance, setAccountBalance } from "~/services/account";
|
import { getAccountBalance, setAccountBalance } from "~/economy";
|
||||||
import { getClanMembers } from "~/services/wov";
|
import { replyError } from "~/discord";
|
||||||
import { replyError } from "~/utils/discord";
|
import type { Command } from "./index";
|
||||||
|
import { noMention } from "~/discord";
|
||||||
|
|
||||||
export const gemmesCommand: Command = async (message, args) => {
|
export const gemmesCommand: Command = {
|
||||||
// retrieve player name
|
help: "Affiche ou modifie le solde de gemmes d'un membre",
|
||||||
// NOTE: discord members have display name formatted like "🕸 | InGamePseudo"
|
handler: async (message, args) => {
|
||||||
const displayName = message.member?.displayName || message.author.username;
|
// discord members have display name formatted like "🕸 | InGamePseudo"
|
||||||
const playerName = args[0] || displayName.replace("🕸 |", "").trim();
|
const displayName = message.member?.displayName || message.author.username;
|
||||||
|
const playerName = args[0] || displayName.replace("🕸 |", "").trim();
|
||||||
|
|
||||||
// get clan member
|
const clanMembers = await getClanMembers();
|
||||||
const clanMembers = await getClanMembers();
|
const clanMember = clanMembers.find((x) => x.username === playerName);
|
||||||
const clanMember = clanMembers.find((x) => x.username === playerName);
|
if (!clanMember) {
|
||||||
if (!clanMember) {
|
await replyError(message, `\`${playerName}\` n'est pas dans le clan (la honte).\n**Attention les majuscules sont importantes**`);
|
||||||
await replyError(
|
return;
|
||||||
message,
|
}
|
||||||
`\`${playerName}\` n'est pas dans le clan (la honte).\n**Attention les majuscules sont importantes**`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle balance modification (staff only)
|
if (args.length === 2) {
|
||||||
if (args.length === 2) {
|
if (!message.member?.roles.cache.has(env.DISCORD_STAFF_ROLE_ID)) {
|
||||||
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");
|
||||||
await replyError(
|
return;
|
||||||
message,
|
}
|
||||||
"Tu t'es cru chez mémé ou quoi faut être staff",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const op = args[1][0];
|
const op = args[1][0];
|
||||||
const amount = Number(args[1].substring(1));
|
const amount = Number(args[1].substring(1));
|
||||||
if ((op !== "+" && op !== "-") || args[1].length === 1 || isNaN(amount)) {
|
if ((op !== "+" && op !== "-") || args[1].length === 1 || isNaN(amount)) {
|
||||||
await replyError(
|
await replyError(message, "Usage: `@LBF gemmes <pseudo> <+GEMMES | -GEMMES>`\nExemple: `@LBF gemmes Yuno -10000`\n**Attention les majuscules sont importantes**");
|
||||||
message,
|
return;
|
||||||
"Format: `@LBF gemmes <pseudo> <+GEMMES | -GEMMES>`\nExemple: `@LBF gemmes Yuno -10000`\n**Attention les majuscules sont importantes**",
|
}
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const balance = await getAccountBalance(clanMember.playerId);
|
const balance = await getAccountBalance(clanMember.playerId);
|
||||||
const delta = amount * (op === "+" ? 1 : -1);
|
const delta = amount * (op === "+" ? 1 : -1);
|
||||||
await setAccountBalance(clanMember.playerId, Math.max(0, balance + delta));
|
await setAccountBalance(clanMember.playerId, Math.max(0, balance + delta));
|
||||||
}
|
}
|
||||||
|
|
||||||
// display balance
|
const balance = await getAccountBalance(clanMember.playerId);
|
||||||
const balance = await getAccountBalance(clanMember.playerId);
|
await message.reply({
|
||||||
await message.reply({
|
options: noMention,
|
||||||
embeds: [
|
embeds: [
|
||||||
new EmbedBuilder()
|
new EmbedBuilder()
|
||||||
.setDescription(
|
.setDescription(`### 💎 Compte de ${playerName}\n\n\nGemmes disponibles: **${balance}**\n\n-# Voir avec ${env.DISCORD_REWARDS_GIVER} pour échanger contre skin/carte etc`)
|
||||||
// TODO: mention here instead of in the env
|
.setColor(0x4289c1)
|
||||||
`### 💎 Compte de ${playerName}\n\n\nGemmes disponibles: **${balance}**\n\n-# Voir avec ${env.DISCORD_REWARDS_GIVER} pour échanger contre skin/carte etc`,
|
],
|
||||||
)
|
});
|
||||||
.setColor(0x4289c1),
|
|
||||||
],
|
|
||||||
options: {
|
|
||||||
allowedMentions: {
|
|
||||||
repliedUser: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,57 +1,43 @@
|
|||||||
import { EmbedBuilder } from "discord.js";
|
import { EmbedBuilder } from "discord.js";
|
||||||
import type { Command } from "~/commands";
|
import { searchPlayer, getClanInfo } from "~/wov";
|
||||||
import { searchPlayer, getClanInfos } from "~/services/wov";
|
import { replyError } from "~/discord";
|
||||||
import { replyError } from "~/utils/discord";
|
import type { Command } from "./index";
|
||||||
|
import { noMention } from "~/discord";
|
||||||
|
|
||||||
export const iconeCommand: Command = async (message, args) => {
|
export const iconeCommand: Command = {
|
||||||
const playerName = args[0];
|
help: "Affiche l'icone et le nom du clan d'un joueur",
|
||||||
if (!playerName) {
|
handler: async (message, args) => {
|
||||||
await replyError(
|
const playerName = args[0];
|
||||||
message,
|
if (!playerName) {
|
||||||
"Usage:`@LBF icone NOM_JOUEUR`, exemple: `@LBF icone Yuno`.\n**Attention les majuscules sont importantes**",
|
await replyError(message, "Usage:`@LBF icone NOM_JOUEUR`, exemple: `@LBF icone Yuno`.\n**Attention les majuscules sont importantes**");
|
||||||
);
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const player = await searchPlayer(playerName);
|
const player = await searchPlayer(playerName);
|
||||||
if (!player) {
|
if (!player) {
|
||||||
await replyError(
|
await replyError(message, "Joueur·euse non trouvé·e.\n**Attention les majuscules sont importantes**");
|
||||||
message,
|
return;
|
||||||
"Joueur·euse non trouvé·e.\n**Attention les majuscules sont importantes**",
|
}
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!player.clanId) {
|
if (!player.clanId) {
|
||||||
await replyError(
|
await replyError(message, "Cette personne __n'a pas de clan__ ou __a caché son clan__.\n**Attention les majuscules sont importantes**");
|
||||||
message,
|
return;
|
||||||
"Cette personne __n'a pas de clan__ ou __a caché son clan__.\n**Attention les majuscules sont importantes**",
|
}
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const clan = await getClanInfos(player.clanId);
|
const clan = await getClanInfo(player.clanId);
|
||||||
if (!clan) {
|
if (!clan) {
|
||||||
await replyError(
|
await replyError(message, "Impossible de récupérer les informations du clan.");
|
||||||
message,
|
return;
|
||||||
"Impossible de récupérer les informations du clan.",
|
}
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await message.reply({
|
await message.reply({
|
||||||
content: clan.tag,
|
options: noMention,
|
||||||
embeds: [
|
content: clan.tag,
|
||||||
new EmbedBuilder()
|
embeds: [
|
||||||
.setDescription(
|
new EmbedBuilder()
|
||||||
`### ✅ Informations du clan\n\n**Nom:** \`\`\`${clan.name}\`\`\`\n**Tag:** \`\`\`${clan.tag}\`\`\``,
|
.setDescription(`### ✅ Informations du clan\n\n**Nom:** \`\`\`${clan.name}\`\`\`\n**Tag:** \`\`\`${clan.tag}\`\`\``)
|
||||||
)
|
.setColor(65280)
|
||||||
.setColor(65280),
|
],
|
||||||
],
|
});
|
||||||
options: {
|
|
||||||
allowedMentions: {
|
|
||||||
repliedUser: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,34 @@
|
|||||||
import type { Message, OmitPartialGroupDMChannel } from "discord.js";
|
import type { Message, OmitPartialGroupDMChannel } from "discord.js";
|
||||||
|
import { fdpCommand } from "./fdp";
|
||||||
import { pingCommand } from "./ping";
|
import { pingCommand } from "./ping";
|
||||||
import { trackCommand } from "./track";
|
import { trackCommand } from "./tracking";
|
||||||
import { tejtrackCommand } from "./tejtrack";
|
|
||||||
import { iconeCommand } from "./icone";
|
import { iconeCommand } from "./icone";
|
||||||
import { gemmesCommand } from "./gemmes";
|
import { gemmesCommand } from "./gemmes";
|
||||||
import { resultCommand } from "./result";
|
import { resultCommand } from "./result";
|
||||||
import { queteCommand } from "./quete";
|
import { queteCommand } from "./quete";
|
||||||
|
import { tgCommand } from "./tg";
|
||||||
|
import { reportmsgCommand } from "./reportmsg";
|
||||||
|
import { reportsCommand } from "./reports";
|
||||||
|
|
||||||
export type Command = (
|
export type CommandHandler = (
|
||||||
message: OmitPartialGroupDMChannel<Message<boolean>>,
|
message: OmitPartialGroupDMChannel<Message<boolean>>,
|
||||||
args: Array<string>,
|
args: Array<string>,
|
||||||
) => Promise<void> | void;
|
) => Promise<void> | void;
|
||||||
|
|
||||||
export const commands: Record<string, Command> = {
|
export type Command = {
|
||||||
ping: pingCommand,
|
help: string;
|
||||||
track: trackCommand,
|
handler: CommandHandler;
|
||||||
tejtrack: tejtrackCommand,
|
};
|
||||||
icone: iconeCommand,
|
|
||||||
gemmes: gemmesCommand,
|
export const commands: Record<string, Command> = {
|
||||||
result: resultCommand,
|
fdp: fdpCommand,
|
||||||
quete: queteCommand,
|
ping: pingCommand,
|
||||||
|
track: trackCommand,
|
||||||
|
icone: iconeCommand,
|
||||||
|
gemmes: gemmesCommand,
|
||||||
|
quete: queteCommand,
|
||||||
|
result: resultCommand,
|
||||||
|
tg: tgCommand,
|
||||||
|
reportmsg: reportmsgCommand,
|
||||||
|
reports: reportsCommand,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { Command } from "~/commands";
|
import { noMention } from "~/discord";
|
||||||
|
import type { Command } from "./index";
|
||||||
|
|
||||||
export const pingCommand: Command = async (message, args) => {
|
export const pingCommand: Command = {
|
||||||
await message.reply({
|
help: "Pong",
|
||||||
content: "🫵 Pong",
|
handler: async (message) => {
|
||||||
options: {
|
await message.reply({
|
||||||
allowedMentions: {
|
options: noMention,
|
||||||
repliedUser: false,
|
content: `🫵 Pong`,
|
||||||
},
|
});
|
||||||
},
|
},
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
import { EmbedBuilder } from "discord.js";
|
import { EmbedBuilder } from "discord.js";
|
||||||
import type { Command } from "~/commands";
|
import { getActiveQuest, getLatestQuest } from "~/wov";
|
||||||
import { getActiveQuest, getLatestQuest } from "~/services/wov";
|
import { replyError } from "~/discord";
|
||||||
|
import type { Command } from "./index";
|
||||||
|
import { noMention } from "~/discord";
|
||||||
|
|
||||||
export const queteCommand: Command = async (message) => {
|
export const queteCommand: Command = {
|
||||||
const quest = (await getActiveQuest()) ?? (await getLatestQuest());
|
help: "Affiche la quête en cours ou la dernière quête",
|
||||||
const color = parseInt(quest.quest.promoImagePrimaryColor.substring(1), 16);
|
handler: async (message) => {
|
||||||
|
const quest = (await getActiveQuest()) ?? (await getLatestQuest());
|
||||||
|
if (!quest) {
|
||||||
|
await replyError(message, "Impossible de récupérer la quête.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const color = parseInt(quest.quest.promoImagePrimaryColor.substring(1), 16);
|
||||||
|
|
||||||
await message.channel.send({
|
await message.channel.send({
|
||||||
allowedMentions: {
|
options: noMention,
|
||||||
repliedUser: false
|
embeds: [
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setTitle("Quête actuelle")
|
||||||
|
.setImage(quest.quest.promoImageUrl)
|
||||||
|
.setColor(color)
|
||||||
|
],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
embeds: [
|
|
||||||
new EmbedBuilder()
|
|
||||||
.setTitle("Quête actuelle")
|
|
||||||
.setImage(quest.quest.promoImageUrl)
|
|
||||||
.setColor(color),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
23
apps/discord-bot/src/commands/reportmsg.ts
Normal file
23
apps/discord-bot/src/commands/reportmsg.ts
Normal file
@@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
50
apps/discord-bot/src/commands/reports.ts
Normal file
50
apps/discord-bot/src/commands/reports.ts
Normal file
@@ -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 <pseudo>`");
|
||||||
|
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" : ""}` }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
import type { Command } from "~/commands";
|
import { getLatestQuest } from "~/wov";
|
||||||
import { getLatestQuest } from "~/services/wov";
|
import { askForGrinders } from "~/quests";
|
||||||
import { askForGrinders } from "~/utils/quest";
|
import { replyError } from "~/discord";
|
||||||
|
import type { Command } from "./index";
|
||||||
|
|
||||||
export const resultCommand: Command = async (message, args) => {
|
export const resultCommand: Command = {
|
||||||
const client = message.client;
|
help: "Déclenche manuellement la publication des résultats de la dernière quête",
|
||||||
const quest = await getLatestQuest();
|
handler: async (message) => {
|
||||||
await askForGrinders(quest, client);
|
const quest = await getLatestQuest();
|
||||||
|
if (!quest) {
|
||||||
|
await replyError(message, "Impossible de récupérer la dernière quête.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await askForGrinders(quest, message.client);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import type { Command } from "~/commands";
|
|
||||||
import { isWovPlayerTracked, untrackWovPlayer } from "~/services/tracking";
|
|
||||||
import { searchPlayer } from "~/services/wov";
|
|
||||||
import { replyError, createInfoEmbed, replySuccess } from "~/utils/discord";
|
|
||||||
import { env } from "~/env";
|
|
||||||
import { createLogger } from "@lbf-bot/utils";
|
|
||||||
|
|
||||||
const trackingLogger = createLogger({ prefix: "tracking" });
|
|
||||||
|
|
||||||
export const tejtrackCommand: Command = async (message, args) => {
|
|
||||||
// check staff permission
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const playerName = args[0];
|
|
||||||
if (!playerName) {
|
|
||||||
await replyError(
|
|
||||||
message,
|
|
||||||
"Usage:`@LBF untrack NOM_JOUEUR`, exemple: `@LBF untrack Yuno`.\n**Attention les majuscules sont importantes**",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const player = await searchPlayer(playerName);
|
|
||||||
if (!player) {
|
|
||||||
await replyError(
|
|
||||||
message,
|
|
||||||
"Cette personne n'existe pas.\n**Attention les majuscules sont importantes**",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await isWovPlayerTracked(player.id))) {
|
|
||||||
await replyError(
|
|
||||||
message,
|
|
||||||
`Pas de tracker pour \`${playerName}\` [\`${player.id}\`]`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await untrackWovPlayer(player.id);
|
|
||||||
|
|
||||||
await replySuccess(
|
|
||||||
message,
|
|
||||||
`Tracker enlevé pour \`${playerName}\` [\`${player.id}\`]`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const chan = message.client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL);
|
|
||||||
if (!chan?.isSendable()) {
|
|
||||||
return trackingLogger.fatal("Invalid 'DISCORD_TRACKING_CHANNEL'");
|
|
||||||
}
|
|
||||||
|
|
||||||
await chan.send(
|
|
||||||
createInfoEmbed(
|
|
||||||
`### [REMOVED] \`${playerName}\` [\`${player.id}\`]`,
|
|
||||||
0xea0000,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
9
apps/discord-bot/src/commands/tg.ts
Normal file
9
apps/discord-bot/src/commands/tg.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { noMention } from "~/discord";
|
||||||
|
import type { Command } from "./index";
|
||||||
|
|
||||||
|
export const tgCommand: Command = {
|
||||||
|
help: "...",
|
||||||
|
handler: async (message) => {
|
||||||
|
await message.reply({ ...noMention, content: "non toi tg" });
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import type { Command } from "~/commands";
|
|
||||||
import { trackWovPlayer, isWovPlayerTracked } from "~/services/tracking";
|
|
||||||
import { searchPlayer } from "~/services/wov";
|
|
||||||
import { replyError, createInfoEmbed, replySuccess } from "~/utils/discord";
|
|
||||||
import { env } from "~/env";
|
|
||||||
import { createLogger } from "@lbf-bot/utils";
|
|
||||||
|
|
||||||
const trackingLogger = createLogger({ prefix: "tracking" });
|
|
||||||
|
|
||||||
export const trackCommand: Command = async (message, args) => {
|
|
||||||
// check staff permission
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const playerName = args[0];
|
|
||||||
if (!playerName) {
|
|
||||||
await replyError(
|
|
||||||
message,
|
|
||||||
"Usage:`@LBF track NOM_JOUEUR`, exemple: `@LBF track Yuno`.\n**Attention les majuscules sont importantes**",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const player = await searchPlayer(playerName);
|
|
||||||
if (!player) {
|
|
||||||
await replyError(
|
|
||||||
message,
|
|
||||||
"Cette personne n'existe pas.\n**Attention les majuscules sont importantes**",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const alreadyTracked = await isWovPlayerTracked(player.id);
|
|
||||||
if (alreadyTracked) {
|
|
||||||
await replyError(
|
|
||||||
message,
|
|
||||||
`Tracker déjà enregistré pour \`${playerName}\` [\`${player.id}\`]`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await trackWovPlayer(player.id);
|
|
||||||
|
|
||||||
await replySuccess(
|
|
||||||
message,
|
|
||||||
`Tracker enregistré pour \`${playerName}\` [\`${player.id}\`]`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const chan = message.client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL);
|
|
||||||
if (!chan?.isSendable()) {
|
|
||||||
return trackingLogger.fatal("Invalid 'DISCORD_TRACKING_CHANNEL'");
|
|
||||||
}
|
|
||||||
|
|
||||||
await chan.send(
|
|
||||||
createInfoEmbed(`### [NEW] \`${playerName}\` [\`${player.id}\`]`),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
67
apps/discord-bot/src/commands/tracking.ts
Normal file
67
apps/discord-bot/src/commands/tracking.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { env } from "~/env";
|
||||||
|
import { searchPlayer } from "~/wov";
|
||||||
|
import { createInfoEmbed, replyError, replySuccess } from "~/discord";
|
||||||
|
import { isTracked, trackPlayer, untrackPlayer } from "~/tracking";
|
||||||
|
import type { Command } from "./index";
|
||||||
|
|
||||||
|
export const trackCommand: Command = {
|
||||||
|
help: "Gère le tracking d'un joueur",
|
||||||
|
handler: async (message, args) => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [subcommand, playerName] = args;
|
||||||
|
|
||||||
|
if (!playerName) {
|
||||||
|
await replyError(message, "Usage: `@LBF track add/rm NOM_JOUEUR`. \`add\` = ajoute, \`rm\` = retirer\n**Attention les majuscules sont importantes**");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const player = await searchPlayer(playerName);
|
||||||
|
if (!player) {
|
||||||
|
await replyError(message, "Cette personne n'existe pas.\n**Attention les majuscules sont importantes**");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const trackingChannel = message.client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL);
|
||||||
|
if (!trackingChannel?.isSendable()) {
|
||||||
|
await replyError(message, `Impossible d'envoyer un message dans <#${env.DISCORD_TRACKING_CHANNEL}>`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (subcommand) {
|
||||||
|
case "add": {
|
||||||
|
if (await isTracked(player.id)) {
|
||||||
|
await replyError(message, `Tracker déjà enregistré pour \`${playerName}\` [\`${player.id}\`]`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await trackPlayer(player.id);
|
||||||
|
await replySuccess(message, `Tracker enregistré pour \`${playerName}\` [\`${player.id}\`]`);
|
||||||
|
await trackingChannel.send(createInfoEmbed(`### [NEW] \`${playerName}\` [\`${player.id}\`]`));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "rm":
|
||||||
|
case "remove": {
|
||||||
|
if (!(await isTracked(player.id))) {
|
||||||
|
await replyError(message, `Pas de tracker pour \`${playerName}\` [\`${player.id}\`]`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await untrackPlayer(player.id);
|
||||||
|
await replySuccess(message, `Tracker enlevé pour \`${playerName}\` [\`${player.id}\`]`);
|
||||||
|
await trackingChannel.send(createInfoEmbed(`### [REMOVED] \`${playerName}\` [\`${player.id}\`]`, 0xea0000));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
await replyError(message, `Sous-commande inconnue \`${subcommand}\`. Usage: \`@LBF track add/rm NOM_JOUEUR\`. \`add\` = ajoute, \`rm\` = retirer`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
24
apps/discord-bot/src/discord.ts
Normal file
24
apps/discord-bot/src/discord.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { MessageCreateOptions, Message } from "discord.js";
|
||||||
|
|
||||||
|
export const noMention = { allowedMentions: { repliedUser: false } } as const;
|
||||||
|
|
||||||
|
export const createErrorEmbed = (message: string, color = 0xea0000): MessageCreateOptions => ({
|
||||||
|
embeds: [{ description: `### Erreur\n\n\n${message}`, color }],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createSuccessEmbed = (message: string, color = 0x00ea00): MessageCreateOptions => ({
|
||||||
|
embeds: [{ description: `### ${message}`, color }],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createInfoEmbed = (message: string, color = 0x89cff0): MessageCreateOptions => ({
|
||||||
|
embeds: [{ description: message, color }],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const replyError = (message: Message, text: string, color?: number) =>
|
||||||
|
message.reply(createErrorEmbed(text, color));
|
||||||
|
|
||||||
|
export const replySuccess = (message: Message, text: string, color?: number) =>
|
||||||
|
message.reply(createSuccessEmbed(text, color));
|
||||||
|
|
||||||
|
export const replyInfo = (message: Message, text: string, color?: number) =>
|
||||||
|
message.reply(createInfoEmbed(text, color));
|
||||||
23
apps/discord-bot/src/economy.ts
Normal file
23
apps/discord-bot/src/economy.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { db, tables, eq } from "@lbf-bot/database";
|
||||||
|
|
||||||
|
export const getAccountBalance = async (playerId: string): Promise<number> => {
|
||||||
|
const account = await db.query.accounts.findFirst({
|
||||||
|
where: eq(tables.accounts.playerId, playerId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
await setAccountBalance(playerId, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return account.balance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setAccountBalance = async (playerId: string, balance: number): Promise<void> => {
|
||||||
|
await db
|
||||||
|
.insert(tables.accounts)
|
||||||
|
.values({ playerId, balance })
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: tables.accounts.playerId,
|
||||||
|
set: { balance, updatedAt: new Date() },
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,46 +1,35 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import "dotenv/config";
|
import { parseEnv } from "@lbf-bot/utils";
|
||||||
import { logger } from "@lbf-bot/utils";
|
|
||||||
|
|
||||||
// TODO: use parseEnv from utils
|
// TODO: use parseEnv from utils
|
||||||
|
|
||||||
const schema = z.object({
|
export const env = parseEnv({
|
||||||
DISCORD_BOT_TOKEN: z.string(),
|
DISCORD_BOT_TOKEN: z.string(),
|
||||||
DISCORD_MENTION: z.string(),
|
DISCORD_MENTION: z.string(),
|
||||||
DISCORD_REWARDS_GIVER: z.string(),
|
DISCORD_REWARDS_GIVER: z.string(),
|
||||||
DISCORD_REWARDS_CHANNEL: z.string(),
|
DISCORD_REWARDS_CHANNEL: z.string(),
|
||||||
// TODO: remove and compose from staff role id
|
// TODO: remove and compose from staff role id
|
||||||
DISCORD_ADMIN_MENTION: z.string(),
|
DISCORD_ADMIN_MENTION: z.string(),
|
||||||
// TODO: rename to reward ask channel or smth
|
// TODO: rename to reward ask channel or smth
|
||||||
DISCORD_ADMIN_CHANNEL: z.string(),
|
DISCORD_ADMIN_CHANNEL: z.string(),
|
||||||
DISCORD_TRACKING_CHANNEL: z.string(),
|
DISCORD_TRACKING_CHANNEL: z.string(),
|
||||||
DISCORD_STAFF_ROLE_ID: z.string(),
|
DISCORD_REPORT_CHANNEL: z.string(),
|
||||||
WOV_API_KEY: z.string(),
|
DISCORD_STAFF_ROLE_ID: z.string(),
|
||||||
WOV_CLAN_ID: z.string(),
|
WOV_API_KEY: z.string(),
|
||||||
WOV_FETCH_INTERVAL: z.coerce.number(),
|
WOV_CLAN_ID: z.string(),
|
||||||
WOV_TRACKING_INTERVAL: z.coerce.number(),
|
WOV_FETCH_INTERVAL: z.coerce.number(),
|
||||||
QUEST_REWARDS: z
|
WOV_TRACKING_INTERVAL: z.coerce.number(),
|
||||||
.string()
|
QUEST_REWARDS: z
|
||||||
.transform((x) => x.split(",").map((x) => x.trim()))
|
.string()
|
||||||
.optional(),
|
.transform((x) => x.split(",").map((x) => x.trim()))
|
||||||
QUEST_REWARDS_ARE_GEMS: z
|
.optional(),
|
||||||
.string()
|
QUEST_REWARDS_ARE_GEMS: z
|
||||||
.transform((val) => val.toLowerCase() === "true")
|
.string()
|
||||||
.pipe(z.boolean()),
|
.transform((val) => val.toLowerCase() === "true")
|
||||||
QUEST_EXCLUDE: z
|
.pipe(z.boolean()),
|
||||||
.string()
|
QUEST_EXCLUDE: z
|
||||||
.transform((x) => x.split(",").map((x) => x.trim()))
|
.string()
|
||||||
.optional()
|
.transform((x) => x.split(",").map((x) => x.trim()))
|
||||||
.default(""),
|
.optional()
|
||||||
|
.default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = schema.safeParse(process.env);
|
|
||||||
if (!result.success) {
|
|
||||||
logger.fatal(
|
|
||||||
`❌ Invalid environments variables:\n${result.error.errors
|
|
||||||
.map((x) => `- ${x.path.join(".")}: ${x.message}`)
|
|
||||||
.join("\n")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const env = result.data;
|
|
||||||
|
|||||||
@@ -1,43 +1,58 @@
|
|||||||
|
import { logger } from "@lbf-bot/utils";
|
||||||
|
import { runMigrations } from "@lbf-bot/database";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import { Client, GatewayIntentBits, Partials } from "discord.js";
|
import { Client, GatewayIntentBits, Partials } from "discord.js";
|
||||||
import { setupBotMode } from "~/modes/bot";
|
import { setupBotMode } from "~/modes/bot";
|
||||||
import { setupUserMode } from "~/modes/user";
|
import { setupUserMode } from "~/modes/user";
|
||||||
import { parseArgs } from "~/utils/cli";
|
|
||||||
import { runMigrations } from "@lbf-bot/database";
|
|
||||||
import { logger } from "@lbf-bot/utils";
|
|
||||||
|
|
||||||
logger.info("Running database migrations...");
|
logger.info("Running database migrations...");
|
||||||
await runMigrations();
|
await runMigrations();
|
||||||
|
|
||||||
const mode = parseArgs(process.argv.slice(2));
|
type Mode = { type: "bot" } | { type: "user"; channelId: string };
|
||||||
|
let mode: Mode = { type: "bot" };
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
logger.info(`Mode: ${mode.type}`);
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i];
|
||||||
|
|
||||||
|
switch (arg) {
|
||||||
|
case "-u":
|
||||||
|
case "--as-user": {
|
||||||
|
const channelId = args[(i += 1)];
|
||||||
|
if (!channelId) {
|
||||||
|
console.error(`ERROR: ${arg} requires a channel ID`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
mode = { type: "user", channelId };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
console.error(`ERROR: Unrecognized argument: '${arg}'`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
GatewayIntentBits.Guilds,
|
GatewayIntentBits.Guilds,
|
||||||
GatewayIntentBits.GuildMessages,
|
GatewayIntentBits.GuildMessages,
|
||||||
GatewayIntentBits.MessageContent,
|
GatewayIntentBits.MessageContent,
|
||||||
GatewayIntentBits.DirectMessages,
|
GatewayIntentBits.DirectMessages,
|
||||||
],
|
],
|
||||||
partials: [Partials.Message, Partials.Channel],
|
partials: [Partials.Message, Partials.Channel],
|
||||||
});
|
});
|
||||||
|
|
||||||
switch (mode.type) {
|
logger.info(`Starting as ${mode.type}`);
|
||||||
case "user": {
|
|
||||||
|
if (mode.type === "user") {
|
||||||
setupUserMode(client, mode.channelId);
|
setupUserMode(client, mode.channelId);
|
||||||
break;
|
} else if (mode.type === "bot") {
|
||||||
}
|
|
||||||
|
|
||||||
case "bot": {
|
|
||||||
setupBotMode(client);
|
setupBotMode(client);
|
||||||
break;
|
} else {
|
||||||
}
|
// @ts-expect-error -- exhaustive default for unimplemented mode types
|
||||||
|
|
||||||
default: {
|
|
||||||
// @ts-ignore
|
|
||||||
logger.fatal(`ERROR: Not implemented: '${mode.type}'`);
|
logger.fatal(`ERROR: Not implemented: '${mode.type}'`);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.login(env.DISCORD_BOT_TOKEN);
|
await client.login(env.DISCORD_BOT_TOKEN);
|
||||||
|
|||||||
@@ -1,97 +1,67 @@
|
|||||||
import type { Client } from "discord.js";
|
import type { Client, Interaction, OmitPartialGroupDMChannel, Message } from "discord.js";
|
||||||
import { createLogger, logger } from "@lbf-bot/utils";
|
import { logger } from "@lbf-bot/utils";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import {
|
import { questCheckCron } from "~/quests";
|
||||||
listTrackedPlayers,
|
import { trackingCron } from "~/tracking";
|
||||||
getTrackedPlayerUsernames,
|
|
||||||
addUsernameToHistory,
|
|
||||||
} from "~/services/tracking";
|
|
||||||
import { checkForNewQuest, getPlayer } from "~/services/wov";
|
|
||||||
import { createInfoEmbed } from "~/utils/discord";
|
|
||||||
import { askForGrinders } from "~/utils/quest";
|
|
||||||
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";
|
||||||
|
|
||||||
const questsLogger = createLogger({ prefix: "quests" });
|
const onReady = async (client: Client<true>) => {
|
||||||
const trackingLogger = createLogger({ prefix: "tracking" });
|
|
||||||
|
|
||||||
const questCheckCron = async (client: Client) => {
|
|
||||||
questsLogger.info("Checking for new quest");
|
|
||||||
const quest = await checkForNewQuest();
|
|
||||||
if (quest) {
|
|
||||||
questsLogger.info(`New quest found: '${quest.quest.id}'`);
|
|
||||||
await askForGrinders(quest, client);
|
|
||||||
} else {
|
|
||||||
questsLogger.info("No new quest found");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const trackingCron = async (client: Client) => {
|
|
||||||
trackingLogger.info("Checking for tracked players");
|
|
||||||
const trackedPlayers = await listTrackedPlayers();
|
|
||||||
trackingLogger.info(`${trackedPlayers.length} players to check`);
|
|
||||||
for (const playerId of trackedPlayers) {
|
|
||||||
const player = await getPlayer(playerId);
|
|
||||||
if (!player) continue;
|
|
||||||
|
|
||||||
const usernames = await getTrackedPlayerUsernames(playerId);
|
|
||||||
if (usernames.includes(player.username)) continue;
|
|
||||||
|
|
||||||
await addUsernameToHistory(playerId, player.username);
|
|
||||||
|
|
||||||
const chan = client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL);
|
|
||||||
if (!chan?.isSendable()) {
|
|
||||||
return logger.fatal("Invalid 'DISCORD_TRACKING_CHANNEL'");
|
|
||||||
}
|
|
||||||
const lastUsername = usernames[usernames.length - 1];
|
|
||||||
|
|
||||||
await chan.send(
|
|
||||||
createInfoEmbed(
|
|
||||||
`### [UPDATE] \`${lastUsername}\` -> \`${player.username}\` [\`${playerId}\`]\n\n**Nouveau pseudo:** \`${player.username}\`\n**Anciens pseudos:**\n${usernames.map((x) => `- \`${x}\``).join("\n")}`,
|
|
||||||
0x00ea00,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
trackingLogger.info(
|
|
||||||
`Username changed: ${lastUsername} -> ${player.username} [${playerId}]`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setupBotMode = (client: Client) => {
|
|
||||||
client.on("clientReady", async (client) => {
|
|
||||||
logger.info(`Client ready`);
|
logger.info(`Client ready`);
|
||||||
logger.info(`Connected as @${client.user.username}`);
|
logger.info(`Connected as @${client.user.username}`);
|
||||||
|
|
||||||
await questCheckCron(client);
|
await questCheckCron(client);
|
||||||
setInterval(() => questCheckCron(client), env.WOV_FETCH_INTERVAL);
|
setInterval(() => void questCheckCron(client), env.WOV_FETCH_INTERVAL);
|
||||||
|
|
||||||
await trackingCron(client);
|
await trackingCron(client);
|
||||||
setInterval(() => trackingCron(client), env.WOV_TRACKING_INTERVAL);
|
setInterval(() => void trackingCron(client), env.WOV_TRACKING_INTERVAL);
|
||||||
});
|
};
|
||||||
|
|
||||||
client.on("messageCreate", async (message) => {
|
const onMessage = async (message: OmitPartialGroupDMChannel<Message>) => {
|
||||||
if (message.author.bot) return;
|
if (message.author.bot) return;
|
||||||
|
|
||||||
if (message.content.startsWith(`<@${client.user!.id}>`)) {
|
const parts = message.content.trim().split(/\s+/);
|
||||||
const [command, ...args] = message.content
|
if (parts[0]?.toLowerCase() === "lbf") {
|
||||||
.replace(`<@${client.user!.id}>`, "")
|
const [commandName, ...args] = parts.slice(1);
|
||||||
.trim()
|
|
||||||
.split(" ");
|
|
||||||
|
|
||||||
const commandHandler = commands[command];
|
const command = commands[commandName];
|
||||||
if (commandHandler) {
|
if (!command) return;
|
||||||
const child = logger.child(
|
|
||||||
`cmd:${command}${args.length > 0 ? " " : ""}${args.join(" ")}`,
|
const child = logger.child(`cmd:${commandName}${args.length > 0 ? " " : ""}${args.join(" ")}`);
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
await commandHandler(message, args);
|
await command.handler(message, args);
|
||||||
const end = Date.now();
|
child.info(`Done in ${(Date.now() - start).toFixed(2)}ms`);
|
||||||
child.info(`Done in ${(end - start).toFixed(2)}ms`);
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
child.error("Failed:", error);
|
child.error("Failed:", error);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const onInteraction = async (interaction: Interaction, 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); });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
import { logger } from "@lbf-bot/utils";
|
import { logger } from "@lbf-bot/utils";
|
||||||
import type { Client, TextChannel } from "discord.js";
|
import type { Client } from "discord.js";
|
||||||
import { ChannelType } from "discord.js";
|
import { ChannelType } from "discord.js";
|
||||||
import * as readline from "node:readline";
|
import * as readline from "node:readline";
|
||||||
|
|
||||||
export const setupUserMode = (client: Client, channelId: string) => {
|
export const setupUserMode = (client: Client, channelId: string) => {
|
||||||
client.on("clientReady", (client) => {
|
client.on("clientReady", (client) => {
|
||||||
logger.info(`Client ready`);
|
logger.info(`Client ready`);
|
||||||
logger.info(`Connected as @${client.user.username}`);
|
logger.info(`Connected as @${client.user.username}`);
|
||||||
|
|
||||||
const chan = client.channels.cache.get(channelId);
|
const chan = client.channels.cache.get(channelId);
|
||||||
if (chan?.type !== ChannelType.GuildText) {
|
if (chan?.type !== ChannelType.GuildText) {
|
||||||
console.error("ERROR: invalid channel");
|
console.error("ERROR: invalid channel");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout,
|
output: process.stdout,
|
||||||
prompt: `${chan.name} ~ `,
|
prompt: `${chan.name} ~ `,
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.prompt();
|
||||||
|
|
||||||
|
rl.on("line", (line) => {
|
||||||
|
if (line.trim().length > 0) {
|
||||||
|
void chan.send(line);
|
||||||
|
}
|
||||||
|
rl.prompt();
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.on("close", () => {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
rl.prompt();
|
|
||||||
|
|
||||||
rl.on("line", async (line) => {
|
|
||||||
if (line.trim().length > 0) {
|
|
||||||
await (chan as TextChannel).send(line);
|
|
||||||
}
|
|
||||||
rl.prompt();
|
|
||||||
});
|
|
||||||
|
|
||||||
rl.on("close", () => {
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
160
apps/discord-bot/src/quests.ts
Normal file
160
apps/discord-bot/src/quests.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, ComponentType, EmbedBuilder, StringSelectMenuBuilder, type Client, type MessageCreateOptions } from "discord.js";
|
||||||
|
import { createLogger } from "@lbf-bot/utils";
|
||||||
|
import { env } from "~/env";
|
||||||
|
import { checkForNewQuest, type QuestResult } from "~/wov";
|
||||||
|
import { getAccountBalance, setAccountBalance } from "~/economy";
|
||||||
|
|
||||||
|
const logger = createLogger({ prefix: "quests" });
|
||||||
|
|
||||||
|
export const makeResultEmbed = async (result: QuestResult, exclude: Array<string>): Promise<MessageCreateOptions> => {
|
||||||
|
const imageUrl = result.quest.promoImageUrl;
|
||||||
|
const color = parseInt(result.quest.promoImagePrimaryColor.substring(1), 16);
|
||||||
|
const participants = result.participants.toSorted((a, b) => b.xp - a.xp);
|
||||||
|
|
||||||
|
let rewardsEmbed: EmbedBuilder | undefined;
|
||||||
|
if (env.QUEST_REWARDS) {
|
||||||
|
const rewarded = participants
|
||||||
|
.map((x) => ({ id: x.playerId, username: x.username }))
|
||||||
|
.filter((x) => !exclude.includes(x.username));
|
||||||
|
const medals = ["🥇", "🥈", "🥉"].concat(new Array(rewarded.length).fill("🏅"));
|
||||||
|
|
||||||
|
const rewards = rewarded
|
||||||
|
.slice(0, Math.min(rewarded.length, env.QUEST_REWARDS.length))
|
||||||
|
.map((x, i) => `- ${medals[i]} ${x.username} - ${env.QUEST_REWARDS![i]} ${env.QUEST_REWARDS_ARE_GEMS ? "gemmes" : ""}`);
|
||||||
|
|
||||||
|
if (env.QUEST_REWARDS_ARE_GEMS) {
|
||||||
|
const arr = rewarded.slice(0, Math.min(rewarded.length, env.QUEST_REWARDS.length));
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
const balance = await getAccountBalance(arr[i].id);
|
||||||
|
await setAccountBalance(arr[i].id, balance + parseInt(env.QUEST_REWARDS[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rewardsEmbed = new EmbedBuilder()
|
||||||
|
.setTitle("Récompenses")
|
||||||
|
.setDescription(rewards.join("\n"))
|
||||||
|
.setColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: `-# ||${env.DISCORD_MENTION}||`,
|
||||||
|
embeds: [
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setDescription(`# Résultats de quête\n\nMerci à toutes et à tous d'avoir participé 🫡`)
|
||||||
|
.setColor(color)
|
||||||
|
.setImage(imageUrl),
|
||||||
|
...(rewardsEmbed ? [rewardsEmbed] : []),
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setTitle("Classement")
|
||||||
|
.setColor(color)
|
||||||
|
.setDescription(participants
|
||||||
|
.filter((x) => !exclude.includes(x.username) && x.xp > 0)
|
||||||
|
.filter((_, i) => i < 8)
|
||||||
|
.map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`)
|
||||||
|
.join("\n"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const askForGrinders = async (quest: QuestResult, client: Client) => {
|
||||||
|
const adminChannel = await client.channels.fetch(env.DISCORD_ADMIN_CHANNEL);
|
||||||
|
if (!adminChannel || adminChannel.type !== ChannelType.GuildText) {
|
||||||
|
return logger.fatal("Invalid 'DISCORD_ADMIN_CHANNEL'");
|
||||||
|
}
|
||||||
|
|
||||||
|
let exclude: string[] = [];
|
||||||
|
if (env.QUEST_REWARDS) {
|
||||||
|
const color = parseInt(quest.quest.promoImagePrimaryColor.substring(1), 16);
|
||||||
|
|
||||||
|
const top10 = quest.participants
|
||||||
|
.filter((x) => !env.QUEST_EXCLUDE.includes(x.username) && x.xp > 0)
|
||||||
|
.sort((a, b) => b.xp - a.xp)
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
|
if (!top10.length) {
|
||||||
|
logger.error("No eligible participants for grinder selection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectOptions = top10
|
||||||
|
.map((p) => ({ label: `${p.username} (${p.xp}xp)`, value: p.username }));
|
||||||
|
|
||||||
|
const row1 = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
|
||||||
|
new StringSelectMenuBuilder()
|
||||||
|
.setCustomId("grinders")
|
||||||
|
.setPlaceholder("Sélectionne les tricheurs")
|
||||||
|
.setMinValues(0)
|
||||||
|
.setMaxValues(selectOptions.length)
|
||||||
|
.addOptions(selectOptions),
|
||||||
|
);
|
||||||
|
|
||||||
|
const row2 = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId("validate")
|
||||||
|
.setLabel("Valider")
|
||||||
|
.setStyle(ButtonStyle.Primary),
|
||||||
|
);
|
||||||
|
|
||||||
|
const top10Text = top10
|
||||||
|
.map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const msg = await adminChannel.send({
|
||||||
|
content: `-# ||${env.DISCORD_ADMIN_MENTION}||`,
|
||||||
|
embeds: [
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setTitle("Quête terminée !")
|
||||||
|
.setColor(color),
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setTitle("Top 10 XP")
|
||||||
|
.setDescription(top10Text)
|
||||||
|
.setColor(color),
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setTitle("Qui a grind ?")
|
||||||
|
.setDescription("Sélectionne les joueurs qui ont grind (tricheurs en gros), puis clique sur **Valider**.")
|
||||||
|
.setColor(color),
|
||||||
|
],
|
||||||
|
components: [row1, row2],
|
||||||
|
});
|
||||||
|
|
||||||
|
let grinders: string[] = [];
|
||||||
|
|
||||||
|
let done = false;
|
||||||
|
while (!done) {
|
||||||
|
const interaction = await msg.awaitMessageComponent({ filter: (i) => !i.user.bot });
|
||||||
|
|
||||||
|
if (interaction.componentType === ComponentType.StringSelect) {
|
||||||
|
grinders = interaction.values;
|
||||||
|
await interaction.deferUpdate();
|
||||||
|
} else if (interaction.componentType === ComponentType.Button && interaction.customId === "validate") {
|
||||||
|
await interaction.update({ components: [] });
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exclude = grinders;
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed = await makeResultEmbed(quest, [...env.QUEST_EXCLUDE, ...exclude]);
|
||||||
|
const rewardChannel = await client.channels.fetch(env.DISCORD_REWARDS_CHANNEL);
|
||||||
|
if (!rewardChannel || rewardChannel.type !== ChannelType.GuildText) {
|
||||||
|
return logger.fatal("Invalid 'DISCORD_REWARDS_CHANNEL'");
|
||||||
|
}
|
||||||
|
|
||||||
|
await rewardChannel.send(embed);
|
||||||
|
|
||||||
|
if (env.QUEST_EXCLUDE) await adminChannel.send("Envoyé !");
|
||||||
|
logger.info(`Results posted at: ${new Date().toISOString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const questCheckCron = async (client: Client) => {
|
||||||
|
logger.info("Checking for new quest");
|
||||||
|
const quest = await checkForNewQuest();
|
||||||
|
if (quest) {
|
||||||
|
logger.info(`New quest found: '${quest.quest.id}'`);
|
||||||
|
await askForGrinders(quest, client);
|
||||||
|
} else {
|
||||||
|
logger.info("No new quest found");
|
||||||
|
}
|
||||||
|
};
|
||||||
299
apps/discord-bot/src/reporting.ts
Normal file
299
apps/discord-bot/src/reporting.ts
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
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é." });
|
||||||
|
};
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { db, tables, eq } from "@lbf-bot/database";
|
|
||||||
|
|
||||||
export const getAccountBalance = async (playerId: string): Promise<number> => {
|
|
||||||
const account = await db.query.accounts.findFirst({
|
|
||||||
where: eq(tables.accounts.playerId, playerId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (account) return account.balance;
|
|
||||||
|
|
||||||
await db.insert(tables.accounts).values({
|
|
||||||
playerId,
|
|
||||||
balance: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setAccountBalance = async (
|
|
||||||
playerId: string,
|
|
||||||
balance: number,
|
|
||||||
): Promise<void> => {
|
|
||||||
await db
|
|
||||||
.insert(tables.accounts)
|
|
||||||
.values({
|
|
||||||
playerId,
|
|
||||||
balance,
|
|
||||||
})
|
|
||||||
.onConflictDoUpdate({
|
|
||||||
target: tables.accounts.playerId,
|
|
||||||
set: { balance, updatedAt: new Date() },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { getPlayer } from "~/services/wov";
|
|
||||||
import { db, tables, eq } from "@lbf-bot/database";
|
|
||||||
|
|
||||||
export async function listTrackedPlayers(): Promise<string[]> {
|
|
||||||
const players = await db.query.trackedPlayers.findMany({
|
|
||||||
columns: {
|
|
||||||
playerId: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return players.map((p) => p.playerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function isWovPlayerTracked(playerId: string): Promise<boolean> {
|
|
||||||
const player = await db.query.trackedPlayers.findFirst({
|
|
||||||
where: eq(tables.trackedPlayers.playerId, playerId),
|
|
||||||
});
|
|
||||||
|
|
||||||
return player !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function untrackWovPlayer(playerId: string): Promise<void> {
|
|
||||||
await db
|
|
||||||
.delete(tables.trackedPlayers)
|
|
||||||
.where(eq(tables.trackedPlayers.playerId, playerId));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function trackWovPlayer(playerId: string): Promise<void> {
|
|
||||||
const alreadyTracked = await isWovPlayerTracked(playerId);
|
|
||||||
if (alreadyTracked) return;
|
|
||||||
|
|
||||||
const player = await getPlayer(playerId);
|
|
||||||
if (!player) return;
|
|
||||||
|
|
||||||
await db.insert(tables.trackedPlayers).values({
|
|
||||||
playerId,
|
|
||||||
});
|
|
||||||
|
|
||||||
await db.insert(tables.usernameHistory).values({
|
|
||||||
playerId,
|
|
||||||
username: player.username,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getTrackedPlayerUsernames(
|
|
||||||
playerId: string,
|
|
||||||
): Promise<string[]> {
|
|
||||||
const tracked = await db.query.trackedPlayers.findFirst({
|
|
||||||
where: eq(tables.trackedPlayers.playerId, playerId),
|
|
||||||
with: {
|
|
||||||
usernameHistory: {
|
|
||||||
orderBy: (history, { asc }) => [asc(history.firstSeenAt)],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!tracked) return [];
|
|
||||||
return tracked.usernameHistory.map((h) => h.username);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addUsernameToHistory(
|
|
||||||
playerId: string,
|
|
||||||
username: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await db.insert(tables.usernameHistory).values({
|
|
||||||
playerId,
|
|
||||||
username,
|
|
||||||
});
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(tables.trackedPlayers)
|
|
||||||
.set({ updatedAt: new Date() })
|
|
||||||
.where(eq(tables.trackedPlayers.playerId, playerId));
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import { env } from "~/env";
|
|
||||||
import { redis } from "@lbf-bot/database";
|
|
||||||
|
|
||||||
export type QuestResult = {
|
|
||||||
quest: {
|
|
||||||
id: string;
|
|
||||||
promoImageUrl: string;
|
|
||||||
promoImagePrimaryColor: string;
|
|
||||||
};
|
|
||||||
participants: Array<QuestParticipant>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QuestParticipant = {
|
|
||||||
playerId: string;
|
|
||||||
username: string;
|
|
||||||
xp: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getLatestQuest = async (): Promise<QuestResult> => {
|
|
||||||
const response = await fetch(
|
|
||||||
`https://api.wolvesville.com/clans/${env.WOV_CLAN_ID}/quests/history`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: { Authorization: `Bot ${env.WOV_API_KEY}` },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const history = (await response.json()) as Array<QuestResult>;
|
|
||||||
return history[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getActiveQuest = async (): Promise<QuestResult | null> => {
|
|
||||||
const response = await fetch(
|
|
||||||
`https://api.wolvesville.com/clans/${env.WOV_CLAN_ID}/quests/active`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: { Authorization: `Bot ${env.WOV_API_KEY}` },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (response.status === 404) return null;
|
|
||||||
return (await response.json()) as QuestResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const checkForNewQuest = async (): Promise<QuestResult | null> => {
|
|
||||||
const lastQuest = await getLatestQuest();
|
|
||||||
const lastId = lastQuest.quest.id;
|
|
||||||
|
|
||||||
const cachedQuestId = await redis.get("quest:last_id");
|
|
||||||
if (cachedQuestId === lastId || cachedQuestId === "IGNORE") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
await redis.set("quest:last_id", lastId);
|
|
||||||
return lastQuest;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getClanMembers = async (): Promise<
|
|
||||||
Array<{ playerId: string; username: string }>
|
|
||||||
> => {
|
|
||||||
const cached = await redis.get("clan:members");
|
|
||||||
if (cached) {
|
|
||||||
return JSON.parse(cached);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`https://api.wolvesville.com/clans/${env.WOV_CLAN_ID}/members`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: { Authorization: `Bot ${env.WOV_API_KEY}` },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const data = (await response.json()) as Array<{
|
|
||||||
playerId: string;
|
|
||||||
username: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
await redis.set("clan:members", JSON.stringify(data), "EX", 60 * 60);
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const searchPlayer = async (username: string) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`https://api.wolvesville.com//players/search?username=${username}`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: { Authorization: `Bot ${env.WOV_API_KEY}` },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.status === 404) return null;
|
|
||||||
|
|
||||||
const data = (await response.json()) as {
|
|
||||||
id: string;
|
|
||||||
clanId: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getClanInfos = async (clanId: string) => {
|
|
||||||
const response = await fetch(
|
|
||||||
`https://api.wolvesville.com/clans/${clanId}/info`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: { Authorization: `Bot ${env.WOV_API_KEY}` },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const data = (await response.json()) as {
|
|
||||||
name: string;
|
|
||||||
tag: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getPlayer(playerId: string) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`https://api.wolvesville.com/players/${playerId}`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: { Authorization: `Bot ${env.WOV_API_KEY}` },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.status === 404) return null;
|
|
||||||
|
|
||||||
const data = (await response.json()) as {
|
|
||||||
username: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
82
apps/discord-bot/src/tracking.ts
Normal file
82
apps/discord-bot/src/tracking.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import type { Client } from "discord.js";
|
||||||
|
import { createLogger, logger } from "@lbf-bot/utils";
|
||||||
|
import { db, tables, eq } from "@lbf-bot/database";
|
||||||
|
import { env } from "~/env";
|
||||||
|
import { getPlayer } from "~/wov";
|
||||||
|
import { createInfoEmbed } from "~/discord";
|
||||||
|
|
||||||
|
const trackingLogger = createLogger({ prefix: "tracking" });
|
||||||
|
|
||||||
|
export async function listTrackedPlayers(): Promise<string[]> {
|
||||||
|
const players = await db.query.trackedPlayers.findMany({
|
||||||
|
columns: { playerId: true },
|
||||||
|
});
|
||||||
|
return players.map((p) => p.playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isTracked(playerId: string): Promise<boolean> {
|
||||||
|
const player = await db.query.trackedPlayers.findFirst({
|
||||||
|
where: eq(tables.trackedPlayers.playerId, playerId),
|
||||||
|
});
|
||||||
|
return player !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function trackPlayer(playerId: string): Promise<void> {
|
||||||
|
if (await isTracked(playerId)) return;
|
||||||
|
|
||||||
|
const player = await getPlayer(playerId);
|
||||||
|
if (!player) return;
|
||||||
|
|
||||||
|
await db.insert(tables.trackedPlayers).values({ playerId });
|
||||||
|
await db.insert(tables.usernameHistory).values({ playerId, username: player.username });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function untrackPlayer(playerId: string): Promise<void> {
|
||||||
|
await db.delete(tables.trackedPlayers).where(eq(tables.trackedPlayers.playerId, playerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPlayerUsernames(playerId: string): Promise<string[]> {
|
||||||
|
const tracked = await db.query.trackedPlayers.findFirst({
|
||||||
|
where: eq(tables.trackedPlayers.playerId, playerId),
|
||||||
|
with: {
|
||||||
|
usernameHistory: { orderBy: (history, { asc }) => [asc(history.firstSeenAt)] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!tracked) return [];
|
||||||
|
return tracked.usernameHistory.map((h) => h.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addUsernameToHistory(playerId: string, username: string): Promise<void> {
|
||||||
|
await db.insert(tables.usernameHistory).values({ playerId, username });
|
||||||
|
await db
|
||||||
|
.update(tables.trackedPlayers)
|
||||||
|
.set({ updatedAt: new Date() })
|
||||||
|
.where(eq(tables.trackedPlayers.playerId, playerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const trackingCron = async (client: Client) => {
|
||||||
|
trackingLogger.info("Checking for tracked players");
|
||||||
|
const trackedPlayers = await listTrackedPlayers();
|
||||||
|
trackingLogger.info(`${trackedPlayers.length} players to check`);
|
||||||
|
|
||||||
|
for (const playerId of trackedPlayers) {
|
||||||
|
const player = await getPlayer(playerId);
|
||||||
|
if (!player) continue;
|
||||||
|
|
||||||
|
const usernames = await getPlayerUsernames(playerId);
|
||||||
|
if (usernames.includes(player.username)) continue;
|
||||||
|
|
||||||
|
await addUsernameToHistory(playerId, player.username);
|
||||||
|
|
||||||
|
const chan = client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL);
|
||||||
|
if (!chan?.isSendable()) {
|
||||||
|
return logger.fatal("Invalid 'DISCORD_TRACKING_CHANNEL'");
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastUsername = usernames[usernames.length - 1];
|
||||||
|
const description = `### [UPDATE] \`${lastUsername}\` -> \`${player.username}\` [\`${playerId}\`]\n\n**Nouveau pseudo:** \`${player.username}\`\n**Anciens pseudos:**\n${usernames.map((x) => `- \`${x}\``).join("\n")}`;
|
||||||
|
await chan.send(createInfoEmbed(description, 0x00ea00));
|
||||||
|
|
||||||
|
trackingLogger.info(`Username changed: ${lastUsername} -> ${player.username} [${playerId}]`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
type Mode = { type: "bot" } | { type: "user"; channelId: string };
|
|
||||||
|
|
||||||
export const parseArgs = (args: string[]): Mode => {
|
|
||||||
let mode: Mode = { type: "bot" };
|
|
||||||
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
|
||||||
const arg = args[i];
|
|
||||||
|
|
||||||
switch (arg) {
|
|
||||||
case "-u":
|
|
||||||
case "--as-user": {
|
|
||||||
const channelId = args[(i += 1)];
|
|
||||||
|
|
||||||
if (!channelId) {
|
|
||||||
console.error(`ERROR: ${arg} requires a channel ID`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
mode = { type: "user", channelId };
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
console.error(`ERROR: Unrecognized argument: '${arg}'`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mode;
|
|
||||||
};
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import { getAccountBalance, setAccountBalance } from "~/services/account";
|
|
||||||
import { env } from "~/env";
|
|
||||||
import type { QuestResult } from "~/services/wov";
|
|
||||||
import type { MessageCreateOptions, APIEmbed, Message } from "discord.js";
|
|
||||||
|
|
||||||
export const makeResultEmbed = async (
|
|
||||||
result: QuestResult,
|
|
||||||
exclude: Array<string>,
|
|
||||||
): Promise<MessageCreateOptions> => {
|
|
||||||
const imageUrl = result.quest.promoImageUrl;
|
|
||||||
const color = parseInt(result.quest.promoImagePrimaryColor.substring(1), 16);
|
|
||||||
const participants = result.participants.toSorted((a, b) => b.xp - a.xp);
|
|
||||||
|
|
||||||
let rewardsEmbed: APIEmbed | undefined;
|
|
||||||
if (env.QUEST_REWARDS) {
|
|
||||||
const rewardedParticipants = participants
|
|
||||||
.map((x) => ({ id: x.playerId, username: x.username }))
|
|
||||||
.filter((x) => !exclude.includes(x.username));
|
|
||||||
const medals = ["🥇", "🥈", "🥉"].concat(
|
|
||||||
new Array(rewardedParticipants.length).fill("🏅"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const rewards = rewardedParticipants
|
|
||||||
.slice(0, Math.min(rewardedParticipants.length, env.QUEST_REWARDS.length))
|
|
||||||
.map(
|
|
||||||
(x, i) =>
|
|
||||||
`- ${medals[i]} ${x.username} - ${env.QUEST_REWARDS![i]} ${env.QUEST_REWARDS_ARE_GEMS ? "gemmes" : ""}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (env.QUEST_REWARDS_ARE_GEMS) {
|
|
||||||
const arr = rewardedParticipants.slice(
|
|
||||||
0,
|
|
||||||
Math.min(rewardedParticipants.length, env.QUEST_REWARDS.length),
|
|
||||||
);
|
|
||||||
for (let i = 0; i < arr.length; i++) {
|
|
||||||
const balance = await getAccountBalance(arr[i].id);
|
|
||||||
await setAccountBalance(
|
|
||||||
arr[i].id,
|
|
||||||
balance + parseInt(env.QUEST_REWARDS![i]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rewardsEmbed = {
|
|
||||||
title: "Récompenses",
|
|
||||||
description: `${rewards.join("\n")}\n\n-# \`@LBF gemmes\` pour voir votre nombre de gemmes. Puis avec ${env.DISCORD_REWARDS_GIVER} pour échanger contre des cadeaux !`,
|
|
||||||
color,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: `-# ||${env.DISCORD_MENTION}||`,
|
|
||||||
embeds: [
|
|
||||||
{
|
|
||||||
description: `# Résultats de quête\n\nMerci à toutes et à tous d'avoir participé 🫡`,
|
|
||||||
color,
|
|
||||||
image: {
|
|
||||||
url: imageUrl,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(rewardsEmbed ? [rewardsEmbed] : []),
|
|
||||||
{
|
|
||||||
title: "Classement",
|
|
||||||
description: participants
|
|
||||||
.filter((x) => !exclude.includes(x.username))
|
|
||||||
.filter((_, i) => i < 8)
|
|
||||||
.map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`)
|
|
||||||
.join("\n"),
|
|
||||||
color,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createErrorEmbed = (
|
|
||||||
message: string,
|
|
||||||
color = 0xea0000,
|
|
||||||
): MessageCreateOptions => ({
|
|
||||||
embeds: [
|
|
||||||
{
|
|
||||||
description: `### ❌ Erreur\n\n\n${message}`,
|
|
||||||
color,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createSuccessEmbed = (
|
|
||||||
message: string,
|
|
||||||
color = 0x00ea00,
|
|
||||||
): MessageCreateOptions => ({
|
|
||||||
embeds: [
|
|
||||||
{
|
|
||||||
description: `### ✅ ${message}`,
|
|
||||||
color,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const createInfoEmbed = (
|
|
||||||
message: string,
|
|
||||||
color = 0x89cff0,
|
|
||||||
): MessageCreateOptions => ({
|
|
||||||
embeds: [
|
|
||||||
{
|
|
||||||
description: message,
|
|
||||||
color,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const replyError = (message: Message, text: string, color?: number) =>
|
|
||||||
message.reply(createErrorEmbed(text, color));
|
|
||||||
|
|
||||||
export const replySuccess = (message: Message, text: string, color?: number) =>
|
|
||||||
message.reply(createSuccessEmbed(text, color));
|
|
||||||
|
|
||||||
export const replyInfo = (message: Message, text: string, color?: number) =>
|
|
||||||
message.reply(createInfoEmbed(text, color));
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import { ChannelType, type Client, type Message } from "discord.js";
|
|
||||||
import { env } from "~/env";
|
|
||||||
import { makeResultEmbed } from "~/utils/discord";
|
|
||||||
import type { QuestResult } from "~/services/wov";
|
|
||||||
import { createLogger } from "@lbf-bot/utils";
|
|
||||||
|
|
||||||
const questLogger = createLogger({ prefix: "quests" });
|
|
||||||
|
|
||||||
export const askForGrinders = async (quest: QuestResult, client: Client) => {
|
|
||||||
const adminChannel = await client.channels.fetch(env.DISCORD_ADMIN_CHANNEL);
|
|
||||||
if (!adminChannel || adminChannel.type !== ChannelType.GuildText) {
|
|
||||||
return questLogger.fatal("Invalid 'DISCORD_ADMIN_CHANNEL'");
|
|
||||||
}
|
|
||||||
|
|
||||||
let exclude: string[] = [];
|
|
||||||
if (env.QUEST_REWARDS) {
|
|
||||||
const top10 = quest.participants
|
|
||||||
.filter((x) => !env.QUEST_EXCLUDE.includes(x.username))
|
|
||||||
.sort((a, b) => b.xp - a.xp)
|
|
||||||
.slice(0, 10)
|
|
||||||
.map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`)
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
const color = parseInt(quest.quest.promoImagePrimaryColor.substring(1), 16);
|
|
||||||
|
|
||||||
await adminChannel.send({
|
|
||||||
content: `-# ||${env.DISCORD_ADMIN_MENTION}||`,
|
|
||||||
embeds: [
|
|
||||||
{
|
|
||||||
title: "Quête terminée !",
|
|
||||||
color,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Top 10 XP",
|
|
||||||
description: top10,
|
|
||||||
color,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Qui a grind ?",
|
|
||||||
description:
|
|
||||||
"Merci d'entrer les pseudos des joueurs qui ont grind.\n\nFormat:```@LBF laulau,Yuno,...```\n**Attention les majuscules comptent**\nPour entrer la liste des joueurs, il faut __mentionner le bot__, si personne n'a grind, `@LBF tg`",
|
|
||||||
color,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const filter = (msg: Message) =>
|
|
||||||
msg.channel.id === adminChannel.id &&
|
|
||||||
!msg.author.bot &&
|
|
||||||
msg.content.startsWith(`<@${client.user!.id}>`);
|
|
||||||
|
|
||||||
let confirmed = false;
|
|
||||||
let answer: string | null = null;
|
|
||||||
while (!confirmed) {
|
|
||||||
const collected = await adminChannel.awaitMessages({ filter, max: 1 });
|
|
||||||
answer = collected.first()?.content || null;
|
|
||||||
if (!answer) continue;
|
|
||||||
|
|
||||||
answer = answer.replace(`<@${client.user!.id}>`, "").trim();
|
|
||||||
if (answer.toLowerCase() === "tg") {
|
|
||||||
answer = "";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const players = answer
|
|
||||||
.split(",")
|
|
||||||
.map((x) => x.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
await adminChannel.send({
|
|
||||||
embeds: [
|
|
||||||
{
|
|
||||||
title: "Joueurs entrés",
|
|
||||||
description: players.length
|
|
||||||
? players.map((name) => `- ${name}`).join("\n")
|
|
||||||
: "*Aucun joueur entré*",
|
|
||||||
color,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
content: `Est-ce correct ? (oui/non)`,
|
|
||||||
});
|
|
||||||
const confirmFilter = (msg: Message) =>
|
|
||||||
msg.channel.id === adminChannel.id &&
|
|
||||||
!msg.author.bot &&
|
|
||||||
["oui", "non", "yes", "no"].includes(msg.content.toLowerCase());
|
|
||||||
const confirmCollected = await adminChannel.awaitMessages({
|
|
||||||
filter: confirmFilter,
|
|
||||||
max: 1,
|
|
||||||
});
|
|
||||||
const confirmation = confirmCollected.first()?.content.toLowerCase();
|
|
||||||
if (confirmation === "oui" || confirmation === "yes") {
|
|
||||||
confirmed = true;
|
|
||||||
await adminChannel.send({ content: "Ok" });
|
|
||||||
} else {
|
|
||||||
await adminChannel.send({
|
|
||||||
content: "D'accord, veuillez réessayer. Qui a grind ?",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (answer === null) {
|
|
||||||
return questLogger.fatal("Answer was 'null', this should be unreachable");
|
|
||||||
}
|
|
||||||
|
|
||||||
exclude = answer
|
|
||||||
.split(",")
|
|
||||||
.map((x) => x.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
const embed = await makeResultEmbed(quest, [
|
|
||||||
...env.QUEST_EXCLUDE,
|
|
||||||
...exclude,
|
|
||||||
]);
|
|
||||||
const rewardChannel = await client.channels.fetch(
|
|
||||||
env.DISCORD_REWARDS_CHANNEL,
|
|
||||||
);
|
|
||||||
if (rewardChannel && rewardChannel.type === ChannelType.GuildText) {
|
|
||||||
await rewardChannel.send(embed);
|
|
||||||
} else {
|
|
||||||
return questLogger.fatal("Invalid 'DISCORD_REWARDS_CHANNEL'");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (env.QUEST_EXCLUDE) {
|
|
||||||
await adminChannel.send("Envoyé !");
|
|
||||||
}
|
|
||||||
questLogger.info(`Results posted at: ${new Date().toISOString()}`);
|
|
||||||
};
|
|
||||||
89
apps/discord-bot/src/wov.ts
Normal file
89
apps/discord-bot/src/wov.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { env } from "~/env";
|
||||||
|
import { redis } from "@lbf-bot/database";
|
||||||
|
|
||||||
|
export type QuestParticipant = {
|
||||||
|
playerId: string;
|
||||||
|
username: string;
|
||||||
|
xp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QuestResult = {
|
||||||
|
quest: {
|
||||||
|
id: string;
|
||||||
|
promoImageUrl: string;
|
||||||
|
promoImagePrimaryColor: string;
|
||||||
|
};
|
||||||
|
participants: Array<QuestParticipant>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Player = {
|
||||||
|
id: string;
|
||||||
|
clanId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PlayerDetails = {
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClanMember = {
|
||||||
|
playerId: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClanInfo = {
|
||||||
|
name: string;
|
||||||
|
tag: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BASE_URL = "https://api.wolvesville.com";
|
||||||
|
|
||||||
|
const fetchWovApi = async <T>(path: string): Promise<T | null> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
headers: { Authorization: `Bot ${env.WOV_API_KEY}` },
|
||||||
|
});
|
||||||
|
if (!response.ok) return null;
|
||||||
|
return (await response.json()) as T;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLatestQuest = async (): Promise<QuestResult | null> => {
|
||||||
|
const history = await fetchWovApi<Array<QuestResult>>(`/clans/${env.WOV_CLAN_ID}/quests/history`);
|
||||||
|
return history?.[0] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getActiveQuest = async (): Promise<QuestResult | null> =>
|
||||||
|
fetchWovApi<QuestResult>(`/clans/${env.WOV_CLAN_ID}/quests/active`);
|
||||||
|
|
||||||
|
export const checkForNewQuest = async (): Promise<QuestResult | null> => {
|
||||||
|
const lastQuest = await getLatestQuest();
|
||||||
|
if (!lastQuest) return null;
|
||||||
|
|
||||||
|
const cachedQuestId = await redis.get("quest:last_id");
|
||||||
|
if (cachedQuestId === lastQuest.quest.id || cachedQuestId === "IGNORE") return null;
|
||||||
|
|
||||||
|
await redis.set("quest:last_id", lastQuest.quest.id);
|
||||||
|
return lastQuest;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getClanMembers = async (): Promise<Array<ClanMember>> => {
|
||||||
|
const cached = await redis.get("clan:members");
|
||||||
|
if (cached) return JSON.parse(cached) as Array<ClanMember>;
|
||||||
|
|
||||||
|
const data = await fetchWovApi<Array<ClanMember>>(`/clans/${env.WOV_CLAN_ID}/members`);
|
||||||
|
if (!data) return [];
|
||||||
|
|
||||||
|
await redis.set("clan:members", JSON.stringify(data), "EX", 60 * 60);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchPlayer = async (username: string): Promise<Player | null> =>
|
||||||
|
fetchWovApi<Player>(`/players/search?username=${username}`);
|
||||||
|
|
||||||
|
export const getClanInfo = async (clanId: string): Promise<ClanInfo | null> =>
|
||||||
|
fetchWovApi<ClanInfo>(`/clans/${clanId}/info`);
|
||||||
|
|
||||||
|
export const getPlayer = async (playerId: string): Promise<PlayerDetails | null> =>
|
||||||
|
fetchWovApi<PlayerDetails>(`/players/${playerId}`);
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"removeComments": false
|
"removeComments": false
|
||||||
},
|
},
|
||||||
"tsc-alias": {
|
"tsc-alias": {
|
||||||
"resolveFullPaths": true
|
"resolveFullPaths": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
|
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": false,
|
"allowImportingTsExtensions": false,
|
||||||
"baseUrl": ".",
|
"paths": {
|
||||||
"paths": {
|
"~/*": ["./src/*"],
|
||||||
"~/*": ["./src/*"],
|
"~": ["./src/index"]
|
||||||
"~": ["./src/index"]
|
},
|
||||||
|
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
},
|
},
|
||||||
|
"include": ["src"],
|
||||||
"strict": true,
|
"exclude": ["node_modules", "dist"]
|
||||||
"skipLibCheck": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
|
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"noPropertyAccessFromIndexSignature": false
|
|
||||||
},
|
|
||||||
"include": ["src"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ services:
|
|||||||
|
|
||||||
discord-bot:
|
discord-bot:
|
||||||
container_name: lbf-bot
|
container_name: lbf-bot
|
||||||
image: git.pihkaal.me/pihkaal/lbf-bot:latest
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: apps/discord-bot/Dockerfile
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -47,6 +49,7 @@ services:
|
|||||||
- DISCORD_ADMIN_MENTION
|
- DISCORD_ADMIN_MENTION
|
||||||
- DISCORD_ADMIN_CHANNEL
|
- DISCORD_ADMIN_CHANNEL
|
||||||
- DISCORD_TRACKING_CHANNEL
|
- DISCORD_TRACKING_CHANNEL
|
||||||
|
- DISCORD_REPORT_CHANNEL
|
||||||
- DISCORD_STAFF_ROLE_ID
|
- DISCORD_STAFF_ROLE_ID
|
||||||
- WOV_API_KEY
|
- WOV_API_KEY
|
||||||
- WOV_CLAN_ID
|
- WOV_CLAN_ID
|
||||||
|
|||||||
25
eslint.config.mjs
Normal file
25
eslint.config.mjs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"**/dist/**",
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/drizzle/**",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.ts"],
|
||||||
|
extends: [tseslint.configs.recommendedTypeChecked],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
|
||||||
|
},
|
||||||
|
},]
|
||||||
|
);
|
||||||
@@ -2,9 +2,10 @@
|
|||||||
"name": "lbf-bot",
|
"name": "lbf-bot",
|
||||||
"packageManager": "pnpm@10.24.0",
|
"packageManager": "pnpm@10.24.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write --cache ."
|
"check": "pnpm --filter @lbf-bot/utils run build && pnpm --filter @lbf-bot/database run build && pnpm -r check"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"devDependencies": {
|
||||||
"prettier": "3.6.2"
|
"eslint": "^10.3.0",
|
||||||
|
"typescript-eslint": "^8.59.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { defineConfig } from "drizzle-kit";
|
|||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
dialect: "postgresql",
|
dialect: "postgresql",
|
||||||
out: "./drizzle",
|
out: "./drizzle",
|
||||||
schema: "./src/schema/index.ts",
|
schema: "./src/schema/index.ts",
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: env.DATABASE_URL,
|
url: env.DATABASE_URL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
10
packages/database/drizzle/0001_broken_dorian_gray.sql
Normal file
10
packages/database/drizzle/0001_broken_dorian_gray.sql
Normal file
@@ -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
|
||||||
|
);
|
||||||
1
packages/database/drizzle/0002_lucky_praxagora.sql
Normal file
1
packages/database/drizzle/0002_lucky_praxagora.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "reports" ADD COLUMN "message_link" text;
|
||||||
1
packages/database/drizzle/0003_uneven_mephistopheles.sql
Normal file
1
packages/database/drizzle/0003_uneven_mephistopheles.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "reports" ADD COLUMN "screenshots_message_id" text;
|
||||||
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";
|
||||||
208
packages/database/drizzle/meta/0001_snapshot.json
Normal file
208
packages/database/drizzle/meta/0001_snapshot.json
Normal file
@@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
214
packages/database/drizzle/meta/0002_snapshot.json
Normal file
214
packages/database/drizzle/meta/0002_snapshot.json
Normal file
@@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
220
packages/database/drizzle/meta/0003_snapshot.json
Normal file
220
packages/database/drizzle/meta/0003_snapshot.json
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
{
|
||||||
|
"id": "a19383ff-8e7a-4da4-8c29-888e8ea9f670",
|
||||||
|
"prevId": "4643e6bd-db18-44c9-893d-8f57a94e8c0a",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,34 @@
|
|||||||
"when": 1764882945878,
|
"when": 1764882945878,
|
||||||
"tag": "0000_tan_justin_hammer",
|
"tag": "0000_tan_justin_hammer",
|
||||||
"breakpoints": true
|
"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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1778607030657,
|
||||||
|
"tag": "0003_uneven_mephistopheles",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780166865311,
|
||||||
|
"tag": "0004_curved_imperial_guard",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,36 +1,37 @@
|
|||||||
{
|
{
|
||||||
"name": "@lbf-bot/database",
|
"name": "@lbf-bot/database",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"import": "./dist/index.js",
|
"import": "./dist/index.js",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"drizzle"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
||||||
|
"check": "tsc --noEmit && eslint src/",
|
||||||
|
"db:generate": "drizzle-kit generate",
|
||||||
|
"db:push": "drizzle-kit push",
|
||||||
|
"db:studio": "drizzle-kit studio"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@lbf-bot/utils": "workspace:*",
|
||||||
|
"drizzle-orm": "0.44.7",
|
||||||
|
"ioredis": "5.8.2",
|
||||||
|
"pg": "8.16.3",
|
||||||
|
"zod": "4.1.11"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"drizzle-kit": "0.31.7",
|
||||||
|
"tsc-alias": "1.8.16",
|
||||||
|
"typescript": "5.9.3"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"drizzle"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"build": "rm -rf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
||||||
"db:generate": "drizzle-kit generate",
|
|
||||||
"db:push": "drizzle-kit push",
|
|
||||||
"db:studio": "drizzle-kit studio"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@lbf-bot/utils": "workspace:*",
|
|
||||||
"drizzle-orm": "0.44.7",
|
|
||||||
"ioredis": "5.8.2",
|
|
||||||
"pg": "8.16.3",
|
|
||||||
"zod": "4.1.11"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"drizzle-kit": "0.31.7",
|
|
||||||
"tsc-alias": "1.8.16",
|
|
||||||
"typescript": "5.9.3"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
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(),
|
||||||
|
playerName: text("player_name").notNull(),
|
||||||
|
playerId: text("player_id").notNull(),
|
||||||
|
reason: text("reason").notNull(),
|
||||||
|
screenshots: text("screenshots"),
|
||||||
|
messageLink: text("message_link"),
|
||||||
|
screenshotsMessageId: text("screenshots_message_id"),
|
||||||
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ECONOMY SYSTEM
|
* ECONOMY SYSTEM
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"removeComments": false
|
"removeComments": false
|
||||||
},
|
},
|
||||||
"tsc-alias": {
|
"tsc-alias": {
|
||||||
"resolveFullPaths": true
|
"resolveFullPaths": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
|
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": false,
|
"allowImportingTsExtensions": false,
|
||||||
"baseUrl": ".",
|
"paths": {
|
||||||
"paths": {
|
"~/*": ["./src/*"],
|
||||||
"~/*": ["./src/*"],
|
"~": ["./src/index"]
|
||||||
"~": ["./src/index"]
|
},
|
||||||
|
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
},
|
},
|
||||||
|
"include": ["src", "drizzle.config.ts"],
|
||||||
"strict": true,
|
"exclude": ["node_modules", "dist"]
|
||||||
"skipLibCheck": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
|
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"noPropertyAccessFromIndexSignature": false
|
|
||||||
},
|
|
||||||
"include": ["src", "drizzle.config.ts"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
{
|
{
|
||||||
"name": "@lbf-bot/utils",
|
"name": "@lbf-bot/utils",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"import": "./dist/index.js",
|
"import": "./dist/index.js",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
||||||
|
"check": "tsc --noEmit && eslint src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "17.2.3",
|
||||||
|
"zod": "4.1.11"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "22.19.1",
|
||||||
|
"tsc-alias": "1.8.16",
|
||||||
|
"typescript": "5.9.3"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"build": "rm -rf dist && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"dotenv": "17.2.3",
|
|
||||||
"zod": "4.1.11"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "22.19.1",
|
|
||||||
"tsc-alias": "1.8.16",
|
|
||||||
"typescript": "5.9.3"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ import "dotenv/config";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const parseEnv = <T extends z.ZodRawShape>(vars: T) => {
|
export const parseEnv = <T extends z.ZodRawShape>(vars: T) => {
|
||||||
const schema = z.object(vars);
|
const schema = z.object(vars);
|
||||||
const result = schema.safeParse(process.env);
|
const result = schema.safeParse(process.env);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.error("ERROR: Environment variable validation failed:");
|
console.error("ERROR: Environment variable validation failed:");
|
||||||
for (const issue of result.error.issues) {
|
for (const issue of result.error.issues) {
|
||||||
console.error(`- ${issue.path.join(".")}: ${issue.message}`);
|
console.error(`- ${issue.path.join(".")}: ${issue.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(1);
|
return result.data;
|
||||||
}
|
|
||||||
|
|
||||||
return result.data;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,101 +1,96 @@
|
|||||||
type LogLevel = "debug" | "info" | "warn" | "error";
|
type LogLevel = "debug" | "info" | "warn" | "error";
|
||||||
|
|
||||||
interface LoggerOptions {
|
type LoggerOptions = {
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
level?: LogLevel;
|
level?: LogLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOG_LEVELS = {
|
const LOG_LEVELS = {
|
||||||
debug: 0,
|
debug: 0,
|
||||||
info: 1,
|
info: 1,
|
||||||
warn: 2,
|
warn: 2,
|
||||||
error: 3,
|
error: 3,
|
||||||
} as const satisfies Record<LogLevel, number>;
|
} as const satisfies Record<LogLevel, number>;
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
debug: "\x1b[36m", // cyan
|
debug: "\x1b[36m", // cyan
|
||||||
info: "\x1b[32m", // green
|
info: "\x1b[32m", // green
|
||||||
warn: "\x1b[33m", // yellow
|
warn: "\x1b[33m", // yellow
|
||||||
error: "\x1b[31m", // red
|
error: "\x1b[31m", // red
|
||||||
reset: "\x1b[0m",
|
reset: "\x1b[0m",
|
||||||
gray: "\x1b[90m",
|
gray: "\x1b[90m",
|
||||||
bold: "\x1b[1m",
|
bold: "\x1b[1m",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
class Logger {
|
class Logger {
|
||||||
private prefix: string;
|
private prefix: string;
|
||||||
private minLevel: number;
|
private minLevel: number;
|
||||||
|
|
||||||
constructor(options: LoggerOptions = {}) {
|
constructor(options: LoggerOptions = {}) {
|
||||||
this.prefix = options.prefix || "";
|
this.prefix = options.prefix || "";
|
||||||
this.minLevel = LOG_LEVELS[options.level || "info"];
|
this.minLevel = LOG_LEVELS[options.level || "info"];
|
||||||
}
|
}
|
||||||
|
|
||||||
private formatTimestamp(): string {
|
private formatTimestamp(): string {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const hours = String(now.getHours()).padStart(2, "0");
|
const hours = String(now.getHours()).padStart(2, "0");
|
||||||
const minutes = String(now.getMinutes()).padStart(2, "0");
|
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||||||
const seconds = String(now.getSeconds()).padStart(2, "0");
|
const seconds = String(now.getSeconds()).padStart(2, "0");
|
||||||
return `${hours}:${minutes}:${seconds}`;
|
return `${hours}:${minutes}:${seconds}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private log(level: LogLevel, message: string, ...args: unknown[]): void {
|
private log(level: LogLevel, message: string, ...args: unknown[]): void {
|
||||||
if (LOG_LEVELS[level] < this.minLevel) return;
|
if (LOG_LEVELS[level] < this.minLevel) return;
|
||||||
|
|
||||||
const timestamp = this.formatTimestamp();
|
const timestamp = this.formatTimestamp();
|
||||||
const color = COLORS[level];
|
const color = COLORS[level];
|
||||||
const levelStr = level.toUpperCase().padEnd(5);
|
const levelStr = level.toUpperCase().padEnd(5);
|
||||||
const prefix = this.prefix ? `[${this.prefix}] ` : "";
|
const prefix = this.prefix ? `[${this.prefix}] ` : "";
|
||||||
|
|
||||||
const formattedArgs = args.map((arg) => {
|
const formattedArgs = args.map((arg) => {
|
||||||
if (arg instanceof Error) {
|
if (arg instanceof Error) {
|
||||||
return arg;
|
return arg;
|
||||||
}
|
}
|
||||||
return arg;
|
return arg;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
console.log(`${COLORS.gray}${timestamp}${COLORS.reset} ${color}${COLORS.bold}${levelStr}${COLORS.reset} ${prefix}${message}`, ...formattedArgs);
|
||||||
`${COLORS.gray}${timestamp}${COLORS.reset} ${color}${COLORS.bold}${levelStr}${COLORS.reset} ${prefix}${message}`,
|
}
|
||||||
...formattedArgs,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
debug(message: string, ...args: unknown[]): void {
|
debug(message: string, ...args: unknown[]): void {
|
||||||
this.log("debug", message, ...args);
|
this.log("debug", message, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
info(message: string, ...args: unknown[]): void {
|
info(message: string, ...args: unknown[]): void {
|
||||||
this.log("info", message, ...args);
|
this.log("info", message, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
warn(message: string, ...args: unknown[]): void {
|
warn(message: string, ...args: unknown[]): void {
|
||||||
this.log("warn", message, ...args);
|
this.log("warn", message, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
error(message: string, ...args: unknown[]): void {
|
error(message: string, ...args: unknown[]): void {
|
||||||
this.log("error", message, ...args);
|
this.log("error", message, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
fatal(message: string, ...args: unknown[]): never {
|
fatal(message: string, ...args: unknown[]): never {
|
||||||
this.log("error", message, ...args);
|
this.log("error", message, ...args);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
child(prefix: string): Logger {
|
child(prefix: string): Logger {
|
||||||
const childPrefix = this.prefix ? `${this.prefix}:${prefix}` : prefix;
|
const childPrefix = this.prefix ? `${this.prefix}:${prefix}` : prefix;
|
||||||
return new Logger({ prefix: childPrefix, level: this.getLevel() });
|
return new Logger({ prefix: childPrefix, level: this.getLevel() });
|
||||||
}
|
}
|
||||||
|
|
||||||
private getLevel(): LogLevel {
|
private getLevel(): LogLevel {
|
||||||
const entry = Object.entries(LOG_LEVELS).find(
|
const entry = Object.entries(LOG_LEVELS).find(([, value]) => value === this.minLevel);
|
||||||
([, value]) => value === this.minLevel,
|
return (entry?.[0] as LogLevel) || "info";
|
||||||
);
|
}
|
||||||
return (entry?.[0] as LogLevel) || "info";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createLogger = (options?: LoggerOptions): Logger => {
|
export const createLogger = (options?: LoggerOptions): Logger => {
|
||||||
return new Logger(options);
|
return new Logger(options);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const logger = createLogger();
|
export const logger = createLogger();
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"removeComments": false
|
"removeComments": false
|
||||||
},
|
},
|
||||||
"tsc-alias": {
|
"tsc-alias": {
|
||||||
"resolveFullPaths": true
|
"resolveFullPaths": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"target": "ESNext",
|
"types": ["node"],
|
||||||
"module": "ESNext",
|
"target": "ESNext",
|
||||||
"moduleDetection": "force",
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": false,
|
"allowImportingTsExtensions": false,
|
||||||
"baseUrl": ".",
|
"paths": {
|
||||||
"paths": {
|
"~/*": ["./src/*"],
|
||||||
"~/*": ["./src/*"],
|
"~": ["./src/index"]
|
||||||
"~": ["./src/index"]
|
},
|
||||||
|
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
},
|
},
|
||||||
|
"include": ["src"],
|
||||||
"strict": true,
|
"exclude": ["node_modules", "dist"]
|
||||||
"skipLibCheck": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
|
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"noPropertyAccessFromIndexSignature": false
|
|
||||||
},
|
|
||||||
"include": ["src"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
1101
pnpm-lock.yaml
generated
1101
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user