Upload file index.js via GUI
This commit is contained in:
337
index.js
Normal file
337
index.js
Normal file
@@ -0,0 +1,337 @@
|
||||
import "dotenv/config";
|
||||
import Discord from "discord.js";
|
||||
import { EmbedBuilder, REST, Routes } from "discord.js";
|
||||
import fs from "fs";
|
||||
import { Spiget } from "spiget";
|
||||
|
||||
import configFile from "./config.json" with { type: "json" };
|
||||
import logger from "./util/logger.js";
|
||||
import packageData from "./package.json" with { type: "json" };
|
||||
import queue from "./util/queue.js";
|
||||
import stats from "./util/stats.js";
|
||||
import {
|
||||
generateAvatarLink,
|
||||
generateAuthorURL,
|
||||
generateResourceIconURL,
|
||||
getLatestVersion,
|
||||
getUpdateDescription,
|
||||
} from "./util/helpers.js";
|
||||
|
||||
// Token und Owner-ID aus .env laden
|
||||
const config = {
|
||||
...configFile,
|
||||
token: process.env.DISCORD_TOKEN,
|
||||
ownerID: process.env.OWNER_ID,
|
||||
};
|
||||
|
||||
if (!config.token) {
|
||||
console.error("❌ DISCORD_TOKEN fehlt in der .env Datei!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
const spiget = new Spiget(config.botName);
|
||||
|
||||
// Wie oft darf eine Ressource hintereinander fehlschlagen, bevor eine DM gesendet wird
|
||||
const ERROR_THRESHOLD = 3;
|
||||
// Zählt aufeinanderfolgende Fehler pro Ressource: "guildID:resourceID" → count
|
||||
const errorCounts = new Map();
|
||||
|
||||
/**
|
||||
* ─── CLIENT ──────────────────────────────────────────────────────────────────
|
||||
*/
|
||||
const client = {
|
||||
bot: new Discord.Client({
|
||||
intents: [
|
||||
Discord.IntentsBitField.Flags.Guilds,
|
||||
Discord.IntentsBitField.Flags.GuildMessages,
|
||||
Discord.IntentsBitField.Flags.MessageContent,
|
||||
],
|
||||
partials: ["MESSAGE", "CHANNEL"],
|
||||
}),
|
||||
commands: new Map(),
|
||||
aliases: new Map(),
|
||||
cooldowns: new Map(),
|
||||
config,
|
||||
logger,
|
||||
packageData,
|
||||
queue,
|
||||
stats,
|
||||
};
|
||||
|
||||
stats.markStart();
|
||||
client.logger.info(`${config.botName} wird gestartet...`);
|
||||
|
||||
/**
|
||||
* ─── EVENTS ──────────────────────────────────────────────────────────────────
|
||||
*/
|
||||
const eventFiles = fs
|
||||
.readdirSync("./events")
|
||||
.filter((f) => f.endsWith(".js"));
|
||||
|
||||
for (const file of eventFiles) {
|
||||
const event = (await import(`./events/${file}`)).default;
|
||||
client.bot.on(event.name, (...args) =>
|
||||
event.execute(client, ...args).catch(logger.error)
|
||||
);
|
||||
}
|
||||
client.logger.info(`${eventFiles.length} Events geladen`);
|
||||
|
||||
/**
|
||||
* ─── COMMANDS ────────────────────────────────────────────────────────────────
|
||||
*/
|
||||
const commandFiles = fs
|
||||
.readdirSync("./commands")
|
||||
.filter((f) => f.endsWith(".js"));
|
||||
|
||||
const slashCommands = [];
|
||||
|
||||
for (const file of commandFiles) {
|
||||
const command = (await import(`./commands/${file}`)).default;
|
||||
client.commands.set(command.name, command);
|
||||
|
||||
if (command.aliases?.length > 0) {
|
||||
for (const alias of command.aliases) {
|
||||
client.aliases.set(alias, command.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (command.data) slashCommands.push(command.data.toJSON());
|
||||
}
|
||||
client.logger.info(`${commandFiles.length} Befehle registriert`);
|
||||
|
||||
/**
|
||||
* ─── SLASH COMMAND REGISTRIERUNG ─────────────────────────────────────────────
|
||||
*/
|
||||
async function registerSlashCommands() {
|
||||
if (slashCommands.length === 0) return;
|
||||
try {
|
||||
const rest = new REST().setToken(config.token);
|
||||
await rest.put(Routes.applicationCommands(config.inviteClientID), { body: slashCommands });
|
||||
client.logger.info(`${slashCommands.length} Slash-Befehle registriert`);
|
||||
} catch (e) {
|
||||
client.logger.error(`Slash-Registrierung fehlgeschlagen: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ─── RECONNECT-HANDLING ──────────────────────────────────────────────────────
|
||||
*/
|
||||
client.bot.on("shardDisconnect", (event, shardID) => {
|
||||
client.logger.warn(`[Shard ${shardID}] Verbindung getrennt (Code ${event.code}) – versuche Reconnect...`);
|
||||
});
|
||||
|
||||
client.bot.on("shardReconnecting", (shardID) => {
|
||||
client.logger.info(`[Shard ${shardID}] Verbinde erneut...`);
|
||||
});
|
||||
|
||||
client.bot.on("shardResume", (shardID, replayedEvents) => {
|
||||
client.logger.info(`[Shard ${shardID}] Wiederverbunden – ${replayedEvents} Events nachgeholt`);
|
||||
});
|
||||
|
||||
client.bot.on("shardError", (error, shardID) => {
|
||||
client.logger.error(`[Shard ${shardID}] WebSocket-Fehler: ${error.message}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* ─── OWNER DM HELPER ─────────────────────────────────────────────────────────
|
||||
*/
|
||||
async function dmOwner(subject, description) {
|
||||
if (!config.ownerID) return;
|
||||
try {
|
||||
const owner = await client.bot.users.fetch(config.ownerID);
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor("#FF0000")
|
||||
.setTitle(`⚠️ ${config.botName} – ${subject}`)
|
||||
.setDescription(description)
|
||||
.setTimestamp();
|
||||
await owner.send({ embeds: [embed] });
|
||||
stats.increment("dmsSent");
|
||||
client.logger.info(`[DM] Owner benachrichtigt: ${subject}`);
|
||||
} catch (e) {
|
||||
client.logger.error(`[DM] Owner-DM fehlgeschlagen: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ─── UPDATE-CHECK QUEUE ───────────────────────────────────────────────────────
|
||||
* Läuft jede Minute; füllt die Queue mit einem Job pro Ressource.
|
||||
*/
|
||||
function scheduleNextCheck() {
|
||||
const intervalMs = 60 * 1000;
|
||||
queue.nextRunAt = Date.now() + intervalMs;
|
||||
setTimeout(() => {
|
||||
enqueueUpdateChecks();
|
||||
scheduleNextCheck();
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
async function enqueueUpdateChecks() {
|
||||
let serverDataDir;
|
||||
try {
|
||||
serverDataDir = fs.readdirSync("./serverdata");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverFiles = serverDataDir.filter((f) => f.endsWith(".json"));
|
||||
stats.increment("checksRun");
|
||||
|
||||
for (const serverFile of serverFiles) {
|
||||
const filePath = `./serverdata/${serverFile}`;
|
||||
let jsonData;
|
||||
|
||||
try {
|
||||
jsonData = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
} catch (e) {
|
||||
client.logger.error(`Fehler beim Lesen von ${filePath}: ${e.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Per-Server-Intervall prüfen (Standard: 5 Minuten)
|
||||
const intervalMs = (jsonData.updateInterval ?? 5) * 60 * 1000;
|
||||
const lastChecked = jsonData.lastChecked ?? 0;
|
||||
if (Date.now() - lastChecked < intervalMs) continue;
|
||||
|
||||
jsonData.lastChecked = Date.now();
|
||||
|
||||
for (const watchedResource of jsonData.watchedResources) {
|
||||
// Job in Queue einreihen – wird sequenziell abgearbeitet
|
||||
queue.enqueue(() =>
|
||||
checkResource(watchedResource, jsonData, filePath, serverFile)
|
||||
);
|
||||
}
|
||||
|
||||
// lastChecked sofort speichern
|
||||
try {
|
||||
fs.writeFileSync(filePath, JSON.stringify(jsonData, null, 2));
|
||||
} catch (e) {
|
||||
client.logger.error(`Fehler beim Schreiben von ${filePath}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft eine einzelne Ressource auf Updates.
|
||||
*/
|
||||
async function checkResource(watchedResource, jsonData, filePath, serverFile) {
|
||||
const { resourceID: id, channelID } = watchedResource;
|
||||
const errorKey = `${serverFile}:${id}`;
|
||||
|
||||
const channel = client.bot.channels.cache.get(channelID);
|
||||
if (!channel) {
|
||||
client.logger.warn(`Kanal ${channelID} nicht gefunden – Ressource ${id} übersprungen`);
|
||||
|
||||
// Fehler zählen und Owner benachrichtigen wenn Schwelle erreicht
|
||||
const count = (errorCounts.get(errorKey) ?? 0) + 1;
|
||||
errorCounts.set(errorKey, count);
|
||||
if (count === ERROR_THRESHOLD) {
|
||||
await dmOwner(
|
||||
"Kanal nicht gefunden",
|
||||
`Kanal <#${channelID}> (ID: \`${channelID}\`) für Ressource \`${id}\` konnte **${count}x** hintereinander nicht gefunden werden.\n\nBitte prüfe ob der Kanal noch existiert oder entferne die Ressource mit \`${config.prefix}remove ${id}\`.`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Kanal gefunden – Fehlerzähler zurücksetzen
|
||||
errorCounts.delete(errorKey);
|
||||
|
||||
let resource, author;
|
||||
try {
|
||||
resource = await spiget.getResource(id);
|
||||
author = await resource.getAuthor();
|
||||
} catch (e) {
|
||||
client.logger.error(`Spiget-Fehler für Ressource ${id}: ${e.message}`);
|
||||
stats.increment("apiErrors");
|
||||
|
||||
const count = (errorCounts.get(`api:${id}`) ?? 0) + 1;
|
||||
errorCounts.set(`api:${id}`, count);
|
||||
if (count === ERROR_THRESHOLD) {
|
||||
await dmOwner(
|
||||
"Spiget API-Fehler",
|
||||
`Ressource \`${id}\` konnte **${count}x** hintereinander nicht von der Spiget-API abgerufen werden.\n**Fehler:** ${e.message}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
errorCounts.delete(`api:${id}`);
|
||||
|
||||
let latestVersion;
|
||||
try {
|
||||
latestVersion = await getLatestVersion(id);
|
||||
} catch (e) {
|
||||
client.logger.error(`Versionsfehler für Ressource ${id}: ${e.message}`);
|
||||
stats.increment("apiErrors");
|
||||
return;
|
||||
}
|
||||
|
||||
// Bereits aktuell
|
||||
const previousVersion = watchedResource.lastCheckedVersion;
|
||||
if (previousVersion === latestVersion) return;
|
||||
|
||||
stats.increment("updatesFound");
|
||||
|
||||
let updateDesc;
|
||||
try {
|
||||
updateDesc = await getUpdateDescription(id);
|
||||
} catch {
|
||||
updateDesc = "Update-Beschreibung konnte nicht abgerufen werden.";
|
||||
}
|
||||
|
||||
const authorURL = generateAuthorURL(author.name, author.id);
|
||||
const authorAvatarURL = generateAvatarLink(author.id);
|
||||
const resourceIconURL = generateResourceIconURL(resource);
|
||||
const resourceURL = `https://spigotmc.org/resources/.${id}/`;
|
||||
|
||||
const updateEmbed = new EmbedBuilder()
|
||||
.setAuthor({ name: `Autor: ${author.name}`, iconURL: authorAvatarURL, url: authorURL })
|
||||
.setColor(channel.guild.members.me.displayHexColor)
|
||||
.setTitle(`🔔 Update verfügbar: ${resource.name}`)
|
||||
.setDescription(resource.tag)
|
||||
.addFields([
|
||||
{
|
||||
name: "📦 Version",
|
||||
value: previousVersion
|
||||
? `~~${previousVersion}~~ → **${latestVersion}**`
|
||||
: `**${latestVersion}**`,
|
||||
inline: false,
|
||||
},
|
||||
{ name: "📝 Update-Beschreibung", value: updateDesc, inline: false },
|
||||
{ name: "⬇️ Download", value: resourceURL, inline: false },
|
||||
])
|
||||
.setThumbnail(resourceIconURL)
|
||||
.setTimestamp();
|
||||
|
||||
// Version speichern
|
||||
watchedResource.lastCheckedVersion = latestVersion;
|
||||
watchedResource.resourceName = resource.name;
|
||||
try {
|
||||
fs.writeFileSync(filePath, JSON.stringify(jsonData, null, 2));
|
||||
} catch (e) {
|
||||
client.logger.error(`Fehler beim Schreiben von ${filePath}: ${e.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const mentionContent = watchedResource.mentionRoleID ? `<@&${watchedResource.mentionRoleID}>` : undefined;
|
||||
await channel.send({ content: mentionContent, embeds: [updateEmbed] });
|
||||
stats.increment("updatesPosted");
|
||||
} catch (e) {
|
||||
client.logger.error(`Fehler beim Senden in Kanal ${channelID}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ─── START ────────────────────────────────────────────────────────────────────
|
||||
*/
|
||||
client.logger.info("Anmeldung läuft...");
|
||||
client.bot.login(client.config.token);
|
||||
|
||||
client.bot.once("clientReady", () => {
|
||||
registerSlashCommands();
|
||||
scheduleNextCheck();
|
||||
client.logger.info("Update-Check-Queue gestartet");
|
||||
});
|
||||
|
||||
export { dmOwner };
|
||||
Reference in New Issue
Block a user