diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a514fdf --- /dev/null +++ b/.env.example @@ -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= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9b1ee42..2f85aa5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore +.quest_cache + # Logs logs diff --git a/bun.lockb b/bun.lockb index e22b888..093b2d9 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 254e614..38ac160 100644 --- a/package.json +++ b/package.json @@ -11,5 +11,8 @@ }, "peerDependencies": { "typescript": "^5.0.0" + }, + "dependencies": { + "zod": "^3.24.4" } } diff --git a/src/discord.ts b/src/discord.ts new file mode 100644 index 0000000..985543c --- /dev/null +++ b/src/discord.ts @@ -0,0 +1,81 @@ +import { env } from "./env"; +import type { QuestResult } from "./wov"; + +export type DiscordMessage = { + content: string; + embeds: Array; +}; + +export type DiscordEmbed = { + title?: string; + description: string; + image?: { + url: string; + }; + color: number; +}; + +export const postEmbed = async (result: QuestResult): Promise => { + 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, + }, + ], + }; +}; diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 0000000..d0dc981 --- /dev/null +++ b/src/env.ts @@ -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; diff --git a/src/index.ts b/src/index.ts index 2a5e4b8..c57ca14 100644 --- a/src/index.ts +++ b/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); diff --git a/src/wov.ts b/src/wov.ts new file mode 100644 index 0000000..9306bf7 --- /dev/null +++ b/src/wov.ts @@ -0,0 +1,36 @@ +import { env } from "./env"; + +export type QuestResult = { + quest: { + id: string; + promoImageUrl: string; + promoImagePrimaryColor: string; + }; + participants: Array; +}; + +export type QuestParticipant = { + playerId: string; + username: string; + xp: number; +}; + +export const checkForNewQuest = async (): Promise => { + 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; + + 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]; +}; diff --git a/tsconfig.json b/tsconfig.json index 238655f..c453b49 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,11 @@ // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false + "noPropertyAccessFromIndexSignature": false, + + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + } } }