feat: check for new quests, post result embed to discord webhook and cache posted quests
This commit is contained in:
10
.env.example
Normal file
10
.env.example
Normal 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
2
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
.quest_cache
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
|
||||
@@ -11,5 +11,8 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.24.4"
|
||||
}
|
||||
}
|
||||
|
||||
81
src/discord.ts
Normal file
81
src/discord.ts
Normal 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
31
src/env.ts
Normal 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;
|
||||
15
src/index.ts
15
src/index.ts
@@ -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
36
src/wov.ts
Normal 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];
|
||||
};
|
||||
@@ -22,6 +22,11 @@
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user