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
|
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||||
|
|
||||||
|
.quest_cache
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
|
|
||||||
logs
|
logs
|
||||||
|
|||||||
@@ -11,5 +11,8 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.0.0"
|
"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)
|
// Some stricter flags (disabled by default)
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noPropertyAccessFromIndexSignature": false
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./src/*"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user