diff --git a/apps/discord-bot/src/commands/gemmes.ts b/apps/discord-bot/src/commands/gemmes.ts new file mode 100644 index 0000000..64347fd --- /dev/null +++ b/apps/discord-bot/src/commands/gemmes.ts @@ -0,0 +1,65 @@ +import type { Command } from "~/commands"; +import { getAccountBalance, setAccountBalance } from "~/services/account"; +import { getClanMembers } from "~/services/wov"; +import { createErrorEmbed } from "~/utils/discord"; + +const STAFF_ROLE_ID = "1147963065640439900"; + +export const gemmesCommand: Command = async (message, args) => { + const displayName = message.member?.displayName || message.author.username; + let playerName = displayName.replace("🕾 |", "").trim(); + if (args.length >= 1) { + playerName = args[0]; + } + + const clanMembers = await getClanMembers(); + + let clanMember = clanMembers.find((x) => x.username === playerName); + if (!clanMember) { + await message.reply( + createErrorEmbed( + `\`${playerName}\` n'est pas dans le clan (la honte).\n**Attention les majuscules sont importantes**`, + ), + ); + } else { + if (args.length === 2 && message.member) { + if (!message.member.roles.cache.has(STAFF_ROLE_ID)) { + await message.reply( + createErrorEmbed("Tu t'es cru chez mĂ©mĂ© ou quoi faut ĂȘtre staff"), + ); + return; + } + + if ( + (args[1][0] !== "+" && args[1][0] !== "-") || + !args[1] || + isNaN(Number(args[1].substring(1))) + ) { + await message.reply( + createErrorEmbed( + "Format: `@LBF gemmes <+GEMMES | -GEMMES>`\nExemple:`@LBF gemmes Yuno -10000`\n**Attention les majuscules sont importantes**", + ), + ); + return; + } + + const mult = args[1][0] === "+" ? 1 : -1; + const delta = Number(args[1].substring(1)) * mult; + const balance = await getAccountBalance(clanMember.playerId); + await setAccountBalance( + clanMember.playerId, + Math.max(0, balance + delta), + ); + } + + const balance = await getAccountBalance(clanMember.playerId); + await message.reply({ + embeds: [ + { + description: `### 💎 Compte de ${playerName}\n\n\nGemmes disponibles: **${balance}**\n\n-# Voir avec <@294871767820795904> pour Ă©changer contre skin/carte etc`, + color: 4360641, + }, + ], + }); + } +}; diff --git a/apps/discord-bot/src/commands/icone.ts b/apps/discord-bot/src/commands/icone.ts new file mode 100644 index 0000000..8c139cb --- /dev/null +++ b/apps/discord-bot/src/commands/icone.ts @@ -0,0 +1,51 @@ +import type { Command } from "~/commands"; +import { searchPlayer, getClanInfos } from "~/services/wov"; +import { createErrorEmbed } from "~/utils/discord"; + +export const iconeCommand: Command = async (message, args) => { + let playerName = args[0]; + if (!playerName) { + await message.reply( + createErrorEmbed( + "Usage:`@LBF icone NOM_JOUEUR`, exemple: `@LBF icone Yuno`.\n**Attention les majuscules sont importantes**", + ), + ); + return; + } + + const player = await searchPlayer(playerName); + if (!player) { + await message.reply( + createErrorEmbed( + "Joueur·euse non trouvé·e.\n**Attention les majuscules sont importantes**", + ), + ); + return; + } + + if (!player.clanId) { + await message.reply( + createErrorEmbed( + "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 message.reply( + createErrorEmbed("Impossible de rĂ©cupĂ©rer les informations du clan."), + ); + return; + } + + await message.reply({ + embeds: [ + { + description: `### ✅ Informations du clan\n\n**Nom:** \`\`\`${clan.name}\`\`\`\n**Tag:** \`\`\`${clan.tag}\`\`\``, + color: 65280, + }, + ], + }); +}; diff --git a/apps/discord-bot/src/commands/index.ts b/apps/discord-bot/src/commands/index.ts new file mode 100644 index 0000000..2eb8e67 --- /dev/null +++ b/apps/discord-bot/src/commands/index.ts @@ -0,0 +1,21 @@ +import type { Message, OmitPartialGroupDMChannel } from "discord.js"; +import { pingCommand } from "./ping"; +import { trackCommand } from "./track"; +import { tejtrackCommand } from "./tejtrack"; +import { iconeCommand } from "./icone"; +import { gemmesCommand } from "./gemmes"; +import { resultCommand } from "./result"; + +export type Command = ( + message: OmitPartialGroupDMChannel>, + args: Array, +) => Promise | void; + +export const commands: Record = { + ping: pingCommand, + track: trackCommand, + tejtrack: tejtrackCommand, + icone: iconeCommand, + gemmes: gemmesCommand, + result: resultCommand, +}; diff --git a/apps/discord-bot/src/commands/ping.ts b/apps/discord-bot/src/commands/ping.ts new file mode 100644 index 0000000..5f2b555 --- /dev/null +++ b/apps/discord-bot/src/commands/ping.ts @@ -0,0 +1,5 @@ +import type { Command } from "~/commands"; + +export const pingCommand: Command = async (message, args) => { + await message.reply("pong"); +}; diff --git a/apps/discord-bot/src/commands/result.ts b/apps/discord-bot/src/commands/result.ts new file mode 100644 index 0000000..6ae419e --- /dev/null +++ b/apps/discord-bot/src/commands/result.ts @@ -0,0 +1,9 @@ +import type { Command } from "~/commands"; +import { getLatestQuest } from "~/services/wov"; +import { askForGrinders } from "~/utils/quest"; + +export const resultCommand: Command = async (message, args) => { + const client = message.client; + const quest = await getLatestQuest(); + await askForGrinders(quest, client); +}; diff --git a/apps/discord-bot/src/commands/tejtrack.ts b/apps/discord-bot/src/commands/tejtrack.ts new file mode 100644 index 0000000..6ab1254 --- /dev/null +++ b/apps/discord-bot/src/commands/tejtrack.ts @@ -0,0 +1,65 @@ +import type { Command } from "~/commands"; +import { untrackWovPlayer } from "~/services/tracking"; +import { searchPlayer } from "~/services/wov"; +import { createErrorEmbed, createInfoEmbed } from "~/utils/discord"; +import { env } from "~/env"; + +const STAFF_ROLE_ID = "1147963065640439900"; + +export const tejtrackCommand: Command = async (message, args) => { + const client = message.client; + if (!message.member) return; + if (!message.member.roles.cache.has(STAFF_ROLE_ID)) { + await message.reply( + createErrorEmbed("Tu t'es cru chez mĂ©mĂ© ou quoi faut ĂȘtre staff"), + ); + return; + } + + let playerName = args[0]; + if (!playerName) { + await message.reply( + createErrorEmbed( + "Usage:`@LBF untrack NOM_JOUEUR`, exemple: `@LBF untrack Yuno`.\n**Attention les majuscules sont importantes**", + ), + ); + return; + } + + const player = await searchPlayer(playerName); + if (!player) { + await message.reply( + createErrorEmbed( + "Cette personne n'existe pas.\n**Attention les majuscules sont importantes**", + ), + ); + return; + } + + const res = await untrackWovPlayer(player.id); + switch (res.event) { + case "notTracked": { + await message.reply( + createInfoEmbed( + `Pas de tracker pour \`${playerName}\` [\`${player.id}\`]`, + ), + ); + break; + } + case "trackerRemoved": { + await message.reply( + createInfoEmbed( + `Tracker enlevĂ© pour \`${playerName}\` [\`${player.id}\`]`, + ), + ); + + const chan = client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL); + if (!chan?.isSendable()) throw "Invalid tracking channel"; + + await chan.send( + createInfoEmbed(`### [REMOVED] \`${playerName}\` [\`${player.id}\`]`), + ); + break; + } + } +}; diff --git a/apps/discord-bot/src/commands/track.ts b/apps/discord-bot/src/commands/track.ts new file mode 100644 index 0000000..97c277e --- /dev/null +++ b/apps/discord-bot/src/commands/track.ts @@ -0,0 +1,78 @@ +import type { Command } from "~/commands"; +import { trackWovPlayer } from "~/services/tracking"; +import { searchPlayer } from "~/services/wov"; +import { createErrorEmbed, createInfoEmbed } from "~/utils/discord"; +import { env } from "~/env"; + +const STAFF_ROLE_ID = "1147963065640439900"; + +export const trackCommand: Command = async (message, args) => { + const client = message.client; + if (!message.member) return; + if (!message.member.roles.cache.has(STAFF_ROLE_ID)) { + await message.reply( + createErrorEmbed("Tu t'es cru chez mĂ©mĂ© ou quoi faut ĂȘtre staff"), + ); + return; + } + + let playerName = args[0]; + if (!playerName) { + await message.reply( + createErrorEmbed( + "Usage:`@LBF track NOM_JOUEUR`, exemple: `@LBF track Yuno`.\n**Attention les majuscules sont importantes**", + ), + ); + return; + } + + const player = await searchPlayer(playerName); + if (!player) { + await message.reply( + createErrorEmbed( + "Cette personne n'existe pas.\n**Attention les majuscules sont importantes**", + ), + ); + return; + } + + const res = await trackWovPlayer(player.id); + switch (res.event) { + case "notFound": { + await message.reply( + createErrorEmbed( + "Cette personne n'existe pas.\n**Attention les majuscules sont importantes**", + ), + ); + return; + } + + case "registered": { + await message.reply( + createInfoEmbed( + `Tracker enregistrĂ© pour \`${playerName}\` [\`${player.id}\`]`, + ), + ); + + const chan = client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL); + if (!chan?.isSendable()) throw "Invalid tracking channel"; + + await chan.send( + 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; + } + } +}; diff --git a/apps/discord-bot/src/index.ts b/apps/discord-bot/src/index.ts index daa830a..c3507fc 100644 --- a/apps/discord-bot/src/index.ts +++ b/apps/discord-bot/src/index.ts @@ -1,29 +1,16 @@ -import { getAccountBalance, initAccounts, setAccountBalance } from "./account"; -import { makeResultEmbed } from "./discordUtils"; -import { env } from "./env"; +import { initAccounts } from "~/services/account"; +import { env } from "~/env"; import { initTracking, listTrackedPlayers, trackWovPlayer, - untrackWovPlayer, -} from "./tracking"; -import { - checkForNewQuest, - getClanInfos, - getClanMembers, - getLatestQuest, - searchPlayer, - type QuestResult, -} from "./wov"; +} from "~/services/tracking"; +import { checkForNewQuest } from "~/services/wov"; +import { createInfoEmbed } from "~/utils/discord"; +import { askForGrinders } from "~/utils/quest"; +import { commands } from "~/commands"; -import { - ChannelType, - Client, - EmbedBuilder, - GatewayIntentBits, - Message, - Partials, -} from "discord.js"; +import { ChannelType, Client, GatewayIntentBits, Partials } from "discord.js"; import * as readline from "node:readline"; // user mode = write in console, send in channel @@ -53,121 +40,10 @@ const client = new Client({ partials: [Partials.Message, Partials.Channel], }); -const askForGrinders = async (quest: QuestResult) => { - const adminChannel = await client.channels.fetch(env.DISCORD_ADMIN_CHANNEL); - if (!adminChannel || adminChannel.type !== ChannelType.GuildText) - throw "Invalid admin channel provided"; - - 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) throw "unreachable"; - - const 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 { - throw "Invalid reward channel"; - } - - await adminChannel.send("EnvoyĂ© !"); - console.log(`Quest result posted at: ${new Date().toISOString()}`); -}; - -const fn = async () => { +const questCheckCron = async () => { const quest = await checkForNewQuest(); if (quest) { - await askForGrinders(quest); + await askForGrinders(quest, client); } }; @@ -182,21 +58,11 @@ const trackingCron = async () => { const lastUsername = res.oldUsernames[res.oldUsernames.length - 1]; - await chan.send({ - embeds: [ - { - description: `### [UPDATE] \`${lastUsername}\` -> \`${res.newUsername}\` [\`${playerId}\`]`, - fields: [ - { name: "Nouveau pseudo", value: `\`${res.newUsername}\`` }, - { - name: "Anciens pseudos", - value: res.oldUsernames.map((x) => `- \`${x}\``).join("\n"), - }, - ], - color: 0x89cff0, - }, - ], - }); + await chan.send( + createInfoEmbed( + `### [UPDATE] \`${lastUsername}\` -> \`${res.newUsername}\` [\`${playerId}\`]\n\n**Nouveau pseudo:** \`${res.newUsername}\`\n**Anciens pseudos:**\n${res.oldUsernames.map((x) => `- \`${x}\``).join("\n")}`, + ), + ); } }; @@ -230,8 +96,8 @@ client.on("ready", async (client) => { await initAccounts(); await initTracking(); - await fn(); - setInterval(fn, env.WOV_FETCH_INTERVAL); + await questCheckCron(); + setInterval(questCheckCron, env.WOV_FETCH_INTERVAL); await trackingCron(); setInterval(trackingCron, env.WOV_TRACKING_INTERVAL); @@ -241,320 +107,15 @@ client.on("ready", async (client) => { client.on("messageCreate", async (message) => { if (message.author.bot || userMode.enabled) return; - const displayName = message.member?.displayName || message.author.username; - if (message.content.startsWith(`<@${client.user!.id}>`)) { const [command, ...args] = message.content .replace(`<@${client.user!.id}>`, "") .trim() .split(" "); - if (command === "ping") { - await message.reply("pong"); - } else if (command === "tejtrack") { - if (!message.member) return; - if (!message.member.roles.cache.has("1147963065640439900")) { - await message.reply({ - embeds: [ - { - description: - "### ❌ Erreur\n\n\nTu t'es cru chez mĂ©mĂ© ou quoi faut ĂȘtre staff", - color: 15335424, - }, - ], - }); - return; - } - let playerName = args[0]; - if (!playerName) { - await message.reply({ - embeds: [ - { - description: `### ❌ Erreur\n\n\n\nUsage:\`@LBF untrack NOM_JOUEUR\`, exemple: \`@LBF untrack Yuno\`.\n**Attention les majuscules sont importantes**`, - color: 15335424, - }, - ], - }); - return; - } - - const player = await searchPlayer(playerName); - if (!player) { - await message.reply({ - embeds: [ - { - description: `### ❌ Erreur\n\n\n\nCette personne n'existe pas.\n**Attention les majuscules sont importantes**`, - color: 15335424, - }, - ], - }); - return; - } - - const res = await untrackWovPlayer(player.id); - switch (res.event) { - case "notTracked": { - await message.reply({ - embeds: [ - { - description: `Pas de tracker pour \`${playerName}\` [\`${player.id}\`]`, - color: 0x89cff0, - }, - ], - }); - break; - } - case "trackerRemoved": { - await message.reply({ - embeds: [ - { - description: `Tracker enlevĂ© pour \`${playerName}\` [\`${player.id}\`]`, - color: 0x89cff0, - }, - ], - }); - - const chan = client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL); - if (!chan?.isSendable()) throw "Invalid tracking channel"; - - await chan.send({ - embeds: [ - { - description: `### [REMOVED] \`${playerName}\` [\`${player.id}\`]`, - color: 0x89cff0, - }, - ], - }); - break; - } - } - } else if (command === "track") { - if (!message.member) return; - if (!message.member.roles.cache.has("1147963065640439900")) { - await message.reply({ - embeds: [ - { - description: - "### ❌ Erreur\n\n\nTu t'es cru chez mĂ©mĂ© ou quoi faut ĂȘtre staff", - color: 15335424, - }, - ], - }); - return; - } - - let playerName = args[0]; - if (!playerName) { - await message.reply({ - embeds: [ - { - description: `### ❌ Erreur\n\n\n\nUsage:\`@LBF track NOM_JOUEUR\`, exemple: \`@LBF track Yuno\`.\n**Attention les majuscules sont importantes**`, - color: 15335424, - }, - ], - }); - return; - } - - const player = await searchPlayer(playerName); - if (!player) { - await message.reply({ - embeds: [ - { - description: `### ❌ Erreur\n\n\n\nCette personne n'existe pas.\n**Attention les majuscules sont importantes**`, - color: 15335424, - }, - ], - }); - return; - } - - const res = await trackWovPlayer(player.id); - switch (res.event) { - case "notFound": { - await message.reply({ - embeds: [ - { - description: `### ❌ Erreur\n\n\n\nCette personne n'existe pas.\n**Attention les majuscules sont importantes**`, - color: 15335424, - }, - ], - }); - return; - } - - case "registered": { - await message.reply({ - embeds: [ - { - description: `Tracker enregistrĂ© pour \`${playerName}\` [\`${player.id}\`]`, - color: 0x89cff0, - }, - ], - }); - - const chan = client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL); - if (!chan?.isSendable()) throw "Invalid tracking channel"; - - await chan.send({ - embeds: [ - { - description: `### [NEW] \`${playerName}\` [\`${player.id}\`]`, - color: 0x89cff0, - }, - ], - }); - return; - } - case "none": { - await message.reply({ - embeds: [ - { - description: `Tracker dĂ©jĂ  enregistrĂ© pour \`${playerName}\` [\`${player.id}\`]`, - color: 0x89cff0, - }, - ], - }); - return; - } - case "changed": { - // ignored - break; - } - } - } else if (command === "icone") { - let playerName = args[0]; - if (!playerName) { - await message.reply({ - embeds: [ - { - description: `### ❌ Erreur\n\n\n\nUsage:\`@LBF icone NOM_JOUEUR\`, exemple: \`@LBF icone Yuno\`.\n**Attention les majuscules sont importantes**`, - color: 15335424, - }, - ], - }); - return; - } - - const player = await searchPlayer(playerName); - if (!player) { - await message.reply({ - embeds: [ - { - description: `### ❌ Erreur\n\n\n\nJoueur·euse non trouvé·e.\n**Attention les majuscules sont importantes**`, - color: 15335424, - }, - ], - }); - return; - } - - if (!player.clanId) { - await message.reply({ - embeds: [ - { - description: `### ❌ Erreur\n\n\n\nCette personne __n'a pas de clan__ ou __a cachĂ© son clan__.\n**Attention les majuscules sont importantes**`, - color: 15335424, - }, - ], - }); - return; - } - - const clan = await getClanInfos(player.clanId); - if (!clan) { - await message.reply({ - embeds: [ - { - description: `### ❌ Erreur\n\n\n\nImpossible de rĂ©cupĂ©rer les informations du clan.`, - color: 15335424, - }, - ], - }); - return; - } - - await message.reply({ - embeds: [ - { - description: `### ✅ Informations du clan\n\n**Nom:** \`\`\`${clan.name}\`\`\`\n**Tag:** \`\`\`${clan.tag}\`\`\``, - color: 65280, - }, - ], - }); - } else if (command === "result") { - const quest = await getLatestQuest(); - await askForGrinders(quest); - } else if (command === "gemmes") { - let playerName = displayName.replace("🕾 |", "").trim(); - if (args.length >= 1) { - playerName = args[0]; - } - - const clanMembers = await getClanMembers(); - - let clanMember = clanMembers.find((x) => x.username === playerName); - if (!clanMember) { - await message.reply({ - embeds: [ - { - description: `### ❌ Erreur\n\n\n\`${playerName}\` n'est pas dans le clan (la honte).\n**Attention les majuscules sont importantes**`, - color: 15335424, - }, - ], - }); - } else { - if (args.length === 2 && message.member) { - if (!message.member.roles.cache.has("1147963065640439900")) { - await message.reply({ - embeds: [ - { - description: - "### ❌ Erreur\n\n\nTu t'es cru chez mĂ©mĂ© ou quoi faut ĂȘtre staff", - color: 15335424, - }, - ], - }); - return; - } - - if ( - (args[1][0] !== "+" && args[1][0] !== "-") || - !args[1] || - isNaN(Number(args[1].substring(1))) - ) { - await message.reply({ - embeds: [ - { - description: - "### ❌ Erreur\n\n\nFormat: \`@LBF gemmes <+GEMMES|-GEMMES>\`\nExemple:\`@LBF gemmes Yuno -10000\`\n**Attention les majuscules sont importantes**", - color: 15335424, - }, - ], - }); - return; - } - - const mult = args[1][0] === "+" ? 1 : -1; - const delta = Number(args[1].substring(1)) * mult; - const balance = await getAccountBalance(clanMember.playerId); - await setAccountBalance( - clanMember.playerId, - Math.max(0, balance + delta), - ); - } - - const balance = await getAccountBalance(clanMember.playerId); - // await message.reply(`Gemmes accumulĂ©es par ${playerName}: ${balance}`); - await message.reply({ - embeds: [ - { - description: `### 💎 Compte de ${playerName}\n\n\nGemmes disponibles: **${balance}**\n\n-# Voir avec <@294871767820795904> pour Ă©changer contre skin/carte etc`, - color: 4360641, - }, - ], - }); - } + const commandHandler = commands[command]; + if (commandHandler) { + await commandHandler(message, args); } } }); diff --git a/apps/discord-bot/src/account.ts b/apps/discord-bot/src/services/account.ts similarity index 100% rename from apps/discord-bot/src/account.ts rename to apps/discord-bot/src/services/account.ts diff --git a/apps/discord-bot/src/tracking.ts b/apps/discord-bot/src/services/tracking.ts similarity index 95% rename from apps/discord-bot/src/tracking.ts rename to apps/discord-bot/src/services/tracking.ts index 1abf29a..422f9a7 100644 --- a/apps/discord-bot/src/tracking.ts +++ b/apps/discord-bot/src/services/tracking.ts @@ -1,11 +1,10 @@ -import { getPlayer } from "./wov"; +import { getPlayer } from "~/services/wov"; import { readFile, writeFile, access } from "node:fs/promises"; import { constants } from "node:fs"; +import type { TrackedPlayers } from "~/types"; const TRACKED_PLAYER_FILE = "./.cache/tracked.json"; -type TrackedPlayers = Record; - export async function initTracking(): Promise { try { await access(TRACKED_PLAYER_FILE, constants.F_OK); diff --git a/apps/discord-bot/src/wov.ts b/apps/discord-bot/src/services/wov.ts similarity index 92% rename from apps/discord-bot/src/wov.ts rename to apps/discord-bot/src/services/wov.ts index bc79b51..3ee5a44 100644 --- a/apps/discord-bot/src/wov.ts +++ b/apps/discord-bot/src/services/wov.ts @@ -1,21 +1,7 @@ -import { env } from "./env"; +import { env } from "~/env"; import { mkdir, readFile, writeFile, access } from "node:fs/promises"; import { constants } from "node:fs"; - -export type QuestResult = { - quest: { - id: string; - promoImageUrl: string; - promoImagePrimaryColor: string; - }; - participants: Array; -}; - -export type QuestParticipant = { - playerId: string; - username: string; - xp: number; -}; +import type { QuestResult } from "~/types"; export const getLatestQuest = async (): Promise => { const response = await fetch( @@ -49,6 +35,7 @@ export const checkForNewQuest = async (): Promise => { await writeFile(cacheFilePath, lastId); return lastQuest; }; + export const getClanMembers = async (): Promise< Array<{ playerId: string; username: string }> > => { diff --git a/apps/discord-bot/src/types/index.ts b/apps/discord-bot/src/types/index.ts new file mode 100644 index 0000000..4db9887 --- /dev/null +++ b/apps/discord-bot/src/types/index.ts @@ -0,0 +1,30 @@ +export type QuestResult = { + quest: { + id: string; + promoImageUrl: string; + promoImagePrimaryColor: string; + }; + participants: Array; +}; + +export type QuestParticipant = { + playerId: string; + username: string; + xp: number; +}; + +export type DiscordMessage = { + content: string; + embeds: Array; +}; + +export type DiscordEmbed = { + title?: string; + description: string; + image?: { + url: string; + }; + color: number; +}; + +export type TrackedPlayers = Record; diff --git a/apps/discord-bot/src/discordUtils.ts b/apps/discord-bot/src/utils/discord.ts similarity index 76% rename from apps/discord-bot/src/discordUtils.ts rename to apps/discord-bot/src/utils/discord.ts index 3ab4b7d..0502af7 100644 --- a/apps/discord-bot/src/discordUtils.ts +++ b/apps/discord-bot/src/utils/discord.ts @@ -1,20 +1,6 @@ -import { getAccountBalance, setAccountBalance } from "./account"; -import { env } from "./env"; -import type { QuestResult } from "./wov"; - -export type DiscordMessage = { - content: string; - embeds: Array; -}; - -export type DiscordEmbed = { - title?: string; - description: string; - image?: { - url: string; - }; - color: number; -}; +import { getAccountBalance, setAccountBalance } from "~/services/account"; +import { env } from "~/env"; +import type { QuestResult, DiscordMessage, DiscordEmbed } from "~/types"; export const makeResultEmbed = async ( result: QuestResult, @@ -82,3 +68,30 @@ export const makeResultEmbed = async ( ], }; }; + +export const createErrorEmbed = (message: string, color = 15335424) => ({ + embeds: [ + { + description: `### ❌ Erreur\n\n\n${message}`, + color, + }, + ], +}); + +export const createSuccessEmbed = (message: string, color = 65280) => ({ + embeds: [ + { + description: `### ✅ ${message}`, + color, + }, + ], +}); + +export const createInfoEmbed = (message: string, color = 0x89cff0) => ({ + embeds: [ + { + description: message, + color, + }, + ], +}); diff --git a/apps/discord-bot/src/utils/quest.ts b/apps/discord-bot/src/utils/quest.ts new file mode 100644 index 0000000..70a404b --- /dev/null +++ b/apps/discord-bot/src/utils/quest.ts @@ -0,0 +1,115 @@ +import { ChannelType, type Client, type Message } from "discord.js"; +import { env } from "~/env"; +import { makeResultEmbed } from "~/utils/discord"; +import type { QuestResult } from "~/types"; + +export const askForGrinders = async (quest: QuestResult, client: Client) => { + const adminChannel = await client.channels.fetch(env.DISCORD_ADMIN_CHANNEL); + if (!adminChannel || adminChannel.type !== ChannelType.GuildText) + throw "Invalid admin channel provided"; + + 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) throw "unreachable"; + + const 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 { + throw "Invalid reward channel"; + } + + await adminChannel.send("EnvoyĂ© !"); + console.log(`Quest result posted at: ${new Date().toISOString()}`); +}; diff --git a/apps/discord-bot/tsconfig.build.json b/apps/discord-bot/tsconfig.build.json index f78cadf..a1d59a5 100644 --- a/apps/discord-bot/tsconfig.build.json +++ b/apps/discord-bot/tsconfig.build.json @@ -1,16 +1,14 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "noEmit": false, - "noEmitOnError": true, "outDir": "dist", - "rootDir": "src", - "sourceMap": true + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": false }, "tsc-alias": { - "resolveFullPaths": true, - "verbose": false + "resolveFullPaths": true }, - "include": ["src"], - "exclude": ["node_modules", "dist"] + "include": ["src"] } diff --git a/apps/discord-bot/tsconfig.json b/apps/discord-bot/tsconfig.json index 7f7da8c..61dc659 100644 --- a/apps/discord-bot/tsconfig.json +++ b/apps/discord-bot/tsconfig.json @@ -1,27 +1,28 @@ { "compilerOptions": { + "lib": ["ESNext"], "target": "ESNext", "module": "ESNext", - "moduleResolution": "Bundler", "moduleDetection": "force", - "allowJs": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "resolveJsonModule": true, - "noEmit": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": false, + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"], + "~": ["./src/index"] + }, "strict": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "resolveJsonModule": true, "noUnusedLocals": false, "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false, - - "baseUrl": ".", - "paths": { - "~/*": ["./src/*"] - } - } + "noPropertyAccessFromIndexSignature": false + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] }