From d67e29109ddd4c4f0ab1023aea7f4040cef98c15 Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Fri, 27 Jun 2025 18:27:57 +0200 Subject: [PATCH] feat: ask for any grinders before sending the results --- bun.lockb | Bin 3456 -> 12217 bytes package.json | 1 + src/discord.ts | 21 +++----- src/env.ts | 4 +- src/index.ts | 142 ++++++++++++++++++++++++++++++++++++++++++++++--- src/wov.ts | 11 ++-- 6 files changed, 154 insertions(+), 25 deletions(-) diff --git a/bun.lockb b/bun.lockb index 093b2d970e13e3879297342a7d019807df957a39..09f9120f98467cc5f9859137a475779ee9c0d3c8 100755 GIT binary patch literal 12217 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 delta 634 zcmdlP-yl6fPqX}Ca7x+7&0fn^U;d^gSI+aWJZ1iq%@>9T^f}+%tlFZa1X&{daD1He@a{_5GMg|6MARP^)fhIKo^?>E` znpCcz+|iJ&YxelHyG#AS$qq~s0u%M4deyetT5{&w9D5ZwYxm>`rT|9O$t#)cIYG8q z0QECZ-pFjuG>2ib7N;HSZJ?m_WL-{cCPl`{S{!zq=|Ec?fvVUhe`GRe+QK+_6{i&= z%j7@_VOm~F8zJ;PYR_wNT(*$bY@QEJ|kCu&MhKBXoySzPbX NWCz_zlW*!h008OYih=+D 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; };