Update from Git Manager GUI
This commit is contained in:
178
commands/add.js
Normal file
178
commands/add.js
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import {
|
||||||
|
EmbedBuilder,
|
||||||
|
PermissionsBitField,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
} from "discord.js";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import { Spiget } from "spiget";
|
||||||
|
import {
|
||||||
|
generateAvatarLink,
|
||||||
|
generateAuthorURL,
|
||||||
|
generateResourceIconURL,
|
||||||
|
} from "../util/helpers.js";
|
||||||
|
|
||||||
|
const spiget = new Spiget("Viper-Network");
|
||||||
|
export default {
|
||||||
|
name: "add",
|
||||||
|
description: "Richtet einen Listener ein, der Plugin-Updates in einem bestimmten Kanal postet",
|
||||||
|
aliases: [],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [PermissionsBitField.Flags.Administrator],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 2,
|
||||||
|
args_usage: "[ressourcen_id] [kanal] Beispiel: vn!add 72678 #ankündigungen",
|
||||||
|
cooldown: 5,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("add")
|
||||||
|
.setDescription("Richtet einen Listener für Plugin-Updates ein")
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt.setName("ressourcen_id").setDescription("Spiget Ressourcen-ID").setRequired(true)
|
||||||
|
)
|
||||||
|
.addChannelOption((opt) =>
|
||||||
|
opt.setName("kanal").setDescription("Kanal für Update-Benachrichtigungen").setRequired(true)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(client, ctx, args) {
|
||||||
|
const resourceArg = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getString("ressourcen_id")
|
||||||
|
: args[0];
|
||||||
|
|
||||||
|
const channel = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getChannel("kanal")
|
||||||
|
: ctx.mentions?.channels?.first();
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
return ctx.reply("Dieser Kanal ist ungültig. Bitte erwähne einen Kanal auf diesem Server.");
|
||||||
|
}
|
||||||
|
if (!ctx.guild.channels.cache.has(channel.id)) {
|
||||||
|
return ctx.reply("Dieser Kanal befindet sich nicht auf diesem Server!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let resource;
|
||||||
|
try {
|
||||||
|
resource = await spiget.getResource(resourceArg);
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply(`Ups! \`${resourceArg}\` ist keine gültige Ressourcen-ID!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let author;
|
||||||
|
try {
|
||||||
|
author = await resource.getAuthor();
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply(`Ups! Der Autor für Ressource \`${resourceArg}\` konnte nicht gefunden werden.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let latestVersion;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://api.spigotmc.org/legacy/update.php?resource=${resourceArg}`);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
latestVersion = (await res.text()).trim();
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply("Die neueste Version konnte nicht von SpigotMC abgerufen werden. Bitte versuche es erneut.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildID = ctx.guild.id;
|
||||||
|
const filePath = `./serverdata/${guildID}.json`;
|
||||||
|
|
||||||
|
// Check existing data & limits
|
||||||
|
let saveData = { watchedResources: [] };
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(filePath, "utf8");
|
||||||
|
saveData = JSON.parse(raw);
|
||||||
|
|
||||||
|
const duplicate = saveData.watchedResources.find((r) => r.resourceID == resource.id);
|
||||||
|
if (duplicate) {
|
||||||
|
return ctx.reply(`Diese Ressource wird bereits in <#${duplicate.channelID}> beobachtet.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
// Datei existiert noch nicht
|
||||||
|
}
|
||||||
|
|
||||||
|
const authorURL = generateAuthorURL(author.name, author.id);
|
||||||
|
const authorAvatarURL = generateAvatarLink(author.id);
|
||||||
|
const resourceIconURL = generateResourceIconURL(resource);
|
||||||
|
const resourceURL = `https://spigotmc.org/resources/.${resourceArg}/`;
|
||||||
|
|
||||||
|
// Build confirmation embed with buttons
|
||||||
|
const confirmEmbed = new EmbedBuilder()
|
||||||
|
.setAuthor({ name: `Autor: ${author.name}`, iconURL: authorAvatarURL, url: authorURL })
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setTitle(`Plugin beobachten: ${resource.name}`)
|
||||||
|
.setDescription(`${resource.tag}\n\nSoll dieses Plugin in <#${channel.id}> beobachtet werden?`)
|
||||||
|
.addFields([
|
||||||
|
{ name: "Kanal", value: `<#${channel.id}>`, inline: true },
|
||||||
|
{ name: "Version", value: latestVersion, inline: true },
|
||||||
|
{ name: "Download", value: resourceURL, inline: false },
|
||||||
|
])
|
||||||
|
.setThumbnail(resourceIconURL)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
const row = new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId("add_confirm")
|
||||||
|
.setLabel("✅ Bestätigen")
|
||||||
|
.setStyle(ButtonStyle.Success),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId("add_cancel")
|
||||||
|
.setLabel("❌ Abbrechen")
|
||||||
|
.setStyle(ButtonStyle.Danger)
|
||||||
|
);
|
||||||
|
|
||||||
|
const confirmMsg = await ctx.reply({ embeds: [confirmEmbed], components: [row], fetchReply: true });
|
||||||
|
|
||||||
|
// Wait for button click (30 seconds)
|
||||||
|
let btnInteraction;
|
||||||
|
try {
|
||||||
|
btnInteraction = await confirmMsg.awaitMessageComponent({
|
||||||
|
filter: (i) => i.user.id === ctx.author.id,
|
||||||
|
time: 30_000,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Timeout
|
||||||
|
const timeoutEmbed = EmbedBuilder.from(confirmEmbed)
|
||||||
|
.setColor("#808080")
|
||||||
|
.setDescription("⏱️ Zeit abgelaufen – Befehl wurde abgebrochen.");
|
||||||
|
return confirmMsg.edit({ embeds: [timeoutEmbed], components: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnInteraction.customId === "add_cancel") {
|
||||||
|
const cancelEmbed = EmbedBuilder.from(confirmEmbed)
|
||||||
|
.setColor("#FF0000")
|
||||||
|
.setDescription("❌ Abgebrochen.");
|
||||||
|
return btnInteraction.update({ embeds: [cancelEmbed], components: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirmed – save data
|
||||||
|
saveData.watchedResources.push({
|
||||||
|
resourceID: resource.id,
|
||||||
|
resourceName: resource.name,
|
||||||
|
channelID: channel.id,
|
||||||
|
lastCheckedVersion: latestVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.mkdir("./serverdata", { recursive: true });
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(saveData, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return btnInteraction.update({ content: "Beim Speichern der Beobachtungsdaten ist ein Fehler aufgetreten.", components: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const successEmbed = EmbedBuilder.from(confirmEmbed)
|
||||||
|
.setTitle(`✅ Wird jetzt beobachtet: ${resource.name}`)
|
||||||
|
.setDescription(resource.tag)
|
||||||
|
.setColor("#00FF00");
|
||||||
|
|
||||||
|
return btnInteraction.update({ embeds: [successEmbed], components: [] });
|
||||||
|
},
|
||||||
|
};
|
||||||
216
commands/addauthor.js
Normal file
216
commands/addauthor.js
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import {
|
||||||
|
EmbedBuilder,
|
||||||
|
PermissionsBitField,
|
||||||
|
SlashCommandBuilder,
|
||||||
|
ActionRowBuilder,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
} from "discord.js";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "addauthor",
|
||||||
|
description: "Fügt alle Plugins eines SpigotMC-Autors zur Beobachtungsliste hinzu",
|
||||||
|
aliases: ["addall"],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [PermissionsBitField.Flags.Administrator],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 2,
|
||||||
|
args_usage: "[author_id] [#kanal] Beispiel: vn!addauthor 618600 #plugin-updates",
|
||||||
|
cooldown: 10,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("addauthor")
|
||||||
|
.setDescription("Fügt alle Plugins eines SpigotMC-Autors zur Beobachtungsliste hinzu")
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt
|
||||||
|
.setName("author_id")
|
||||||
|
.setDescription("SpigotMC Autor-ID (aus der Profil-URL)")
|
||||||
|
.setRequired(true)
|
||||||
|
)
|
||||||
|
.addChannelOption((opt) =>
|
||||||
|
opt
|
||||||
|
.setName("kanal")
|
||||||
|
.setDescription("Kanal für Update-Benachrichtigungen")
|
||||||
|
.setRequired(true)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(client, ctx, args) {
|
||||||
|
const authorID = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getString("author_id")
|
||||||
|
: args[0];
|
||||||
|
|
||||||
|
const channel = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getChannel("kanal")
|
||||||
|
: ctx.mentions?.channels?.first();
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
return ctx.reply("Dieser Kanal ist ungültig. Bitte erwähne einen Kanal auf diesem Server.");
|
||||||
|
}
|
||||||
|
if (!ctx.guild.channels.cache.has(channel.id)) {
|
||||||
|
return ctx.reply("Dieser Kanal befindet sich nicht auf diesem Server!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch author info
|
||||||
|
let authorName = authorID;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://api.spiget.org/v2/authors/${authorID}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
authorName = data.name ?? authorID;
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
|
// Fetch all resources by author
|
||||||
|
let resources;
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://api.spiget.org/v2/authors/${authorID}/resources?size=50&sort=-updateDate`
|
||||||
|
);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
resources = await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply("Ressourcen konnten nicht von der Spiget-API abgerufen werden. Bitte versuche es erneut.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(resources) || resources.length === 0) {
|
||||||
|
return ctx.reply(`Für Autor \`${authorID}\` wurden keine Ressourcen gefunden.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildID = ctx.guild.id;
|
||||||
|
const filePath = `./serverdata/${guildID}.json`;
|
||||||
|
|
||||||
|
// Load existing data
|
||||||
|
let saveData = { watchedResources: [] };
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(filePath, "utf8");
|
||||||
|
saveData = JSON.parse(raw);
|
||||||
|
} catch { /* Datei existiert noch nicht */ }
|
||||||
|
|
||||||
|
// Filter out already watched and check limit
|
||||||
|
const alreadyWatched = resources.filter((r) =>
|
||||||
|
saveData.watchedResources.some((w) => String(w.resourceID) === String(r.id))
|
||||||
|
);
|
||||||
|
const toAdd = resources.filter((r) =>
|
||||||
|
!saveData.watchedResources.some((w) => String(w.resourceID) === String(r.id))
|
||||||
|
);
|
||||||
|
|
||||||
|
const willAdd = toAdd;
|
||||||
|
|
||||||
|
if (willAdd.length === 0 && alreadyWatched.length > 0) {
|
||||||
|
return ctx.reply(
|
||||||
|
`Alle ${alreadyWatched.length} Ressourcen von **${authorName}** werden bereits beobachtet.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (willAdd.length === 0) {
|
||||||
|
return ctx.reply("Es gibt keine neuen Ressourcen zum Hinzufügen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build confirmation embed
|
||||||
|
const resourceList = willAdd
|
||||||
|
.map((r) => `• **${r.name}** (\`${r.id}\`)`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const warnings = [];
|
||||||
|
if (alreadyWatched.length > 0)
|
||||||
|
warnings.push(`⚠️ ${alreadyWatched.length} bereits beobachtet (übersprungen)`);
|
||||||
|
|
||||||
|
const confirmEmbed = new EmbedBuilder()
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setTitle(`📦 Alle Plugins von ${authorName} beobachten`)
|
||||||
|
.setDescription(
|
||||||
|
`Folgende **${willAdd.length}** Ressourcen werden in <#${channel.id}> beobachtet:\n\n${resourceList}` +
|
||||||
|
(warnings.length > 0 ? `\n\n${warnings.join("\n")}` : "")
|
||||||
|
)
|
||||||
|
.setFooter({ text: `Autor-ID: ${authorID} • SpigotMC` })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
const row = new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId("addauthor_confirm")
|
||||||
|
.setLabel(`✅ Alle ${willAdd.length} hinzufügen`)
|
||||||
|
.setStyle(ButtonStyle.Success),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId("addauthor_cancel")
|
||||||
|
.setLabel("❌ Abbrechen")
|
||||||
|
.setStyle(ButtonStyle.Danger)
|
||||||
|
);
|
||||||
|
|
||||||
|
const confirmMsg = await ctx.reply({ embeds: [confirmEmbed], components: [row], fetchReply: true });
|
||||||
|
|
||||||
|
// Wait for button click (30 seconds)
|
||||||
|
let btnInteraction;
|
||||||
|
try {
|
||||||
|
btnInteraction = await confirmMsg.awaitMessageComponent({
|
||||||
|
filter: (i) => i.user.id === ctx.author.id,
|
||||||
|
time: 30_000,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
const timeoutEmbed = EmbedBuilder.from(confirmEmbed)
|
||||||
|
.setColor("#808080")
|
||||||
|
.setDescription("⏱️ Zeit abgelaufen – Befehl wurde abgebrochen.");
|
||||||
|
return confirmMsg.edit({ embeds: [timeoutEmbed], components: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnInteraction.customId === "addauthor_cancel") {
|
||||||
|
const cancelEmbed = EmbedBuilder.from(confirmEmbed)
|
||||||
|
.setColor("#FF0000")
|
||||||
|
.setDescription("❌ Abgebrochen.");
|
||||||
|
return btnInteraction.update({ embeds: [cancelEmbed], components: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer immediately – version fetching can take longer than 3 seconds
|
||||||
|
await btnInteraction.deferUpdate();
|
||||||
|
|
||||||
|
// Fetch latest versions and save
|
||||||
|
const added = [];
|
||||||
|
const failed = [];
|
||||||
|
|
||||||
|
for (const resource of willAdd) {
|
||||||
|
let latestVersion = "unbekannt";
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://api.spigotmc.org/legacy/update.php?resource=${resource.id}`);
|
||||||
|
if (res.ok) latestVersion = (await res.text()).trim();
|
||||||
|
} catch { /* ignore – speichern ohne Version */ }
|
||||||
|
|
||||||
|
saveData.watchedResources.push({
|
||||||
|
resourceID: resource.id,
|
||||||
|
resourceName: resource.name,
|
||||||
|
channelID: channel.id,
|
||||||
|
lastCheckedVersion: latestVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
added.push(`✅ **${resource.name}** (v${latestVersion})`);
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
await new Promise((r) => setTimeout(r, 300));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.mkdir("./serverdata", { recursive: true });
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(saveData, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return btnInteraction.editReply({
|
||||||
|
content: "Beim Speichern der Daten ist ein Fehler aufgetreten.",
|
||||||
|
components: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const successEmbed = new EmbedBuilder()
|
||||||
|
.setColor("#00FF00")
|
||||||
|
.setTitle(`✅ ${added.length} Plugins von ${authorName} werden jetzt beobachtet`)
|
||||||
|
.setDescription(added.join("\n"))
|
||||||
|
.addFields([
|
||||||
|
{ name: "📢 Kanal", value: `<#${channel.id}>`, inline: true },
|
||||||
|
{ name: "📊 Gesamt", value: `${saveData.watchedResources.length} beobachtet`, inline: true },
|
||||||
|
])
|
||||||
|
.setFooter({ text: `Autor-ID: ${authorID}` })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return btnInteraction.editReply({ embeds: [successEmbed], components: [] });
|
||||||
|
},
|
||||||
|
};
|
||||||
90
commands/changelog.js
Normal file
90
commands/changelog.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
|
||||||
|
import { formatText } from "../util/helpers.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "changelog",
|
||||||
|
description: "Zeigt die letzten Update-Beschreibungen eines Plugins an",
|
||||||
|
aliases: ["cl"],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 1,
|
||||||
|
args_usage: "[ressourcen_id] [anzahl] Beispiel: vn!changelog 72678 5",
|
||||||
|
cooldown: 5,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("changelog")
|
||||||
|
.setDescription("Zeigt die letzten Updates eines Plugins")
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt.setName("ressourcen_id").setDescription("Spiget Ressourcen-ID").setRequired(true)
|
||||||
|
)
|
||||||
|
.addIntegerOption((opt) =>
|
||||||
|
opt
|
||||||
|
.setName("anzahl")
|
||||||
|
.setDescription("Anzahl der Updates (1–10, Standard: 3)")
|
||||||
|
.setMinValue(1)
|
||||||
|
.setMaxValue(10)
|
||||||
|
.setRequired(false)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(client, ctx, args) {
|
||||||
|
const resourceID = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getString("ressourcen_id")
|
||||||
|
: args[0];
|
||||||
|
|
||||||
|
let count = ctx.isSlash
|
||||||
|
? (ctx.interaction.options.getInteger("anzahl") ?? 3)
|
||||||
|
: (parseInt(args[1], 10) || 3);
|
||||||
|
|
||||||
|
count = Math.min(Math.max(count, 1), 10);
|
||||||
|
|
||||||
|
// Fetch resource name
|
||||||
|
let resourceName = resourceID;
|
||||||
|
try {
|
||||||
|
const resInfo = await fetch(`https://api.spiget.org/v2/resources/${resourceID}`);
|
||||||
|
if (resInfo.ok) {
|
||||||
|
const data = await resInfo.json();
|
||||||
|
resourceName = data.name ?? resourceID;
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
|
// Fetch updates list
|
||||||
|
let updates;
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://api.spiget.org/v2/resources/${resourceID}/updates?size=${count}&sort=-date`
|
||||||
|
);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
updates = await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply("Update-Verlauf konnte nicht abgerufen werden. Bitte versuche es erneut.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(updates) || updates.length === 0) {
|
||||||
|
return ctx.reply(`Für Ressource \`${resourceID}\` wurden keine Updates gefunden.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = updates.map((u, i) => {
|
||||||
|
const date = new Date(u.date * 1000).toLocaleDateString("de-DE");
|
||||||
|
let desc = formatText(Buffer.from(u.description, "base64").toString("utf8"));
|
||||||
|
if (desc.length > 300) desc = desc.substring(0, 297) + "...";
|
||||||
|
return {
|
||||||
|
name: `${i + 1}. ${u.title ?? "Update"} – v${u.likes ?? ""} (${date})`,
|
||||||
|
value: desc || "Keine Beschreibung.",
|
||||||
|
inline: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const changelogEmbed = new EmbedBuilder()
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setTitle(`📋 Changelog: ${resourceName}`)
|
||||||
|
.setDescription(`Die letzten **${updates.length}** Updates:`)
|
||||||
|
.addFields(fields)
|
||||||
|
.setFooter({ text: `Ressource ID: ${resourceID}` })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [changelogEmbed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
167
commands/check.js
Normal file
167
commands/check.js
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
|
||||||
|
import fs from "fs";
|
||||||
|
import { t, getLang } from "../util/i18n.js";
|
||||||
|
import {
|
||||||
|
generateAvatarLink,
|
||||||
|
generateAuthorURL,
|
||||||
|
generateResourceIconURL,
|
||||||
|
} from "../util/helpers.js";
|
||||||
|
import { Spiget } from "spiget";
|
||||||
|
|
||||||
|
const spiget = new Spiget("Viper-Network");
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "check",
|
||||||
|
description: "Prüft ob ein Plugin mit einer Minecraft-Version kompatibel ist",
|
||||||
|
aliases: [],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 1,
|
||||||
|
args_usage: "[ressourcen_id] [mc-version] Beispiel: vn!check 72678 1.21",
|
||||||
|
cooldown: 5,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("check")
|
||||||
|
.setDescription("Checks if a plugin is compatible with a Minecraft version")
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt.setName("ressourcen_id").setDescription("Spiget Ressourcen-ID").setRequired(true)
|
||||||
|
)
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt
|
||||||
|
.setName("mc_version")
|
||||||
|
.setDescription("Minecraft-Version (z.B. 1.21) – leer = aktuellste prüfen")
|
||||||
|
.setRequired(false)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(client, ctx, args) {
|
||||||
|
const guildID = ctx.guild.id;
|
||||||
|
const lang = loadLang(guildID);
|
||||||
|
const resourceID = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getString("ressourcen_id")
|
||||||
|
: args[0];
|
||||||
|
const mcVersion = ctx.isSlash
|
||||||
|
? (ctx.interaction.options.getString("mc_version") ?? null)
|
||||||
|
: (args[1] ?? null);
|
||||||
|
|
||||||
|
// Fetch resource details from Spiget
|
||||||
|
let resource, author;
|
||||||
|
try {
|
||||||
|
resource = await spiget.getResource(resourceID);
|
||||||
|
author = await resource.getAuthor();
|
||||||
|
} catch {
|
||||||
|
return ctx.reply(t(lang, "error.invalidID", { id: resourceID }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch full resource data for tested versions
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://api.spiget.org/v2/resources/${resourceID}`);
|
||||||
|
if (!res.ok) throw new Error();
|
||||||
|
data = await res.json();
|
||||||
|
} catch {
|
||||||
|
return ctx.reply(t(lang, "error.apiDown"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// testedVersions is a string like "1.8 - 1.21" or array
|
||||||
|
const testedRaw = data.testedVersions ?? [];
|
||||||
|
const tested = Array.isArray(testedRaw)
|
||||||
|
? testedRaw
|
||||||
|
: [testedRaw];
|
||||||
|
|
||||||
|
// Supported versions from tags/description often contains "1.x"
|
||||||
|
const supportedVersions = tested.length > 0
|
||||||
|
? tested.join(", ")
|
||||||
|
: "Keine Angabe";
|
||||||
|
|
||||||
|
// Determine compatibility for requested version
|
||||||
|
let compatStatus;
|
||||||
|
let compatColor;
|
||||||
|
|
||||||
|
if (mcVersion) {
|
||||||
|
const matches = tested.some((v) => {
|
||||||
|
// Handle ranges like "1.8-1.21" or "1.8 - 1.21"
|
||||||
|
const rangeMatch = v.match(/(\d+\.\d+)\s*[-–]\s*(\d+\.\d+)/);
|
||||||
|
if (rangeMatch) {
|
||||||
|
const [, from, to] = rangeMatch;
|
||||||
|
return compareVersions(mcVersion, from) >= 0 &&
|
||||||
|
compareVersions(mcVersion, to) <= 0;
|
||||||
|
}
|
||||||
|
// Exact match or starts-with
|
||||||
|
return v.trim().startsWith(mcVersion.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tested.length === 0) {
|
||||||
|
compatStatus = t(lang, "check.unknown");
|
||||||
|
compatColor = "#FFA500";
|
||||||
|
} else if (matches) {
|
||||||
|
compatStatus = t(lang, "check.compatible");
|
||||||
|
compatColor = "#00FF00";
|
||||||
|
} else {
|
||||||
|
compatStatus = t(lang, "check.incompatible");
|
||||||
|
compatColor = "#FF0000";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
compatStatus = tested.length > 0
|
||||||
|
? `${t(lang, "check.compatible")} (${supportedVersions})`
|
||||||
|
: t(lang, "check.unknown");
|
||||||
|
compatColor = tested.length > 0 ? "#00FF00" : "#FFA500";
|
||||||
|
}
|
||||||
|
|
||||||
|
const authorURL = generateAuthorURL(author.name, author.id);
|
||||||
|
const authorAvatarURL = generateAvatarLink(author.id);
|
||||||
|
const resourceIconURL = generateResourceIconURL(resource);
|
||||||
|
const resourceURL = `https://spigotmc.org/resources/.${resourceID}/`;
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{
|
||||||
|
name: t(lang, "check.supportedVersions"),
|
||||||
|
value: supportedVersions,
|
||||||
|
inline: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (mcVersion) {
|
||||||
|
fields.unshift({
|
||||||
|
name: `Minecraft ${mcVersion}`,
|
||||||
|
value: compatStatus,
|
||||||
|
inline: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.push(
|
||||||
|
{ name: "📦 Version", value: data.version?.id ?? "?", inline: true },
|
||||||
|
{ name: "⬇️ Download", value: resourceURL, inline: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setAuthor({ name: t(lang, "general.author", { name: author.name }), iconURL: authorAvatarURL, url: authorURL })
|
||||||
|
.setColor(compatColor)
|
||||||
|
.setTitle(t(lang, "check.title", { name: resource.name }))
|
||||||
|
.setDescription(resource.tag)
|
||||||
|
.addFields(fields)
|
||||||
|
.setThumbnail(resourceIconURL)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [embed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Vergleicht zwei Versions-Strings (z.B. "1.8" und "1.21") */
|
||||||
|
function compareVersions(a, b) {
|
||||||
|
const pa = a.split(".").map(Number);
|
||||||
|
const pb = b.split(".").map(Number);
|
||||||
|
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||||
|
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
||||||
|
if (diff !== 0) return diff;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLang(guildID) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(fs.readFileSync(`./serverdata/${guildID}.json`, "utf8"));
|
||||||
|
return getLang(data);
|
||||||
|
} catch { return "de"; }
|
||||||
|
}
|
||||||
130
commands/compare.js
Normal file
130
commands/compare.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
|
||||||
|
import fs from "fs";
|
||||||
|
import { t, getLang } from "../util/i18n.js";
|
||||||
|
import { generateResourceIconURL } from "../util/helpers.js";
|
||||||
|
import { Spiget } from "spiget";
|
||||||
|
|
||||||
|
const spiget = new Spiget("Viper-Network");
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "compare",
|
||||||
|
description: "Vergleicht zwei Plugins nebeneinander",
|
||||||
|
aliases: ["cmp"],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 2,
|
||||||
|
args_usage: "[id1] [id2] Beispiel: vn!compare 72678 34315",
|
||||||
|
cooldown: 10,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("compare")
|
||||||
|
.setDescription("Compares two plugins side by side")
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt.setName("id1").setDescription("Erste Ressourcen-ID").setRequired(true)
|
||||||
|
)
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt.setName("id2").setDescription("Zweite Ressourcen-ID").setRequired(true)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(client, ctx, args) {
|
||||||
|
const lang = loadLang(ctx.guild.id);
|
||||||
|
const id1 = ctx.isSlash ? ctx.interaction.options.getString("id1") : args[0];
|
||||||
|
const id2 = ctx.isSlash ? ctx.interaction.options.getString("id2") : args[1];
|
||||||
|
|
||||||
|
// Fetch both resources in parallel
|
||||||
|
let [dataA, dataB] = await Promise.all([
|
||||||
|
fetchResource(id1),
|
||||||
|
fetchResource(id2),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!dataA) return ctx.reply(t(lang, "error.invalidID", { id: id1 }));
|
||||||
|
if (!dataB) return ctx.reply(t(lang, "error.invalidID", { id: id2 }));
|
||||||
|
|
||||||
|
// Fetch latest versions
|
||||||
|
const [verA, verB] = await Promise.all([
|
||||||
|
fetchVersion(id1),
|
||||||
|
fetchVersion(id2),
|
||||||
|
]);
|
||||||
|
|
||||||
|
dataA.latestVersion = verA;
|
||||||
|
dataB.latestVersion = verB;
|
||||||
|
|
||||||
|
// Helper: format a stat with winner indicator
|
||||||
|
const win = (valA, valB, higherIsBetter = true) => {
|
||||||
|
if (valA === valB) return ["–", "–"];
|
||||||
|
const aWins = higherIsBetter ? valA > valB : valA < valB;
|
||||||
|
return aWins ? ["🏆", ""] : ["", "🏆"];
|
||||||
|
};
|
||||||
|
|
||||||
|
const [dlWinA, dlWinB] = win(dataA.downloads, dataB.downloads);
|
||||||
|
const [rtWinA, rtWinB] = win(dataA.rating?.average ?? 0, dataB.rating?.average ?? 0);
|
||||||
|
|
||||||
|
const dateA = dataA.updateDate ? new Date(dataA.updateDate * 1000).toLocaleDateString("de-DE") : "?";
|
||||||
|
const dateB = dataB.updateDate ? new Date(dataB.updateDate * 1000).toLocaleDateString("de-DE") : "?";
|
||||||
|
const [dtWinA, dtWinB] = win(dataA.updateDate ?? 0, dataB.updateDate ?? 0);
|
||||||
|
|
||||||
|
const dlA = (dataA.downloads ?? 0).toLocaleString("de-DE");
|
||||||
|
const dlB = (dataB.downloads ?? 0).toLocaleString("de-DE");
|
||||||
|
const rtA = dataA.rating?.average?.toFixed(1) ?? "–";
|
||||||
|
const rtB = dataB.rating?.average?.toFixed(1) ?? "–";
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setTitle(t(lang, "compare.title"))
|
||||||
|
.addFields([
|
||||||
|
// Header row
|
||||||
|
{ name: "📦 Plugin", value: `**${dataA.name}**`, inline: true },
|
||||||
|
{ name: "\u200b", value: "**vs**", inline: true },
|
||||||
|
{ name: "\u200b", value: `**${dataB.name}**`, inline: true },
|
||||||
|
// Downloads
|
||||||
|
{ name: t(lang, "compare.downloads"), value: `${dlWinA} ${dlA}`, inline: true },
|
||||||
|
{ name: "\u200b", value: "⬇️", inline: true },
|
||||||
|
{ name: "\u200b", value: `${dlWinB} ${dlB}`, inline: true },
|
||||||
|
// Rating
|
||||||
|
{ name: t(lang, "compare.rating"), value: `${rtWinA} ⭐ ${rtA}`, inline: true },
|
||||||
|
{ name: "\u200b", value: "⭐", inline: true },
|
||||||
|
{ name: "\u200b", value: `${rtWinB} ⭐ ${rtB}`, inline: true },
|
||||||
|
// Version
|
||||||
|
{ name: t(lang, "compare.version"), value: `v${verA ?? "?"}`, inline: true },
|
||||||
|
{ name: "\u200b", value: "📦", inline: true },
|
||||||
|
{ name: "\u200b", value: `v${verB ?? "?"}`, inline: true },
|
||||||
|
// Last update
|
||||||
|
{ name: t(lang, "compare.updated"), value: `${dtWinA} ${dateA}`, inline: true },
|
||||||
|
{ name: "\u200b", value: "🕐", inline: true },
|
||||||
|
{ name: "\u200b", value: `${dtWinB} ${dateB}`, inline: true },
|
||||||
|
// Links
|
||||||
|
{ name: "🔗 Link", value: `[SpigotMC](https://spigotmc.org/resources/.${id1}/)`, inline: true },
|
||||||
|
{ name: "\u200b", value: "\u200b", inline: true },
|
||||||
|
{ name: "\u200b", value: `[SpigotMC](https://spigotmc.org/resources/.${id2}/)`, inline: true },
|
||||||
|
])
|
||||||
|
.setFooter({ text: `IDs: ${id1} vs ${id2}` })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [embed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchResource(id) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://api.spiget.org/v2/resources/${id}`);
|
||||||
|
if (!res.ok) return null;
|
||||||
|
return await res.json();
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchVersion(id) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://api.spigotmc.org/legacy/update.php?resource=${id}`);
|
||||||
|
if (!res.ok) return null;
|
||||||
|
return (await res.text()).trim();
|
||||||
|
} catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLang(guildID) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(fs.readFileSync(`./serverdata/${guildID}.json`, "utf8"));
|
||||||
|
return getLang(data);
|
||||||
|
} catch { return "de"; }
|
||||||
|
}
|
||||||
73
commands/help.js
Normal file
73
commands/help.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "help",
|
||||||
|
description: "Alle Befehle des Bots und Informationen zur Nutzung!",
|
||||||
|
aliases: ["commands", "cmds"],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 0,
|
||||||
|
args_usage: "[befehlsname]",
|
||||||
|
cooldown: 0,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("help")
|
||||||
|
.setDescription("Zeigt alle Befehle des Bots an")
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt.setName("befehl").setDescription("Name eines bestimmten Befehls").setRequired(false)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(client, ctx, args) {
|
||||||
|
const helpEmbed = new EmbedBuilder();
|
||||||
|
const requestedName = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getString("befehl")
|
||||||
|
: args?.[0];
|
||||||
|
|
||||||
|
if (!requestedName) {
|
||||||
|
const commandList = `\`${Array.from(client.commands.keys()).join("` `")}\``;
|
||||||
|
const { me } = ctx.guild.members;
|
||||||
|
const displayName = me.displayName || client.bot.user.username;
|
||||||
|
|
||||||
|
helpEmbed
|
||||||
|
.setAuthor({ name: client.config.authorName, url: client.config.authorGithub })
|
||||||
|
.setColor(me.displayHexColor)
|
||||||
|
.setTitle(`${displayName} Hilfe`)
|
||||||
|
.setDescription(commandList)
|
||||||
|
.setFooter({
|
||||||
|
text: `Nutze \`${client.config.prefix}${this.name} ${this.args_usage}\` für mehr Details!`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const requestedCommand = requestedName.replace(client.config.prefix, "");
|
||||||
|
const command =
|
||||||
|
client.commands.get(requestedCommand) ||
|
||||||
|
client.commands.get(client.aliases.get(requestedCommand));
|
||||||
|
|
||||||
|
if (
|
||||||
|
!command ||
|
||||||
|
(!command.guild.includes("all") && !command.guild.includes(ctx.guild.id))
|
||||||
|
)
|
||||||
|
return ctx.reply(`Ich konnte \`${requestedCommand}\` nicht in der Befehlsliste finden!`);
|
||||||
|
|
||||||
|
helpEmbed
|
||||||
|
.setTitle(`Befehl: ${command.name}`)
|
||||||
|
.setDescription(command.description)
|
||||||
|
.addFields([
|
||||||
|
{ name: "Nutzung", value: `\`${client.config.prefix}${command.name} ${command.args_usage}\`` },
|
||||||
|
{ name: "NSFW", value: `${command.nsfw}`, inline: true },
|
||||||
|
{ name: "Abklingzeit", value: `${command.cooldown} Sek.`, inline: true },
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (command.aliases?.length > 0)
|
||||||
|
helpEmbed.addFields([{ name: "Aliasse", value: `\`${command.aliases.join("` `")}\`` }]);
|
||||||
|
|
||||||
|
if (command.user_permissions?.length > 0)
|
||||||
|
helpEmbed.addFields([{ name: "Berechtigungen", value: `\`${command.user_permissions.join("` `")}\`` }]);
|
||||||
|
|
||||||
|
helpEmbed.setFooter({ text: `Nutze \`${client.config.prefix}${this.name}\` um alle Befehle zu sehen!` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [helpEmbed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
68
commands/info.js
Normal file
68
commands/info.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "info",
|
||||||
|
description: "Informationen über den Bot",
|
||||||
|
aliases: ["botinfo"],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 0,
|
||||||
|
args_usage: "",
|
||||||
|
cooldown: 5,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("info")
|
||||||
|
.setDescription("Zeigt Informationen über den Bot an"),
|
||||||
|
|
||||||
|
async execute(client, ctx) {
|
||||||
|
let totalMembers = 0;
|
||||||
|
client.bot.guilds.cache.forEach((guild) => { totalMembers += guild.memberCount; });
|
||||||
|
|
||||||
|
const uptime = getUptime(client.bot);
|
||||||
|
const memUsed = (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1);
|
||||||
|
const nodeVer = process.version;
|
||||||
|
|
||||||
|
const infoEmbed = new EmbedBuilder()
|
||||||
|
.setAuthor({
|
||||||
|
name: client.config.authorName,
|
||||||
|
url: client.config.authorGithub,
|
||||||
|
iconURL: client.bot.user.displayAvatarURL(),
|
||||||
|
})
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setTitle(`ℹ️ ${client.bot.user.username} – Bot-Informationen`)
|
||||||
|
.setThumbnail(client.bot.user.displayAvatarURL())
|
||||||
|
.addFields([
|
||||||
|
{ name: "🤖 Bot-Name", value: client.config.botName, inline: true },
|
||||||
|
{ name: "👨💻 Entwickler", value: `[${client.config.authorName}](${client.config.authorGithub})`, inline: true },
|
||||||
|
{ name: "📦 Version", value: client.packageData.version, inline: true },
|
||||||
|
{ name: "⏱️ Laufzeit", value: uptime, inline: true },
|
||||||
|
{ name: "🌐 Server", value: `${client.bot.guilds.cache.size}`, inline: true },
|
||||||
|
{ name: "👥 Nutzer", value: `${totalMembers.toLocaleString("de-DE")}`, inline: true },
|
||||||
|
{ name: "💬 Kanäle", value: `${client.bot.channels.cache.size}`, inline: true },
|
||||||
|
{ name: "🔧 Befehle", value: `${client.commands.size}`, inline: true },
|
||||||
|
{ name: "💾 Speicher", value: `${memUsed} MB`, inline: true },
|
||||||
|
{ name: "🟢 Node.js", value: nodeVer, inline: true },
|
||||||
|
{ name: "📚 discord.js", value: `v14`, inline: true },
|
||||||
|
{ name: "🔗 Einladen", value: `[Bot einladen](https://discord.com/oauth2/authorize?client_id=${client.config.inviteClientID}&scope=bot&permissions=8)`, inline: true },
|
||||||
|
])
|
||||||
|
.setFooter({ text: `${client.config.botName} • SpigotMC Plugin Update Tracker` })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [infoEmbed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getUptime(bot) {
|
||||||
|
let total = bot.uptime / 1000;
|
||||||
|
const days = Math.floor(total / 86400); total %= 86400;
|
||||||
|
const hours = Math.floor(total / 3600); total %= 3600;
|
||||||
|
const minutes = Math.floor(total / 60);
|
||||||
|
const seconds = Math.floor(total % 60);
|
||||||
|
|
||||||
|
if (days > 0) return `${days}T ${hours}Std ${minutes}Min ${seconds}Sek`;
|
||||||
|
if (hours > 0) return `${hours}Std ${minutes}Min ${seconds}Sek`;
|
||||||
|
if (minutes > 0) return `${minutes}Min ${seconds}Sek`;
|
||||||
|
return `${seconds}Sek`;
|
||||||
|
}
|
||||||
30
commands/invite.js
Normal file
30
commands/invite.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "invite",
|
||||||
|
description: "Der Einladungslink des Bots!",
|
||||||
|
aliases: [],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 0,
|
||||||
|
args_usage: "",
|
||||||
|
cooldown: 5,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("invite")
|
||||||
|
.setDescription("Gibt den Einladungslink des Bots aus"),
|
||||||
|
|
||||||
|
async execute(client, ctx) {
|
||||||
|
const inviteURL = `https://discord.com/oauth2/authorize?client_id=${client.config.inviteClientID}&scope=bot&permissions=8`;
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setURL(inviteURL)
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setTitle("🔗 Bot-Einladungslink")
|
||||||
|
.setDescription("Klicke auf diesen Link, um den Bot zu deinem Discord-Server hinzuzufügen!");
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [embed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
60
commands/list.js
Normal file
60
commands/list.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { EmbedBuilder, PermissionsBitField, SlashCommandBuilder } from "discord.js";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "list",
|
||||||
|
description: "Listet alle Plugin-Update-Listener für diesen Server auf",
|
||||||
|
aliases: [],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [PermissionsBitField.Flags.Administrator],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 0,
|
||||||
|
args_usage: "",
|
||||||
|
cooldown: 5,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("list")
|
||||||
|
.setDescription("Listet alle beobachteten Plugins dieses Servers auf"),
|
||||||
|
|
||||||
|
async execute(client, ctx) {
|
||||||
|
const guildID = ctx.guild.id;
|
||||||
|
const filePath = `./serverdata/${guildID}.json`;
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(filePath, "utf8");
|
||||||
|
data = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return ctx.reply(
|
||||||
|
`Dieser Server hat keine beobachteten Ressourcen. Nutze \`${client.config.prefix}add\` um eine hinzuzufügen.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.watchedResources || data.watchedResources.length === 0) {
|
||||||
|
return ctx.reply(
|
||||||
|
`Dieser Server hat keine beobachteten Ressourcen. Nutze \`${client.config.prefix}add\` um eine hinzuzufügen.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = data.watchedResources
|
||||||
|
.map((r) => {
|
||||||
|
const name = r.resourceName ? `**${r.resourceName}**` : `ID: \`${r.resourceID}\``;
|
||||||
|
const version = r.lastCheckedVersion ? ` • v${r.lastCheckedVersion}` : "";
|
||||||
|
return `${name}${version}\n└ Kanal: <#${r.channelID}> • ID: \`${r.resourceID}\``;
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
const intervalInfo = data.updateInterval
|
||||||
|
? `⏱️ Update-Intervall: ${data.updateInterval} Minute(n)`
|
||||||
|
: "⏱️ Update-Intervall: 5 Minuten (Standard)";
|
||||||
|
|
||||||
|
const listEmbed = new EmbedBuilder()
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setTitle(`📋 Beobachtete Plugins (${data.watchedResources.length})`)
|
||||||
|
.setDescription(list)
|
||||||
|
.setFooter({ text: intervalInfo });
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [listEmbed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
42
commands/ping.js
Normal file
42
commands/ping.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { SlashCommandBuilder } from "discord.js";
|
||||||
|
import { requireOwner } from "../util/ownerOnly.js";
|
||||||
|
import { t, getLang } from "../util/i18n.js";
|
||||||
|
import fsSync from "fs";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ping",
|
||||||
|
description: "Der Ping des Bots!",
|
||||||
|
aliases: ["pingpong"],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 0,
|
||||||
|
args_usage: "",
|
||||||
|
cooldown: 5,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("ping")
|
||||||
|
.setDescription("Zeigt die aktuelle Latenz des Bots"),
|
||||||
|
|
||||||
|
async execute(client, ctx) {
|
||||||
|
const lang = loadLang(ctx.guild.id);
|
||||||
|
if (!await requireOwner(client, ctx, lang)) return;
|
||||||
|
if (ctx.isSlash) {
|
||||||
|
await ctx.interaction.reply("Ping?");
|
||||||
|
const reply = await ctx.interaction.fetchReply();
|
||||||
|
const latency = reply.createdTimestamp - ctx.interaction.createdTimestamp;
|
||||||
|
return ctx.interaction.editReply(`:ping_pong: Pong! Die Latenz beträgt **${latency}ms**.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const m = await ctx.channel.send("Ping?");
|
||||||
|
m.edit(`:ping_pong: Pong! Die Latenz beträgt **${m.createdTimestamp - ctx.createdTimestamp}ms**.`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadLang(guildID) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(fsSync.readFileSync(`./serverdata/${guildID}.json`, "utf8"));
|
||||||
|
return getLang(data);
|
||||||
|
} catch { return "de"; }
|
||||||
|
}
|
||||||
81
commands/plugin.js
Normal file
81
commands/plugin.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
|
||||||
|
import { Spiget } from "spiget";
|
||||||
|
import {
|
||||||
|
generateAvatarLink,
|
||||||
|
generateAuthorURL,
|
||||||
|
generateResourceIconURL,
|
||||||
|
generateResourceURL,
|
||||||
|
} from "../util/helpers.js";
|
||||||
|
|
||||||
|
const spiget = new Spiget("Viper-Network");
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "plugin",
|
||||||
|
description: "Ruft ein Plugin anhand seiner Ressourcen-ID ab und gibt Details zurück",
|
||||||
|
aliases: [],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 1,
|
||||||
|
args_usage: "[ressourcen_id]",
|
||||||
|
cooldown: 5,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("plugin")
|
||||||
|
.setDescription("Zeigt Details zu einem Plugin an")
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt.setName("ressourcen_id").setDescription("Spiget Ressourcen-ID").setRequired(true)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(client, ctx, args) {
|
||||||
|
const resourceID = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getString("ressourcen_id")
|
||||||
|
: args[0];
|
||||||
|
|
||||||
|
let resource;
|
||||||
|
try {
|
||||||
|
resource = await spiget.getResource(resourceID);
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply(`Ups! \`${resourceID}\` ist keine gültige Ressourcen-ID!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let author;
|
||||||
|
try {
|
||||||
|
author = await resource.getAuthor();
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply(`Ups! Der Autor für Ressource \`${resourceID}\` konnte nicht gefunden werden.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let latestVersion;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://api.spigotmc.org/legacy/update.php?resource=${resourceID}`);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
latestVersion = (await res.text()).trim();
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply("Versionsinformationen konnten nicht abgerufen werden. Bitte versuche es erneut.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const authorURL = generateAuthorURL(author.name, author.id);
|
||||||
|
const authorAvatarURL = generateAvatarLink(author.id);
|
||||||
|
const resourceIconURL = generateResourceIconURL(resource);
|
||||||
|
const resourceURL = generateResourceURL(resourceID);
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setAuthor({ name: `Autor: ${author.name}`, iconURL: authorAvatarURL, url: authorURL })
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setTitle(resource.name)
|
||||||
|
.setDescription(resource.tag)
|
||||||
|
.addFields([
|
||||||
|
{ name: "📦 Version", value: latestVersion, inline: true },
|
||||||
|
{ name: "⬇️ Download", value: resourceURL, inline: true },
|
||||||
|
])
|
||||||
|
.setThumbnail(resourceIconURL)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [embed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
63
commands/remove.js
Normal file
63
commands/remove.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { PermissionsBitField, SlashCommandBuilder } from "discord.js";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "remove",
|
||||||
|
description: "Stoppt den Listener für Plugin-Updates",
|
||||||
|
aliases: [],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [PermissionsBitField.Flags.Administrator],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 1,
|
||||||
|
args_usage: "[ressourcen_id]",
|
||||||
|
cooldown: 5,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("remove")
|
||||||
|
.setDescription("Entfernt einen Plugin-Update-Listener")
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt.setName("ressourcen_id").setDescription("Spiget Ressourcen-ID").setRequired(true)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(client, ctx, args) {
|
||||||
|
const resourceID = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getString("ressourcen_id")
|
||||||
|
: args[0];
|
||||||
|
|
||||||
|
const guildID = ctx.guild.id;
|
||||||
|
const filePath = `./serverdata/${guildID}.json`;
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(filePath, "utf8");
|
||||||
|
data = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return ctx.reply("Dieser Server hat keine beobachteten Ressourcen!");
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = data.watchedResources.findIndex(
|
||||||
|
(r) => String(r.resourceID) === String(resourceID)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return ctx.reply(`Fehler: Ressource \`${resourceID}\` wurde auf diesem Server nicht beobachtet.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedName = data.watchedResources[index].resourceName ?? resourceID;
|
||||||
|
data.watchedResources.splice(index, 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (data.watchedResources.length === 0) {
|
||||||
|
await fs.unlink(filePath);
|
||||||
|
} else {
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply("Beim Aktualisieren der Beobachtungsdaten ist ein Fehler aufgetreten.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.reply(`✅ **${removedName}** (\`${resourceID}\`) wird nicht mehr beobachtet.`);
|
||||||
|
},
|
||||||
|
};
|
||||||
117
commands/search.js
Normal file
117
commands/search.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
|
||||||
|
import { Spiget } from "spiget";
|
||||||
|
|
||||||
|
const spiget = new Spiget("Viper-Network");
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "search",
|
||||||
|
description: "Durchsucht die Spiget-API nach einer Ressource und gibt die Top-Ergebnisse zurück",
|
||||||
|
aliases: [],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 1,
|
||||||
|
args_usage: `[ressourcenname] Beispiel: vn!search Vault\nTipp: -n [anzahl] für Suchmenge (Standard: 100), -r für Sortierung nach Bewertung\nBeispiel: vn!search a -n 5000 -r`,
|
||||||
|
cooldown: 3,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("search")
|
||||||
|
.setDescription("Sucht nach einem Plugin auf SpigotMC")
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt.setName("name").setDescription("Name des Plugins").setRequired(true)
|
||||||
|
)
|
||||||
|
.addIntegerOption((opt) =>
|
||||||
|
opt.setName("anzahl").setDescription("Suchmenge (Standard: 100)").setRequired(false)
|
||||||
|
)
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt
|
||||||
|
.setName("sortierung")
|
||||||
|
.setDescription("Sortierung der Ergebnisse")
|
||||||
|
.setRequired(false)
|
||||||
|
.addChoices(
|
||||||
|
{ name: "Downloads", value: "downloads" },
|
||||||
|
{ name: "Bewertung", value: "rating" }
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(client, ctx, args) {
|
||||||
|
let search, size, sortBy;
|
||||||
|
|
||||||
|
if (ctx.isSlash) {
|
||||||
|
search = ctx.interaction.options.getString("name");
|
||||||
|
size = ctx.interaction.options.getInteger("anzahl") ?? 100;
|
||||||
|
sortBy = ctx.interaction.options.getString("sortierung") ?? "downloads";
|
||||||
|
} else {
|
||||||
|
size = 100;
|
||||||
|
sortBy = "downloads";
|
||||||
|
|
||||||
|
// Parse -n flag
|
||||||
|
const nIdx = args.indexOf("-n");
|
||||||
|
if (nIdx !== -1 && args[nIdx + 1]) {
|
||||||
|
const parsed = parseInt(args[nIdx + 1], 10);
|
||||||
|
if (!isNaN(parsed) && parsed > 0) size = parsed;
|
||||||
|
args.splice(nIdx, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse -r flag (sort by rating)
|
||||||
|
const rIdx = args.indexOf("-r");
|
||||||
|
if (rIdx !== -1) {
|
||||||
|
sortBy = "rating";
|
||||||
|
args.splice(rIdx, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length === 0) {
|
||||||
|
return ctx.reply("Bitte gib einen Suchbegriff an!");
|
||||||
|
}
|
||||||
|
search = args.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiURL = `https://api.spiget.org/v2/search/resources/${encodeURIComponent(search)}?size=${size}`;
|
||||||
|
|
||||||
|
let results;
|
||||||
|
try {
|
||||||
|
const res = await fetch(apiURL);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
results = await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply("Die Spiget-API konnte nicht erreicht werden. Bitte versuche es später erneut.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(results) || results.length === 0) {
|
||||||
|
return ctx.reply(`Keine Ergebnisse für \`${search}\` gefunden.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by downloads or rating
|
||||||
|
const topResults = results
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (sortBy === "rating") {
|
||||||
|
const ratingA = a.rating?.average ?? 0;
|
||||||
|
const ratingB = b.rating?.average ?? 0;
|
||||||
|
return ratingB - ratingA;
|
||||||
|
}
|
||||||
|
return b.downloads - a.downloads;
|
||||||
|
})
|
||||||
|
.slice(0, 5);
|
||||||
|
|
||||||
|
const list = topResults
|
||||||
|
.map(({ id, name, downloads, rating }) => {
|
||||||
|
const stars = rating?.average ? `⭐ ${rating.average.toFixed(1)}` : "⭐ k.A.";
|
||||||
|
return `**${name}**\n⬇️ *${downloads.toLocaleString("de-DE")}* ${stars} 🆔 ${id}`;
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
const sortLabel = sortBy === "rating" ? "Bewertung" : "Downloads";
|
||||||
|
|
||||||
|
const resultEmbed = new EmbedBuilder()
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setTitle(`:mag: Top-Ergebnisse für '${search}'`)
|
||||||
|
.setDescription(`${list}\n\n⬇️ = Downloads ⭐ = Bewertung 🆔 = Plugin-ID`)
|
||||||
|
.setFooter({
|
||||||
|
text: `Sortiert nach: ${sortLabel} | ${client.config.prefix}plugin [id] für mehr Details`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [resultEmbed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
77
commands/setchannel.js
Normal file
77
commands/setchannel.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { EmbedBuilder, PermissionsBitField, SlashCommandBuilder } from "discord.js";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import { t, getLang } from "../util/i18n.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "setchannel",
|
||||||
|
description: "Ändert den Update-Kanal für eine beobachtete Ressource",
|
||||||
|
aliases: [],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [PermissionsBitField.Flags.Administrator],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 2,
|
||||||
|
args_usage: "[ressourcen_id] [#kanal] Beispiel: vn!setchannel 72678 #updates",
|
||||||
|
cooldown: 5,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("setchannel")
|
||||||
|
.setDescription("Changes the update channel for a watched resource")
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt.setName("ressourcen_id").setDescription("Spiget Ressourcen-ID").setRequired(true)
|
||||||
|
)
|
||||||
|
.addChannelOption((opt) =>
|
||||||
|
opt.setName("kanal").setDescription("Neuer Update-Kanal").setRequired(true)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(client, ctx, args) {
|
||||||
|
const guildID = ctx.guild.id;
|
||||||
|
const filePath = `./serverdata/${guildID}.json`;
|
||||||
|
|
||||||
|
let saveData;
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(filePath, "utf8");
|
||||||
|
saveData = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return ctx.reply("Dieser Server hat keine beobachteten Ressourcen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const lang = getLang(saveData);
|
||||||
|
|
||||||
|
const resourceID = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getString("ressourcen_id")
|
||||||
|
: args[0];
|
||||||
|
|
||||||
|
const channel = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getChannel("kanal")
|
||||||
|
: ctx.mentions?.channels?.first();
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
return ctx.reply(t(lang, "add.invalidChannel"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const watched = saveData.watchedResources.find(
|
||||||
|
(r) => String(r.resourceID) === String(resourceID)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!watched) {
|
||||||
|
return ctx.reply(t(lang, "setchannel.notWatched", { id: resourceID }));
|
||||||
|
}
|
||||||
|
|
||||||
|
watched.channelID = channel.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(saveData, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply(t(lang, "error.saveFailed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = watched.resourceName ?? resourceID;
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setDescription(t(lang, "setchannel.success", { name, channelID: channel.id }));
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [embed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
83
commands/setinterval.js
Normal file
83
commands/setinterval.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { EmbedBuilder, PermissionsBitField, SlashCommandBuilder } from "discord.js";
|
||||||
|
import { requireOwner } from "../util/ownerOnly.js";
|
||||||
|
import { t, getLang } from "../util/i18n.js";
|
||||||
|
import fsSync from "fs";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
|
||||||
|
const MIN_INTERVAL = 1;
|
||||||
|
const MAX_INTERVAL = 60;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "setinterval",
|
||||||
|
description: "Legt das Update-Check-Intervall für diesen Server fest (in Minuten)",
|
||||||
|
aliases: ["interval"],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [PermissionsBitField.Flags.Administrator],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 1,
|
||||||
|
args_usage: `[minuten] Beispiel: vn!setinterval 10 (erlaubt: ${MIN_INTERVAL}–${MAX_INTERVAL})`,
|
||||||
|
cooldown: 5,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("setinterval")
|
||||||
|
.setDescription("Legt das Update-Check-Intervall für diesen Server fest")
|
||||||
|
.addIntegerOption((opt) =>
|
||||||
|
opt
|
||||||
|
.setName("minuten")
|
||||||
|
.setDescription(`Intervall in Minuten (${MIN_INTERVAL}–${MAX_INTERVAL})`)
|
||||||
|
.setMinValue(MIN_INTERVAL)
|
||||||
|
.setMaxValue(MAX_INTERVAL)
|
||||||
|
.setRequired(true)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(client, ctx, args) {
|
||||||
|
const lang = loadLang(ctx.guild.id);
|
||||||
|
if (!await requireOwner(client, ctx, lang)) return;
|
||||||
|
const minutes = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getInteger("minuten")
|
||||||
|
: parseInt(args[0], 10);
|
||||||
|
|
||||||
|
if (isNaN(minutes) || minutes < MIN_INTERVAL || minutes > MAX_INTERVAL) {
|
||||||
|
return ctx.reply(
|
||||||
|
`Ungültiger Wert. Bitte gib eine Zahl zwischen **${MIN_INTERVAL}** und **${MAX_INTERVAL}** Minuten an.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildID = ctx.guild.id;
|
||||||
|
const filePath = `./serverdata/${guildID}.json`;
|
||||||
|
|
||||||
|
let saveData = { watchedResources: [] };
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(filePath, "utf8");
|
||||||
|
saveData = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
// Datei existiert noch nicht – wird erstellt
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData.updateInterval = minutes;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.mkdir("./serverdata", { recursive: true });
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(saveData, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply("Beim Speichern der Einstellung ist ein Fehler aufgetreten.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setTitle("⏱️ Update-Intervall aktualisiert")
|
||||||
|
.setDescription(`Das Update-Check-Intervall für diesen Server wurde auf **${minutes} Minute(n)** gesetzt.`)
|
||||||
|
.setFooter({ text: "Der nächste Check erfolgt nach Ablauf des neuen Intervalls." });
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [embed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadLang(guildID) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(fsSync.readFileSync(`./serverdata/${guildID}.json`, "utf8"));
|
||||||
|
return getLang(data);
|
||||||
|
} catch { return "de"; }
|
||||||
|
}
|
||||||
67
commands/setlang.js
Normal file
67
commands/setlang.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { EmbedBuilder, PermissionsBitField, SlashCommandBuilder } from "discord.js";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import { t } from "../util/i18n.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "setlang",
|
||||||
|
description: "Stellt die Sprache des Bots für diesen Server ein (DE/EN)",
|
||||||
|
aliases: ["language", "lang"],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [PermissionsBitField.Flags.Administrator],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 1,
|
||||||
|
args_usage: "[de|en] Beispiel: vn!setlang en",
|
||||||
|
cooldown: 5,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("setlang")
|
||||||
|
.setDescription("Set the bot language for this server (DE/EN)")
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt
|
||||||
|
.setName("sprache")
|
||||||
|
.setDescription("Sprache / Language")
|
||||||
|
.setRequired(true)
|
||||||
|
.addChoices(
|
||||||
|
{ name: "🇩🇪 Deutsch", value: "de" },
|
||||||
|
{ name: "🇬🇧 English", value: "en" }
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(client, ctx, args) {
|
||||||
|
const input = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getString("sprache")
|
||||||
|
: args[0]?.toLowerCase();
|
||||||
|
|
||||||
|
if (!["de", "en"].includes(input)) {
|
||||||
|
return ctx.reply("Ungültige Sprache. Nutze `de` oder `en`.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildID = ctx.guild.id;
|
||||||
|
const filePath = `./serverdata/${guildID}.json`;
|
||||||
|
|
||||||
|
let saveData = { watchedResources: [] };
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(filePath, "utf8");
|
||||||
|
saveData = JSON.parse(raw);
|
||||||
|
} catch { /* Datei noch nicht vorhanden */ }
|
||||||
|
|
||||||
|
saveData.lang = input;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.mkdir("./serverdata", { recursive: true });
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(saveData, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply(t(input, "error.saveFailed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const flag = input === "de" ? "🇩🇪" : "🇬🇧";
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setTitle(t(input, "setlang.title"))
|
||||||
|
.setDescription(`${flag} ${t(input, "setlang.success")}`);
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [embed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
82
commands/setmention.js
Normal file
82
commands/setmention.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { EmbedBuilder, PermissionsBitField, SlashCommandBuilder } from "discord.js";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import { t, getLang } from "../util/i18n.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "setmention",
|
||||||
|
description: "Legt eine Rolle fest die bei Updates einer Ressource gepingt wird",
|
||||||
|
aliases: [],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [PermissionsBitField.Flags.Administrator],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 1,
|
||||||
|
args_usage: "[ressourcen_id] [@rolle] Beispiel: vn!setmention 72678 @Updates (ohne Rolle = entfernen)",
|
||||||
|
cooldown: 5,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("setmention")
|
||||||
|
.setDescription("Sets a role to ping when a plugin gets an update")
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt.setName("ressourcen_id").setDescription("Spiget Ressourcen-ID").setRequired(true)
|
||||||
|
)
|
||||||
|
.addRoleOption((opt) =>
|
||||||
|
opt.setName("rolle").setDescription("Zu pingende Rolle (weglassen = entfernen)").setRequired(false)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(client, ctx, args) {
|
||||||
|
const guildID = ctx.guild.id;
|
||||||
|
const filePath = `./serverdata/${guildID}.json`;
|
||||||
|
|
||||||
|
let saveData;
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(filePath, "utf8");
|
||||||
|
saveData = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return ctx.reply("Dieser Server hat keine beobachteten Ressourcen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const lang = getLang(saveData);
|
||||||
|
|
||||||
|
const resourceID = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getString("ressourcen_id")
|
||||||
|
: args[0];
|
||||||
|
|
||||||
|
const role = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getRole("rolle")
|
||||||
|
: ctx.mentions?.roles?.first() ?? null;
|
||||||
|
|
||||||
|
const watched = saveData.watchedResources.find(
|
||||||
|
(r) => String(r.resourceID) === String(resourceID)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!watched) {
|
||||||
|
return ctx.reply(t(lang, "setmention.notWatched", { id: resourceID }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = watched.resourceName ?? resourceID;
|
||||||
|
|
||||||
|
if (role) {
|
||||||
|
watched.mentionRoleID = role.id;
|
||||||
|
} else {
|
||||||
|
delete watched.mentionRoleID;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(saveData, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply(t(lang, "error.saveFailed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = role
|
||||||
|
? t(lang, "setmention.set", { name, roleID: role.id })
|
||||||
|
: t(lang, "setmention.removed", { name });
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setDescription(msg);
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [embed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
51
commands/stats.js
Normal file
51
commands/stats.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "stats",
|
||||||
|
description: "Verschiedene Statistiken des Bots!",
|
||||||
|
aliases: [],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 0,
|
||||||
|
args_usage: "",
|
||||||
|
cooldown: 5,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("stats")
|
||||||
|
.setDescription("Zeigt Statistiken des Bots an"),
|
||||||
|
|
||||||
|
async execute(client, ctx) {
|
||||||
|
let members = 0;
|
||||||
|
client.bot.guilds.cache.forEach((guild) => { members += guild.memberCount; });
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setAuthor({ name: client.config.authorName, url: client.config.authorGithub })
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setTitle(`📊 ${ctx.guild.members.me.displayName} Statistiken`)
|
||||||
|
.addFields([
|
||||||
|
{ name: "📦 Version", value: `${client.packageData.version}`, inline: true },
|
||||||
|
{ name: "👥 Nutzer", value: `${members.toLocaleString("de-DE")}`, inline: true },
|
||||||
|
{ name: "💬 Kanäle", value: `${client.bot.channels.cache.size}`, inline: true },
|
||||||
|
{ name: "🌐 Server", value: `${client.bot.guilds.cache.size}`, inline: true },
|
||||||
|
{ name: "🔧 Befehle", value: `${client.commands.size}`, inline: true },
|
||||||
|
{ name: "⏱️ Laufzeit", value: getUptime(client.bot), inline: true },
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [embed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getUptime(bot) {
|
||||||
|
let total = bot.uptime / 1000;
|
||||||
|
const days = Math.floor(total / 86400); total %= 86400;
|
||||||
|
const hours = Math.floor(total / 3600); total %= 3600;
|
||||||
|
const minutes = Math.floor(total / 60);
|
||||||
|
const seconds = Math.floor(total % 60);
|
||||||
|
|
||||||
|
if (days > 0) return `${days}T ${hours}Std ${minutes}Min ${seconds}Sek`;
|
||||||
|
if (hours > 0) return `${hours}Std ${minutes}Min ${seconds}Sek`;
|
||||||
|
if (minutes > 0) return `${minutes}Min ${seconds}Sek`;
|
||||||
|
return `${seconds}Sek`;
|
||||||
|
}
|
||||||
105
commands/status.js
Normal file
105
commands/status.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
|
||||||
|
import stats from "../util/stats.js";
|
||||||
|
import queue from "../util/queue.js";
|
||||||
|
import { requireOwner } from "../util/ownerOnly.js";
|
||||||
|
import { t, getLang } from "../util/i18n.js";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "status",
|
||||||
|
description: "Zeigt den Bot-Status, API-Verfügbarkeit und den nächsten Update-Check",
|
||||||
|
aliases: ["botstatus"],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 0,
|
||||||
|
args_usage: "",
|
||||||
|
cooldown: 10,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("status")
|
||||||
|
.setDescription("Zeigt Bot-Status, API-Verfügbarkeit und Update-Check-Info"),
|
||||||
|
|
||||||
|
async execute(client, ctx) {
|
||||||
|
const lang = loadLang(ctx.guild.id);
|
||||||
|
if (!await requireOwner(client, ctx, lang)) return;
|
||||||
|
// --- Spiget API Health-Check ---
|
||||||
|
let spigetStatus = "✅ Erreichbar";
|
||||||
|
let spigetPing = "–";
|
||||||
|
try {
|
||||||
|
const start = Date.now();
|
||||||
|
const res = await fetch("https://api.spiget.org/v2/status", { signal: AbortSignal.timeout(5000) });
|
||||||
|
spigetPing = `${Date.now() - start}ms`;
|
||||||
|
if (!res.ok) spigetStatus = `⚠️ HTTP ${res.status}`;
|
||||||
|
} catch {
|
||||||
|
spigetStatus = "❌ Nicht erreichbar";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SpigotMC API Health-Check (Vault = 34315, existiert garantiert) ---
|
||||||
|
let spigotStatus = "✅ Erreichbar";
|
||||||
|
try {
|
||||||
|
const res = await fetch("https://api.spigotmc.org/legacy/update.php?resource=34315", {
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
});
|
||||||
|
// 200 mit Versionstext = OK; alles andere = Problem
|
||||||
|
if (!res.ok) spigotStatus = `⚠️ HTTP ${res.status}`;
|
||||||
|
} catch {
|
||||||
|
spigotStatus = "❌ Nicht erreichbar";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Queue-Status ---
|
||||||
|
const queueStatus = queue.isRunning
|
||||||
|
? `🔄 Aktiv (${queue.size} ausstehend)`
|
||||||
|
: queue.size > 0
|
||||||
|
? `⏳ Wartend (${queue.size} Jobs)`
|
||||||
|
: "✅ Leerlauf";
|
||||||
|
|
||||||
|
// --- Nächster Check ---
|
||||||
|
const nextRun = queue.nextRunAt
|
||||||
|
? `<t:${Math.floor(queue.nextRunAt / 1000)}:R>`
|
||||||
|
: "Unbekannt";
|
||||||
|
|
||||||
|
// --- Statistiken ---
|
||||||
|
const s = stats.all();
|
||||||
|
const startedAt = s.startedAt
|
||||||
|
? `<t:${Math.floor(new Date(s.startedAt).getTime() / 1000)}:R>`
|
||||||
|
: "Unbekannt";
|
||||||
|
|
||||||
|
// --- Discord Latenz ---
|
||||||
|
const discordPing = `${client.bot.ws.ping}ms`;
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setTitle(`📡 ${client.config.botName} – Status`)
|
||||||
|
.addFields([
|
||||||
|
// APIs
|
||||||
|
{ name: "🌐 Spiget API", value: `${spigetStatus} (${spigetPing})`, inline: true },
|
||||||
|
{ name: "🌐 SpigotMC API", value: spigotStatus, inline: true },
|
||||||
|
{ name: "💬 Discord Ping", value: discordPing, inline: true },
|
||||||
|
// Update-Check
|
||||||
|
{ name: "⚙️ Queue", value: queueStatus, inline: true },
|
||||||
|
{ name: "⏱️ Nächster Check", value: nextRun, inline: true },
|
||||||
|
{ name: "🔄 Checks gesamt", value: `${s.checksRun}`, inline: true },
|
||||||
|
// Statistiken
|
||||||
|
{ name: "🔔 Updates gefunden", value: `${s.updatesFound}`, inline: true },
|
||||||
|
{ name: "📤 Updates gepostet", value: `${s.updatesPosted}`, inline: true },
|
||||||
|
{ name: "⚠️ API-Fehler", value: `${s.apiErrors}`, inline: true },
|
||||||
|
// Bot-Info
|
||||||
|
{ name: "🕐 Online seit", value: startedAt, inline: true },
|
||||||
|
{ name: "🏓 Jobs verarbeitet", value: `${queue.processed}`, inline: true },
|
||||||
|
{ name: "📩 Owner-DMs", value: `${s.dmsSent}`, inline: true },
|
||||||
|
])
|
||||||
|
.setFooter({ text: `${client.config.botName} • Version ${client.packageData.version}` })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [embed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function loadLang(guildID) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(fs.readFileSync(`./serverdata/${guildID}.json`, "utf8"));
|
||||||
|
return getLang(data);
|
||||||
|
} catch { return "de"; }
|
||||||
|
}
|
||||||
100
commands/top.js
Normal file
100
commands/top.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import { t, getLang } from "../util/i18n.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "top",
|
||||||
|
description: "Zeigt die meistgeladenen/bestbewerteten beobachteten Plugins",
|
||||||
|
aliases: [],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 0,
|
||||||
|
args_usage: "[-r] -r = nach Bewertung sortieren",
|
||||||
|
cooldown: 10,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("top")
|
||||||
|
.setDescription("Shows the top watched plugins by downloads or rating")
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt
|
||||||
|
.setName("sortierung")
|
||||||
|
.setDescription("Sortierung")
|
||||||
|
.setRequired(false)
|
||||||
|
.addChoices(
|
||||||
|
{ name: "⬇️ Downloads", value: "downloads" },
|
||||||
|
{ name: "⭐ Bewertung", value: "rating" }
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(client, ctx, args) {
|
||||||
|
const guildID = ctx.guild.id;
|
||||||
|
const filePath = `./serverdata/${guildID}.json`;
|
||||||
|
|
||||||
|
let saveData;
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(filePath, "utf8");
|
||||||
|
saveData = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return ctx.reply("Dieser Server hat keine beobachteten Ressourcen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const lang = getLang(saveData);
|
||||||
|
const sortBy = ctx.isSlash
|
||||||
|
? (ctx.interaction.options.getString("sortierung") ?? "downloads")
|
||||||
|
: (args.includes("-r") ? "rating" : "downloads");
|
||||||
|
|
||||||
|
const watched = saveData.watchedResources;
|
||||||
|
if (!watched || watched.length === 0) {
|
||||||
|
return ctx.reply(t(lang, "top.noData"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch stats for all watched resources
|
||||||
|
const results = [];
|
||||||
|
for (const w of watched) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://api.spiget.org/v2/resources/${w.resourceID}`);
|
||||||
|
if (!res.ok) continue;
|
||||||
|
const data = await res.json();
|
||||||
|
results.push({
|
||||||
|
id: w.resourceID,
|
||||||
|
name: data.name ?? w.resourceName ?? w.resourceID,
|
||||||
|
downloads: data.downloads ?? 0,
|
||||||
|
rating: data.rating?.average ?? 0,
|
||||||
|
version: w.lastCheckedVersion ?? "?",
|
||||||
|
channelID: w.channelID,
|
||||||
|
});
|
||||||
|
await new Promise((r) => setTimeout(r, 300));
|
||||||
|
} catch { /* Plugin überspringen */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
return ctx.reply(t(lang, "error.apiDown"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
results.sort((a, b) =>
|
||||||
|
sortBy === "rating" ? b.rating - a.rating : b.downloads - a.downloads
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortLabel = t(lang, sortBy === "rating" ? "top.rating" : "top.downloads");
|
||||||
|
const medal = ["🥇", "🥈", "🥉"];
|
||||||
|
|
||||||
|
const list = results.map((r, i) => {
|
||||||
|
const icon = medal[i] ?? `${i + 1}.`;
|
||||||
|
const dl = r.downloads.toLocaleString("de-DE");
|
||||||
|
const stars = r.rating > 0 ? `⭐ ${r.rating.toFixed(1)}` : "⭐ –";
|
||||||
|
return `${icon} **${r.name}** (v${r.version})\n⬇️ ${dl} ${stars} 🆔 \`${r.id}\``;
|
||||||
|
}).join("\n\n");
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(ctx.guild.members.me.displayHexColor)
|
||||||
|
.setTitle(t(lang, "top.title", { sort: sortLabel }))
|
||||||
|
.setDescription(list)
|
||||||
|
.setFooter({ text: `${results.length} Plugins • ${client.config.prefix}plugin [id] für Details` })
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [embed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
143
commands/update.js
Normal file
143
commands/update.js
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { EmbedBuilder, PermissionsBitField, SlashCommandBuilder } from "discord.js";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import { Spiget } from "spiget";
|
||||||
|
import {
|
||||||
|
generateAvatarLink,
|
||||||
|
generateAuthorURL,
|
||||||
|
generateResourceIconURL,
|
||||||
|
getLatestVersion,
|
||||||
|
getUpdateDescription,
|
||||||
|
} from "../util/helpers.js";
|
||||||
|
|
||||||
|
const spiget = new Spiget("Viper-Network");
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "update",
|
||||||
|
description: "Führt manuell einen Update-Check für eine bestimmte Ressource durch",
|
||||||
|
aliases: [],
|
||||||
|
guild: ["all"],
|
||||||
|
nsfw: false,
|
||||||
|
user_permissions: [PermissionsBitField.Flags.Administrator],
|
||||||
|
bot_permissions: [],
|
||||||
|
args_required: 1,
|
||||||
|
args_usage: "[ressourcen_id] Beispiel: vn!update 72678",
|
||||||
|
cooldown: 10,
|
||||||
|
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("update")
|
||||||
|
.setDescription("Manueller Update-Check für eine Ressource")
|
||||||
|
.addStringOption((opt) =>
|
||||||
|
opt.setName("ressourcen_id").setDescription("Spiget Ressourcen-ID").setRequired(true)
|
||||||
|
),
|
||||||
|
|
||||||
|
async execute(client, ctx, args) {
|
||||||
|
const resourceID = ctx.isSlash
|
||||||
|
? ctx.interaction.options.getString("ressourcen_id")
|
||||||
|
: args[0];
|
||||||
|
|
||||||
|
const guildID = ctx.guild.id;
|
||||||
|
const filePath = `./serverdata/${guildID}.json`;
|
||||||
|
|
||||||
|
// Load server data
|
||||||
|
let saveData;
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(filePath, "utf8");
|
||||||
|
saveData = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
return ctx.reply("Dieser Server hat keine beobachteten Ressourcen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const watched = saveData.watchedResources.find(
|
||||||
|
(r) => String(r.resourceID) === String(resourceID)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!watched) {
|
||||||
|
return ctx.reply(
|
||||||
|
`Ressource \`${resourceID}\` wird auf diesem Server nicht beobachtet. Füge sie zuerst mit \`${client.config.prefix}add\` hinzu.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch resource info
|
||||||
|
let resource, author;
|
||||||
|
try {
|
||||||
|
resource = await spiget.getResource(resourceID);
|
||||||
|
author = await resource.getAuthor();
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply("Ressourcen-Informationen konnten nicht abgerufen werden.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let latestVersion;
|
||||||
|
try {
|
||||||
|
latestVersion = await getLatestVersion(resourceID);
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
return ctx.reply("Version konnte nicht abgerufen werden.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousVersion = watched.lastCheckedVersion;
|
||||||
|
const hasUpdate = previousVersion !== latestVersion;
|
||||||
|
|
||||||
|
const authorURL = generateAuthorURL(author.name, author.id);
|
||||||
|
const authorAvatarURL = generateAvatarLink(author.id);
|
||||||
|
const resourceIconURL = generateResourceIconURL(resource);
|
||||||
|
const resourceURL = `https://spigotmc.org/resources/.${resourceID}/`;
|
||||||
|
|
||||||
|
if (!hasUpdate) {
|
||||||
|
const upToDateEmbed = new EmbedBuilder()
|
||||||
|
.setAuthor({ name: `Autor: ${author.name}`, iconURL: authorAvatarURL, url: authorURL })
|
||||||
|
.setColor("#00FF00")
|
||||||
|
.setTitle(`✅ ${resource.name} ist aktuell`)
|
||||||
|
.setDescription(resource.tag)
|
||||||
|
.addFields([
|
||||||
|
{ name: "Version", value: `**${latestVersion}**`, inline: true },
|
||||||
|
{ name: "Status", value: "Kein Update verfügbar", inline: true },
|
||||||
|
])
|
||||||
|
.setThumbnail(resourceIconURL)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [upToDateEmbed] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update available
|
||||||
|
let updateDesc;
|
||||||
|
try {
|
||||||
|
updateDesc = await getUpdateDescription(resourceID);
|
||||||
|
} catch {
|
||||||
|
updateDesc = "Update-Beschreibung konnte nicht abgerufen werden.";
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateEmbed = new EmbedBuilder()
|
||||||
|
.setAuthor({ name: `Autor: ${author.name}`, iconURL: authorAvatarURL, url: authorURL })
|
||||||
|
.setColor("#FFA500")
|
||||||
|
.setTitle(`🔔 Update verfügbar: ${resource.name}`)
|
||||||
|
.setDescription(resource.tag)
|
||||||
|
.addFields([
|
||||||
|
{
|
||||||
|
name: "📦 Version",
|
||||||
|
value: `~~${previousVersion}~~ → **${latestVersion}**`,
|
||||||
|
inline: false,
|
||||||
|
},
|
||||||
|
{ name: "📝 Update-Beschreibung", value: updateDesc, inline: false },
|
||||||
|
{ name: "⬇️ Download", value: resourceURL, inline: false },
|
||||||
|
])
|
||||||
|
.setThumbnail(resourceIconURL)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
// Update saved version
|
||||||
|
watched.lastCheckedVersion = latestVersion;
|
||||||
|
try {
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(saveData, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
client.logger.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also send to the watched channel if different from current
|
||||||
|
const watchedChannel = ctx.guild.channels.cache.get(watched.channelID);
|
||||||
|
if (watchedChannel && watchedChannel.id !== ctx.channel.id) {
|
||||||
|
watchedChannel.send({ embeds: [updateEmbed] }).catch(client.logger.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.reply({ embeds: [updateEmbed] });
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user