feat: implement account system
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
29
src/account.ts
Normal 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));
|
||||||
|
};
|
||||||
@@ -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 !`,
|
||||||
|
|||||||
71
src/index.ts
71
src/index.ts
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
33
src/wov.ts
33
src/wov.ts
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user