From 4f73000585ba307f44d28c650c62f13eb826c8af Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Mon, 28 Jul 2025 21:29:21 +0200 Subject: [PATCH] feat: implement account system --- .gitignore | 2 +- src/account.ts | 29 +++++++++++++++++++++ src/discord.ts | 25 +++++++++++++----- src/index.ts | 71 +++++++++++++++++++++++++++++++++++++++----------- src/wov.ts | 33 +++++++++++++++++++++++ 5 files changed, 138 insertions(+), 22 deletions(-) create mode 100644 src/account.ts diff --git a/.gitignore b/.gitignore index 2f85aa5..ad69ce3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore -.quest_cache +.*_cache # Logs diff --git a/src/account.ts b/src/account.ts new file mode 100644 index 0000000..9fcb74c --- /dev/null +++ b/src/account.ts @@ -0,0 +1,29 @@ +const ACCOUNTS_FILE = "./accounts.json"; + +const Accounts = Bun.file(ACCOUNTS_FILE); + +export const initAccounts = async (): Promise => { + if (!(await Accounts.exists())) { + Accounts.write("{}"); + } +}; + +export const getAccountBalance = async (playerId: string): Promise => { + const accounts: Record = await Accounts.json(); + if (accounts[playerId]) return accounts[playerId]; + + accounts[playerId] = 0; + await Accounts.write(JSON.stringify(accounts)); + + return 0; +}; + +export const setAccountBalance = async ( + playerId: string, + balance: number, +): Promise => { + const accounts: Record = await Accounts.json(); + accounts[playerId] = balance; + + await Accounts.write(JSON.stringify(accounts)); +}; diff --git a/src/discord.ts b/src/discord.ts index 54d3ca8..90d4c17 100644 --- a/src/discord.ts +++ b/src/discord.ts @@ -1,3 +1,4 @@ +import { getAccountBalance, setAccountBalance } from "./account"; import { env } from "./env"; import type { QuestResult } from "./wov"; @@ -15,10 +16,10 @@ export type DiscordEmbed = { color: number; }; -export const makeResultEmbed = ( +export const makeResultEmbed = async ( result: QuestResult, exclude: Array, -): DiscordMessage => { +): Promise => { 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); @@ -26,8 +27,8 @@ export const makeResultEmbed = ( let rewardsEmbed: DiscordEmbed | undefined; if (env.QUEST_REWARDS) { const rewardedParticipants = participants - .map((x) => x.username) - .filter((x) => !exclude.includes(x)); + .map((x) => ({ id: x.playerId, username: x.username })) + .filter((x) => !exclude.includes(x.username)); const medals = ["🥇", "🥈", "🥉"].concat( new Array(rewardedParticipants.length).fill("🏅"), ); @@ -35,10 +36,22 @@ export const makeResultEmbed = ( const rewards = rewardedParticipants .slice(0, Math.min(rewardedParticipants.length, env.QUEST_REWARDS.length)) .map( - (username, i) => - `- ${medals[i]} ${username} - ${env.QUEST_REWARDS![i]}`, + (x, i) => + `- ${medals[i]} ${x.username} - ${env.QUEST_REWARDS![i]} gemmes`, ); + 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-# Voir avec ${env.DISCORD_REWARDS_GIVER} pour récupérer les récompenses !`, diff --git a/src/index.ts b/src/index.ts index f766436..804da1e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,12 @@ +import { getAccountBalance, initAccounts, setAccountBalance } from "./account"; import { makeResultEmbed } from "./discord"; import { env } from "./env"; -import { checkForNewQuest, getLatestQuest, type QuestResult } from "./wov"; +import { + checkForNewQuest, + getClanMembers, + getLatestQuest, + type QuestResult, +} from "./wov"; import { ChannelType, Client, GatewayIntentBits, Message } from "discord.js"; @@ -13,8 +19,8 @@ const client = new Client({ }); const askForGrinders = async (quest: QuestResult) => { - const channel = await client.channels.fetch(env.DISCORD_ADMIN_CHANNEL); - if (!channel || channel.type !== ChannelType.GuildText) + 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 @@ -26,7 +32,7 @@ const askForGrinders = async (quest: QuestResult) => { const color = parseInt(quest.quest.promoImagePrimaryColor.substring(1), 16); - await channel.send({ + await adminChannel.send({ content: `-# ||${env.DISCORD_ADMIN_MENTION}||`, embeds: [ { @@ -48,14 +54,14 @@ const askForGrinders = async (quest: QuestResult) => { }); const filter = (msg: Message) => - msg.channel.id === channel.id && + 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 channel.awaitMessages({ filter, max: 1 }); + const collected = await adminChannel.awaitMessages({ filter, max: 1 }); answer = collected.first()?.content || null; if (!answer) continue; @@ -69,7 +75,7 @@ const askForGrinders = async (quest: QuestResult) => { .split(",") .map((x) => x.trim()) .filter(Boolean); - await channel.send({ + await adminChannel.send({ embeds: [ { title: "Joueurs entrés", @@ -82,19 +88,19 @@ const askForGrinders = async (quest: QuestResult) => { content: `Est-ce correct ? (oui/non)`, }); const confirmFilter = (msg: Message) => - msg.channel.id === channel.id && + msg.channel.id === adminChannel.id && !msg.author.bot && ["oui", "non", "yes", "no"].includes(msg.content.toLowerCase()); - const confirmCollected = await channel.awaitMessages({ + const confirmCollected = await adminChannel.awaitMessages({ filter: confirmFilter, max: 1, }); const confirmation = confirmCollected.first()?.content.toLowerCase(); if (confirmation === "oui" || confirmation === "yes") { confirmed = true; - await channel.send({ content: "Ok" }); + await adminChannel.send({ content: "Ok" }); } else { - await channel.send({ + await adminChannel.send({ content: "D'accord, veuillez réessayer. Qui a grind ?", }); } @@ -106,7 +112,7 @@ const askForGrinders = async (quest: QuestResult) => { .split(",") .map((x) => x.trim()) .filter(Boolean); - const embed = makeResultEmbed(quest, [...env.QUEST_EXCLUDE, ...exclude]); + const embed = await makeResultEmbed(quest, [...env.QUEST_EXCLUDE, ...exclude]); const rewardChannel = await client.channels.fetch( env.DISCORD_REWARDS_CHANNEL, ); @@ -115,6 +121,8 @@ const askForGrinders = async (quest: QuestResult) => { } else { throw "Invalid reward channel"; } + + await adminChannel.send("Envoyé !"); console.log(`Quest result posted at: ${new Date().toISOString()}`); }; @@ -128,20 +136,53 @@ const fn = async () => { client.on("ready", async (client) => { console.log(`Logged in as ${client.user.username}`); - await fn(); - setInterval(fn, env.WOV_FETCH_INTERVAL); + await initAccounts(); + + // await fn(); + // setInterval(fn, env.WOV_FETCH_INTERVAL); }); client.on("messageCreate", async (message) => { if (message.author.bot) return; if (message.content.startsWith(`<@${client.user!.id}>`)) { - const command = message.content.replace(`<@${client.user!.id}>`, "").trim(); + const [command, ...args] = message.content + .replace(`<@${client.user!.id}>`, "") + .trim() + .split(" "); if (command === "ping") { await message.reply("pong"); } else if (command === "result") { const quest = await getLatestQuest(); await askForGrinders(quest); + } else if (command === "gemmes") { + let playerName = message.author.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( + `'${args[0]}' n'est pas dans le clan (la honte). **Attention les majuscules sont importantes**`, + ); + } else { + const balance = await getAccountBalance(clanMember.playerId); + await message.reply(`Gemmes accumulées par ${playerName}: ${balance}`); + } + } else if (command === "zero") { + const playerName = message.author.displayName.replace("🕸 |", "").trim(); + const clanMembers = await getClanMembers(); + const clanMember = clanMembers.find((x) => x.username === playerName); + + if (!clanMember) { + await message.reply("Pas du clan pas de gemmes"); + } else { + await setAccountBalance(clanMember.playerId, 0); + await message.reply("Zero gemmes mtn bouuh"); + } } } }); diff --git a/src/wov.ts b/src/wov.ts index 8c2b230..21949af 100644 --- a/src/wov.ts +++ b/src/wov.ts @@ -41,3 +41,36 @@ export const checkForNewQuest = async (): Promise => { await cacheFile.write(lastId); return lastQuest; }; +export const getClanMembers = async (): Promise< + Array<{ playerId: string; username: string }> +> => { + const cacheFile = Bun.file(".clan_members_cache"); + await mkdir(".cache", { recursive: true }); + + let cached: { + timestamp: number; + data: Array<{ playerId: string; username: string }>; + } | null = null; + if (await cacheFile.exists()) { + try { + cached = JSON.parse(await cacheFile.text()); + if (cached && Date.now() - cached.timestamp < 60 * 60 * 1000) { + return cached.data; + } + } catch {} + } + + 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 cacheFile.write(JSON.stringify({ timestamp: Date.now(), data })); + return data; +};