feat: implement account system

This commit is contained in:
Pihkaal
2025-07-28 21:29:21 +02:00
parent 97a16c361e
commit 4f73000585
5 changed files with 138 additions and 22 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,6 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
.quest_cache .*_cache
# Logs # Logs

29
src/account.ts Normal file
View File

@@ -0,0 +1,29 @@
const ACCOUNTS_FILE = "./accounts.json";
const Accounts = Bun.file(ACCOUNTS_FILE);
export const initAccounts = async (): Promise<void> => {
if (!(await Accounts.exists())) {
Accounts.write("{}");
}
};
export const getAccountBalance = async (playerId: string): Promise<number> => {
const accounts: Record<string, number> = 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<void> => {
const accounts: Record<string, number> = await Accounts.json();
accounts[playerId] = balance;
await Accounts.write(JSON.stringify(accounts));
};

View File

@@ -1,3 +1,4 @@
import { getAccountBalance, setAccountBalance } from "./account";
import { env } from "./env"; import { env } from "./env";
import type { QuestResult } from "./wov"; import type { QuestResult } from "./wov";
@@ -15,10 +16,10 @@ export type DiscordEmbed = {
color: number; color: number;
}; };
export const makeResultEmbed = ( export const makeResultEmbed = async (
result: QuestResult, result: QuestResult,
exclude: Array<string>, exclude: Array<string>,
): DiscordMessage => { ): Promise<DiscordMessage> => {
const imageUrl = result.quest.promoImageUrl; const imageUrl = result.quest.promoImageUrl;
const color = parseInt(result.quest.promoImagePrimaryColor.substring(1), 16); const color = parseInt(result.quest.promoImagePrimaryColor.substring(1), 16);
const participants = result.participants.toSorted((a, b) => b.xp - a.xp); const participants = result.participants.toSorted((a, b) => b.xp - a.xp);
@@ -26,8 +27,8 @@ export const makeResultEmbed = (
let rewardsEmbed: DiscordEmbed | undefined; let rewardsEmbed: DiscordEmbed | undefined;
if (env.QUEST_REWARDS) { if (env.QUEST_REWARDS) {
const rewardedParticipants = participants const rewardedParticipants = participants
.map((x) => x.username) .map((x) => ({ id: x.playerId, username: x.username }))
.filter((x) => !exclude.includes(x)); .filter((x) => !exclude.includes(x.username));
const medals = ["🥇", "🥈", "🥉"].concat( const medals = ["🥇", "🥈", "🥉"].concat(
new Array(rewardedParticipants.length).fill("🏅"), new Array(rewardedParticipants.length).fill("🏅"),
); );
@@ -35,10 +36,22 @@ export const makeResultEmbed = (
const rewards = rewardedParticipants const rewards = rewardedParticipants
.slice(0, Math.min(rewardedParticipants.length, env.QUEST_REWARDS.length)) .slice(0, Math.min(rewardedParticipants.length, env.QUEST_REWARDS.length))
.map( .map(
(username, i) => (x, i) =>
`- ${medals[i]} ${username} - ${env.QUEST_REWARDS![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 = { rewardsEmbed = {
title: "Récompenses", title: "Récompenses",
description: `${rewards.join("\n")}\n\n-# Voir avec ${env.DISCORD_REWARDS_GIVER} pour récupérer les récompenses !`, description: `${rewards.join("\n")}\n\n-# Voir avec ${env.DISCORD_REWARDS_GIVER} pour récupérer les récompenses !`,

View File

@@ -1,6 +1,12 @@
import { getAccountBalance, initAccounts, setAccountBalance } from "./account";
import { makeResultEmbed } from "./discord"; import { makeResultEmbed } from "./discord";
import { env } from "./env"; 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"; import { ChannelType, Client, GatewayIntentBits, Message } from "discord.js";
@@ -13,8 +19,8 @@ const client = new Client({
}); });
const askForGrinders = async (quest: QuestResult) => { const askForGrinders = async (quest: QuestResult) => {
const channel = await client.channels.fetch(env.DISCORD_ADMIN_CHANNEL); const adminChannel = await client.channels.fetch(env.DISCORD_ADMIN_CHANNEL);
if (!channel || channel.type !== ChannelType.GuildText) if (!adminChannel || adminChannel.type !== ChannelType.GuildText)
throw "Invalid admin channel provided"; throw "Invalid admin channel provided";
const top10 = quest.participants const top10 = quest.participants
@@ -26,7 +32,7 @@ const askForGrinders = async (quest: QuestResult) => {
const color = parseInt(quest.quest.promoImagePrimaryColor.substring(1), 16); const color = parseInt(quest.quest.promoImagePrimaryColor.substring(1), 16);
await channel.send({ await adminChannel.send({
content: `-# ||${env.DISCORD_ADMIN_MENTION}||`, content: `-# ||${env.DISCORD_ADMIN_MENTION}||`,
embeds: [ embeds: [
{ {
@@ -48,14 +54,14 @@ const askForGrinders = async (quest: QuestResult) => {
}); });
const filter = (msg: Message) => const filter = (msg: Message) =>
msg.channel.id === channel.id && msg.channel.id === adminChannel.id &&
!msg.author.bot && !msg.author.bot &&
msg.content.startsWith(`<@${client.user!.id}>`); msg.content.startsWith(`<@${client.user!.id}>`);
let confirmed = false; let confirmed = false;
let answer: string | null = null; let answer: string | null = null;
while (!confirmed) { while (!confirmed) {
const collected = await channel.awaitMessages({ filter, max: 1 }); const collected = await adminChannel.awaitMessages({ filter, max: 1 });
answer = collected.first()?.content || null; answer = collected.first()?.content || null;
if (!answer) continue; if (!answer) continue;
@@ -69,7 +75,7 @@ const askForGrinders = async (quest: QuestResult) => {
.split(",") .split(",")
.map((x) => x.trim()) .map((x) => x.trim())
.filter(Boolean); .filter(Boolean);
await channel.send({ await adminChannel.send({
embeds: [ embeds: [
{ {
title: "Joueurs entrés", title: "Joueurs entrés",
@@ -82,19 +88,19 @@ const askForGrinders = async (quest: QuestResult) => {
content: `Est-ce correct ? (oui/non)`, content: `Est-ce correct ? (oui/non)`,
}); });
const confirmFilter = (msg: Message) => const confirmFilter = (msg: Message) =>
msg.channel.id === channel.id && msg.channel.id === adminChannel.id &&
!msg.author.bot && !msg.author.bot &&
["oui", "non", "yes", "no"].includes(msg.content.toLowerCase()); ["oui", "non", "yes", "no"].includes(msg.content.toLowerCase());
const confirmCollected = await channel.awaitMessages({ const confirmCollected = await adminChannel.awaitMessages({
filter: confirmFilter, filter: confirmFilter,
max: 1, max: 1,
}); });
const confirmation = confirmCollected.first()?.content.toLowerCase(); const confirmation = confirmCollected.first()?.content.toLowerCase();
if (confirmation === "oui" || confirmation === "yes") { if (confirmation === "oui" || confirmation === "yes") {
confirmed = true; confirmed = true;
await channel.send({ content: "Ok" }); await adminChannel.send({ content: "Ok" });
} else { } else {
await channel.send({ await adminChannel.send({
content: "D'accord, veuillez réessayer. Qui a grind ?", content: "D'accord, veuillez réessayer. Qui a grind ?",
}); });
} }
@@ -106,7 +112,7 @@ const askForGrinders = async (quest: QuestResult) => {
.split(",") .split(",")
.map((x) => x.trim()) .map((x) => x.trim())
.filter(Boolean); .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( const rewardChannel = await client.channels.fetch(
env.DISCORD_REWARDS_CHANNEL, env.DISCORD_REWARDS_CHANNEL,
); );
@@ -115,6 +121,8 @@ const askForGrinders = async (quest: QuestResult) => {
} else { } else {
throw "Invalid reward channel"; throw "Invalid reward channel";
} }
await adminChannel.send("Envoyé !");
console.log(`Quest result posted at: ${new Date().toISOString()}`); console.log(`Quest result posted at: ${new Date().toISOString()}`);
}; };
@@ -128,20 +136,53 @@ const fn = async () => {
client.on("ready", async (client) => { client.on("ready", async (client) => {
console.log(`Logged in as ${client.user.username}`); console.log(`Logged in as ${client.user.username}`);
await fn(); await initAccounts();
setInterval(fn, env.WOV_FETCH_INTERVAL);
// await fn();
// setInterval(fn, env.WOV_FETCH_INTERVAL);
}); });
client.on("messageCreate", async (message) => { client.on("messageCreate", async (message) => {
if (message.author.bot) return; if (message.author.bot) return;
if (message.content.startsWith(`<@${client.user!.id}>`)) { 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") { if (command === "ping") {
await message.reply("pong"); await message.reply("pong");
} else if (command === "result") { } else if (command === "result") {
const quest = await getLatestQuest(); const quest = await getLatestQuest();
await askForGrinders(quest); 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");
}
} }
} }
}); });

View File

@@ -41,3 +41,36 @@ export const checkForNewQuest = async (): Promise<QuestResult | null> => {
await cacheFile.write(lastId); await cacheFile.write(lastId);
return lastQuest; 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;
};