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