upgrade project and add deploy workflow
Some checks failed
Deploy Abondance Bot / deploy (push) Failing after 24s
Some checks failed
Deploy Abondance Bot / deploy (push) Failing after 24s
This commit is contained in:
parent
0dabb7c3c0
commit
3a50bd061a
10 changed files with 358 additions and 108 deletions
6
.env.example
Normal file
6
.env.example
Normal file
|
|
@ -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
|
||||
28
.forgejo/workflows/deploy.yml
Normal file
28
.forgejo/workflows/deploy.yml
Normal file
|
|
@ -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"
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
.env
|
||||
*.tmp
|
||||
7
index.js
7
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;
|
||||
}
|
||||
|
|
|
|||
124
package-lock.json
generated
124
package-lock.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,12 +11,13 @@ 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);
|
||||
|
||||
if (!channel?.isTextBased()) {
|
||||
throw new Error(`Le canal Discord ${config.textChannelId} n'est pas textuel`);
|
||||
}
|
||||
|
||||
await channel.send(message);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
function parseGuildMembers(html) {
|
||||
const $ = cheerio.load(html);
|
||||
const members = $(memberSelector)
|
||||
.map((_, element) => $(element).text().trim())
|
||||
.get()
|
||||
.filter(Boolean);
|
||||
|
||||
if (members.length === 0) {
|
||||
throw new Error(
|
||||
"Aucun membre trouvé : la page est invalide ou sa structure HTML a changé",
|
||||
);
|
||||
}
|
||||
|
||||
async function getGuildMembers(guildUrl) {
|
||||
const res = await fetch(guildUrl);
|
||||
const html = await res.text();
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
const members = [];
|
||||
|
||||
$('.box_list_area .adventure_list_table li .guild_name .text a').each((i, el) => {
|
||||
members.push($(el).text().trim());
|
||||
});
|
||||
if (new Set(members).size !== members.length) {
|
||||
throw new Error("La liste extraite contient des membres en double");
|
||||
}
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
async function checkGuildMembers() {
|
||||
try {
|
||||
const newMembers = await getGuildMembers(config.guild_url);
|
||||
async function getGuildMembers(guildUrl, fetchImpl = fetch) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
|
||||
|
||||
let oldMembers = [];
|
||||
if (fs.existsSync(membersJSON)) {
|
||||
oldMembers = JSON.parse(fs.readFileSync(membersJSON, "utf-8"));
|
||||
try {
|
||||
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,
|
||||
});
|
||||
|
||||
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));
|
||||
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`);
|
||||
}
|
||||
|
||||
if (added.length || removed.length) {
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
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(", ")}`;
|
||||
await sendDiscordAlert(message);
|
||||
return message.trimEnd();
|
||||
}
|
||||
|
||||
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);
|
||||
async function checkGuildMembers({
|
||||
guildUrl = config.guild_url,
|
||||
fileUrl = membersJSON,
|
||||
notify = sendDiscordAlert,
|
||||
fetchImpl = fetch,
|
||||
} = {}) {
|
||||
try {
|
||||
await sendDiscordAlert(`⚠️ Erreur lors du check des membres: \`${err.message}\``);
|
||||
} catch {}
|
||||
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 {
|
||||
initMembersChecker
|
||||
}
|
||||
assertPlausibleChange,
|
||||
checkGuildMembers,
|
||||
formatChanges,
|
||||
getChanges,
|
||||
getGuildMembers,
|
||||
initMembersChecker,
|
||||
parseGuildMembers,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
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 };
|
||||
65
test/membersChecker.test.js
Normal file
65
test/membersChecker.test.js
Normal file
|
|
@ -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) => `
|
||||
<div class="box_list_area">
|
||||
<ul class="adventure_list_table">
|
||||
<li>
|
||||
${members
|
||||
.map(
|
||||
(member) =>
|
||||
`<span class="guild_name"><span class="text"><a>${member}</a></span></span>`,
|
||||
)
|
||||
.join("")}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
test("parseGuildMembers extrait et nettoie les noms", () => {
|
||||
assert.deepEqual(parseGuildMembers(guildPage([" Alpha ", "Beta"])), ["Alpha", "Beta"]);
|
||||
});
|
||||
|
||||
test("parseGuildMembers refuse une page sans membres", () => {
|
||||
assert.throws(() => parseGuildMembers("<html></html>"), /Aucun membre trouvé/);
|
||||
});
|
||||
|
||||
test("parseGuildMembers refuse les doublons", () => {
|
||||
assert.throws(() => parseGuildMembers(guildPage(["Alpha", "Alpha"])), /double/);
|
||||
});
|
||||
|
||||
test("getChanges détecte les arrivées et départs", () => {
|
||||
assert.deepEqual(getChanges(["Alpha", "Beta"], ["Beta", "Gamma"]), {
|
||||
added: ["Gamma"],
|
||||
removed: ["Alpha"],
|
||||
});
|
||||
});
|
||||
|
||||
test("assertPlausibleChange refuse une chute brutale de l'effectif", () => {
|
||||
const oldMembers = Array.from({ length: 20 }, (_, index) => `Membre-${index}`);
|
||||
assert.throws(
|
||||
() => assertPlausibleChange(oldMembers, ["Membre-0", "Membre-1"]),
|
||||
/Extraction suspecte/,
|
||||
);
|
||||
});
|
||||
|
||||
test("formatChanges produit le message Discord", () => {
|
||||
assert.equal(
|
||||
formatChanges({ added: ["Gamma"], removed: ["Alpha"] }),
|
||||
"⚔️ **Changement dans la liste des membres :**\n" +
|
||||
"➕ Nouveaux membres : Gamma\n" +
|
||||
"➖ Membres partis : Alpha",
|
||||
);
|
||||
});
|
||||
|
||||
test("getGuildMembers refuse une erreur HTTP", async () => {
|
||||
const fakeFetch = async () => ({ ok: false, status: 503 });
|
||||
await assert.rejects(getGuildMembers("https://example.test", fakeFetch), /HTTP 503/);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue