feat: check for new quests, post result embed to discord webhook and cache posted quests

This commit is contained in:
Pihkaal
2025-05-07 18:42:08 +02:00
parent 7b8c47d88d
commit 416d7675f2
9 changed files with 183 additions and 2 deletions

10
.env.example Normal file
View File

@@ -0,0 +1,10 @@
WOV_CLAN_ID=
WOV_API_KEY=
WOV_FETCH_INTERVAL="14400000" # 4 hours
QUEST_REWARDS=
QUEST_EXCLUDE=
DISCORD_WEBHOOK_URL=
DISCORD_MENTION=
DISCORD_REWARDS_GIVER=

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
.quest_cache
# Logs
logs

BIN
bun.lockb

Binary file not shown.

View File

@@ -11,5 +11,8 @@
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"zod": "^3.24.4"
}
}

81
src/discord.ts Normal file
View File

@@ -0,0 +1,81 @@
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 postEmbed = async (result: QuestResult): Promise<void> => {
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 => {
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) {
console.log(env.QUEST_REWARDS);
const rewardedParticipants = participants
.map((x) => x.username)
.filter((x) => !env.QUEST_EXCLUDE.includes(x));
const medals = ["🥇", "🥈", "🥉"].concat(
new Array(rewardedParticipants.length).fill("🏅"),
);
const rewards = rewardedParticipants
.slice(0, Math.min(rewardedParticipants.length, env.QUEST_REWARDS.length))
.map(
(username, i) =>
`- ${medals[i]} ${username} - ${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 !`,
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) => !env.QUEST_EXCLUDE.includes(x.username))
.filter((_, i) => i < 8)
.map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`)
.join("\n"),
color,
},
],
};
};

31
src/env.ts Normal file
View File

@@ -0,0 +1,31 @@
import { env as bunEnv } from "bun";
import { z } from "zod";
const schema = z.object({
DISCORD_WEBHOOK_URL: z.string(),
DISCORD_MENTION: z.string(),
DISCORD_REWARDS_GIVER: z.string(),
WOV_API_KEY: z.string(),
WOV_CLAN_ID: z.string(),
WOV_FETCH_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(bunEnv);
if (!result.success) {
console.log("❌ Invalid environments variables:");
console.log(
result.error.errors.map((x) => `- ${x.path.join(".")}: ${x.message}`),
);
process.exit(1);
}
export const env = result.data;

View File

@@ -1 +1,14 @@
console.log("Hello via Bun!");
import { postEmbed } from "./discord";
import { env } from "./env";
import { checkForNewQuest } from "./wov";
const fn = async () => {
const quest = await checkForNewQuest();
if (quest) {
await postEmbed(quest);
console.log(`Quest result posted at: ${new Date().toISOString()}`);
}
};
await fn();
setInterval(fn, env.WOV_FETCH_INTERVAL);

36
src/wov.ts Normal file
View File

@@ -0,0 +1,36 @@
import { env } from "./env";
export type QuestResult = {
quest: {
id: string;
promoImageUrl: string;
promoImagePrimaryColor: string;
};
participants: Array<QuestParticipant>;
};
export type QuestParticipant = {
playerId: string;
username: string;
xp: number;
};
export const checkForNewQuest = async (): Promise<QuestResult | null> => {
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>;
const lastId = history[0].quest.id;
const cacheFile = Bun.file(".quest_cache");
if ((await cacheFile.exists()) && (await cacheFile.text()) === lastId) {
return null;
}
cacheFile.write(lastId);
return history[0];
};

View File

@@ -22,6 +22,11 @@
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
"noPropertyAccessFromIndexSignature": false,
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"]
}
}
}