Compare commits
13 Commits
d82c6589c6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d2ab20975 | ||
|
|
1297050339 | ||
|
|
877402090f | ||
|
|
1cbd752977 | ||
|
|
d3561c019d | ||
|
|
2024d830bb | ||
|
|
ade59d6101 | ||
|
|
a984daddfe | ||
|
|
30cc00efa8 | ||
|
|
cf6cc7ff7b | ||
|
|
a541b82404 | ||
|
|
8c78725e65 | ||
|
|
66474dc813 |
29
.gitea/workflows/docker-build.yml
Normal file
29
.gitea/workflows/docker-build.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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.xyz
|
||||||
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
run: |
|
||||||
|
docker build -t git.pihkaal.xyz/pihkaal/lbf-bot:latest -f apps/discord-bot/Dockerfile .
|
||||||
|
docker push git.pihkaal.xyz/pihkaal/lbf-bot:latest
|
||||||
28
.github/workflows/docker-build.yml
vendored
28
.github/workflows/docker-build.yml
vendored
@@ -1,28 +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@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Log in to DockerHub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
run: |
|
|
||||||
docker build -t pihkaal/lbf-bot-discord-bot:latest -f apps/discord-bot/Dockerfile .
|
|
||||||
docker push pihkaal/lbf-bot-discord-bot:latest
|
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@lbf-bot/utils": "workspace:*",
|
||||||
"@lbf-bot/database": "workspace:*",
|
"@lbf-bot/database": "workspace:*",
|
||||||
"discord.js": "^14.21.0",
|
"discord.js": "^14.21.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
|
|||||||
@@ -58,5 +58,10 @@ export const gemmesCommand: Command = async (message, args) => {
|
|||||||
)
|
)
|
||||||
.setColor(0x4289c1),
|
.setColor(0x4289c1),
|
||||||
],
|
],
|
||||||
|
options: {
|
||||||
|
allowedMentions: {
|
||||||
|
repliedUser: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,5 +47,10 @@ export const iconeCommand: Command = async (message, args) => {
|
|||||||
)
|
)
|
||||||
.setColor(65280),
|
.setColor(65280),
|
||||||
],
|
],
|
||||||
|
options: {
|
||||||
|
allowedMentions: {
|
||||||
|
repliedUser: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import type { Command } from "~/commands";
|
import type { Command } from "~/commands";
|
||||||
|
|
||||||
export const pingCommand: Command = async (message, args) => {
|
export const pingCommand: Command = async (message, args) => {
|
||||||
await message.reply("pong");
|
await message.reply({
|
||||||
|
content: "🫵 Pong",
|
||||||
|
options: {
|
||||||
|
allowedMentions: {
|
||||||
|
repliedUser: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,65 +1,61 @@
|
|||||||
import type { Command } from "~/commands";
|
import type { Command } from "~/commands";
|
||||||
import { untrackWovPlayer } from "~/services/tracking";
|
import { isWovPlayerTracked, untrackWovPlayer } from "~/services/tracking";
|
||||||
import { searchPlayer } from "~/services/wov";
|
import { searchPlayer } from "~/services/wov";
|
||||||
import { createErrorEmbed, createInfoEmbed } from "~/utils/discord";
|
import { replyError, createInfoEmbed, replySuccess } from "~/utils/discord";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
|
import { createLogger } from "@lbf-bot/utils";
|
||||||
|
|
||||||
const STAFF_ROLE_ID = "1147963065640439900";
|
const trackingLogger = createLogger({ prefix: "tracking" });
|
||||||
|
|
||||||
export const tejtrackCommand: Command = async (message, args) => {
|
export const tejtrackCommand: Command = async (message, args) => {
|
||||||
const client = message.client;
|
// check staff permission
|
||||||
if (!message.member) return;
|
if (!message.member?.roles.cache.has(env.DISCORD_STAFF_ROLE_ID)) {
|
||||||
if (!message.member.roles.cache.has(STAFF_ROLE_ID)) {
|
await replyError(message, "Tu t'es cru chez mémé ou quoi faut être staff");
|
||||||
await message.reply(
|
|
||||||
createErrorEmbed("Tu t'es cru chez mémé ou quoi faut être staff"),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let playerName = args[0];
|
const playerName = args[0];
|
||||||
if (!playerName) {
|
if (!playerName) {
|
||||||
await message.reply(
|
await replyError(
|
||||||
createErrorEmbed(
|
message,
|
||||||
"Usage:`@LBF untrack NOM_JOUEUR`, exemple: `@LBF untrack Yuno`.\n**Attention les majuscules sont importantes**",
|
"Usage:`@LBF untrack NOM_JOUEUR`, exemple: `@LBF untrack Yuno`.\n**Attention les majuscules sont importantes**",
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = await searchPlayer(playerName);
|
const player = await searchPlayer(playerName);
|
||||||
if (!player) {
|
if (!player) {
|
||||||
await message.reply(
|
await replyError(
|
||||||
createErrorEmbed(
|
message,
|
||||||
"Cette personne n'existe pas.\n**Attention les majuscules sont importantes**",
|
"Cette personne n'existe pas.\n**Attention les majuscules sont importantes**",
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await untrackWovPlayer(player.id);
|
if (!(await isWovPlayerTracked(player.id))) {
|
||||||
switch (res.event) {
|
await replyError(
|
||||||
case "notTracked": {
|
message,
|
||||||
await message.reply(
|
|
||||||
createInfoEmbed(
|
|
||||||
`Pas de tracker pour \`${playerName}\` [\`${player.id}\`]`,
|
`Pas de tracker pour \`${playerName}\` [\`${player.id}\`]`,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
break;
|
return;
|
||||||
}
|
}
|
||||||
case "trackerRemoved": {
|
|
||||||
await message.reply(
|
await untrackWovPlayer(player.id);
|
||||||
createInfoEmbed(
|
|
||||||
|
await replySuccess(
|
||||||
|
message,
|
||||||
`Tracker enlevé pour \`${playerName}\` [\`${player.id}\`]`,
|
`Tracker enlevé pour \`${playerName}\` [\`${player.id}\`]`,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const chan = client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL);
|
const chan = message.client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL);
|
||||||
if (!chan?.isSendable()) throw "Invalid tracking channel";
|
if (!chan?.isSendable()) {
|
||||||
|
return trackingLogger.fatal("Invalid 'DISCORD_TRACKING_CHANNEL'");
|
||||||
|
}
|
||||||
|
|
||||||
await chan.send(
|
await chan.send(
|
||||||
createInfoEmbed(`### [REMOVED] \`${playerName}\` [\`${player.id}\`]`),
|
createInfoEmbed(
|
||||||
|
`### [REMOVED] \`${playerName}\` [\`${player.id}\`]`,
|
||||||
|
0xea0000,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,78 +1,59 @@
|
|||||||
import type { Command } from "~/commands";
|
import type { Command } from "~/commands";
|
||||||
import { trackWovPlayer } from "~/services/tracking";
|
import { trackWovPlayer, isWovPlayerTracked } from "~/services/tracking";
|
||||||
import { searchPlayer } from "~/services/wov";
|
import { searchPlayer } from "~/services/wov";
|
||||||
import { createErrorEmbed, createInfoEmbed } from "~/utils/discord";
|
import { replyError, createInfoEmbed, replySuccess } from "~/utils/discord";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
|
import { createLogger } from "@lbf-bot/utils";
|
||||||
|
|
||||||
const STAFF_ROLE_ID = "1147963065640439900";
|
const trackingLogger = createLogger({ prefix: "tracking" });
|
||||||
|
|
||||||
export const trackCommand: Command = async (message, args) => {
|
export const trackCommand: Command = async (message, args) => {
|
||||||
const client = message.client;
|
// check staff permission
|
||||||
if (!message.member) return;
|
if (!message.member?.roles.cache.has(env.DISCORD_STAFF_ROLE_ID)) {
|
||||||
if (!message.member.roles.cache.has(STAFF_ROLE_ID)) {
|
await replyError(message, "Tu t'es cru chez mémé ou quoi faut être staff");
|
||||||
await message.reply(
|
|
||||||
createErrorEmbed("Tu t'es cru chez mémé ou quoi faut être staff"),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let playerName = args[0];
|
const playerName = args[0];
|
||||||
if (!playerName) {
|
if (!playerName) {
|
||||||
await message.reply(
|
await replyError(
|
||||||
createErrorEmbed(
|
message,
|
||||||
"Usage:`@LBF track NOM_JOUEUR`, exemple: `@LBF track Yuno`.\n**Attention les majuscules sont importantes**",
|
"Usage:`@LBF track NOM_JOUEUR`, exemple: `@LBF track Yuno`.\n**Attention les majuscules sont importantes**",
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = await searchPlayer(playerName);
|
const player = await searchPlayer(playerName);
|
||||||
if (!player) {
|
if (!player) {
|
||||||
await message.reply(
|
await replyError(
|
||||||
createErrorEmbed(
|
message,
|
||||||
"Cette personne n'existe pas.\n**Attention les majuscules sont importantes**",
|
"Cette personne n'existe pas.\n**Attention les majuscules sont importantes**",
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await trackWovPlayer(player.id);
|
const alreadyTracked = await isWovPlayerTracked(player.id);
|
||||||
switch (res.event) {
|
if (alreadyTracked) {
|
||||||
case "notFound": {
|
await replyError(
|
||||||
await message.reply(
|
message,
|
||||||
createErrorEmbed(
|
`Tracker déjà enregistré pour \`${playerName}\` [\`${player.id}\`]`,
|
||||||
"Cette personne n'existe pas.\n**Attention les majuscules sont importantes**",
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "registered": {
|
await trackWovPlayer(player.id);
|
||||||
await message.reply(
|
|
||||||
createInfoEmbed(
|
await replySuccess(
|
||||||
|
message,
|
||||||
`Tracker enregistré pour \`${playerName}\` [\`${player.id}\`]`,
|
`Tracker enregistré pour \`${playerName}\` [\`${player.id}\`]`,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const chan = client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL);
|
const chan = message.client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL);
|
||||||
if (!chan?.isSendable()) throw "Invalid tracking channel";
|
if (!chan?.isSendable()) {
|
||||||
|
return trackingLogger.fatal("Invalid 'DISCORD_TRACKING_CHANNEL'");
|
||||||
|
}
|
||||||
|
|
||||||
await chan.send(
|
await chan.send(
|
||||||
createInfoEmbed(`### [NEW] \`${playerName}\` [\`${player.id}\`]`),
|
createInfoEmbed(`### [NEW] \`${playerName}\` [\`${player.id}\`]`),
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
case "none": {
|
|
||||||
await message.reply(
|
|
||||||
createInfoEmbed(
|
|
||||||
`Tracker déjà enregistré pour \`${playerName}\` [\`${player.id}\`]`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
case "changed": {
|
|
||||||
// ignored
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
|
import { logger } from "@lbf-bot/utils";
|
||||||
|
|
||||||
|
// TODO: use parseEnv from utils
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
DISCORD_BOT_TOKEN: z.string(),
|
DISCORD_BOT_TOKEN: z.string(),
|
||||||
@@ -20,6 +23,10 @@ const schema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.transform((x) => x.split(",").map((x) => x.trim()))
|
.transform((x) => x.split(",").map((x) => x.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
|
QUEST_REWARDS_ARE_GEMS: z
|
||||||
|
.string()
|
||||||
|
.transform((val) => val.toLowerCase() === "true")
|
||||||
|
.pipe(z.boolean()),
|
||||||
QUEST_EXCLUDE: z
|
QUEST_EXCLUDE: z
|
||||||
.string()
|
.string()
|
||||||
.transform((x) => x.split(",").map((x) => x.trim()))
|
.transform((x) => x.split(",").map((x) => x.trim()))
|
||||||
@@ -29,13 +36,11 @@ const schema = z.object({
|
|||||||
|
|
||||||
const result = schema.safeParse(process.env);
|
const result = schema.safeParse(process.env);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.log("❌ Invalid environments variables:");
|
logger.fatal(
|
||||||
console.log(
|
`❌ Invalid environments variables:\n${result.error.errors
|
||||||
result.error.errors
|
|
||||||
.map((x) => `- ${x.path.join(".")}: ${x.message}`)
|
.map((x) => `- ${x.path.join(".")}: ${x.message}`)
|
||||||
.join("\n"),
|
.join("\n")}`,
|
||||||
);
|
);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const env = result.data;
|
export const env = result.data;
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { setupBotMode } from "~/modes/bot";
|
|||||||
import { setupUserMode } from "~/modes/user";
|
import { setupUserMode } from "~/modes/user";
|
||||||
import { parseArgs } from "~/utils/cli";
|
import { parseArgs } from "~/utils/cli";
|
||||||
import { runMigrations } from "@lbf-bot/database";
|
import { runMigrations } from "@lbf-bot/database";
|
||||||
|
import { logger } from "@lbf-bot/utils";
|
||||||
|
|
||||||
console.log("Running database migrations...");
|
logger.info("Running database migrations...");
|
||||||
await runMigrations();
|
await runMigrations();
|
||||||
|
|
||||||
const mode = parseArgs(process.argv.slice(2));
|
const mode = parseArgs(process.argv.slice(2));
|
||||||
|
|
||||||
console.log(`Mode: ${mode.type}`);
|
logger.info(`Mode: ${mode.type}`);
|
||||||
|
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
@@ -35,8 +36,7 @@ switch (mode.type) {
|
|||||||
|
|
||||||
default: {
|
default: {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
console.error(`ERROR: Not implemented: '${mode.type}'`);
|
logger.fatal(`ERROR: Not implemented: '${mode.type}'`);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,66 @@
|
|||||||
import type { Client } from "discord.js";
|
import type { Client } from "discord.js";
|
||||||
|
import { createLogger, logger } from "@lbf-bot/utils";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import { listTrackedPlayers, trackWovPlayer } from "~/services/tracking";
|
import {
|
||||||
import { checkForNewQuest } from "~/services/wov";
|
listTrackedPlayers,
|
||||||
|
getTrackedPlayerUsernames,
|
||||||
|
addUsernameToHistory,
|
||||||
|
} from "~/services/tracking";
|
||||||
|
import { checkForNewQuest, getPlayer } from "~/services/wov";
|
||||||
import { createInfoEmbed } from "~/utils/discord";
|
import { createInfoEmbed } from "~/utils/discord";
|
||||||
import { askForGrinders } from "~/utils/quest";
|
import { askForGrinders } from "~/utils/quest";
|
||||||
import { commands } from "~/commands";
|
import { commands } from "~/commands";
|
||||||
|
|
||||||
|
const questsLogger = createLogger({ prefix: "quests" });
|
||||||
|
const trackingLogger = createLogger({ prefix: "tracking" });
|
||||||
|
|
||||||
const questCheckCron = async (client: Client) => {
|
const questCheckCron = async (client: Client) => {
|
||||||
|
questsLogger.info("Checking for new quest");
|
||||||
const quest = await checkForNewQuest();
|
const quest = await checkForNewQuest();
|
||||||
if (quest) {
|
if (quest) {
|
||||||
|
questsLogger.info(`New quest found: '${quest.quest.id}'`);
|
||||||
await askForGrinders(quest, client);
|
await askForGrinders(quest, client);
|
||||||
|
} else {
|
||||||
|
questsLogger.info("No new quest found");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const trackingCron = async (client: Client) => {
|
const trackingCron = async (client: Client) => {
|
||||||
|
trackingLogger.info("Checking for tracked players");
|
||||||
const trackedPlayers = await listTrackedPlayers();
|
const trackedPlayers = await listTrackedPlayers();
|
||||||
|
trackingLogger.info(`${trackedPlayers.length} players to check`);
|
||||||
for (const playerId of trackedPlayers) {
|
for (const playerId of trackedPlayers) {
|
||||||
const res = await trackWovPlayer(playerId);
|
const player = await getPlayer(playerId);
|
||||||
if (res.event !== "changed") return;
|
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);
|
const chan = client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL);
|
||||||
if (!chan?.isSendable()) throw "Invalid tracking channel";
|
if (!chan?.isSendable()) {
|
||||||
|
return logger.fatal("Invalid 'DISCORD_TRACKING_CHANNEL'");
|
||||||
const lastUsername = res.oldUsernames[res.oldUsernames.length - 1];
|
}
|
||||||
|
const lastUsername = usernames[usernames.length - 1];
|
||||||
|
|
||||||
await chan.send(
|
await chan.send(
|
||||||
createInfoEmbed(
|
createInfoEmbed(
|
||||||
`### [UPDATE] \`${lastUsername}\` -> \`${res.newUsername}\` [\`${playerId}\`]\n\n**Nouveau pseudo:** \`${res.newUsername}\`\n**Anciens pseudos:**\n${res.oldUsernames.map((x) => `- \`${x}\``).join("\n")}`,
|
`### [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) => {
|
export const setupBotMode = (client: Client) => {
|
||||||
client.on("clientReady", async (client) => {
|
client.on("clientReady", async (client) => {
|
||||||
console.log(`Logged in as ${client.user.username}`);
|
logger.info(`Client ready`);
|
||||||
|
logger.info(`Connected as @${client.user.username}`);
|
||||||
|
|
||||||
await questCheckCron(client);
|
await questCheckCron(client);
|
||||||
setInterval(() => questCheckCron(client), env.WOV_FETCH_INTERVAL);
|
setInterval(() => questCheckCron(client), env.WOV_FETCH_INTERVAL);
|
||||||
@@ -54,7 +80,17 @@ export const setupBotMode = (client: Client) => {
|
|||||||
|
|
||||||
const commandHandler = commands[command];
|
const commandHandler = commands[command];
|
||||||
if (commandHandler) {
|
if (commandHandler) {
|
||||||
|
const child = logger.child(
|
||||||
|
`cmd:${command}${args.length > 0 ? " " : ""}${args.join(" ")}`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const start = Date.now();
|
||||||
await commandHandler(message, args);
|
await commandHandler(message, args);
|
||||||
|
const end = Date.now();
|
||||||
|
child.info(`Done in ${(end - start).toFixed(2)}ms`);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
child.error("Failed:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import { logger } from "@lbf-bot/utils";
|
||||||
import type { Client, TextChannel } from "discord.js";
|
import type { Client, TextChannel } 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) => {
|
||||||
console.log(`Logged in as ${client.user.username}`);
|
logger.info(`Client ready`);
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -11,33 +11,40 @@ export async function listTrackedPlayers(): Promise<string[]> {
|
|||||||
return players.map((p) => p.playerId);
|
return players.map((p) => p.playerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function untrackWovPlayer(
|
export async function isWovPlayerTracked(playerId: string): Promise<boolean> {
|
||||||
playerId: string,
|
|
||||||
): Promise<{ event: "notTracked" } | { event: "trackerRemoved" }> {
|
|
||||||
const player = await db.query.trackedPlayers.findFirst({
|
const player = await db.query.trackedPlayers.findFirst({
|
||||||
where: eq(tables.trackedPlayers.playerId, playerId),
|
where: eq(tables.trackedPlayers.playerId, playerId),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!player) return { event: "notTracked" };
|
return player !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function untrackWovPlayer(playerId: string): Promise<void> {
|
||||||
await db
|
await db
|
||||||
.delete(tables.trackedPlayers)
|
.delete(tables.trackedPlayers)
|
||||||
.where(eq(tables.trackedPlayers.playerId, playerId));
|
.where(eq(tables.trackedPlayers.playerId, playerId));
|
||||||
|
|
||||||
return { event: "trackerRemoved" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function trackWovPlayer(playerId: string): Promise<
|
export async function trackWovPlayer(playerId: string): Promise<void> {
|
||||||
| { event: "notFound" }
|
const alreadyTracked = await isWovPlayerTracked(playerId);
|
||||||
| {
|
if (alreadyTracked) return;
|
||||||
event: "registered";
|
|
||||||
}
|
|
||||||
| { event: "changed"; oldUsernames: string[]; newUsername: string }
|
|
||||||
| { event: "none" }
|
|
||||||
> {
|
|
||||||
const player = await getPlayer(playerId);
|
|
||||||
if (!player) return { event: "notFound" };
|
|
||||||
|
|
||||||
|
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({
|
const tracked = await db.query.trackedPlayers.findFirst({
|
||||||
where: eq(tables.trackedPlayers.playerId, playerId),
|
where: eq(tables.trackedPlayers.playerId, playerId),
|
||||||
with: {
|
with: {
|
||||||
@@ -47,40 +54,21 @@ export async function trackWovPlayer(playerId: string): Promise<
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (tracked) {
|
if (!tracked) return [];
|
||||||
const currentUsernames = tracked.usernameHistory.map((h) => h.username);
|
return tracked.usernameHistory.map((h) => h.username);
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentUsernames.includes(player.username)) {
|
export async function addUsernameToHistory(
|
||||||
|
playerId: string,
|
||||||
|
username: string,
|
||||||
|
): Promise<void> {
|
||||||
await db.insert(tables.usernameHistory).values({
|
await db.insert(tables.usernameHistory).values({
|
||||||
playerId,
|
playerId,
|
||||||
username: player.username,
|
username,
|
||||||
});
|
});
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(tables.trackedPlayers)
|
.update(tables.trackedPlayers)
|
||||||
.set({ updatedAt: new Date() })
|
.set({ updatedAt: new Date() })
|
||||||
.where(eq(tables.trackedPlayers.playerId, playerId));
|
.where(eq(tables.trackedPlayers.playerId, playerId));
|
||||||
|
|
||||||
return {
|
|
||||||
event: "changed",
|
|
||||||
oldUsernames: currentUsernames,
|
|
||||||
newUsername: player.username,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
event: "none",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await db.insert(tables.trackedPlayers).values({
|
|
||||||
playerId,
|
|
||||||
});
|
|
||||||
|
|
||||||
await db.insert(tables.usernameHistory).values({
|
|
||||||
playerId,
|
|
||||||
username: player.username,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { event: "registered" };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ export const makeResultEmbed = async (
|
|||||||
.slice(0, Math.min(rewardedParticipants.length, env.QUEST_REWARDS.length))
|
.slice(0, Math.min(rewardedParticipants.length, env.QUEST_REWARDS.length))
|
||||||
.map(
|
.map(
|
||||||
(x, i) =>
|
(x, i) =>
|
||||||
`- ${medals[i]} ${x.username} - ${env.QUEST_REWARDS![i]} gemmes`,
|
`- ${medals[i]} ${x.username} - ${env.QUEST_REWARDS![i]} ${env.QUEST_REWARDS_ARE_GEMS ? "gemmes" : ""}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (env.QUEST_REWARDS_ARE_GEMS) {
|
||||||
const arr = rewardedParticipants.slice(
|
const arr = rewardedParticipants.slice(
|
||||||
0,
|
0,
|
||||||
Math.min(rewardedParticipants.length, env.QUEST_REWARDS.length),
|
Math.min(rewardedParticipants.length, env.QUEST_REWARDS.length),
|
||||||
@@ -38,6 +39,7 @@ export const makeResultEmbed = async (
|
|||||||
balance + parseInt(env.QUEST_REWARDS![i]),
|
balance + parseInt(env.QUEST_REWARDS![i]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rewardsEmbed = {
|
rewardsEmbed = {
|
||||||
title: "Récompenses",
|
title: "Récompenses",
|
||||||
@@ -72,7 +74,7 @@ export const makeResultEmbed = async (
|
|||||||
|
|
||||||
export const createErrorEmbed = (
|
export const createErrorEmbed = (
|
||||||
message: string,
|
message: string,
|
||||||
color = 15335424,
|
color = 0xea0000,
|
||||||
): MessageCreateOptions => ({
|
): MessageCreateOptions => ({
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
@@ -84,7 +86,7 @@ export const createErrorEmbed = (
|
|||||||
|
|
||||||
export const createSuccessEmbed = (
|
export const createSuccessEmbed = (
|
||||||
message: string,
|
message: string,
|
||||||
color = 65280,
|
color = 0x00ea00,
|
||||||
): MessageCreateOptions => ({
|
): MessageCreateOptions => ({
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,12 +2,18 @@ import { ChannelType, type Client, type Message } from "discord.js";
|
|||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import { makeResultEmbed } from "~/utils/discord";
|
import { makeResultEmbed } from "~/utils/discord";
|
||||||
import type { QuestResult } from "~/services/wov";
|
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) => {
|
export const askForGrinders = async (quest: QuestResult, client: Client) => {
|
||||||
const adminChannel = await client.channels.fetch(env.DISCORD_ADMIN_CHANNEL);
|
const adminChannel = await client.channels.fetch(env.DISCORD_ADMIN_CHANNEL);
|
||||||
if (!adminChannel || adminChannel.type !== ChannelType.GuildText)
|
if (!adminChannel || adminChannel.type !== ChannelType.GuildText) {
|
||||||
throw "Invalid admin channel provided";
|
return questLogger.fatal("Invalid 'DISCORD_ADMIN_CHANNEL'");
|
||||||
|
}
|
||||||
|
|
||||||
|
let exclude: string[] = [];
|
||||||
|
if (env.QUEST_REWARDS) {
|
||||||
const top10 = quest.participants
|
const top10 = quest.participants
|
||||||
.filter((x) => !env.QUEST_EXCLUDE.includes(x.username))
|
.filter((x) => !env.QUEST_EXCLUDE.includes(x.username))
|
||||||
.sort((a, b) => b.xp - a.xp)
|
.sort((a, b) => b.xp - a.xp)
|
||||||
@@ -91,12 +97,16 @@ export const askForGrinders = async (quest: QuestResult, client: Client) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (answer === null) throw "unreachable";
|
if (answer === null) {
|
||||||
|
return questLogger.fatal("Answer was 'null', this should be unreachable");
|
||||||
|
}
|
||||||
|
|
||||||
const exclude = answer
|
exclude = answer
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((x) => x.trim())
|
.map((x) => x.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
const embed = await makeResultEmbed(quest, [
|
const embed = await makeResultEmbed(quest, [
|
||||||
...env.QUEST_EXCLUDE,
|
...env.QUEST_EXCLUDE,
|
||||||
...exclude,
|
...exclude,
|
||||||
@@ -107,9 +117,11 @@ export const askForGrinders = async (quest: QuestResult, client: Client) => {
|
|||||||
if (rewardChannel && rewardChannel.type === ChannelType.GuildText) {
|
if (rewardChannel && rewardChannel.type === ChannelType.GuildText) {
|
||||||
await rewardChannel.send(embed);
|
await rewardChannel.send(embed);
|
||||||
} else {
|
} else {
|
||||||
throw "Invalid reward channel";
|
return questLogger.fatal("Invalid 'DISCORD_REWARDS_CHANNEL'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (env.QUEST_EXCLUDE) {
|
||||||
await adminChannel.send("Envoyé !");
|
await adminChannel.send("Envoyé !");
|
||||||
console.log(`Quest result posted at: ${new Date().toISOString()}`);
|
}
|
||||||
|
questLogger.info(`Results posted at: ${new Date().toISOString()}`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
container_name: lbf-bot-postgres
|
||||||
image: postgres:17-alpine
|
image: postgres:17-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -17,6 +18,7 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
|
container_name: lbf-bot-redis
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
@@ -25,7 +27,8 @@ services:
|
|||||||
- lbf-network
|
- lbf-network
|
||||||
|
|
||||||
discord-bot:
|
discord-bot:
|
||||||
image: pihkaal/lbf-bot-discord-bot:latest
|
container_name: lbf-bot
|
||||||
|
image: git.pihkaal.xyz/pihkaal/lbf-bot:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -44,11 +47,13 @@ services:
|
|||||||
- DISCORD_ADMIN_MENTION
|
- DISCORD_ADMIN_MENTION
|
||||||
- DISCORD_ADMIN_CHANNEL
|
- DISCORD_ADMIN_CHANNEL
|
||||||
- DISCORD_TRACKING_CHANNEL
|
- DISCORD_TRACKING_CHANNEL
|
||||||
|
- DISCORD_STAFF_ROLE_ID
|
||||||
- WOV_API_KEY
|
- WOV_API_KEY
|
||||||
- WOV_CLAN_ID
|
- WOV_CLAN_ID
|
||||||
- WOV_FETCH_INTERVAL
|
- WOV_FETCH_INTERVAL
|
||||||
- WOV_TRACKING_INTERVAL
|
- WOV_TRACKING_INTERVAL
|
||||||
- QUEST_REWARDS
|
- QUEST_REWARDS
|
||||||
|
- QUEST_REWARDS_ARE_GEMS
|
||||||
- QUEST_EXCLUDE
|
- QUEST_EXCLUDE
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "lbf",
|
"name": "lbf-bot",
|
||||||
"packageManager": "pnpm@10.24.0",
|
"packageManager": "pnpm@10.24.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write --cache ."
|
"format": "prettier --write --cache ."
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { drizzle } from "drizzle-orm/node-postgres";
|
import { drizzle } from "drizzle-orm/node-postgres";
|
||||||
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
||||||
import { env } from "~/env";
|
import { createLogger } from "@lbf-bot/utils";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
|
import { env } from "~/env";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const dbLogger = createLogger({ prefix: "db" });
|
||||||
|
|
||||||
export async function runMigrations() {
|
export async function runMigrations() {
|
||||||
console.log("Connecting to database...");
|
|
||||||
const db = drizzle(env.DATABASE_URL);
|
const db = drizzle(env.DATABASE_URL);
|
||||||
|
|
||||||
const migrationsFolder = join(__dirname, "..", "drizzle");
|
const migrationsFolder = join(__dirname, "..", "drizzle");
|
||||||
console.log(`Running migrations from: ${migrationsFolder}`);
|
|
||||||
|
|
||||||
|
dbLogger.info(`Running migrations`);
|
||||||
await migrate(db, { migrationsFolder });
|
await migrate(db, { migrationsFolder });
|
||||||
|
dbLogger.info("Migrations completed");
|
||||||
console.log("✅ Database migrations completed");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from "./env";
|
export * from "./env";
|
||||||
|
export * from "./logger";
|
||||||
|
|||||||
101
packages/utils/src/logger.ts
Normal file
101
packages/utils/src/logger.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
type LogLevel = "debug" | "info" | "warn" | "error";
|
||||||
|
|
||||||
|
interface LoggerOptions {
|
||||||
|
prefix?: string;
|
||||||
|
level?: LogLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOG_LEVELS = {
|
||||||
|
debug: 0,
|
||||||
|
info: 1,
|
||||||
|
warn: 2,
|
||||||
|
error: 3,
|
||||||
|
} as const satisfies Record<LogLevel, number>;
|
||||||
|
|
||||||
|
const COLORS = {
|
||||||
|
debug: "\x1b[36m", // cyan
|
||||||
|
info: "\x1b[32m", // green
|
||||||
|
warn: "\x1b[33m", // yellow
|
||||||
|
error: "\x1b[31m", // red
|
||||||
|
reset: "\x1b[0m",
|
||||||
|
gray: "\x1b[90m",
|
||||||
|
bold: "\x1b[1m",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
private prefix: string;
|
||||||
|
private minLevel: number;
|
||||||
|
|
||||||
|
constructor(options: LoggerOptions = {}) {
|
||||||
|
this.prefix = options.prefix || "";
|
||||||
|
this.minLevel = LOG_LEVELS[options.level || "info"];
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatTimestamp(): string {
|
||||||
|
const now = new Date();
|
||||||
|
const hours = String(now.getHours()).padStart(2, "0");
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, "0");
|
||||||
|
const seconds = String(now.getSeconds()).padStart(2, "0");
|
||||||
|
return `${hours}:${minutes}:${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private log(level: LogLevel, message: string, ...args: unknown[]): void {
|
||||||
|
if (LOG_LEVELS[level] < this.minLevel) return;
|
||||||
|
|
||||||
|
const timestamp = this.formatTimestamp();
|
||||||
|
const color = COLORS[level];
|
||||||
|
const levelStr = level.toUpperCase().padEnd(5);
|
||||||
|
const prefix = this.prefix ? `[${this.prefix}] ` : "";
|
||||||
|
|
||||||
|
const formattedArgs = args.map((arg) => {
|
||||||
|
if (arg instanceof Error) {
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`${COLORS.gray}${timestamp}${COLORS.reset} ${color}${COLORS.bold}${levelStr}${COLORS.reset} ${prefix}${message}`,
|
||||||
|
...formattedArgs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(message: string, ...args: unknown[]): void {
|
||||||
|
this.log("debug", message, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message: string, ...args: unknown[]): void {
|
||||||
|
this.log("info", message, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message: string, ...args: unknown[]): void {
|
||||||
|
this.log("warn", message, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string, ...args: unknown[]): void {
|
||||||
|
this.log("error", message, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
fatal(message: string, ...args: unknown[]): never {
|
||||||
|
this.log("error", message, ...args);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
child(prefix: string): Logger {
|
||||||
|
const childPrefix = this.prefix ? `${this.prefix}:${prefix}` : prefix;
|
||||||
|
return new Logger({ prefix: childPrefix, level: this.getLevel() });
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLevel(): LogLevel {
|
||||||
|
const entry = Object.entries(LOG_LEVELS).find(
|
||||||
|
([, value]) => value === this.minLevel,
|
||||||
|
);
|
||||||
|
return (entry?.[0] as LogLevel) || "info";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createLogger = (options?: LoggerOptions): Logger => {
|
||||||
|
return new Logger(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logger = createLogger();
|
||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
|||||||
'@lbf-bot/database':
|
'@lbf-bot/database':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/database
|
version: link:../../packages/database
|
||||||
|
'@lbf-bot/utils':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/utils
|
||||||
discord.js:
|
discord.js:
|
||||||
specifier: ^14.21.0
|
specifier: ^14.21.0
|
||||||
version: 14.25.1
|
version: 14.25.1
|
||||||
|
|||||||
Reference in New Issue
Block a user