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: [] }); }, };