Modul-System: alle 35 Funktionen im Panel ein- und ausschaltbar
Bisher waren die Schalter über die Oberfläche verstreut: fünf Funktionen
hatten einen Haken, 22 weitere gingen nur über den Umweg „Kanal leeren".
Für jeden außer dem Owner war das nicht auffindbar.
Jetzt steht in src/modules.js ein zentrales Register aller Funktionen mit
Beschreibung, Standard-Zustand und den Einstellungen, die sie brauchen.
Daraus entsteht ein neuer Setup-Tab mit Karten: Schiebeschalter, kurze
Erklärung, Hinweis was noch fehlt ("Fehlt noch: Release-Kanal") und ein
Sprung zu den Einstellungen.
Damit die Schalter keine Attrappen sind, prüfen die Bot-Module jetzt an
18 Stellen zentral, ob sie laufen dürfen — Starboard, Galerie, Modmail,
Willkommen, Protokoll, Auto-Antworten, Sprachkanäle, Erinnerungen,
Events, Twitch/YouTube, Monitor, Wächter, Releases, Rückblick,
Geburtstage, Verlosungen und geplante Beiträge.
Funktionen mit vorhandenem Schalter (Level-System, Backups, Member-Gate …)
nutzen weiterhin dieselbe Einstellung, damit keine zweite Wahrheit
entsteht und bestehende Konfigurationen unverändert weiterlaufen.
Nebenbei gefunden und behoben: Beim Anlegen eines Modmail-Threads wurde
`member.client` benutzt, obwohl es an der Stelle kein `member` gibt — der
erste Modmail-Thread wäre mit einem Fehler abgebrochen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { getSetting, setSetting, birthdaysToday } from '../db.js';
|
||||
import { birthdayChannelId, birthdayRoleId, discordGuildId, brandColor2 } from '../runtime-settings.js';
|
||||
import { brandEmbed } from '../embeds.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 15 * 60 * 1000;
|
||||
|
||||
@@ -16,6 +17,7 @@ function berlinNow() {
|
||||
}
|
||||
|
||||
export async function birthdayTick(client) {
|
||||
if (!moduleEnabled('birthdays')) return;
|
||||
const { day, month, hour, dateKey } = berlinNow();
|
||||
if (hour < 9) return; // erst ab 09:00
|
||||
if (getSetting('last_birthday_run') === dateKey) return; // heute schon gelaufen
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
saveGalleryItem, hasGalleryItem, deleteGalleryItem,
|
||||
} from '../db.js';
|
||||
import { starboardChannelId, starboardThreshold, screenshotChannelId, brandColor } from '../runtime-settings.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
import { config } from '../config.js';
|
||||
|
||||
const MAX_GALLERY_IMAGES = 4;
|
||||
@@ -23,6 +24,7 @@ mkdirSync(galleryDir, { recursive: true });
|
||||
* @returns {Promise<boolean>} true, wenn neu gespeichert
|
||||
*/
|
||||
export async function archiveGalleryMessage(message) {
|
||||
if (!moduleEnabled('gallery')) return;
|
||||
if (hasGalleryItem(message.id)) return false;
|
||||
|
||||
const files = [];
|
||||
@@ -62,6 +64,7 @@ export async function removeGalleryMessage(messageId) {
|
||||
/* ── Starboard ─────────────────────────────────────── */
|
||||
|
||||
async function handleStarReaction(reaction) {
|
||||
if (!moduleEnabled('starboard')) return;
|
||||
const boardChannelId = starboardChannelId();
|
||||
if (!boardChannelId) return;
|
||||
if (reaction.emoji.name !== '⭐') return;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import {
|
||||
tempVoiceChannelId, eventsAnnounceChannelId, brandColor, brandFooter,
|
||||
} from '../runtime-settings.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
|
||||
/* ── Triggers ──────────────────────────────────────── */
|
||||
|
||||
@@ -18,6 +19,7 @@ const triggerCooldown = new Map(); // `${channelId}:${keyword}` → ts
|
||||
const TRIGGER_COOLDOWN_MS = 30_000;
|
||||
|
||||
async function handleTriggers(message) {
|
||||
if (!moduleEnabled('triggers')) return;
|
||||
const content = message.content?.toLowerCase();
|
||||
if (!content) return;
|
||||
for (const t of listTriggers()) {
|
||||
@@ -33,6 +35,7 @@ async function handleTriggers(message) {
|
||||
/* ── Temp-Voice (Join to Create) ───────────────────── */
|
||||
|
||||
async function handleVoiceState(oldState, newState) {
|
||||
if (!moduleEnabled('temp_voice')) return;
|
||||
const hubId = tempVoiceChannelId();
|
||||
|
||||
// Join in den Hub → eigenen Kanal erstellen und rüberschieben
|
||||
@@ -95,6 +98,7 @@ async function cleanupTempVoice(client) {
|
||||
/* ── Reminder ──────────────────────────────────────── */
|
||||
|
||||
export async function remindersTick(client) {
|
||||
if (!moduleEnabled('reminders')) return;
|
||||
for (const r of dueReminders()) {
|
||||
markReminderSent(r.id);
|
||||
const user = await client.users.fetch(r.user_id).catch(() => null);
|
||||
@@ -105,6 +109,7 @@ export async function remindersTick(client) {
|
||||
/* ── Event-Ankündigung ─────────────────────────────── */
|
||||
|
||||
async function announceEvent(event) {
|
||||
if (!moduleEnabled('events')) return;
|
||||
const channelId = eventsAnnounceChannelId();
|
||||
if (!channelId) return;
|
||||
const channel = await event.client.channels.fetch(channelId).catch(() => null);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, MessageFlags, PermissionFlagsBits } from 'discord.js';
|
||||
import { dueGiveaways, markGiveawayEnded, giveawayEntries, getGiveaway } from '../db.js';
|
||||
import { brandFooter } from '../runtime-settings.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 60 * 1000;
|
||||
|
||||
@@ -18,6 +19,7 @@ function drawWinners(entries, count) {
|
||||
|
||||
/** Fällige Giveaways beenden — exportiert für Tests und den Timer */
|
||||
export async function giveawayTick(client) {
|
||||
if (!moduleEnabled('giveaways')) return;
|
||||
for (const g of dueGiveaways()) {
|
||||
markGiveawayEnded(g.message_id);
|
||||
try {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { AttachmentBuilder, ChannelType, EmbedBuilder, Events } from 'discord.js';
|
||||
import { saveModmail, modmailByUser, modmailByThread, saveStickyRoles, stickyRolesOf } from '../db.js';
|
||||
import { modmailChannelId, welcomeChannelId, modlogChannelId, publicUrl, autoroleId, stickyRolesEnabled, brandColor, brandColor2, brandFooter } from '../runtime-settings.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
|
||||
|
||||
/* ── Modmail ───────────────────────────────────────── */
|
||||
@@ -36,7 +37,7 @@ async function ensureModmailThread(client, user) {
|
||||
`Neue Modmail-Konversation mit **${user.username}** (<@${user.id}>).\n` +
|
||||
'Antworten in diesem Thread gehen als DM an den User.'
|
||||
)
|
||||
.setFooter({ text: brandFooter('MODMAIL'), iconURL: member.client?.user?.displayAvatarURL?.({ size: 64 }) }),
|
||||
.setFooter({ text: brandFooter('MODMAIL'), iconURL: client.user?.displayAvatarURL?.({ size: 64 }) }),
|
||||
],
|
||||
});
|
||||
return thread;
|
||||
@@ -44,6 +45,7 @@ async function ensureModmailThread(client, user) {
|
||||
|
||||
/** DM vom User → in den Staff-Thread spiegeln */
|
||||
async function handleIncomingDm(client, message) {
|
||||
if (!moduleEnabled('modmail')) return;
|
||||
const thread = await ensureModmailThread(client, message.author);
|
||||
if (!thread) return; // Feature aus → DM ignorieren
|
||||
|
||||
@@ -78,6 +80,7 @@ async function handleThreadReply(client, message) {
|
||||
/* ── Willkommens-Embed ─────────────────────────────── */
|
||||
|
||||
async function handleMemberAdd(member) {
|
||||
if (!moduleEnabled('welcome')) return;
|
||||
const channelId = welcomeChannelId();
|
||||
if (!channelId) return;
|
||||
const channel = await member.client.channels.fetch(channelId).catch(() => null);
|
||||
@@ -137,6 +140,7 @@ async function restoreRoles(member) {
|
||||
/* ── Mod-Log ───────────────────────────────────────── */
|
||||
|
||||
async function logToModlog(client, embed) {
|
||||
if (!moduleEnabled('modlog')) return;
|
||||
const channelId = modlogChannelId();
|
||||
if (!channelId) return;
|
||||
const channel = await client.channels.fetch(channelId).catch(() => null);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
|
||||
import { releaseChannelId, publicUrl, brandColor2, brandFooter } from '../runtime-settings.js';
|
||||
import { config } from '../config.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
|
||||
|
||||
/**
|
||||
@@ -12,6 +13,7 @@ import { config } from '../config.js';
|
||||
* @returns {Promise<boolean>} true, wenn gepostet
|
||||
*/
|
||||
export async function postReleaseEmbed(client, release) {
|
||||
if (!moduleEnabled('releases')) return false;
|
||||
const channelId = releaseChannelId();
|
||||
if (!channelId) return false;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { GameDig } from 'gamedig';
|
||||
import { getSetting, listGameservers, setGameserverMessage, recordPlayerSample, prunePlayerHistory } from '../db.js';
|
||||
import { brandFooter, botStatusText } from '../runtime-settings.js';
|
||||
import { brandEmbed } from '../embeds.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 2 * 60 * 1000;
|
||||
const TIMEOUT_MS = 8000;
|
||||
@@ -154,6 +155,7 @@ async function handleAlerts(client, r) {
|
||||
|
||||
/** Ein Poll-Durchlauf — exportiert für Tests und den Timer */
|
||||
export async function monitorTick(client) {
|
||||
if (!moduleEnabled('server_monitor')) return;
|
||||
const servers = listGameservers();
|
||||
if (servers.length === 0) return;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
twitchChannel, twitchClientId, twitchClientSecret,
|
||||
brandColor, brandColor2, brandFooter,
|
||||
} from '../runtime-settings.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 5 * 60 * 1000;
|
||||
let twitchToken = null; // { token, expiresAt }
|
||||
@@ -22,6 +23,7 @@ async function announce(client, embed) {
|
||||
/* ── YouTube (RSS, ohne API-Key) ───────────────────── */
|
||||
|
||||
async function checkYouTube(client) {
|
||||
if (!moduleEnabled('social')) return;
|
||||
const channelId = youtubeChannelId();
|
||||
if (!channelId) return;
|
||||
const res = await fetch(`https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`, {
|
||||
@@ -67,6 +69,7 @@ async function getTwitchToken() {
|
||||
}
|
||||
|
||||
async function checkTwitch(client) {
|
||||
if (!moduleEnabled('social')) return;
|
||||
const login = twitchChannel();
|
||||
if (!login || !twitchClientId() || !twitchClientSecret()) return;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// URLs kommen aus dem Setting watchdog_urls (Setup-Seite), leer = deaktiviert.
|
||||
import { getSetting } from '../db.js';
|
||||
import { config } from '../config.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 2 * 60 * 1000;
|
||||
const FAILS_BEFORE_ALERT = 2; // 2 Fehlschläge in Folge → Alarm (gegen Flattern)
|
||||
@@ -24,6 +25,7 @@ async function dmAdmin(client, content) {
|
||||
|
||||
/** Ein Prüfdurchlauf — exportiert für Tests und den Intervall-Timer */
|
||||
export async function watchdogTick(client, { failsBeforeAlert = FAILS_BEFORE_ALERT } = {}) {
|
||||
if (!moduleEnabled('watchdog')) return;
|
||||
for (const url of watchedUrls()) {
|
||||
const s = state.get(url) ?? { fails: 0, down: false, since: null };
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { EmbedBuilder } from 'discord.js';
|
||||
import { weeklyStats, getSetting, setSetting } from '../db.js';
|
||||
import { devlogChannelId, publicUrl, brandColor, brandFooter } from '../runtime-settings.js';
|
||||
import { config } from '../config.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 5 * 60 * 1000;
|
||||
|
||||
@@ -32,6 +33,7 @@ function buildBars(perDay) {
|
||||
|
||||
/** Rückblick-Embed bauen und in den Devlog-Kanal posten */
|
||||
export async function postWeeklyRecap(client) {
|
||||
if (!moduleEnabled('weekly_recap')) return;
|
||||
const channelId = devlogChannelId();
|
||||
if (!channelId) throw new Error('Kein Devlog-Kanal konfiguriert');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user