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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
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 };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue