refactor: improve code quality
This commit is contained in:
@@ -15,4 +15,7 @@ RUN pnpm --filter @lbf-bot/utils run build
|
||||
RUN pnpm --filter @lbf-bot/database run build
|
||||
RUN pnpm --filter @lbf/discord-bot run build
|
||||
|
||||
ARG COMMIT_SHA=dev
|
||||
ENV COMMIT_SHA=$COMMIT_SHA
|
||||
|
||||
CMD ["node", "apps/discord-bot/dist/index.js"]
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"name": "@lbf/discord-bot",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx src/index.ts",
|
||||
"start": "node dist/index.js",
|
||||
"build": "rm -rf dist && tsc --project tsconfig.build.json && tsc-alias --project tsconfig.build.json",
|
||||
"dev:user": "tsx src/index.ts -- --user"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lbf-bot/utils": "workspace:*",
|
||||
"@lbf-bot/database": "workspace:*",
|
||||
"discord.js": "^14.21.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"zod": "^3.24.4"
|
||||
}
|
||||
"name": "@lbf/discord-bot",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "COMMIT_SHA=$(git rev-parse --short HEAD)-dev tsx src/index.ts",
|
||||
"start": "node dist/index.js",
|
||||
"build": "rm -rf dist && tsc --project tsconfig.build.json && tsc-alias --project tsconfig.build.json",
|
||||
"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",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lbf-bot/utils": "workspace:*",
|
||||
"@lbf-bot/database": "workspace:*",
|
||||
"discord.js": "^14.21.0",
|
||||
"zod": "4.1.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,51 @@
|
||||
import { EmbedBuilder } from "discord.js";
|
||||
import { env } from "~/env";
|
||||
import type { Command } from "~/commands";
|
||||
import { getAccountBalance, setAccountBalance } from "~/services/account";
|
||||
import { getClanMembers } from "~/services/wov";
|
||||
import { replyError } from "~/utils/discord";
|
||||
import { getClanMembers } from "~/wov";
|
||||
import { getAccountBalance, setAccountBalance } from "~/economy";
|
||||
import { replyError } from "~/discord";
|
||||
import type { Command } from "./index";
|
||||
import { noMention } from "~/discord";
|
||||
|
||||
export const gemmesCommand: Command = async (message, args) => {
|
||||
// retrieve player name
|
||||
// NOTE: discord members have display name formatted like "🕸 | InGamePseudo"
|
||||
const displayName = message.member?.displayName || message.author.username;
|
||||
const playerName = args[0] || displayName.replace("🕸 |", "").trim();
|
||||
export const gemmesCommand: Command = {
|
||||
help: "Affiche ou modifie le solde de gemmes d'un membre",
|
||||
handler: async (message, args) => {
|
||||
// discord members have display name formatted like "🕸 | InGamePseudo"
|
||||
const displayName = message.member?.displayName || message.author.username;
|
||||
const playerName = args[0] || displayName.replace("🕸 |", "").trim();
|
||||
|
||||
// get clan member
|
||||
const clanMembers = await getClanMembers();
|
||||
const clanMember = clanMembers.find((x) => x.username === playerName);
|
||||
if (!clanMember) {
|
||||
await replyError(
|
||||
message,
|
||||
`\`${playerName}\` n'est pas dans le clan (la honte).\n**Attention les majuscules sont importantes**`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const clanMembers = await getClanMembers();
|
||||
const clanMember = clanMembers.find((x) => x.username === playerName);
|
||||
if (!clanMember) {
|
||||
await replyError(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 (!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 (args.length === 2) {
|
||||
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 op = args[1][0];
|
||||
const amount = Number(args[1].substring(1));
|
||||
if ((op !== "+" && op !== "-") || args[1].length === 1 || isNaN(amount)) {
|
||||
await replyError(
|
||||
message,
|
||||
"Format: `@LBF gemmes <pseudo> <+GEMMES | -GEMMES>`\nExemple: `@LBF gemmes Yuno -10000`\n**Attention les majuscules sont importantes**",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const op = args[1][0];
|
||||
const amount = Number(args[1].substring(1));
|
||||
if ((op !== "+" && op !== "-") || args[1].length === 1 || isNaN(amount)) {
|
||||
await replyError(message, "Usage: `@LBF gemmes <pseudo> <+GEMMES | -GEMMES>`\nExemple: `@LBF gemmes Yuno -10000`\n**Attention les majuscules sont importantes**");
|
||||
return;
|
||||
}
|
||||
|
||||
const balance = await getAccountBalance(clanMember.playerId);
|
||||
const delta = amount * (op === "+" ? 1 : -1);
|
||||
await setAccountBalance(clanMember.playerId, Math.max(0, balance + delta));
|
||||
}
|
||||
const balance = await getAccountBalance(clanMember.playerId);
|
||||
const delta = amount * (op === "+" ? 1 : -1);
|
||||
await setAccountBalance(clanMember.playerId, Math.max(0, balance + delta));
|
||||
}
|
||||
|
||||
// display balance
|
||||
const balance = await getAccountBalance(clanMember.playerId);
|
||||
await message.reply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setDescription(
|
||||
// TODO: mention here instead of in the env
|
||||
`### 💎 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,
|
||||
},
|
||||
const balance = await getAccountBalance(clanMember.playerId);
|
||||
await message.reply({
|
||||
options: noMention,
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setDescription(`### 💎 Compte de ${playerName}\n\n\nGemmes disponibles: **${balance}**\n\n-# Voir avec ${env.DISCORD_REWARDS_GIVER} pour échanger contre skin/carte etc`)
|
||||
.setColor(0x4289c1)
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,57 +1,43 @@
|
||||
import { EmbedBuilder } from "discord.js";
|
||||
import type { Command } from "~/commands";
|
||||
import { searchPlayer, getClanInfos } from "~/services/wov";
|
||||
import { replyError } from "~/utils/discord";
|
||||
import { searchPlayer, getClanInfo } from "~/wov";
|
||||
import { replyError } from "~/discord";
|
||||
import type { Command } from "./index";
|
||||
import { noMention } from "~/discord";
|
||||
|
||||
export const iconeCommand: Command = async (message, args) => {
|
||||
const playerName = args[0];
|
||||
if (!playerName) {
|
||||
await replyError(
|
||||
message,
|
||||
"Usage:`@LBF icone NOM_JOUEUR`, exemple: `@LBF icone Yuno`.\n**Attention les majuscules sont importantes**",
|
||||
);
|
||||
return;
|
||||
}
|
||||
export const iconeCommand: Command = {
|
||||
help: "Affiche l'icone et le nom du clan d'un joueur",
|
||||
handler: async (message, args) => {
|
||||
const playerName = args[0];
|
||||
if (!playerName) {
|
||||
await replyError(message, "Usage:`@LBF icone NOM_JOUEUR`, exemple: `@LBF icone Yuno`.\n**Attention les majuscules sont importantes**");
|
||||
return;
|
||||
}
|
||||
|
||||
const player = await searchPlayer(playerName);
|
||||
if (!player) {
|
||||
await replyError(
|
||||
message,
|
||||
"Joueur·euse non trouvé·e.\n**Attention les majuscules sont importantes**",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const player = await searchPlayer(playerName);
|
||||
if (!player) {
|
||||
await replyError(message, "Joueur·euse non trouvé·e.\n**Attention les majuscules sont importantes**");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player.clanId) {
|
||||
await replyError(
|
||||
message,
|
||||
"Cette personne __n'a pas de clan__ ou __a caché son clan__.\n**Attention les majuscules sont importantes**",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!player.clanId) {
|
||||
await replyError(message, "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);
|
||||
if (!clan) {
|
||||
await replyError(
|
||||
message,
|
||||
"Impossible de récupérer les informations du clan.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const clan = await getClanInfo(player.clanId);
|
||||
if (!clan) {
|
||||
await replyError(message, "Impossible de récupérer les informations du clan.");
|
||||
return;
|
||||
}
|
||||
|
||||
await message.reply({
|
||||
content: clan.tag,
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setDescription(
|
||||
`### ✅ Informations du clan\n\n**Nom:** \`\`\`${clan.name}\`\`\`\n**Tag:** \`\`\`${clan.tag}\`\`\``,
|
||||
)
|
||||
.setColor(65280),
|
||||
],
|
||||
options: {
|
||||
allowedMentions: {
|
||||
repliedUser: false,
|
||||
},
|
||||
await message.reply({
|
||||
options: noMention,
|
||||
content: clan.tag,
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setDescription(`### ✅ Informations du clan\n\n**Nom:** \`\`\`${clan.name}\`\`\`\n**Tag:** \`\`\`${clan.tag}\`\`\``)
|
||||
.setColor(65280)
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import type { Message, OmitPartialGroupDMChannel } from "discord.js";
|
||||
import { pingCommand } from "./ping";
|
||||
import { trackCommand } from "./track";
|
||||
import { tejtrackCommand } from "./tejtrack";
|
||||
import { trackCommand } from "./tracking";
|
||||
import { iconeCommand } from "./icone";
|
||||
import { gemmesCommand } from "./gemmes";
|
||||
import { resultCommand } from "./result";
|
||||
import { queteCommand } from "./quete";
|
||||
|
||||
export type Command = (
|
||||
message: OmitPartialGroupDMChannel<Message<boolean>>,
|
||||
args: Array<string>,
|
||||
export type CommandHandler = (
|
||||
message: OmitPartialGroupDMChannel<Message<boolean>>,
|
||||
args: Array<string>,
|
||||
) => Promise<void> | void;
|
||||
|
||||
export const commands: Record<string, Command> = {
|
||||
ping: pingCommand,
|
||||
track: trackCommand,
|
||||
tejtrack: tejtrackCommand,
|
||||
icone: iconeCommand,
|
||||
gemmes: gemmesCommand,
|
||||
result: resultCommand,
|
||||
quete: queteCommand,
|
||||
export type Command = {
|
||||
help: string;
|
||||
handler: CommandHandler;
|
||||
};
|
||||
|
||||
export const commands: Record<string, Command> = {
|
||||
ping: pingCommand,
|
||||
track: trackCommand,
|
||||
icone: iconeCommand,
|
||||
gemmes: gemmesCommand,
|
||||
quete: queteCommand,
|
||||
result: resultCommand,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { Command } from "~/commands";
|
||||
import { noMention } from "~/discord";
|
||||
import { env } from "~/env";
|
||||
import type { Command } from "./index";
|
||||
|
||||
export const pingCommand: Command = async (message, args) => {
|
||||
await message.reply({
|
||||
content: "🫵 Pong",
|
||||
options: {
|
||||
allowedMentions: {
|
||||
repliedUser: false,
|
||||
},
|
||||
export const pingCommand: Command = {
|
||||
help: "Pong",
|
||||
handler: async (message) => {
|
||||
await message.reply({
|
||||
options: noMention,
|
||||
content: `🫵 Pong \`${env.COMMIT_SHA}\``,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import { EmbedBuilder } from "discord.js";
|
||||
import type { Command } from "~/commands";
|
||||
import { getActiveQuest, getLatestQuest } from "~/services/wov";
|
||||
import { getActiveQuest, getLatestQuest } from "~/wov";
|
||||
import { replyError } from "~/discord";
|
||||
import type { Command } from "./index";
|
||||
import { noMention } from "~/discord";
|
||||
|
||||
export const queteCommand: Command = async (message) => {
|
||||
const quest = (await getActiveQuest()) ?? (await getLatestQuest());
|
||||
const color = parseInt(quest.quest.promoImagePrimaryColor.substring(1), 16);
|
||||
export const queteCommand: Command = {
|
||||
help: "Affiche la quête en cours ou la dernière quête",
|
||||
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({
|
||||
allowedMentions: {
|
||||
repliedUser: false
|
||||
await message.channel.send({
|
||||
options: noMention,
|
||||
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),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import type { Command } from "~/commands";
|
||||
import { getLatestQuest } from "~/services/wov";
|
||||
import { askForGrinders } from "~/utils/quest";
|
||||
import { getLatestQuest } from "~/wov";
|
||||
import { askForGrinders } from "~/quests";
|
||||
import { replyError } from "~/discord";
|
||||
import type { Command } from "./index";
|
||||
|
||||
export const resultCommand: Command = async (message, args) => {
|
||||
const client = message.client;
|
||||
const quest = await getLatestQuest();
|
||||
await askForGrinders(quest, client);
|
||||
export const resultCommand: Command = {
|
||||
help: "Déclenche manuellement la publication des résultats de la dernière quête",
|
||||
handler: async (message) => {
|
||||
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,
|
||||
),
|
||||
);
|
||||
};
|
||||
@@ -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 "dotenv/config";
|
||||
import { logger } from "@lbf-bot/utils";
|
||||
import { parseEnv, logger } from "@lbf-bot/utils";
|
||||
|
||||
// TODO: use parseEnv from utils
|
||||
|
||||
const schema = z.object({
|
||||
DISCORD_BOT_TOKEN: z.string(),
|
||||
DISCORD_MENTION: z.string(),
|
||||
DISCORD_REWARDS_GIVER: z.string(),
|
||||
DISCORD_REWARDS_CHANNEL: z.string(),
|
||||
// TODO: remove and compose from staff role id
|
||||
DISCORD_ADMIN_MENTION: z.string(),
|
||||
// TODO: rename to reward ask channel or smth
|
||||
DISCORD_ADMIN_CHANNEL: z.string(),
|
||||
DISCORD_TRACKING_CHANNEL: z.string(),
|
||||
DISCORD_STAFF_ROLE_ID: z.string(),
|
||||
WOV_API_KEY: z.string(),
|
||||
WOV_CLAN_ID: z.string(),
|
||||
WOV_FETCH_INTERVAL: z.coerce.number(),
|
||||
WOV_TRACKING_INTERVAL: z.coerce.number(),
|
||||
QUEST_REWARDS: z
|
||||
.string()
|
||||
.transform((x) => x.split(",").map((x) => x.trim()))
|
||||
.optional(),
|
||||
QUEST_REWARDS_ARE_GEMS: z
|
||||
.string()
|
||||
.transform((val) => val.toLowerCase() === "true")
|
||||
.pipe(z.boolean()),
|
||||
QUEST_EXCLUDE: z
|
||||
.string()
|
||||
.transform((x) => x.split(",").map((x) => x.trim()))
|
||||
.optional()
|
||||
.default(""),
|
||||
export const env = parseEnv({
|
||||
DISCORD_BOT_TOKEN: z.string(),
|
||||
DISCORD_MENTION: z.string(),
|
||||
DISCORD_REWARDS_GIVER: z.string(),
|
||||
DISCORD_REWARDS_CHANNEL: z.string(),
|
||||
// TODO: remove and compose from staff role id
|
||||
DISCORD_ADMIN_MENTION: z.string(),
|
||||
// TODO: rename to reward ask channel or smth
|
||||
DISCORD_ADMIN_CHANNEL: z.string(),
|
||||
DISCORD_TRACKING_CHANNEL: z.string(),
|
||||
DISCORD_STAFF_ROLE_ID: z.string(),
|
||||
COMMIT_SHA: z.string().default("dev"),
|
||||
WOV_API_KEY: z.string(),
|
||||
WOV_CLAN_ID: z.string(),
|
||||
WOV_FETCH_INTERVAL: z.coerce.number(),
|
||||
WOV_TRACKING_INTERVAL: z.coerce.number(),
|
||||
QUEST_REWARDS: z
|
||||
.string()
|
||||
.transform((x) => x.split(",").map((x) => x.trim()))
|
||||
.optional(),
|
||||
QUEST_REWARDS_ARE_GEMS: z
|
||||
.string()
|
||||
.transform((val) => val.toLowerCase() === "true")
|
||||
.pipe(z.boolean()),
|
||||
QUEST_EXCLUDE: z
|
||||
.string()
|
||||
.transform((x) => x.split(",").map((x) => x.trim()))
|
||||
.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 { Client, GatewayIntentBits, Partials } from "discord.js";
|
||||
import { setupBotMode } from "~/modes/bot";
|
||||
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...");
|
||||
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({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.DirectMessages,
|
||||
],
|
||||
partials: [Partials.Message, Partials.Channel],
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.DirectMessages,
|
||||
],
|
||||
partials: [Partials.Message, Partials.Channel],
|
||||
});
|
||||
|
||||
switch (mode.type) {
|
||||
case "user": {
|
||||
logger.info(`Starting as ${mode.type}`);
|
||||
|
||||
if (mode.type === "user") {
|
||||
setupUserMode(client, mode.channelId);
|
||||
break;
|
||||
}
|
||||
|
||||
case "bot": {
|
||||
} else if (mode.type === "bot") {
|
||||
setupBotMode(client);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// @ts-ignore
|
||||
} else {
|
||||
// @ts-expect-error -- exhaustive default for unimplemented mode types
|
||||
logger.fatal(`ERROR: Not implemented: '${mode.type}'`);
|
||||
}
|
||||
}
|
||||
|
||||
await client.login(env.DISCORD_BOT_TOKEN);
|
||||
|
||||
@@ -1,97 +1,45 @@
|
||||
import type { Client } from "discord.js";
|
||||
import { createLogger, logger } from "@lbf-bot/utils";
|
||||
import type { Client, OmitPartialGroupDMChannel, Message } from "discord.js";
|
||||
import { logger } from "@lbf-bot/utils";
|
||||
import { env } from "~/env";
|
||||
import {
|
||||
listTrackedPlayers,
|
||||
getTrackedPlayerUsernames,
|
||||
addUsernameToHistory,
|
||||
} from "~/services/tracking";
|
||||
import { checkForNewQuest, getPlayer } from "~/services/wov";
|
||||
import { createInfoEmbed } from "~/utils/discord";
|
||||
import { askForGrinders } from "~/utils/quest";
|
||||
import { questCheckCron } from "~/quests";
|
||||
import { trackingCron } from "~/tracking";
|
||||
import { commands } from "~/commands";
|
||||
|
||||
const questsLogger = createLogger({ prefix: "quests" });
|
||||
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) => {
|
||||
const onReady = async (client: Client<true>) => {
|
||||
logger.info(`Client ready`);
|
||||
logger.info(`Connected as @${client.user.username}`);
|
||||
|
||||
await questCheckCron(client);
|
||||
setInterval(() => questCheckCron(client), env.WOV_FETCH_INTERVAL);
|
||||
setInterval(() => void questCheckCron(client), env.WOV_FETCH_INTERVAL);
|
||||
|
||||
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>, client: Client) => {
|
||||
if (message.author.bot) return;
|
||||
|
||||
if (message.content.startsWith(`<@${client.user!.id}>`)) {
|
||||
const [command, ...args] = message.content
|
||||
const [commandName, ...args] = message.content
|
||||
.replace(`<@${client.user!.id}>`, "")
|
||||
.trim()
|
||||
.split(" ");
|
||||
|
||||
const commandHandler = commands[command];
|
||||
if (commandHandler) {
|
||||
const child = logger.child(
|
||||
`cmd:${command}${args.length > 0 ? " " : ""}${args.join(" ")}`,
|
||||
);
|
||||
const command = commands[commandName];
|
||||
if (!command) return;
|
||||
|
||||
const child = logger.child(`cmd:${commandName}${args.length > 0 ? " " : ""}${args.join(" ")}`);
|
||||
try {
|
||||
const start = Date.now();
|
||||
await commandHandler(message, args);
|
||||
const end = Date.now();
|
||||
child.info(`Done in ${(end - start).toFixed(2)}ms`);
|
||||
const start = Date.now();
|
||||
await command.handler(message, args);
|
||||
child.info(`Done in ${(Date.now() - start).toFixed(2)}ms`);
|
||||
} catch (error: unknown) {
|
||||
child.error("Failed:", error);
|
||||
child.error("Failed:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const setupBotMode = (client: Client) => {
|
||||
client.on("clientReady", (client) => { void onReady(client); });
|
||||
client.on("messageCreate", (message) => { void onMessage(message, client); });
|
||||
};
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
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 * as readline from "node:readline";
|
||||
|
||||
export const setupUserMode = (client: Client, channelId: string) => {
|
||||
client.on("clientReady", (client) => {
|
||||
logger.info(`Client ready`);
|
||||
logger.info(`Connected as @${client.user.username}`);
|
||||
client.on("clientReady", (client) => {
|
||||
logger.info(`Client ready`);
|
||||
logger.info(`Connected as @${client.user.username}`);
|
||||
|
||||
const chan = client.channels.cache.get(channelId);
|
||||
if (chan?.type !== ChannelType.GuildText) {
|
||||
console.error("ERROR: invalid channel");
|
||||
process.exit(1);
|
||||
}
|
||||
const chan = client.channels.cache.get(channelId);
|
||||
if (chan?.type !== ChannelType.GuildText) {
|
||||
console.error("ERROR: invalid channel");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
prompt: `${chan.name} ~ `,
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
169
apps/discord-bot/src/quests.ts
Normal file
169
apps/discord-bot/src/quests.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { ChannelType, EmbedBuilder, type Client, type Message, 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" });
|
||||
|
||||
// --- rewards ---
|
||||
|
||||
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")}\n\n-# \`@LBF gemmes\` pour voir votre nombre de gemmes. Puis avec ${env.DISCORD_REWARDS_GIVER} pour échanger contre des cadeaux !`)
|
||||
.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))
|
||||
.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 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: [
|
||||
new EmbedBuilder()
|
||||
.setTitle("Quête terminée !")
|
||||
.setColor(color),
|
||||
new EmbedBuilder()
|
||||
.setTitle("Top 10 XP")
|
||||
.setDescription(top10)
|
||||
.setColor(color),
|
||||
new EmbedBuilder()
|
||||
.setTitle("Qui a grind ?")
|
||||
.setDescription("Merci d'entrer les pseudos des joueurs qui ont grind.\n\nFormat:```@LBF onion,Yuno,...```\n**Attention les majuscules comptent**\nPour entrer la liste des joueurs, il faut __mentionner le bot__, si personne n'a grind, `@LBF tg`")
|
||||
.setColor(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({
|
||||
content: `Est-ce correct ? (oui/non)`,
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setTitle("Joueurs entrés")
|
||||
.setDescription(players.length ? players.map((name) => `- ${name}`).join("\n") : "*Aucun joueur entré*")
|
||||
.setColor(color),
|
||||
],
|
||||
});
|
||||
|
||||
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 logger.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) {
|
||||
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");
|
||||
}
|
||||
};
|
||||
@@ -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",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": false
|
||||
},
|
||||
"tsc-alias": {
|
||||
"resolveFullPaths": true
|
||||
},
|
||||
"include": ["src"]
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": false
|
||||
},
|
||||
"tsc-alias": {
|
||||
"resolveFullPaths": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"],
|
||||
"~": ["./src/index"]
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"paths": {
|
||||
"~/*": ["./src/*"],
|
||||
"~": ["./src/index"]
|
||||
},
|
||||
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
},
|
||||
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user