From ec1f32ddc25805e2bab0d5fab71b6b011a0e09ee Mon Sep 17 00:00:00 2001 From: M_Viper Date: Wed, 25 Feb 2026 18:50:41 +0100 Subject: [PATCH] Upload file index.js via GUI --- index.js | 337 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 index.js diff --git a/index.js b/index.js new file mode 100644 index 0000000..a5f170d --- /dev/null +++ b/index.js @@ -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 }; \ No newline at end of file