refactor: monorepo structure
NOTE: discord bot moved to apps only
This commit is contained in:
34
apps/discord-bot/src/account.ts
Normal file
34
apps/discord-bot/src/account.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { readFile, writeFile, access } from "node:fs/promises";
|
||||
import { constants } from "node:fs";
|
||||
|
||||
const ACCOUNTS_FILE = "./.cache/accounts.json";
|
||||
|
||||
export const initAccounts = async (): Promise<void> => {
|
||||
try {
|
||||
await access(ACCOUNTS_FILE, constants.F_OK);
|
||||
} catch {
|
||||
await writeFile(ACCOUNTS_FILE, "{}");
|
||||
}
|
||||
};
|
||||
|
||||
export const getAccountBalance = async (playerId: string): Promise<number> => {
|
||||
const content = await readFile(ACCOUNTS_FILE, "utf-8");
|
||||
const accounts: Record<string, number> = JSON.parse(content);
|
||||
if (accounts[playerId]) return accounts[playerId];
|
||||
|
||||
accounts[playerId] = 0;
|
||||
await writeFile(ACCOUNTS_FILE, JSON.stringify(accounts));
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const setAccountBalance = async (
|
||||
playerId: string,
|
||||
balance: number,
|
||||
): Promise<void> => {
|
||||
const content = await readFile(ACCOUNTS_FILE, "utf-8");
|
||||
const accounts: Record<string, number> = JSON.parse(content);
|
||||
accounts[playerId] = balance;
|
||||
|
||||
await writeFile(ACCOUNTS_FILE, JSON.stringify(accounts));
|
||||
};
|
||||
84
apps/discord-bot/src/discordUtils.ts
Normal file
84
apps/discord-bot/src/discordUtils.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { getAccountBalance, setAccountBalance } from "./account";
|
||||
import { env } from "./env";
|
||||
import type { QuestResult } from "./wov";
|
||||
|
||||
export type DiscordMessage = {
|
||||
content: string;
|
||||
embeds: Array<DiscordEmbed>;
|
||||
};
|
||||
|
||||
export type DiscordEmbed = {
|
||||
title?: string;
|
||||
description: string;
|
||||
image?: {
|
||||
url: string;
|
||||
};
|
||||
color: number;
|
||||
};
|
||||
|
||||
export const makeResultEmbed = async (
|
||||
result: QuestResult,
|
||||
exclude: Array<string>,
|
||||
): Promise<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);
|
||||
|
||||
let rewardsEmbed: DiscordEmbed | undefined;
|
||||
if (env.QUEST_REWARDS) {
|
||||
const rewardedParticipants = participants
|
||||
.map((x) => ({ id: x.playerId, username: x.username }))
|
||||
.filter((x) => !exclude.includes(x.username));
|
||||
const medals = ["🥇", "🥈", "🥉"].concat(
|
||||
new Array(rewardedParticipants.length).fill("🏅"),
|
||||
);
|
||||
|
||||
const rewards = rewardedParticipants
|
||||
.slice(0, Math.min(rewardedParticipants.length, env.QUEST_REWARDS.length))
|
||||
.map(
|
||||
(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-# \`@LBF gemmes\` pour voir votre nombre de gemmes. Puis avec ${env.DISCORD_REWARDS_GIVER} pour échanger contre des cadeaux !`,
|
||||
color,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: `-# ||${env.DISCORD_MENTION}||`,
|
||||
embeds: [
|
||||
{
|
||||
description: `# Résultats de quête\n\nMerci à toutes et à tous d'avoir participé 🫡`,
|
||||
color,
|
||||
image: {
|
||||
url: imageUrl,
|
||||
},
|
||||
},
|
||||
...(rewardsEmbed ? [rewardsEmbed] : []),
|
||||
{
|
||||
title: "Classement",
|
||||
description: participants
|
||||
.filter((x) => !exclude.includes(x.username))
|
||||
.filter((_, i) => i < 8)
|
||||
.map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`)
|
||||
.join("\n"),
|
||||
color,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
38
apps/discord-bot/src/env.ts
Normal file
38
apps/discord-bot/src/env.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { z } from "zod";
|
||||
import "dotenv/config";
|
||||
|
||||
const schema = z.object({
|
||||
DISCORD_BOT_TOKEN: z.string(),
|
||||
DISCORD_MENTION: z.string(),
|
||||
DISCORD_REWARDS_GIVER: z.string(),
|
||||
DISCORD_REWARDS_CHANNEL: z.string(),
|
||||
DISCORD_ADMIN_MENTION: z.string(),
|
||||
DISCORD_ADMIN_CHANNEL: z.string(),
|
||||
DISCORD_TRACKING_CHANNEL: z.string(),
|
||||
WOV_API_KEY: z.string(),
|
||||
WOV_CLAN_ID: z.string(),
|
||||
WOV_FETCH_INTERVAL: z.coerce.number(),
|
||||
WOV_TRACKING_INTERVAL: z.coerce.number(),
|
||||
QUEST_REWARDS: z
|
||||
.string()
|
||||
.transform((x) => x.split(",").map((x) => x.trim()))
|
||||
.optional(),
|
||||
QUEST_EXCLUDE: z
|
||||
.string()
|
||||
.transform((x) => x.split(",").map((x) => x.trim()))
|
||||
.optional()
|
||||
.default(""),
|
||||
});
|
||||
|
||||
const result = schema.safeParse(process.env);
|
||||
if (!result.success) {
|
||||
console.log("❌ Invalid environments variables:");
|
||||
console.log(
|
||||
result.error.errors
|
||||
.map((x) => `- ${x.path.join(".")}: ${x.message}`)
|
||||
.join("\n"),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export const env = result.data;
|
||||
562
apps/discord-bot/src/index.ts
Normal file
562
apps/discord-bot/src/index.ts
Normal file
@@ -0,0 +1,562 @@
|
||||
import { getAccountBalance, initAccounts, setAccountBalance } from "./account";
|
||||
import { makeResultEmbed } from "./discordUtils";
|
||||
import { env } from "./env";
|
||||
import {
|
||||
initTracking,
|
||||
listTrackedPlayers,
|
||||
trackWovPlayer,
|
||||
untrackWovPlayer,
|
||||
} from "./tracking";
|
||||
import {
|
||||
checkForNewQuest,
|
||||
getClanInfos,
|
||||
getClanMembers,
|
||||
getLatestQuest,
|
||||
searchPlayer,
|
||||
type QuestResult,
|
||||
} from "./wov";
|
||||
|
||||
import {
|
||||
ChannelType,
|
||||
Client,
|
||||
EmbedBuilder,
|
||||
GatewayIntentBits,
|
||||
Message,
|
||||
Partials,
|
||||
} from "discord.js";
|
||||
import * as readline from "node:readline";
|
||||
|
||||
// user mode = write in console, send in channel
|
||||
const flagIndex = process.argv.indexOf("--user");
|
||||
let userMode: { enabled: true; channelId: string } | { enabled: false } = {
|
||||
enabled: false,
|
||||
};
|
||||
if (flagIndex !== -1) {
|
||||
const channelId = process.argv[flagIndex + 1];
|
||||
if (channelId === undefined) {
|
||||
console.error("ERROR: --user expects channelId as a paramater");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
userMode = { enabled: true, channelId };
|
||||
}
|
||||
|
||||
console.log(`User mode: ${userMode.enabled ? "enabled" : "disabled"}`);
|
||||
|
||||
const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.DirectMessages,
|
||||
],
|
||||
partials: [Partials.Message, Partials.Channel],
|
||||
});
|
||||
|
||||
const askForGrinders = async (quest: QuestResult) => {
|
||||
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
|
||||
.filter((x) => !env.QUEST_EXCLUDE.includes(x.username))
|
||||
.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 adminChannel.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:```@LBF laulau,Yuno,...```\n**Attention les majuscules comptent**\nPour entrer la liste des joueurs, il faut __mentionner le bot__, si personne n'a grind, `@LBF tg`",
|
||||
color,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const filter = (msg: Message) =>
|
||||
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 adminChannel.awaitMessages({ filter, max: 1 });
|
||||
answer = collected.first()?.content || null;
|
||||
if (!answer) continue;
|
||||
|
||||
answer = answer.replace(`<@${client.user!.id}>`, "").trim();
|
||||
if (answer.toLowerCase() === "tg") {
|
||||
answer = "";
|
||||
break;
|
||||
}
|
||||
|
||||
const players = answer
|
||||
.split(",")
|
||||
.map((x) => x.trim())
|
||||
.filter(Boolean);
|
||||
await adminChannel.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 === adminChannel.id &&
|
||||
!msg.author.bot &&
|
||||
["oui", "non", "yes", "no"].includes(msg.content.toLowerCase());
|
||||
const confirmCollected = await adminChannel.awaitMessages({
|
||||
filter: confirmFilter,
|
||||
max: 1,
|
||||
});
|
||||
const confirmation = confirmCollected.first()?.content.toLowerCase();
|
||||
if (confirmation === "oui" || confirmation === "yes") {
|
||||
confirmed = true;
|
||||
await adminChannel.send({ content: "Ok" });
|
||||
} else {
|
||||
await adminChannel.send({
|
||||
content: "D'accord, veuillez réessayer. Qui a grind ?",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (answer === null) throw "unreachable";
|
||||
|
||||
const exclude = answer
|
||||
.split(",")
|
||||
.map((x) => x.trim())
|
||||
.filter(Boolean);
|
||||
const embed = await makeResultEmbed(quest, [
|
||||
...env.QUEST_EXCLUDE,
|
||||
...exclude,
|
||||
]);
|
||||
const rewardChannel = await client.channels.fetch(
|
||||
env.DISCORD_REWARDS_CHANNEL,
|
||||
);
|
||||
if (rewardChannel && rewardChannel.type === ChannelType.GuildText) {
|
||||
await rewardChannel.send(embed);
|
||||
} else {
|
||||
throw "Invalid reward channel";
|
||||
}
|
||||
|
||||
await adminChannel.send("Envoyé !");
|
||||
console.log(`Quest result posted at: ${new Date().toISOString()}`);
|
||||
};
|
||||
|
||||
const fn = async () => {
|
||||
const quest = await checkForNewQuest();
|
||||
if (quest) {
|
||||
await askForGrinders(quest);
|
||||
}
|
||||
};
|
||||
|
||||
const trackingCron = async () => {
|
||||
const trackedPlayers = await listTrackedPlayers();
|
||||
for (const playerId of trackedPlayers) {
|
||||
const res = await trackWovPlayer(playerId);
|
||||
if (res.event !== "changed") return;
|
||||
|
||||
const chan = client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL);
|
||||
if (!chan?.isSendable()) throw "Invalid tracking channel";
|
||||
|
||||
const lastUsername = res.oldUsernames[res.oldUsernames.length - 1];
|
||||
|
||||
await chan.send({
|
||||
embeds: [
|
||||
{
|
||||
description: `### [UPDATE] \`${lastUsername}\` -> \`${res.newUsername}\` [\`${playerId}\`]`,
|
||||
fields: [
|
||||
{ name: "Nouveau pseudo", value: `\`${res.newUsername}\`` },
|
||||
{
|
||||
name: "Anciens pseudos",
|
||||
value: res.oldUsernames.map((x) => `- \`${x}\``).join("\n"),
|
||||
},
|
||||
],
|
||||
color: 0x89cff0,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
client.on("ready", async (client) => {
|
||||
console.log(`Logged in as ${client.user.username}`);
|
||||
|
||||
if (userMode.enabled) {
|
||||
const chan = client.channels.cache.get(userMode.channelId);
|
||||
if (chan?.type !== ChannelType.GuildText) {
|
||||
console.error("ERROR: invalid channel");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
prompt: `${chan.name} ~ `,
|
||||
});
|
||||
|
||||
rl.prompt();
|
||||
|
||||
rl.on("line", async (line) => {
|
||||
await chan.send(line);
|
||||
rl.prompt();
|
||||
});
|
||||
|
||||
rl.on("close", () => {
|
||||
process.exit(0);
|
||||
});
|
||||
} else {
|
||||
await initAccounts();
|
||||
await initTracking();
|
||||
|
||||
await fn();
|
||||
setInterval(fn, env.WOV_FETCH_INTERVAL);
|
||||
|
||||
await trackingCron();
|
||||
setInterval(trackingCron, env.WOV_TRACKING_INTERVAL);
|
||||
}
|
||||
});
|
||||
|
||||
client.on("messageCreate", async (message) => {
|
||||
if (message.author.bot || userMode.enabled) return;
|
||||
|
||||
const displayName = message.member?.displayName || message.author.username;
|
||||
|
||||
if (message.content.startsWith(`<@${client.user!.id}>`)) {
|
||||
const [command, ...args] = message.content
|
||||
.replace(`<@${client.user!.id}>`, "")
|
||||
.trim()
|
||||
.split(" ");
|
||||
if (command === "ping") {
|
||||
await message.reply("pong");
|
||||
} else if (command === "tejtrack") {
|
||||
if (!message.member) return;
|
||||
if (!message.member.roles.cache.has("1147963065640439900")) {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description:
|
||||
"### ❌ Erreur\n\n\nTu t'es cru chez mémé ou quoi faut être staff",
|
||||
color: 15335424,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let playerName = args[0];
|
||||
if (!playerName) {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `### ❌ Erreur\n\n\n\nUsage:\`@LBF untrack NOM_JOUEUR\`, exemple: \`@LBF untrack Yuno\`.\n**Attention les majuscules sont importantes**`,
|
||||
color: 15335424,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const player = await searchPlayer(playerName);
|
||||
if (!player) {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `### ❌ Erreur\n\n\n\nCette personne n'existe pas.\n**Attention les majuscules sont importantes**`,
|
||||
color: 15335424,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await untrackWovPlayer(player.id);
|
||||
switch (res.event) {
|
||||
case "notTracked": {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `Pas de tracker pour \`${playerName}\` [\`${player.id}\`]`,
|
||||
color: 0x89cff0,
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "trackerRemoved": {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `Tracker enlevé pour \`${playerName}\` [\`${player.id}\`]`,
|
||||
color: 0x89cff0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const chan = client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL);
|
||||
if (!chan?.isSendable()) throw "Invalid tracking channel";
|
||||
|
||||
await chan.send({
|
||||
embeds: [
|
||||
{
|
||||
description: `### [REMOVED] \`${playerName}\` [\`${player.id}\`]`,
|
||||
color: 0x89cff0,
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (command === "track") {
|
||||
if (!message.member) return;
|
||||
if (!message.member.roles.cache.has("1147963065640439900")) {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description:
|
||||
"### ❌ Erreur\n\n\nTu t'es cru chez mémé ou quoi faut être staff",
|
||||
color: 15335424,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let playerName = args[0];
|
||||
if (!playerName) {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `### ❌ Erreur\n\n\n\nUsage:\`@LBF track NOM_JOUEUR\`, exemple: \`@LBF track Yuno\`.\n**Attention les majuscules sont importantes**`,
|
||||
color: 15335424,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const player = await searchPlayer(playerName);
|
||||
if (!player) {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `### ❌ Erreur\n\n\n\nCette personne n'existe pas.\n**Attention les majuscules sont importantes**`,
|
||||
color: 15335424,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await trackWovPlayer(player.id);
|
||||
switch (res.event) {
|
||||
case "notFound": {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `### ❌ Erreur\n\n\n\nCette personne n'existe pas.\n**Attention les majuscules sont importantes**`,
|
||||
color: 15335424,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
case "registered": {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `Tracker enregistré pour \`${playerName}\` [\`${player.id}\`]`,
|
||||
color: 0x89cff0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const chan = client.channels.cache.get(env.DISCORD_TRACKING_CHANNEL);
|
||||
if (!chan?.isSendable()) throw "Invalid tracking channel";
|
||||
|
||||
await chan.send({
|
||||
embeds: [
|
||||
{
|
||||
description: `### [NEW] \`${playerName}\` [\`${player.id}\`]`,
|
||||
color: 0x89cff0,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
case "none": {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `Tracker déjà enregistré pour \`${playerName}\` [\`${player.id}\`]`,
|
||||
color: 0x89cff0,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
case "changed": {
|
||||
// ignored
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (command === "icone") {
|
||||
let playerName = args[0];
|
||||
if (!playerName) {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `### ❌ Erreur\n\n\n\nUsage:\`@LBF icone NOM_JOUEUR\`, exemple: \`@LBF icone Yuno\`.\n**Attention les majuscules sont importantes**`,
|
||||
color: 15335424,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const player = await searchPlayer(playerName);
|
||||
if (!player) {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `### ❌ Erreur\n\n\n\nJoueur·euse non trouvé·e.\n**Attention les majuscules sont importantes**`,
|
||||
color: 15335424,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player.clanId) {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `### ❌ Erreur\n\n\n\nCette personne __n'a pas de clan__ ou __a caché son clan__.\n**Attention les majuscules sont importantes**`,
|
||||
color: 15335424,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const clan = await getClanInfos(player.clanId);
|
||||
if (!clan) {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `### ❌ Erreur\n\n\n\nImpossible de récupérer les informations du clan.`,
|
||||
color: 15335424,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `### ✅ Informations du clan\n\n**Nom:** \`\`\`${clan.name}\`\`\`\n**Tag:** \`\`\`${clan.tag}\`\`\``,
|
||||
color: 65280,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else if (command === "result") {
|
||||
const quest = await getLatestQuest();
|
||||
await askForGrinders(quest);
|
||||
} else if (command === "gemmes") {
|
||||
let playerName = 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({
|
||||
embeds: [
|
||||
{
|
||||
description: `### ❌ Erreur\n\n\n\`${playerName}\` n'est pas dans le clan (la honte).\n**Attention les majuscules sont importantes**`,
|
||||
color: 15335424,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
if (args.length === 2 && message.member) {
|
||||
if (!message.member.roles.cache.has("1147963065640439900")) {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description:
|
||||
"### ❌ Erreur\n\n\nTu t'es cru chez mémé ou quoi faut être staff",
|
||||
color: 15335424,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(args[1][0] !== "+" && args[1][0] !== "-") ||
|
||||
!args[1] ||
|
||||
isNaN(Number(args[1].substring(1)))
|
||||
) {
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description:
|
||||
"### ❌ Erreur\n\n\nFormat: \`@LBF gemmes <pseudo> <+GEMMES|-GEMMES>\`\nExemple:\`@LBF gemmes Yuno -10000\`\n**Attention les majuscules sont importantes**",
|
||||
color: 15335424,
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const mult = args[1][0] === "+" ? 1 : -1;
|
||||
const delta = Number(args[1].substring(1)) * mult;
|
||||
const balance = await getAccountBalance(clanMember.playerId);
|
||||
await setAccountBalance(
|
||||
clanMember.playerId,
|
||||
Math.max(0, balance + delta),
|
||||
);
|
||||
}
|
||||
|
||||
const balance = await getAccountBalance(clanMember.playerId);
|
||||
// await message.reply(`Gemmes accumulées par ${playerName}: ${balance}`);
|
||||
await message.reply({
|
||||
embeds: [
|
||||
{
|
||||
description: `### 💎 Compte de ${playerName}\n\n\nGemmes disponibles: **${balance}**\n\n-# Voir avec <@294871767820795904> pour échanger contre skin/carte etc`,
|
||||
color: 4360641,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await client.login(env.DISCORD_BOT_TOKEN);
|
||||
75
apps/discord-bot/src/tracking.ts
Normal file
75
apps/discord-bot/src/tracking.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { getPlayer } from "./wov";
|
||||
import { readFile, writeFile, access } from "node:fs/promises";
|
||||
import { constants } from "node:fs";
|
||||
|
||||
const TRACKED_PLAYER_FILE = "./.cache/tracked.json";
|
||||
|
||||
type TrackedPlayers = Record<string, string[]>;
|
||||
|
||||
export async function initTracking(): Promise<void> {
|
||||
try {
|
||||
await access(TRACKED_PLAYER_FILE, constants.F_OK);
|
||||
} catch {
|
||||
await writeFile(TRACKED_PLAYER_FILE, "{}");
|
||||
}
|
||||
}
|
||||
|
||||
export async function listTrackedPlayers(): Promise<string[]> {
|
||||
const content = await readFile(TRACKED_PLAYER_FILE, "utf-8");
|
||||
const trackedPlayers: TrackedPlayers = JSON.parse(content);
|
||||
|
||||
return Object.keys(trackedPlayers);
|
||||
}
|
||||
|
||||
export async function untrackWovPlayer(
|
||||
playerId: string,
|
||||
): Promise<{ event: "notTracked" } | { event: "trackerRemoved" }> {
|
||||
const content = await readFile(TRACKED_PLAYER_FILE, "utf-8");
|
||||
const trackedPlayers: TrackedPlayers = JSON.parse(content);
|
||||
|
||||
if (!trackedPlayers[playerId]) return { event: "notTracked" };
|
||||
|
||||
delete trackedPlayers[playerId];
|
||||
await writeFile(TRACKED_PLAYER_FILE, JSON.stringify(trackedPlayers));
|
||||
|
||||
return { event: "trackerRemoved" };
|
||||
}
|
||||
|
||||
export async function trackWovPlayer(playerId: string): Promise<
|
||||
| { event: "notFound" }
|
||||
| {
|
||||
event: "registered";
|
||||
}
|
||||
| { event: "changed"; oldUsernames: string[]; newUsername: string }
|
||||
| { event: "none" }
|
||||
> {
|
||||
const content = await readFile(TRACKED_PLAYER_FILE, "utf-8");
|
||||
const trackedPlayers: TrackedPlayers = JSON.parse(content);
|
||||
|
||||
const player = await getPlayer(playerId);
|
||||
if (!player) return { event: "notFound" };
|
||||
|
||||
const currentUsernames = trackedPlayers[playerId];
|
||||
if (currentUsernames) {
|
||||
const oldUsernames = [...currentUsernames];
|
||||
if (!currentUsernames.includes(player.username)) {
|
||||
currentUsernames.push(player.username);
|
||||
|
||||
await writeFile(TRACKED_PLAYER_FILE, JSON.stringify(trackedPlayers));
|
||||
|
||||
return {
|
||||
event: "changed",
|
||||
oldUsernames,
|
||||
newUsername: player.username,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
event: "none",
|
||||
};
|
||||
}
|
||||
} else {
|
||||
trackedPlayers[playerId] = [player.username];
|
||||
await writeFile(TRACKED_PLAYER_FILE, JSON.stringify(trackedPlayers));
|
||||
return { event: "registered" };
|
||||
}
|
||||
}
|
||||
151
apps/discord-bot/src/wov.ts
Normal file
151
apps/discord-bot/src/wov.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { env } from "./env";
|
||||
import { mkdir, readFile, writeFile, access } from "node:fs/promises";
|
||||
import { constants } from "node:fs";
|
||||
|
||||
export type QuestResult = {
|
||||
quest: {
|
||||
id: string;
|
||||
promoImageUrl: string;
|
||||
promoImagePrimaryColor: string;
|
||||
};
|
||||
participants: Array<QuestParticipant>;
|
||||
};
|
||||
|
||||
export type QuestParticipant = {
|
||||
playerId: string;
|
||||
username: string;
|
||||
xp: number;
|
||||
};
|
||||
|
||||
export const getLatestQuest = async (): Promise<QuestResult> => {
|
||||
const response = await fetch(
|
||||
`https://api.wolvesville.com/clans/${env.WOV_CLAN_ID}/quests/history`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bot ${env.WOV_API_KEY}` },
|
||||
},
|
||||
);
|
||||
const history = (await response.json()) as Array<QuestResult>;
|
||||
return history[0];
|
||||
};
|
||||
|
||||
export const checkForNewQuest = async (): Promise<QuestResult | null> => {
|
||||
const lastQuest = await getLatestQuest();
|
||||
|
||||
const lastId = lastQuest.quest.id;
|
||||
const cacheFilePath = ".cache/.quest_cache";
|
||||
await mkdir(".cache", { recursive: true });
|
||||
|
||||
try {
|
||||
await access(cacheFilePath, constants.F_OK);
|
||||
const cachedQuestId = await readFile(cacheFilePath, "utf-8");
|
||||
if (cachedQuestId === lastId || cachedQuestId === "IGNORE") {
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist, continue
|
||||
}
|
||||
|
||||
await writeFile(cacheFilePath, lastId);
|
||||
return lastQuest;
|
||||
};
|
||||
export const getClanMembers = async (): Promise<
|
||||
Array<{ playerId: string; username: string }>
|
||||
> => {
|
||||
const cacheFilePath = ".clan_members_cache";
|
||||
await mkdir(".cache", { recursive: true });
|
||||
|
||||
let cached: {
|
||||
timestamp: number;
|
||||
data: Array<{ playerId: string; username: string }>;
|
||||
} | null = null;
|
||||
|
||||
try {
|
||||
await access(cacheFilePath, constants.F_OK);
|
||||
const content = await readFile(cacheFilePath, "utf-8");
|
||||
cached = JSON.parse(content);
|
||||
if (cached && Date.now() - cached.timestamp < 60 * 60 * 1000) {
|
||||
return cached.data;
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist or is invalid, continue
|
||||
}
|
||||
|
||||
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 writeFile(
|
||||
cacheFilePath,
|
||||
JSON.stringify({ timestamp: Date.now(), data }),
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const searchPlayer = async (username: string) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://api.wolvesville.com//players/search?username=${username}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bot ${env.WOV_API_KEY}` },
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status === 404) return null;
|
||||
|
||||
const data = (await response.json()) as {
|
||||
id: string;
|
||||
clanId: string | null;
|
||||
};
|
||||
|
||||
return data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getClanInfos = async (clanId: string) => {
|
||||
const response = await fetch(
|
||||
`https://api.wolvesville.com/clans/${clanId}/info`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bot ${env.WOV_API_KEY}` },
|
||||
},
|
||||
);
|
||||
const data = (await response.json()) as {
|
||||
name: string;
|
||||
tag: string;
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export async function getPlayer(playerId: string) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://api.wolvesville.com/players/${playerId}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bot ${env.WOV_API_KEY}` },
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status === 404) return null;
|
||||
|
||||
const data = (await response.json()) as {
|
||||
username: string;
|
||||
};
|
||||
|
||||
return data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user