Upload file index.js via GUI

This commit is contained in:
2026-02-25 18:50:41 +01:00
parent 4e4c0a552a
commit ec1f32ddc2

337
index.js Normal file
View 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 };