Update from Git Manager GUI
This commit is contained in:
75
util/helpers.js
Normal file
75
util/helpers.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Shared utility functions for PluginBot
|
||||||
|
* Centralised here to avoid duplication across commands
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function generateAvatarLink(authorID) {
|
||||||
|
const idStr = authorID.toString();
|
||||||
|
const splitPoint = Math.ceil(idStr.length / 2);
|
||||||
|
const firstHalf = idStr.substring(0, splitPoint);
|
||||||
|
return `https://www.spigotmc.org/data/avatars/l/${firstHalf}/${authorID}.jpg`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateAuthorURL(authorName, authorID) {
|
||||||
|
return `https://www.spigotmc.org/members/${authorName}.${authorID}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateResourceIconURL(resource) {
|
||||||
|
return resource.icon
|
||||||
|
.fullUrl()
|
||||||
|
.replace("orgdata", "org/data")
|
||||||
|
.replace("https://spigotmc.org", "https://www.spigotmc.org");
|
||||||
|
// www must be present – embeds don't render without it
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateResourceURL(resourceID) {
|
||||||
|
return `https://spigotmc.org/resources/.${resourceID}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts basic SpigotMC HTML to Discord Markdown.
|
||||||
|
* BUG FIX: </i> was previously mapped to "**" (bold) instead of "*" (italic)
|
||||||
|
*/
|
||||||
|
export function formatText(description) {
|
||||||
|
return description
|
||||||
|
.replace(/<b>/gi, "**")
|
||||||
|
.replace(/<\/b>/gi, "**")
|
||||||
|
.replace(/<i>/gi, "*")
|
||||||
|
.replace(/<\/i>/gi, "*") // ← was "**" before (bug)
|
||||||
|
.replace(/<ul>/gi, "")
|
||||||
|
.replace(/<\/ul>/gi, "")
|
||||||
|
.replace(/<li>/gi, "• ")
|
||||||
|
.replace(/<\/li>/gi, "\n")
|
||||||
|
.replace(/<br\s*\/?>/gi, "\n")
|
||||||
|
.replace(/<[^>]+>/g, ""); // strip any remaining HTML tags
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the latest version name for a Spiget resource.
|
||||||
|
* Uses native fetch instead of the legacy xmlhttprequest package.
|
||||||
|
*/
|
||||||
|
export async function getLatestVersion(resourceID) {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://api.spiget.org/v2/resources/${resourceID}/versions/latest`
|
||||||
|
);
|
||||||
|
if (!res.ok) throw new Error(`Spiget version request failed: HTTP ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
return data.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the latest update description for a Spiget resource.
|
||||||
|
* Returns a Discord-safe string (max 1024 chars).
|
||||||
|
*/
|
||||||
|
export async function getUpdateDescription(resourceID) {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://api.spiget.org/v2/resources/${resourceID}/updates/latest`
|
||||||
|
);
|
||||||
|
if (!res.ok) throw new Error(`Spiget update request failed: HTTP ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
const decoded = Buffer.from(data.description, "base64").toString("utf8");
|
||||||
|
const formatted = formatText(decoded);
|
||||||
|
return formatted.length > 1024
|
||||||
|
? "Description is greater than 1024 characters – visit the resource page for details."
|
||||||
|
: formatted;
|
||||||
|
}
|
||||||
228
util/i18n.js
Normal file
228
util/i18n.js
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
/**
|
||||||
|
* util/i18n.js
|
||||||
|
* Übersetzungen für DE und EN.
|
||||||
|
* Nutzung: t(lang, "key") oder t(lang, "key", { var: "wert" })
|
||||||
|
*/
|
||||||
|
|
||||||
|
const strings = {
|
||||||
|
de: {
|
||||||
|
// Allgemein
|
||||||
|
"error.noArgs": "Du hast nicht genug Argumente angegeben!",
|
||||||
|
"error.noPermission": "Du hast keine Berechtigung diesen Befehl auszuführen!",
|
||||||
|
"error.onlyOwner": "Dieser Befehl kann nur vom Bot-Besitzer genutzt werden!",
|
||||||
|
"error.cooldown": "Bitte warte {seconds} Sekunde(n) bevor du diesen Befehl erneut verwendest!",
|
||||||
|
"error.nsfw": "Dieser Befehl ist als NSFW markiert. Bitte verwende ihn in einem NSFW-Kanal!",
|
||||||
|
"error.notFound": "Ich konnte `{name}` nicht in der Befehlsliste finden!",
|
||||||
|
"error.invalidID": "Ups! `{id}` ist keine gültige Ressourcen-ID!",
|
||||||
|
"error.noAuthor": "Ups! Der Autor für Ressource `{id}` konnte nicht gefunden werden.",
|
||||||
|
"error.apiDown": "Die API konnte nicht erreicht werden. Bitte versuche es später erneut.",
|
||||||
|
"error.saveFailed": "Beim Speichern der Daten ist ein Fehler aufgetreten.",
|
||||||
|
"error.timeout": "⏱️ Zeit abgelaufen – Befehl wurde abgebrochen.",
|
||||||
|
"general.confirm": "✅ Bestätigen",
|
||||||
|
"general.cancel": "❌ Abbrechen",
|
||||||
|
"general.cancelled": "❌ Abgebrochen.",
|
||||||
|
"general.author": "Autor: {name}",
|
||||||
|
// add
|
||||||
|
"add.alreadyWatched": "Diese Ressource wird bereits in <#{channelID}> beobachtet.",
|
||||||
|
"add.invalidChannel": "Dieser Kanal ist ungültig. Bitte erwähne einen Kanal auf diesem Server.",
|
||||||
|
"add.notInGuild": "Dieser Kanal befindet sich nicht auf diesem Server!",
|
||||||
|
"add.versionFail": "Die neueste Version konnte nicht von SpigotMC abgerufen werden. Bitte versuche es erneut.",
|
||||||
|
"add.confirmTitle": "Plugin beobachten: {name}",
|
||||||
|
"add.confirmDesc": "{tag}\n\nSoll dieses Plugin in <#{channelID}> beobachtet werden?",
|
||||||
|
"add.successTitle": "✅ Wird jetzt beobachtet: {name}",
|
||||||
|
"add.buttonConfirm": "✅ Bestätigen",
|
||||||
|
"add.channel": "Kanal",
|
||||||
|
"add.version": "Version",
|
||||||
|
"add.download": "Download",
|
||||||
|
// remove
|
||||||
|
"remove.noResources": "Dieser Server hat keine beobachteten Ressourcen!",
|
||||||
|
"remove.notFound": "Fehler: Ressource `{id}` wurde auf diesem Server nicht beobachtet.",
|
||||||
|
"remove.success": "✅ **{name}** (`{id}`) wird nicht mehr beobachtet.",
|
||||||
|
"remove.saveFail": "Beim Aktualisieren der Beobachtungsdaten ist ein Fehler aufgetreten.",
|
||||||
|
// list
|
||||||
|
"list.noResources": "Dieser Server hat keine beobachteten Ressourcen. Nutze `{prefix}add` um eine hinzuzufügen.",
|
||||||
|
"list.title": "📋 Beobachtete Plugins ({count})",
|
||||||
|
"list.interval": "⏱️ Update-Intervall: {min} Minute(n)",
|
||||||
|
"list.intervalDefault": "⏱️ Update-Intervall: 5 Minuten (Standard)",
|
||||||
|
"list.channel": "Kanal",
|
||||||
|
// setinterval
|
||||||
|
"setinterval.invalid": "Ungültiger Wert. Bitte gib eine Zahl zwischen {min} und {max} Minuten an.",
|
||||||
|
"setinterval.success": "Das Update-Intervall für diesen Server wurde auf **{min} Minute(n)** gesetzt.",
|
||||||
|
"setinterval.title": "⏱️ Update-Intervall aktualisiert",
|
||||||
|
"setinterval.footer": "Der nächste Check erfolgt nach Ablauf des neuen Intervalls.",
|
||||||
|
// setchannel
|
||||||
|
"setchannel.notWatched": "Ressource `{id}` wird auf diesem Server nicht beobachtet.",
|
||||||
|
"setchannel.success": "✅ Update-Kanal für **{name}** wurde auf <#{channelID}> geändert.",
|
||||||
|
// setmention
|
||||||
|
"setmention.set": "✅ Bei Updates für **{name}** wird jetzt <@&{roleID}> gepingt.",
|
||||||
|
"setmention.removed": "✅ Mention für **{name}** wurde entfernt.",
|
||||||
|
"setmention.notWatched": "Ressource `{id}` wird auf diesem Server nicht beobachtet.",
|
||||||
|
// setlang
|
||||||
|
"setlang.success": "✅ Sprache wurde auf **Deutsch** gesetzt.",
|
||||||
|
"setlang.title": "🌐 Sprache geändert",
|
||||||
|
// status
|
||||||
|
"status.title": "📡 {botName} – Status",
|
||||||
|
"status.spiget": "🌐 Spiget API",
|
||||||
|
"status.spigot": "🌐 SpigotMC API",
|
||||||
|
"status.discord": "💬 Discord Ping",
|
||||||
|
"status.queue": "⚙️ Queue",
|
||||||
|
"status.nextCheck": "⏱️ Nächster Check",
|
||||||
|
"status.checksTotal": "🔄 Checks gesamt",
|
||||||
|
"status.updatesFound": "🔔 Updates gefunden",
|
||||||
|
"status.updatesPosted": "📤 Updates gepostet",
|
||||||
|
"status.apiErrors": "⚠️ API-Fehler",
|
||||||
|
"status.onlineSince": "🕐 Online seit",
|
||||||
|
"status.jobsDone": "🏓 Jobs verarbeitet",
|
||||||
|
"status.ownerDMs": "📩 Owner-DMs",
|
||||||
|
"status.ok": "✅ Erreichbar",
|
||||||
|
"status.unreachable": "❌ Nicht erreichbar",
|
||||||
|
"status.queueActive": "🔄 Aktiv ({count} ausstehend)",
|
||||||
|
"status.queueWaiting": "⏳ Wartend ({count} Jobs)",
|
||||||
|
"status.queueIdle": "✅ Leerlauf",
|
||||||
|
// top
|
||||||
|
"top.title": "🏆 Top Plugins – {sort}",
|
||||||
|
"top.downloads": "Downloads",
|
||||||
|
"top.rating": "Bewertung",
|
||||||
|
"top.noData": "Keine beobachteten Ressourcen gefunden.",
|
||||||
|
// check
|
||||||
|
"check.title": "🔍 Kompatibilität: {name}",
|
||||||
|
"check.compatible": "✅ Kompatibel",
|
||||||
|
"check.incompatible": "❌ Nicht kompatibel",
|
||||||
|
"check.unknown": "❓ Unbekannt",
|
||||||
|
"check.testedVersions": "Getestete Versionen",
|
||||||
|
"check.supportedVersions": "Unterstützte Versionen",
|
||||||
|
// compare
|
||||||
|
"compare.title": "⚖️ Vergleich",
|
||||||
|
"compare.downloads": "⬇️ Downloads",
|
||||||
|
"compare.rating": "⭐ Bewertung",
|
||||||
|
"compare.version": "📦 Version",
|
||||||
|
"compare.updated": "🕐 Letztes Update",
|
||||||
|
"compare.winner": "🏆 Besser",
|
||||||
|
// ping
|
||||||
|
"ping.measuring": "Ping?",
|
||||||
|
"ping.result": ":ping_pong: Pong! Die Latenz beträgt **{ms}ms**.",
|
||||||
|
},
|
||||||
|
|
||||||
|
en: {
|
||||||
|
// General
|
||||||
|
"error.noArgs": "You didn't provide enough arguments!",
|
||||||
|
"error.noPermission": "You don't have permission to run that command!",
|
||||||
|
"error.onlyOwner": "This command can only be used by the bot owner!",
|
||||||
|
"error.cooldown": "Please wait {seconds} second(s) before using that command again!",
|
||||||
|
"error.nsfw": "This command is marked NSFW. Please run it in an NSFW channel!",
|
||||||
|
"error.notFound": "I couldn't find `{name}` in the command list!",
|
||||||
|
"error.invalidID": "Oops! `{id}` is not a valid resource ID!",
|
||||||
|
"error.noAuthor": "Oops! I couldn't find the author for resource `{id}`.",
|
||||||
|
"error.apiDown": "Could not reach the API. Please try again later.",
|
||||||
|
"error.saveFailed": "An error occurred while saving the data.",
|
||||||
|
"error.timeout": "⏱️ Timed out – command was cancelled.",
|
||||||
|
"general.confirm": "✅ Confirm",
|
||||||
|
"general.cancel": "❌ Cancel",
|
||||||
|
"general.cancelled": "❌ Cancelled.",
|
||||||
|
"general.author": "Author: {name}",
|
||||||
|
// add
|
||||||
|
"add.alreadyWatched": "That resource is already being watched in <#{channelID}>.",
|
||||||
|
"add.invalidChannel": "That channel is invalid. Please mention a channel in this server.",
|
||||||
|
"add.notInGuild": "That channel is not in this server!",
|
||||||
|
"add.versionFail": "Could not fetch the latest version from SpigotMC. Please try again.",
|
||||||
|
"add.confirmTitle": "Watch plugin: {name}",
|
||||||
|
"add.confirmDesc": "{tag}\n\nShould this plugin be watched in <#{channelID}>?",
|
||||||
|
"add.successTitle": "✅ Now watching: {name}",
|
||||||
|
"add.buttonConfirm": "✅ Confirm",
|
||||||
|
"add.channel": "Channel",
|
||||||
|
"add.version": "Version",
|
||||||
|
"add.download": "Download",
|
||||||
|
// remove
|
||||||
|
"remove.noResources": "This server has no watched resources!",
|
||||||
|
"remove.notFound": "Error: Resource `{id}` was not being watched in this server.",
|
||||||
|
"remove.success": "✅ **{name}** (`{id}`) is no longer being watched.",
|
||||||
|
"remove.saveFail": "An error occurred while updating the watch data.",
|
||||||
|
// list
|
||||||
|
"list.noResources": "This server has no watched resources. Use `{prefix}add` to add one.",
|
||||||
|
"list.title": "📋 Watched Plugins ({count})",
|
||||||
|
"list.interval": "⏱️ Update interval: {min} minute(s)",
|
||||||
|
"list.intervalDefault": "⏱️ Update interval: 5 minutes (default)",
|
||||||
|
"list.channel": "Channel",
|
||||||
|
// setinterval
|
||||||
|
"setinterval.invalid": "Invalid value. Please provide a number between {min} and {max} minutes.",
|
||||||
|
"setinterval.success": "The update interval for this server has been set to **{min} minute(s)**.",
|
||||||
|
"setinterval.title": "⏱️ Update interval updated",
|
||||||
|
"setinterval.footer": "The next check will run after the new interval has elapsed.",
|
||||||
|
// setchannel
|
||||||
|
"setchannel.notWatched": "Resource `{id}` is not being watched in this server.",
|
||||||
|
"setchannel.success": "✅ Update channel for **{name}** changed to <#{channelID}>.",
|
||||||
|
// setmention
|
||||||
|
"setmention.set": "✅ <@&{roleID}> will now be pinged for updates to **{name}**.",
|
||||||
|
"setmention.removed": "✅ Mention for **{name}** has been removed.",
|
||||||
|
"setmention.notWatched": "Resource `{id}` is not being watched in this server.",
|
||||||
|
// setlang
|
||||||
|
"setlang.success": "✅ Language has been set to **English**.",
|
||||||
|
"setlang.title": "🌐 Language changed",
|
||||||
|
// status
|
||||||
|
"status.title": "📡 {botName} – Status",
|
||||||
|
"status.spiget": "🌐 Spiget API",
|
||||||
|
"status.spigot": "🌐 SpigotMC API",
|
||||||
|
"status.discord": "💬 Discord Ping",
|
||||||
|
"status.queue": "⚙️ Queue",
|
||||||
|
"status.nextCheck": "⏱️ Next Check",
|
||||||
|
"status.checksTotal": "🔄 Total Checks",
|
||||||
|
"status.updatesFound": "🔔 Updates Found",
|
||||||
|
"status.updatesPosted": "📤 Updates Posted",
|
||||||
|
"status.apiErrors": "⚠️ API Errors",
|
||||||
|
"status.onlineSince": "🕐 Online Since",
|
||||||
|
"status.jobsDone": "🏓 Jobs Processed",
|
||||||
|
"status.ownerDMs": "📩 Owner DMs",
|
||||||
|
"status.ok": "✅ Reachable",
|
||||||
|
"status.unreachable": "❌ Unreachable",
|
||||||
|
"status.queueActive": "🔄 Active ({count} pending)",
|
||||||
|
"status.queueWaiting": "⏳ Waiting ({count} jobs)",
|
||||||
|
"status.queueIdle": "✅ Idle",
|
||||||
|
// top
|
||||||
|
"top.title": "🏆 Top Plugins – {sort}",
|
||||||
|
"top.downloads": "Downloads",
|
||||||
|
"top.rating": "Rating",
|
||||||
|
"top.noData": "No watched resources found.",
|
||||||
|
// check
|
||||||
|
"check.title": "🔍 Compatibility: {name}",
|
||||||
|
"check.compatible": "✅ Compatible",
|
||||||
|
"check.incompatible": "❌ Not compatible",
|
||||||
|
"check.unknown": "❓ Unknown",
|
||||||
|
"check.testedVersions": "Tested Versions",
|
||||||
|
"check.supportedVersions": "Supported Versions",
|
||||||
|
// compare
|
||||||
|
"compare.title": "⚖️ Comparison",
|
||||||
|
"compare.downloads": "⬇️ Downloads",
|
||||||
|
"compare.rating": "⭐ Rating",
|
||||||
|
"compare.version": "📦 Version",
|
||||||
|
"compare.updated": "🕐 Last Updated",
|
||||||
|
"compare.winner": "🏆 Better",
|
||||||
|
// ping
|
||||||
|
"ping.measuring": "Ping?",
|
||||||
|
"ping.result": ":ping_pong: Pong! Latency is **{ms}ms**.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den übersetzten String zurück.
|
||||||
|
* Variablen werden mit {key} ersetzt.
|
||||||
|
* @param {string} lang "de" | "en"
|
||||||
|
* @param {string} key
|
||||||
|
* @param {Object} [vars]
|
||||||
|
*/
|
||||||
|
export function t(lang, key, vars = {}) {
|
||||||
|
const dict = strings[lang] ?? strings.de;
|
||||||
|
let str = dict[key] ?? strings.de[key] ?? key;
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(vars)) {
|
||||||
|
str = str.replaceAll(`{${k}}`, v);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die gespeicherte Serversprache zurück (Standard: "de").
|
||||||
|
* @param {Object} jsonData Serverdaten aus serverdata/
|
||||||
|
*/
|
||||||
|
export function getLang(jsonData) {
|
||||||
|
return jsonData?.lang ?? "de";
|
||||||
|
}
|
||||||
22
util/logger.js
Normal file
22
util/logger.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { createLogger, transports, format } from "winston";
|
||||||
|
|
||||||
|
const logFormat = format.printf(({ level, message, timestamp, stack }) => {
|
||||||
|
return `${timestamp} - ${level} - ${stack || message}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const logger = createLogger({
|
||||||
|
format: format.combine(
|
||||||
|
format.colorize(),
|
||||||
|
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), // ← was "SS" (wrong), now "ss"
|
||||||
|
format.errors({ stack: true }),
|
||||||
|
logFormat
|
||||||
|
),
|
||||||
|
transports: [new transports.Console()],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
debug: logger.debug.bind(logger),
|
||||||
|
info: logger.info.bind(logger),
|
||||||
|
warn: logger.warn.bind(logger),
|
||||||
|
error: logger.error.bind(logger),
|
||||||
|
};
|
||||||
26
util/ownerOnly.js
Normal file
26
util/ownerOnly.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* util/ownerOnly.js
|
||||||
|
* Hilfsfunktion um Owner-only Befehle zu schützen.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { t } from "./i18n.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt true zurück wenn der Nutzer der Bot-Owner ist.
|
||||||
|
* @param {Object} client
|
||||||
|
* @param {Object} ctx message oder slash ctx
|
||||||
|
* @param {string} lang
|
||||||
|
*/
|
||||||
|
export function isOwner(client, ctx) {
|
||||||
|
const userID = ctx.isSlash ? ctx.interaction.user.id : ctx.author.id;
|
||||||
|
return userID === client.config.ownerID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob Owner – wenn nicht, antwortet mit Fehlermeldung und gibt false zurück.
|
||||||
|
*/
|
||||||
|
export async function requireOwner(client, ctx, lang = "de") {
|
||||||
|
if (isOwner(client, ctx)) return true;
|
||||||
|
await ctx.reply({ content: t(lang, "error.onlyOwner"), ephemeral: true });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
58
util/queue.js
Normal file
58
util/queue.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* util/queue.js
|
||||||
|
* Verarbeitet Update-Checks als geordnete Warteschlange –
|
||||||
|
* verhindert parallele API-Anfragen und Rate-Limits.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class UpdateQueue {
|
||||||
|
constructor() {
|
||||||
|
this._queue = [];
|
||||||
|
this._running = false;
|
||||||
|
this.processed = 0; // Gesamt-Jobs seit Start
|
||||||
|
this.nextRunAt = null; // Zeitstempel des nächsten geplanten Runs
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Anzahl der wartenden Jobs */
|
||||||
|
get size() {
|
||||||
|
return this._queue.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ist die Queue gerade aktiv? */
|
||||||
|
get isRunning() {
|
||||||
|
return this._running;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fügt einen asynchronen Job zur Queue hinzu.
|
||||||
|
* @param {() => Promise<void>} job
|
||||||
|
*/
|
||||||
|
enqueue(job) {
|
||||||
|
this._queue.push(job);
|
||||||
|
this._run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Interne Abarbeitungsschleife – läuft bis Queue leer ist */
|
||||||
|
async _run() {
|
||||||
|
if (this._running) return;
|
||||||
|
this._running = true;
|
||||||
|
|
||||||
|
while (this._queue.length > 0) {
|
||||||
|
const job = this._queue.shift();
|
||||||
|
try {
|
||||||
|
await job();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Queue] Job-Fehler:", e.message);
|
||||||
|
}
|
||||||
|
this.processed++;
|
||||||
|
|
||||||
|
// 500ms Pause zwischen Jobs
|
||||||
|
if (this._queue.length > 0) {
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new UpdateQueue();
|
||||||
71
util/stats.js
Normal file
71
util/stats.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* util/stats.js
|
||||||
|
* Verfolgt Bot-Statistiken persistent in ./data/stats.json
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const STATS_FILE = "./data/stats.json";
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
updatesFound: 0, // Erkannte Updates gesamt
|
||||||
|
updatesPosted: 0, // Erfolgreich gesendete Update-Embeds
|
||||||
|
checksRun: 0, // Durchgeführte Update-Checks
|
||||||
|
apiErrors: 0, // Spiget-API Fehler
|
||||||
|
dmsSent: 0, // DMs an den Owner gesendet
|
||||||
|
startedAt: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(STATS_FILE, "utf8"));
|
||||||
|
} catch {
|
||||||
|
return { ...defaults };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(data) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(STATS_FILE), { recursive: true });
|
||||||
|
fs.writeFileSync(STATS_FILE, JSON.stringify(data, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Stats] Fehler beim Speichern:", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Stats {
|
||||||
|
constructor() {
|
||||||
|
this._data = load();
|
||||||
|
if (!this._data.startedAt) {
|
||||||
|
this._data.startedAt = new Date().toISOString();
|
||||||
|
save(this._data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Erhöht einen Zähler um 1 und speichert sofort */
|
||||||
|
increment(key, by = 1) {
|
||||||
|
if (key in this._data) {
|
||||||
|
this._data[key] += by;
|
||||||
|
save(this._data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt den aktuellen Wert eines Zählers zurück */
|
||||||
|
get(key) {
|
||||||
|
return this._data[key] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt alle Statistiken zurück */
|
||||||
|
all() {
|
||||||
|
return { ...this._data };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Setzt die Startzeit auf jetzt */
|
||||||
|
markStart() {
|
||||||
|
this._data.startedAt = new Date().toISOString();
|
||||||
|
save(this._data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Stats();
|
||||||
Reference in New Issue
Block a user