diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..64b86f1 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +DISCORD_TOKEN=replace-with-your-discord-token +BOT_OWNER_ID=replace-with-your-user-id +APPLICATION_ID=replace-with-your-application-id +GUILD_URL=https://www.naeu.playblackdesert.com/fr-fr/Adventure/Guild/GuildProfile?guildName=Abondance®ion=EU +DISCORD_GUILD_ID=replace-with-your-server-id +DISCORD_CHANNEL_ID=replace-with-your-channel-id diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..ed69cd9 --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,28 @@ +name: Deploy Abondance Bot + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: docker + + steps: + - name: Deploy on WebApps + env: + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + run: | + apt-get update + apt-get install -y openssh-client + + mkdir -p ~/.ssh + printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + + ssh -o StrictHostKeyChecking=no \ + -i ~/.ssh/id_ed25519 \ + -p 2911 \ + shinuwa@192.168.0.100 \ + "sudo /usr/local/bin/deploy-abondance-bot" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4365763 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.env +*.tmp diff --git a/index.js b/index.js index 4b7ef05..f497549 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,10 @@ import { initMembersChecker } from "./src/features/membersChecker.js"; import { initDiscordBot } from "./src/features/discordBot.js"; -(async () => { +try { await initDiscordBot(); await initMembersChecker(); -})(); +} catch (error) { + console.error("❌ Impossible de démarrer le bot :", error); + process.exitCode = 1; +} diff --git a/package-lock.json b/package-lock.json index a89ae8d..3152861 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,20 +11,21 @@ "dependencies": { "cheerio": "^1.1.2", "discord.js": "^14.23.2", + "dotenv": "^17.4.2", "node-cron": "^4.2.1", "node-fetch": "^3.3.2" } }, "node_modules/@discordjs/builders": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.12.2.tgz", - "integrity": "sha512-AugKmrgRJOHXEyMkABH/hXpAmIBKbYokCEl9VAM4Kh6FvkdobQ+Zhv7AR6K+y5hS7+VQ7gKXPYCe1JQmV07H1g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz", + "integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==", "license": "Apache-2.0", "dependencies": { - "@discordjs/formatters": "^0.6.1", - "@discordjs/util": "^1.1.1", + "@discordjs/formatters": "^0.6.2", + "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "^0.38.26", + "discord-api-types": "^0.38.40", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" @@ -46,12 +47,12 @@ } }, "node_modules/@discordjs/formatters": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", - "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", + "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", "license": "Apache-2.0", "dependencies": { - "discord-api-types": "^0.38.1" + "discord-api-types": "^0.38.33" }, "engines": { "node": ">=16.11.0" @@ -61,20 +62,20 @@ } }, "node_modules/@discordjs/rest": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", - "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz", + "integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==", "license": "Apache-2.0", "dependencies": { "@discordjs/collection": "^2.1.1", - "@discordjs/util": "^1.1.1", + "@discordjs/util": "^1.2.0", "@sapphire/async-queue": "^1.5.3", - "@sapphire/snowflake": "^3.5.3", + "@sapphire/snowflake": "^3.5.5", "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "^0.38.16", - "magic-bytes.js": "^1.10.0", + "discord-api-types": "^0.38.40", + "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", - "undici": "6.21.3" + "undici": "6.24.1" }, "engines": { "node": ">=18" @@ -95,20 +96,33 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, + "node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz", + "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, "node_modules/@discordjs/rest/node_modules/undici": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", - "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", "license": "MIT", "engines": { "node": ">=18.17" } }, "node_modules/@discordjs/util": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", - "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, "engines": { "node": ">=18" }, @@ -298,33 +312,33 @@ } }, "node_modules/discord-api-types": { - "version": "0.38.30", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.30.tgz", - "integrity": "sha512-KhAqlBrg+rVK+Ob7INMF5o63yW4/GUzRatG/AjyVsIO8lgcLyR8qCl2HokIVzWwmzkJYG0CEPXsKMOqau3E8NA==", + "version": "0.38.49", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.49.tgz", + "integrity": "sha512-XnqcWmnFZFAE8ZM8SHAw9DIV8D3Or00rMQ8iQLotrEA2PmXhl+ykaf6L6q4l474hrSUH1JaYcv+iOMRWp2p6Tg==", "license": "MIT", "workspaces": [ "scripts/actions/documentation" ] }, "node_modules/discord.js": { - "version": "14.23.2", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.23.2.tgz", - "integrity": "sha512-tU2NFr823X3TXEc8KyR/4m296KLxPai4nirN3q9kHCpY4TKj96n9lHZnyLzRNMui8EbL07jg9hgH2PWWfKMGIg==", + "version": "14.26.4", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.4.tgz", + "integrity": "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA==", "license": "Apache-2.0", "dependencies": { - "@discordjs/builders": "^1.12.1", + "@discordjs/builders": "^1.14.1", "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.6.1", - "@discordjs/rest": "^2.6.0", - "@discordjs/util": "^1.1.1", + "@discordjs/formatters": "^0.6.2", + "@discordjs/rest": "^2.6.1", + "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", - "discord-api-types": "^0.38.29", + "discord-api-types": "^0.38.40", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", - "magic-bytes.js": "^1.10.0", + "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", - "undici": "6.21.3" + "undici": "6.24.1" }, "engines": { "node": ">=18" @@ -334,9 +348,9 @@ } }, "node_modules/discord.js/node_modules/undici": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", - "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", "license": "MIT", "engines": { "node": ">=18.17" @@ -397,6 +411,18 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/encoding-sniffer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", @@ -507,9 +533,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.snakecase": { @@ -519,9 +545,9 @@ "license": "MIT" }, "node_modules/magic-bytes.js": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", - "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz", + "integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==", "license": "MIT" }, "node_modules/node-cron": { @@ -696,9 +722,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index e1b3a44..33b519b 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,15 @@ { "name": "abondance-bot", "version": "1.0.0", - "description": "", + "description": "Surveille les changements dans la liste des membres de la guilde Abondance", "main": "index.js", "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node index.js", + "test": "node --test" + }, + "engines": { + "node": ">=20" }, "keywords": [], "author": "", @@ -13,6 +17,7 @@ "dependencies": { "cheerio": "^1.1.2", "discord.js": "^14.23.2", + "dotenv": "^17.4.2", "node-cron": "^4.2.1", "node-fetch": "^3.3.2" } diff --git a/src/features/discordBot.js b/src/features/discordBot.js index fac7eba..8261a94 100644 --- a/src/features/discordBot.js +++ b/src/features/discordBot.js @@ -2,7 +2,7 @@ import { Client, GatewayIntentBits } from "discord.js"; import { config } from "../static/config.js"; const client = new Client({ - intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages], + intents: [GatewayIntentBits.Guilds], }); async function initDiscordBot() { @@ -11,15 +11,16 @@ async function initDiscordBot() { } async function sendDiscordAlert(message) { - try { - const channel = await client.channels.fetch(config.textChannelId); - await channel.send(message); - } catch (err) { - console.error("❌ Erreur envoi Discord :", err); + const channel = await client.channels.fetch(config.textChannelId); + + if (!channel?.isTextBased()) { + throw new Error(`Le canal Discord ${config.textChannelId} n'est pas textuel`); } + + await channel.send(message); } export { initDiscordBot, sendDiscordAlert -} \ No newline at end of file +} diff --git a/src/features/membersChecker.js b/src/features/membersChecker.js index eb841de..9d0b679 100644 --- a/src/features/membersChecker.js +++ b/src/features/membersChecker.js @@ -1,63 +1,164 @@ import cron from "node-cron"; import fetch from "node-fetch"; -import fs from "fs"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import * as cheerio from "cheerio"; import { config } from "../static/config.js"; -import { sendDiscordAlert } from "./discordBot.js" +import { sendDiscordAlert } from "./discordBot.js"; -const membersJSON = "./src/static/data/members.json"; +const membersJSON = new URL("../static/data/members.json", import.meta.url); const scheduleTime = "30 9 * * *"; const scheduleTimezone = "Europe/Paris"; +const requestTimeoutMs = 15_000; +const memberSelector = ".box_list_area .adventure_list_table li .guild_name .text a"; -async function initMembersChecker() { - await checkGuildMembers(); - cron.schedule(scheduleTime, checkGuildMembers, { timezone: scheduleTimezone }); -} - -async function getGuildMembers(guildUrl) { - const res = await fetch(guildUrl); - const html = await res.text(); - +function parseGuildMembers(html) { const $ = cheerio.load(html); - const members = []; + const members = $(memberSelector) + .map((_, element) => $(element).text().trim()) + .get() + .filter(Boolean); - $('.box_list_area .adventure_list_table li .guild_name .text a').each((i, el) => { - members.push($(el).text().trim()); - }); + if (members.length === 0) { + throw new Error( + "Aucun membre trouvé : la page est invalide ou sa structure HTML a changé", + ); + } + + if (new Set(members).size !== members.length) { + throw new Error("La liste extraite contient des membres en double"); + } return members; } -async function checkGuildMembers() { +async function getGuildMembers(guildUrl, fetchImpl = fetch) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), requestTimeoutMs); + try { - const newMembers = await getGuildMembers(config.guild_url); + const response = await fetchImpl(guildUrl, { + headers: { + Accept: "text/html,application/xhtml+xml", + "Accept-Language": "fr-FR,fr;q=0.9,en;q=0.8", + "User-Agent": "AbondanceGuildBot/1.0", + }, + signal: controller.signal, + }); - let oldMembers = []; - if (fs.existsSync(membersJSON)) { - oldMembers = JSON.parse(fs.readFileSync(membersJSON, "utf-8")); + if (!response.ok) { + throw new Error(`La page de guilde a répondu avec le statut HTTP ${response.status}`); } - const added = newMembers.filter(m => !oldMembers.includes(m)); - const removed = oldMembers.filter(m => !newMembers.includes(m)); - - if (added.length || removed.length) { - let message = "⚔️ **Changement dans la liste des membres :**\n"; - if (added.length) message += `➕ Nouveaux membres : ${added.join(", ")}\n`; - if (removed.length) message += `➖ Membres partis : ${removed.join(", ")}`; - await sendDiscordAlert(message); + return parseGuildMembers(await response.text()); + } catch (error) { + if (error.name === "AbortError") { + throw new Error(`La page de guilde n'a pas répondu sous ${requestTimeoutMs / 1000}s`); } - fs.mkdirSync("./src/static/data/", { recursive: true }); - fs.writeFileSync(membersJSON, JSON.stringify(newMembers, null, 2)); - console.log("✅ Vérification terminée", new Date().toLocaleString("fr-FR", { timeZone: scheduleTimezone })); - } catch (err) { - console.error("❌ Erreur checkGuildMembers:", err); - try { - await sendDiscordAlert(`⚠️ Erreur lors du check des membres: \`${err.message}\``); - } catch {} + throw error; + } finally { + clearTimeout(timeout); } } -export { - initMembersChecker +function readMembers(fileUrl = membersJSON) { + if (!fs.existsSync(fileUrl)) return null; + + const members = JSON.parse(fs.readFileSync(fileUrl, "utf8")); + if ( + !Array.isArray(members) || + members.length === 0 || + members.some((member) => typeof member !== "string" || !member.trim()) + ) { + throw new Error("Le fichier members.json ne contient pas une liste valide"); + } + + return members; } + +function writeMembers(members, fileUrl = membersJSON) { + const filePath = fileUrl instanceof URL ? fileURLToPath(fileUrl) : fileUrl; + const temporaryFile = `${filePath}.${process.pid}.tmp`; + + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(temporaryFile, `${JSON.stringify(members, null, 2)}\n`); + fs.renameSync(temporaryFile, filePath); +} + +function assertPlausibleChange(oldMembers, newMembers) { + if (oldMembers.length >= 10 && newMembers.length < oldMembers.length / 2) { + throw new Error( + `Extraction suspecte : ${newMembers.length} membres trouvés contre ${oldMembers.length} auparavant`, + ); + } +} + +function getChanges(oldMembers, newMembers) { + const oldSet = new Set(oldMembers); + const newSet = new Set(newMembers); + + return { + added: newMembers.filter((member) => !oldSet.has(member)), + removed: oldMembers.filter((member) => !newSet.has(member)), + }; +} + +function formatChanges({ added, removed }) { + let message = "⚔️ **Changement dans la liste des membres :**\n"; + if (added.length) message += `➕ Nouveaux membres : ${added.join(", ")}\n`; + if (removed.length) message += `➖ Membres partis : ${removed.join(", ")}`; + return message.trimEnd(); +} + +async function checkGuildMembers({ + guildUrl = config.guild_url, + fileUrl = membersJSON, + notify = sendDiscordAlert, + fetchImpl = fetch, +} = {}) { + try { + const newMembers = await getGuildMembers(guildUrl, fetchImpl); + const oldMembers = readMembers(fileUrl); + + if (oldMembers) { + assertPlausibleChange(oldMembers, newMembers); + const changes = getChanges(oldMembers, newMembers); + if (changes.added.length || changes.removed.length) { + await notify(formatChanges(changes)); + } + } + + writeMembers(newMembers, fileUrl); + console.log( + `✅ Vérification terminée : ${newMembers.length} membres`, + new Date().toLocaleString("fr-FR", { timeZone: scheduleTimezone }), + ); + } catch (error) { + console.error("❌ Erreur checkGuildMembers:", error); + try { + await notify(`⚠️ Erreur lors du check des membres : ${error.message}`); + } catch (notificationError) { + console.error("❌ Impossible d'envoyer l'erreur sur Discord :", notificationError); + } + } +} + +async function initMembersChecker() { + await checkGuildMembers(); + cron.schedule(scheduleTime, checkGuildMembers, { + timezone: scheduleTimezone, + noOverlap: true, + }); +} + +export { + assertPlausibleChange, + checkGuildMembers, + formatChanges, + getChanges, + getGuildMembers, + initMembersChecker, + parseGuildMembers, +}; diff --git a/src/static/config.js b/src/static/config.js index 32034d7..223f071 100644 --- a/src/static/config.js +++ b/src/static/config.js @@ -1,10 +1,22 @@ -const config = { - token: "MTQyODI5MDE1Mjk4MzE3MTE2Mw.GFwDOo.-RPnn6meXQL3xAh1S0vCQ9N0H3PeXcxKGSIWAM", - botOwner: "139829632764477440", - applicationId: "1428290152983171163", - guild_url: "https://www.naeu.playblackdesert.com/en-us/Adventure/Guild/GuildProfile?guildName=Abondance®ion=EU", - discordGuildId: "1205611509305057410", - textChannelId: "1428068401825321094", -}; +import "dotenv/config"; -export { config }; \ No newline at end of file +function requiredEnv(name) { + const value = process.env[name]?.trim(); + + if (!value) { + throw new Error(`La variable d'environnement ${name} est obligatoire`); + } + + return value; +} + +const config = Object.freeze({ + token: requiredEnv("DISCORD_TOKEN"), + botOwner: process.env.BOT_OWNER_ID?.trim(), + applicationId: process.env.APPLICATION_ID?.trim(), + guild_url: requiredEnv("GUILD_URL"), + discordGuildId: process.env.DISCORD_GUILD_ID?.trim(), + textChannelId: requiredEnv("DISCORD_CHANNEL_ID"), +}); + +export { config }; diff --git a/test/membersChecker.test.js b/test/membersChecker.test.js new file mode 100644 index 0000000..68bec5c --- /dev/null +++ b/test/membersChecker.test.js @@ -0,0 +1,65 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + assertPlausibleChange, + formatChanges, + getChanges, + getGuildMembers, + parseGuildMembers, +} from "../src/features/membersChecker.js"; + +const guildPage = (members) => ` +