diff --git a/bun.lockb b/bun.lockb index 093b2d9..09f9120 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 38ac160..5453b88 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "typescript": "^5.0.0" }, "dependencies": { + "discord.js": "^14.21.0", "zod": "^3.24.4" } } diff --git a/src/discord.ts b/src/discord.ts index d749672..54d3ca8 100644 --- a/src/discord.ts +++ b/src/discord.ts @@ -15,19 +15,10 @@ export type DiscordEmbed = { color: number; }; -export const postEmbed = async (result: QuestResult): Promise => { - const embed = makeEmbed(result); - await fetch(env.DISCORD_WEBHOOK_URL, { - method: "POST", - body: JSON.stringify(embed), - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - }); -}; - -const makeEmbed = (result: QuestResult): DiscordMessage => { +export const makeResultEmbed = ( + result: QuestResult, + exclude: Array, +): DiscordMessage => { 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); @@ -36,7 +27,7 @@ const makeEmbed = (result: QuestResult): DiscordMessage => { if (env.QUEST_REWARDS) { const rewardedParticipants = participants .map((x) => x.username) - .filter((x) => !env.QUEST_EXCLUDE.includes(x)); + .filter((x) => !exclude.includes(x)); const medals = ["đŸ„‡", "đŸ„ˆ", "đŸ„‰"].concat( new Array(rewardedParticipants.length).fill("🏅"), ); @@ -69,7 +60,7 @@ const makeEmbed = (result: QuestResult): DiscordMessage => { { title: "Classement", description: participants - .filter((x) => !env.QUEST_EXCLUDE.includes(x.username)) + .filter((x) => !exclude.includes(x.username)) .filter((_, i) => i < 8) .map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`) .join("\n"), diff --git a/src/env.ts b/src/env.ts index 0898639..c5410c7 100644 --- a/src/env.ts +++ b/src/env.ts @@ -2,9 +2,11 @@ import { env as bunEnv } from "bun"; import { z } from "zod"; const schema = z.object({ - DISCORD_WEBHOOK_URL: z.string(), + DISCORD_BOT_TOKEN: z.string(), DISCORD_MENTION: z.string(), DISCORD_REWARDS_GIVER: z.string(), + DISCORD_ADMIN_MENTION: z.string(), + DISCORD_ADMIN_CHANNEL: z.string(), WOV_API_KEY: z.string(), WOV_CLAN_ID: z.string(), WOV_FETCH_INTERVAL: z.coerce.number(), diff --git a/src/index.ts b/src/index.ts index c57ca14..8e3c2dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,144 @@ -import { postEmbed } from "./discord"; +import { makeResultEmbed } from "./discord"; import { env } from "./env"; -import { checkForNewQuest } from "./wov"; +import { checkForNewQuest, getLatestQuest, type QuestResult } from "./wov"; + +import { ChannelType, Client, GatewayIntentBits, Message } from "discord.js"; + +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], +}); + +const askForGrinders = async (quest: QuestResult) => { + const channel = await client.channels.fetch(env.DISCORD_ADMIN_CHANNEL); + if (!channel || channel.type !== ChannelType.GuildText) + throw "Invalid admin channel provided"; + + const top10 = quest.participants + .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 channel.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:```laulau18,Yuno,...```\n**Attention aux majuscules**", + color, + }, + ], + }); + + const filter = (msg: Message) => + msg.channel.id === channel.id && !msg.author.bot; + + let confirmed = false; + let answer: string | null = null; + while (!confirmed) { + const collected = await channel.awaitMessages({ filter, max: 1 }); + answer = collected.first()?.content || null; + if (!answer) continue; + + const players = answer + .split(",") + .map((x) => x.trim()) + .filter(Boolean); + await channel.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 === channel.id && + !msg.author.bot && + ["oui", "non", "yes", "no"].includes(msg.content.toLowerCase()); + const confirmCollected = await channel.awaitMessages({ + filter: confirmFilter, + max: 1, + }); + const confirmation = confirmCollected.first()?.content.toLowerCase(); + if (confirmation === "oui" || confirmation === "yes") { + confirmed = true; + await channel.send({ content: "Ok" }); + } else { + await channel.send({ + content: "D'accord, veuillez rĂ©essayer. Qui a grind ?", + }); + } + } + + if (!answer) throw "unreachable"; + + const exclude = answer + .split(",") + .map((x) => x.trim()) + .filter(Boolean); + const embed = makeResultEmbed(quest, [...env.QUEST_EXCLUDE, ...exclude]); + const rewardChannel = await client.channels.fetch(env.DISCORD_ADMIN_CHANNEL); + if (rewardChannel && rewardChannel.type === ChannelType.GuildText) { + await rewardChannel.send(embed); + } else { + throw "Invalid reward channel"; + } + console.log(`Quest result posted at: ${new Date().toISOString()}`); +}; const fn = async () => { const quest = await checkForNewQuest(); if (quest) { - await postEmbed(quest); - console.log(`Quest result posted at: ${new Date().toISOString()}`); + await askForGrinders(quest); } }; -await fn(); -setInterval(fn, env.WOV_FETCH_INTERVAL); +client.on("ready", async (client) => { + console.log(`Logged in as ${client.user.username}`); + + const quest = await getLatestQuest(); + await askForGrinders(quest); + + // await fn(); + // setInterval(fn, env.WOV_FETCH_INTERVAL); +}); + +client.on("messageCreate", async (message) => { + if (message.author.bot) return; + + message.channel; + + // await message.channel.send({ + // content: `-# ||${env.DISCORD_ADMIN_MENTION}||`, + // embeds: [ + // { + // title: "QuĂȘte terminĂ©e !", + // description: "Entrez les pseudos des gens Ă  exclure de la quĂȘte", + // color: 0x3498db, + // }, + // ], + // }) +}); + +await client.login(env.DISCORD_BOT_TOKEN); diff --git a/src/wov.ts b/src/wov.ts index 8325fe9..8c2b230 100644 --- a/src/wov.ts +++ b/src/wov.ts @@ -16,7 +16,7 @@ export type QuestParticipant = { xp: number; }; -export const checkForNewQuest = async (): Promise => { +export const getLatestQuest = async (): Promise => { const response = await fetch( `https://api.wolvesville.com/clans/${env.WOV_CLAN_ID}/quests/history`, { @@ -25,8 +25,13 @@ export const checkForNewQuest = async (): Promise => { }, ); const history = (await response.json()) as Array; + return history[0]; +}; - const lastId = history[0].quest.id; +export const checkForNewQuest = async (): Promise => { + const lastQuest = await getLatestQuest(); + + const lastId = lastQuest.quest.id; const cacheFile = Bun.file(".cache/.quest_cache"); await mkdir(".cache", { recursive: true }); if ((await cacheFile.exists()) && (await cacheFile.text()) === lastId) { @@ -34,5 +39,5 @@ export const checkForNewQuest = async (): Promise => { } await cacheFile.write(lastId); - return history[0]; + return lastQuest; };