From 5c6188ea13ae05180febe76d81f13e10f607e949 Mon Sep 17 00:00:00 2001 From: sarah Date: Sun, 22 Mar 2026 18:53:33 +0100 Subject: [PATCH] Add: Timer and Trigger modules with help subcommands - Add /timer command: add, remove, list, listall, help - Add /trigger command: add, remove, list, help - Add /admin help and /owner help subcommands - Add /twitch help subcommand - Add ReminderManager for timer notifications - Add reminders table to database - Update README.md and AGENTS-BOT.md documentation Co-Authored-By: Claude Opus 4.6 --- AGENTS-BOT.md | 104 ++++++--- README.md | 18 ++ src/commands/utility/admin.ts | 26 +++ src/commands/utility/owner.ts | 20 ++ src/commands/utility/timer.ts | 347 ++++++++++++++++++++++++++++++ src/commands/utility/trigger.ts | 131 +++++++++++ src/commands/utility/twitch.ts | 24 ++- src/index.ts | 4 + src/structures/Database.ts | 13 ++ src/structures/ReminderManager.ts | 62 ++++++ 10 files changed, 716 insertions(+), 33 deletions(-) create mode 100644 src/commands/utility/timer.ts create mode 100644 src/commands/utility/trigger.ts create mode 100644 src/structures/ReminderManager.ts diff --git a/AGENTS-BOT.md b/AGENTS-BOT.md index a087485..d52c838 100644 --- a/AGENTS-BOT.md +++ b/AGENTS-BOT.md @@ -12,7 +12,9 @@ The bot uses a **Modular Command & Event Loading** pattern with **ESM (ECMAScrip 3. **Interface-Driven Commands:** All commands implement `Command` interface. 4. **SQLite Database:** Persistent data storage with `better-sqlite3`. 5. **Twitch Monitoring:** Periodic API polling with Stream ID tracking. -6. **Grouped Commands:** Admin/Owner commands grouped under subcommands. +6. **Reminder System:** Periodic reminder checking with target_time tracking. +7. **Auto-Response System:** Trigger word detection for automatic replies. +8. **Grouped Commands:** Admin/Owner/Trigger/Timer commands grouped under subcommands. ## 📁 Project Structure @@ -26,6 +28,8 @@ pixelpoebel/ │ │ ├── ping.ts │ │ ├── help.ts │ │ ├── twitch.ts +│ │ ├── trigger.ts # Auto-responses +│ │ ├── timer.ts # Reminders │ │ ├── admin.ts # All admin subcommands │ │ └── owner.ts # All owner subcommands │ ├── events/ @@ -36,6 +40,7 @@ pixelpoebel/ │ ├── Command.ts │ ├── Database.ts │ ├── TwitchManager.ts +│ ├── ReminderManager.ts │ └── Deployer.ts ├── data/ # SQLite database (volume mounted) ├── package.json @@ -132,6 +137,17 @@ CREATE TABLE auto_responses ( response_text TEXT, UNIQUE(guild_id, trigger_word) ); + +-- reminders +CREATE TABLE reminders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT, + channel_id TEXT, + user_id TEXT, + reminder_text TEXT, + target_time DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); ``` ### 3. Key Components @@ -167,44 +183,65 @@ export interface Command { } ``` -**Deployer - Auto-Deploy (Option 3):** +### 4. Timer System + +**Time Parsing:** ```typescript -export class Deployer { - static async deployIfMissing(clientId: string, token: string) { - const rest = new REST().setToken(token); +// Duration parsing: "30s", "2h", "1d30m", "3wochen" +function parseDuration(input: string): number | null { + const unitMs: Record = { + 's': 1000, 'sec': 1000, 'sek': 1000, 'sekunden': 1000, + 'm': 60000, 'min': 60000, 'minuten': 60000, + 'h': 3600000, 'hour': 3600000, 'std': 3600000, 'stunden': 3600000, + 'd': 86400000, 'day': 86400000, 't': 86400000, 'tage': 86400000, + 'w': 604800000, 'week': 604800000, 'wochen': 604800000, + 'mo': 2592000000, 'month': 2592000000, 'monat': 2592000000, 'monate': 2592000000, + 'y': 31536000000, 'year': 31536000000, 'j': 31536000000, 'jahr': 31536000000, 'jahre': 31536000000 + }; + // Parse and calculate total milliseconds +} - try { - const data = await rest.get(Routes.applicationCommands(clientId)); - if (data.length > 0) { - console.log('[DEPLOYER] Commands already registered. Skipping deployment.'); - return data.length; - } - } catch (error) { - // Commands don't exist yet +// Date parsing: "25.12.2024 14:30", "morgen 15:00" +function parseDateTime(input: string): Date | null { + // German date format, ISO format, or relative "tomorrow" +} +``` + +**Reminder Polling:** +```typescript +export class ReminderManager { + static async checkReminders(client: any) { + const now = new Date().toISOString(); + const dueReminders = DB.all('SELECT * FROM reminders WHERE target_time <= ?', now); + + for (const reminder of dueReminders) { + const guild = client.guilds.cache.get(reminder.guild_id); + const channel = guild.channels.cache.get(reminder.channel_id); + // Send notification and delete reminder } + } - return this.deploy(clientId, token); + static startPolling(client: any) { + setInterval(() => this.checkReminders(client), 30 * 1000); } } ``` -### 4. Command Examples +### 5. Auto-Response (Trigger) System -**Grouped Command (admin):** ```typescript -data: new SlashCommandBuilder() - .setName('admin') - .setDescription('Admin-Commands') - .addSubcommand(sub => sub.setName('kick').setDescription('Kickt einen Nutzer') - .addUserOption(opt => opt.setName('target').setDescription('Nutzer').setRequired(true)) - .addStringOption(opt => opt.setName('reason').setDescription('Grund'))) - .addSubcommand(sub => sub.setName('warn').setDescription('Verwarnt einen Nutzer') - .addUserOption(opt => opt.setName('target').setDescription('Nutzer').setRequired(true)) - .addStringOption(opt => opt.setName('reason').setDescription('Grund').setRequired(true))) - // ... more subcommands +// Add trigger (admin only) +DB.run('INSERT INTO auto_responses (guild_id, trigger_word, response_text) VALUES (?, ?, ?)', + guildId, triggerWord.toLowerCase(), responseText); + +// List triggers (public) +DB.all('SELECT * FROM auto_responses WHERE guild_id = ? ORDER BY trigger_word ASC', guildId); + +// Remove trigger (admin only) +DB.run('DELETE FROM auto_responses WHERE guild_id = ? AND trigger_word = ?', guildId, triggerWord); ``` -### 5. Twitch Monitoring with Stream ID +### 6. Twitch Monitoring with Stream ID ```typescript // Stream ID tracking prevents duplicate notifications @@ -218,7 +255,7 @@ if (wasOffline || (monitor.last_status === 'online' && isNewStream)) { } ``` -### 6. Configurable Warn System +### 7. Configurable Warn System **Database Schema:** - `warn_threshold`: Number of warns before action @@ -238,7 +275,7 @@ if (warnCount >= warnThreshold) { } ``` -### 7. Mute with Multiple Time Units +### 8. Mute with Multiple Time Units ```typescript const unitSeconds: Record = { @@ -281,6 +318,7 @@ docker-compose up -d --build 3. **Interaction States:** Check `interaction.replied` before replying. 4. **Twitch API:** Max 100 channels per request, batch properly. 5. **Deploy:** Commands NOT auto-registered on start – use `/owner deploy`. +6. **Ephemeral Replies:** Use `flags: [MessageFlags.Ephemeral]` instead of deprecated `ephemeral: true`. ## 📋 Command Summary @@ -288,6 +326,8 @@ docker-compose up -d --build |---------|-------------|------------| | `/ping` | – | Public | | `/help` | – | Public | -| `/twitch` | online, add, remove, list | Public/Admin | -| `/admin` | kick, warn, unwarn, mute, unmute, ban, unban, config | Admin | -| `/owner` | deploy, stats, servers | Owner | \ No newline at end of file +| `/twitch` | online, add, remove, list, help | Public/Admin | +| `/trigger` | add, remove, list, help | Public/Admin | +| `/timer` | add, remove, list, listall, help | Public/Admin | +| `/admin` | kick, warn, unwarn, mute, unmute, ban, unban, config, help | Admin | +| `/owner` | deploy, stats, servers, help | Owner | \ No newline at end of file diff --git a/README.md b/README.md index b93c4f7..14fb9a7 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,20 @@ npm run deploy - `/help` – Alle Befehle - `/twitch online ` – Twitch-Status - `/twitch list` – Überwachte Kanäle +- `/twitch help` – Twitch-Hilfe + +### Trigger (Auto-Antworten) +- `/trigger add ` – Trigger hinzufügen (Mod) +- `/trigger remove ` – Trigger entfernen (Mod) +- `/trigger list` – Alle Trigger anzeigen +- `/trigger help` – Trigger-Hilfe + +### Timer (Erinnerungen) +- `/timer add ` – Timer hinzufügen +- `/timer remove ` – Timer entfernen (Mod) +- `/timer list` – Timer dieses Kanals +- `/timer listall` – Alle Timer (Mod) +- `/timer help` – Timer-Hilfe ### Admin - `/twitch add ` – Kanal überwachen @@ -70,11 +84,13 @@ npm run deploy - `/admin ban ` – Nutzer bannen - `/admin unban ` – Nutzer entbannen - `/admin config` – Warn-System konfigurieren +- `/admin help` – Admin-Hilfe ### Owner - `/owner deploy` – Commands neu registrieren - `/owner stats` – Bot-Statistiken - `/owner servers` – Alle Server +- `/owner help` – Owner-Hilfe ## 📡 Features @@ -85,4 +101,6 @@ npm run deploy - ✅ Auto-Deploy beim Start - ✅ Moderation Tools - ✅ Konfigurierbares Warn-System +- ✅ Auto-Antworten (Trigger) +- ✅ Timer & Erinnerungen - ✅ Docker Support \ No newline at end of file diff --git a/src/commands/utility/admin.ts b/src/commands/utility/admin.ts index 8650bd4..7b72333 100644 --- a/src/commands/utility/admin.ts +++ b/src/commands/utility/admin.ts @@ -116,6 +116,10 @@ const command: Command = { option.setName('duration_value') .setDescription('Dauer in Sekunden') .setMinValue(0))) + .addSubcommand(subcommand => + subcommand + .setName('help') + .setDescription('Zeigt Hilfe zu den Admin-Befehlen an.')) .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers), category: 'Admin', async execute(interaction: any) { @@ -124,6 +128,28 @@ const command: Command = { const guild = interaction.guild; const guildId = interaction.guildId; + if (subcommand === 'help') { + const embed = new EmbedBuilder() + .setTitle('🛡️ Admin Hilfe') + .setColor(0xe74c3c) + .setDescription('Moderations-Tools für Server-Verwaltung.') + .addFields( + { name: '`/admin kick [grund]`', value: 'Kickt einen Nutzer vom Server.', inline: false }, + { name: '`/admin warn `', value: 'Verwarnt einen Nutzer. Bei X Warnungen wird automatisch eine Aktion ausgeführt.', inline: false }, + { name: '`/admin unwarn `', value: 'Entfernt die letzte Warnung eines Nutzers.', inline: false }, + { name: '`/admin mute [grund]`', value: 'Muted einen Nutzer für eine bestimmte Zeit.\nEinheiten: sec, min, hour, day, week, month, year, perm', inline: false }, + { name: '`/admin unmute `', value: 'Entmutet einen Nutzer.', inline: false }, + { name: '`/admin ban [grund] [delete_days]`', value: 'Bannt einen Nutzer vom Server.\ndelete_days: Nachrichten der letzten X Tage löschen (0-7).', inline: false }, + { name: '`/admin unban `', value: 'Entbannt einen Nutzer (via ID oder @username).', inline: false }, + { name: '`/admin config [wert]`', value: 'Konfiguriert das Warn-System.\nSettings: warn_threshold, warn_action, warn_mute_duration, show', inline: false }, + { name: 'Warn-System', value: 'Automatische Aktionen bei X Warnungen:\n`/admin config warn_action kick|mute|ban`', inline: false } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] }); + return; + } + // Get or create guild settings let settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId); if (!settings) { diff --git a/src/commands/utility/owner.ts b/src/commands/utility/owner.ts index 708a826..ac17608 100644 --- a/src/commands/utility/owner.ts +++ b/src/commands/utility/owner.ts @@ -18,6 +18,10 @@ const command: Command = { subcommand .setName('servers') .setDescription('Listet alle Server')) + .addSubcommand(subcommand => + subcommand + .setName('help') + .setDescription('Zeigt Hilfe zu den Owner-Befehlen an.')) .setDMPermission(true), category: 'Owner', async execute(interaction: any, client: any) { @@ -30,6 +34,22 @@ const command: Command = { const subcommand = interaction.options.getSubcommand(); + if (subcommand === 'help') { + const embed = new EmbedBuilder() + .setTitle('🔑 Owner Hilfe') + .setColor(0xf1c40f) + .setDescription('Bot-Einstellungen und Statistiken.') + .addFields( + { name: '`/owner deploy`', value: 'Aktualisiert alle Discord-Befehle.\nNach Code-Änderungen ausführen.', inline: false }, + { name: '`/owner stats`', value: 'Zeigt Bot-Statistiken: Uptime, Latency, Server- und Nutzerzahl.', inline: false }, + { name: '`/owner servers`', value: 'Listet alle Server, auf denen der Bot aktiv ist.', inline: false } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] }); + return; + } + if (subcommand === 'deploy') { await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); diff --git a/src/commands/utility/timer.ts b/src/commands/utility/timer.ts new file mode 100644 index 0000000..3be2f95 --- /dev/null +++ b/src/commands/utility/timer.ts @@ -0,0 +1,347 @@ +import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from 'discord.js'; +import { Command } from '../../structures/Command.js'; + +// Parse time duration strings like "30s", "2h", "1d30m" +function parseDuration(input: string): number | null { + const regex = /(\d+)\s*([a-z]+)/gi; + let match; + let totalMs = 0; + + while ((match = regex.exec(input)) !== null) { + const value = parseInt(match[1]); + const unit = match[2].toLowerCase(); + const unitMs: Record = { + 's': 1000, + 'sec': 1000, + 'sek': 1000, + 'sekunden': 1000, + 'm': 60000, + 'min': 60000, + 'minuten': 60000, + 'h': 3600000, + 'hour': 3600000, + 'std': 3600000, + 'stunden': 3600000, + 'd': 86400000, + 'day': 86400000, + 't': 86400000, + 'tage': 86400000, + 'w': 604800000, + 'week': 604800000, + 'wochen': 604800000, + 'mo': 2592000000, + 'month': 2592000000, + 'monat': 2592000000, + 'monate': 2592000000, + 'y': 31536000000, + 'year': 31536000000, + 'j': 31536000000, + 'jahr': 31536000000, + 'jahre': 31536000000 + }; + + if (unitMs[unit]) { + totalMs += value * unitMs[unit]; + } + } + + return totalMs > 0 ? totalMs : null; +} + +// Parse date-time strings like "25.12.2024 14:30" or "tomorrow 3pm" +function parseDateTime(input: string): Date | null { + const now = new Date(); + + // German date format: DD.MM.YYYY or DD.MM.YYYY HH:MM + const germanDate = /^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{1,2}))?$/.exec(input); + if (germanDate) { + const day = parseInt(germanDate[1]); + const month = parseInt(germanDate[2]) - 1; // Month is 0-indexed + const year = parseInt(germanDate[3]); + const hour = germanDate[4] ? parseInt(germanDate[4]) : 12; + const minute = germanDate[5] ? parseInt(germanDate[5]) : 0; + + const date = new Date(year, month, day, hour, minute); + if (isNaN(date.getTime())) return null; + return date; + } + + // ISO format: YYYY-MM-DD or YYYY-MM-DD HH:MM + const isoDate = /^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T\s](\d{1,2}):(\d{1,2}))?$/.exec(input); + if (isoDate) { + const year = parseInt(isoDate[1]); + const month = parseInt(isoDate[2]) - 1; + const day = parseInt(isoDate[3]); + const hour = isoDate[4] ? parseInt(isoDate[4]) : 12; + const minute = isoDate[5] ? parseInt(isoDate[5]) : 0; + + const date = new Date(year, month, day, hour, minute); + if (isNaN(date.getTime())) return null; + return date; + } + + // Tomorrow/Nächsten Tag + const lowerInput = input.toLowerCase(); + if (lowerInput.includes('morgen') || lowerInput.includes('tomorrow')) { + const tomorrow = new Date(now); + tomorrow.setDate(tomorrow.getDate() + 1); + + // Try to extract time + const timeMatch = /(\d{1,2}):(\d{1,2})/.exec(input); + if (timeMatch) { + tomorrow.setHours(parseInt(timeMatch[1]), parseInt(timeMatch[2]), 0, 0); + } else { + tomorrow.setHours(9, 0, 0, 0); // Default 9 AM + } + return tomorrow; + } + + return null; +} + +const command: Command = { + data: new SlashCommandBuilder() + .setName('timer') + .setDescription('Erinnerungen und Timer verwalten.') + .addSubcommand(subcommand => + subcommand + .setName('add') + .setDescription('Fügt einen neuen Timer hinzu.') + .addStringOption(option => + option.setName('zeit') + .setDescription('Zeit (z.B. "30min", "2h", "25.12.2024 14:30", "morgen")') + .setRequired(true)) + .addStringOption(option => + option.setName('nachricht') + .setDescription('Die Erinnerungs-Nachricht') + .setRequired(true))) + .addSubcommand(subcommand => + subcommand + .setName('remove') + .setDescription('Entfernt einen Timer.') + .addIntegerOption(option => + option.setName('id') + .setDescription('Die Timer-ID (siehe /timer list)') + .setRequired(true))) + .addSubcommand(subcommand => + subcommand + .setName('list') + .setDescription('Zeigt alle Timer dieses Kanals an.')) + .addSubcommand(subcommand => + subcommand + .setName('listall') + .setDescription('Zeigt alle Timer des Servers an (Nur Moderatoren).')) + .addSubcommand(subcommand => + subcommand + .setName('help') + .setDescription('Zeigt Hilfe zu den Timer-Befehlen an.')), + category: 'Public', + async execute(interaction: any) { + const subcommand = interaction.options.getSubcommand(); + const guildId = interaction.guildId; + const channelId = interaction.channelId; + const userId = interaction.user.id; + const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers); + const DB = interaction.client.DB; + + if (subcommand === 'help') { + const embed = new EmbedBuilder() + .setTitle('⏰ Timer Hilfe') + .setColor(0x3498db) + .setDescription('Setze Erinnerungen für dich oder andere.') + .addFields( + { name: '`/timer add `', value: 'Fügt einen neuen Timer hinzu.\nJeder kann Timer setzen.', inline: false }, + { name: '`/timer remove `', value: 'Entfernt einen Timer (Mod).\nDie ID bekommst du über `/timer list`.', inline: false }, + { name: '`/timer list`', value: 'Zeigt alle Timer in diesem Kanal an.', inline: false }, + { name: '`/timer listall`', value: 'Zeigt alle Timer auf dem Server an (Mod).', inline: false }, + { name: 'Zeitformate', value: 'Dauer: `30s`, `2h`, `1d30m`, `3wochen`, `1jahr`\nDatum: `25.12.2024 14:30` oder `morgen 15:00`', inline: false }, + { name: 'Beispiele', value: '`/timer add 30min Meeting erinnern`\n`/timer add morgen 9 Uhr Aufstehen`\n`/timer add 25.12.2024 18:00 Weihnachtsessen`', inline: false } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] }); + return; + } + + if (subcommand === 'add') { + const timeInput = interaction.options.getString('zeit')!; + const message = interaction.options.getString('nachricht')!; + const now = new Date(); + + let targetTime: Date | null = null; + + // Try duration first (e.g., "30min", "2h", "1d30m") + const duration = parseDuration(timeInput); + if (duration !== null) { + targetTime = new Date(now.getTime() + duration); + } else { + // Try date-time parsing + targetTime = parseDateTime(timeInput); + } + + if (!targetTime || targetTime <= now) { + await interaction.reply({ + content: '❌ Ungültige Zeitangabe. Verwende z.B.: "30min", "2h30m", "25.12.2024 14:30" oder "morgen 15:00"', + flags: [MessageFlags.Ephemeral] + }); + return; + } + + // Insert reminder + const result = DB.run( + 'INSERT INTO reminders (guild_id, channel_id, user_id, reminder_text, target_time) VALUES (?, ?, ?, ?, ?)', + guildId, + channelId, + userId, + message, + targetTime.toISOString() + ); + + const timeDiff = targetTime.getTime() - now.getTime(); + const minutes = Math.floor(timeDiff / 60000); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + let timeStr = ''; + if (days > 0) timeStr += `${days}d `; + if (hours % 24 > 0) timeStr += `${hours % 24}h `; + if (minutes % 60 > 0) timeStr += `${minutes % 60}min`; + + await interaction.reply({ + content: `✅ Erinnerung gesetzt (ID: ${result.lastInsertRowid}) für **${timeStr.trim()}**`, + flags: [MessageFlags.Ephemeral] + }); + return; + } + + if (subcommand === 'remove') { + if (!isAdmin) { + await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] }); + return; + } + + const timerId = interaction.options.getInteger('id')!; + + const result = DB.run('DELETE FROM reminders WHERE id = ? AND guild_id = ?', timerId, guildId); + + if (result.changes === 0) { + await interaction.reply({ content: `❌ Timer mit ID ${timerId} nicht gefunden.`, flags: [MessageFlags.Ephemeral] }); + return; + } + + await interaction.reply({ content: `✅ Timer mit ID ${timerId} entfernt.`, flags: [MessageFlags.Ephemeral] }); + return; + } + + if (subcommand === 'list') { + const reminders: any[] = DB.all( + 'SELECT * FROM reminders WHERE guild_id = ? AND channel_id = ? ORDER BY target_time ASC', + guildId, + channelId + ); + + if (reminders.length === 0) { + await interaction.reply({ content: 'Keine Timer in diesem Kanal.', flags: [MessageFlags.Ephemeral] }); + return; + } + + const embed = new EmbedBuilder() + .setTitle('⏰ Timer in diesem Kanal') + .setColor(0x3498db) + .setTimestamp(); + + const now = new Date(); + const chunks: string[] = []; + const chunkSize = 10; + + for (let i = 0; i < reminders.length; i += chunkSize) { + const chunk = reminders.slice(i, i + chunkSize).map((r: any) => { + const targetTime = new Date(r.target_time); + const diffMs = targetTime.getTime() - now.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + let timeLeft = ''; + if (diffDays > 0) timeLeft = `${diffDays}d ${diffHours % 24}h`; + else if (diffHours > 0) timeLeft = `${diffHours}h ${diffMins % 60}min`; + else if (diffMins > 0) timeLeft = `${diffMins}min`; + else timeLeft = '< 1min'; + + const dateStr = targetTime.toLocaleString('de-DE'); + return `• **ID ${r.id}**: ${r.reminder_text}\n 🕐 ${dateStr} (in ${timeLeft})`; + }).join('\n\n'); + chunks.push(chunk); + } + + embed.setDescription(chunks[0]); + await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] }); + + for (let i = 1; i < chunks.length; i++) { + const nextEmbed = new EmbedBuilder() + .setColor(0x3498db) + .setDescription(chunks[i]); + await interaction.followUp({ embeds: [nextEmbed], flags: [MessageFlags.Ephemeral] }); + } + return; + } + + if (subcommand === 'listall') { + if (!isAdmin) { + await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] }); + return; + } + + const reminders: any[] = DB.all( + 'SELECT r.*, c.name as channel_name FROM reminders r LEFT JOIN channels c ON r.channel_id = c.id WHERE r.guild_id = ? ORDER BY r.target_time ASC', + guildId + ); + + if (reminders.length === 0) { + await interaction.reply({ content: 'Keine Timer auf dem Server.', flags: [MessageFlags.Ephemeral] }); + return; + } + + const embed = new EmbedBuilder() + .setTitle('⏰ Alle Timer auf dem Server') + .setColor(0xe74c3c) + .setTimestamp(); + + const now = new Date(); + const chunks: string[] = []; + const chunkSize = 10; + + for (let i = 0; i < reminders.length; i += chunkSize) { + const chunk = reminders.slice(i, i + chunkSize).map((r: any) => { + const targetTime = new Date(r.target_time); + const diffMs = targetTime.getTime() - now.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + let timeLeft = ''; + if (diffDays > 0) timeLeft = `${diffDays}d ${diffHours % 24}h`; + else if (diffHours > 0) timeLeft = `${diffHours}h ${diffMins % 60}min`; + else if (diffMins > 0) timeLeft = `${diffMins}min`; + else timeLeft = '< 1min'; + + const dateStr = targetTime.toLocaleString('de-DE'); + return `• **ID ${r.id}**: ${r.reminder_text}\n 📍 Kanal: ${r.channel_id}\n 🕐 ${dateStr} (in ${timeLeft})`; + }).join('\n\n'); + chunks.push(chunk); + } + + embed.setDescription(chunks[0]); + await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] }); + + for (let i = 1; i < chunks.length; i++) { + const nextEmbed = new EmbedBuilder() + .setColor(0xe74c3c) + .setDescription(chunks[i]); + await interaction.followUp({ embeds: [nextEmbed], flags: [MessageFlags.Ephemeral] }); + } + } + }, +}; + +export default command; \ No newline at end of file diff --git a/src/commands/utility/trigger.ts b/src/commands/utility/trigger.ts new file mode 100644 index 0000000..04a114a --- /dev/null +++ b/src/commands/utility/trigger.ts @@ -0,0 +1,131 @@ +import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from 'discord.js'; +import { Command } from '../../structures/Command.js'; + +const command: Command = { + data: new SlashCommandBuilder() + .setName('trigger') + .setDescription('Auto-Antworten verwalten.') + .addSubcommand(subcommand => + subcommand + .setName('add') + .setDescription('Fügt einen neuen Trigger hinzu.') + .addStringOption(option => + option.setName('trigger') + .setDescription('Das Trigger-Wort') + .setRequired(true)) + .addStringOption(option => + option.setName('antwort') + .setDescription('Die Antwort-Nachricht') + .setRequired(true))) + .addSubcommand(subcommand => + subcommand + .setName('remove') + .setDescription('Entfernt einen Trigger.') + .addStringOption(option => + option.setName('trigger') + .setDescription('Das Trigger-Wort') + .setRequired(true))) + .addSubcommand(subcommand => + subcommand + .setName('list') + .setDescription('Listet alle Trigger auf.')) + .addSubcommand(subcommand => + subcommand + .setName('help') + .setDescription('Zeigt Hilfe zu den Trigger-Befehlen an.')), + category: 'Admin', + async execute(interaction: any) { + const subcommand = interaction.options.getSubcommand(); + const guildId = interaction.guildId; + const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers); + const DB = interaction.client.DB; + + if (subcommand === 'help') { + const embed = new EmbedBuilder() + .setTitle('🎯 Trigger Hilfe') + .setColor(0x3498db) + .setDescription('Automatische Antworten auf Trigger-Wörter.') + .addFields( + { name: '`/trigger add `', value: 'Fügt einen neuen Trigger hinzu (Mod).\nTrigger-Wörter werden automatisch klein geschrieben.', inline: false }, + { name: '`/trigger remove `', value: 'Entfernt einen Trigger (Mod).', inline: false }, + { name: '`/trigger list`', value: 'Zeigt alle Trigger auf diesem Server an.', inline: false }, + { name: 'Beispiel', value: '`/trigger add hallo Hallo! Wie kann ich helfen?`', inline: false } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] }); + return; + } + + if (subcommand === 'list') { + const triggers: any[] = DB.all('SELECT * FROM auto_responses WHERE guild_id = ? ORDER BY trigger_word ASC', guildId); + + if (triggers.length === 0) { + await interaction.reply({ content: 'Keine Trigger konfiguriert.', flags: [MessageFlags.Ephemeral] }); + return; + } + + const embed = new EmbedBuilder() + .setTitle('🎯 Auto-Antworten') + .setColor(0x3498db) + .setTimestamp(); + + const chunks: string[] = []; + const chunkSize = 10; + for (let i = 0; i < triggers.length; i += chunkSize) { + const chunk = triggers.slice(i, i + chunkSize).map((t: any) => + `• **${t.trigger_word}** → ${t.response_text}` + ).join('\n'); + chunks.push(chunk); + } + + embed.setDescription(chunks[0]); + await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] }); + + for (let i = 1; i < chunks.length; i++) { + const nextEmbed = new EmbedBuilder() + .setColor(0x3498db) + .setDescription(chunks[i]); + await interaction.followUp({ embeds: [nextEmbed], flags: [MessageFlags.Ephemeral] }); + } + return; + } + + // Admin only for add/remove + if (!isAdmin) { + await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] }); + return; + } + + if (subcommand === 'add') { + const triggerWord = interaction.options.getString('trigger')!.toLowerCase(); + const responseText = interaction.options.getString('antwort')!; + + DB.run( + 'INSERT INTO auto_responses (guild_id, trigger_word, response_text) VALUES (?, ?, ?) ON CONFLICT(guild_id, trigger_word) DO UPDATE SET response_text = ?', + guildId, + triggerWord, + responseText, + responseText + ); + + await interaction.reply({ content: `✅ Trigger **${triggerWord}** hinzugefügt/aktualisiert.`, flags: [MessageFlags.Ephemeral] }); + return; + } + + if (subcommand === 'remove') { + const triggerWord = interaction.options.getString('trigger')!.toLowerCase(); + + const result = DB.run('DELETE FROM auto_responses WHERE guild_id = ? AND trigger_word = ?', guildId, triggerWord); + + if (result.changes === 0) { + await interaction.reply({ content: `❌ Trigger **${triggerWord}** nicht gefunden.`, flags: [MessageFlags.Ephemeral] }); + return; + } + + await interaction.reply({ content: `✅ Trigger **${triggerWord}** entfernt.`, flags: [MessageFlags.Ephemeral] }); + } + }, +}; + +export default command; \ No newline at end of file diff --git a/src/commands/utility/twitch.ts b/src/commands/utility/twitch.ts index d36a22a..2619641 100644 --- a/src/commands/utility/twitch.ts +++ b/src/commands/utility/twitch.ts @@ -36,7 +36,11 @@ const command: Command = { .addSubcommand(subcommand => subcommand .setName('list') - .setDescription('Listet überwachte Kanäle.')), + .setDescription('Listet überwachte Kanäle.')) + .addSubcommand(subcommand => + subcommand + .setName('help') + .setDescription('Zeigt Hilfe zu den Twitch-Befehlen an.')), category: 'Public', async execute(interaction: any) { const subcommand = interaction.options.getSubcommand(); @@ -44,6 +48,24 @@ const command: Command = { const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers); const DB = interaction.client.DB; + if (subcommand === 'help') { + const embed = new EmbedBuilder() + .setTitle('📺 Twitch Hilfe') + .setColor(0x6441a5) + .setDescription('Überwache Twitch-Streams und bekomme Benachrichtigungen.') + .addFields( + { name: '`/twitch online `', value: 'Prüft, ob ein Kanal gerade online ist.', inline: false }, + { name: '`/twitch add [nachricht]`', value: 'Überwacht einen Kanal in diesem Kanal (Mod).\nWenn der Streamer live geht, wird benachrichtigt.', inline: false }, + { name: '`/twitch remove `', value: 'Entfernt einen Kanal aus der Überwachung (Mod).', inline: false }, + { name: '`/twitch list`', value: 'Zeigt alle überwachten Kanäle in diesem Kanal an.', inline: false }, + { name: 'Beispiel', value: '`/twitch add shroud Hallo! Shroud ist live!`', inline: false } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] }); + return; + } + if (subcommand === 'online') { const channelName = interaction.options.getString('channel')!.toLowerCase(); await interaction.deferReply(); diff --git a/src/index.ts b/src/index.ts index afb40d7..cde96ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { dirname, join } from 'node:path'; import { Command } from './structures/Command.js'; import { DB } from './structures/Database.js'; import { TwitchManager } from './structures/TwitchManager.js'; +import { ReminderManager } from './structures/ReminderManager.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -59,4 +60,7 @@ for (const file of eventFiles) { // Start Twitch Polling TwitchManager.startPolling(client); +// Start Reminder Polling +ReminderManager.startPolling(client); + client.login(process.env.DISCORD_TOKEN); \ No newline at end of file diff --git a/src/structures/Database.ts b/src/structures/Database.ts index 7af7820..86adbb6 100644 --- a/src/structures/Database.ts +++ b/src/structures/Database.ts @@ -86,6 +86,19 @@ export class DB { ) `).run(); + // Reminders + db.prepare(` + CREATE TABLE IF NOT EXISTS reminders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guild_id TEXT, + channel_id TEXT, + user_id TEXT, + reminder_text TEXT, + target_time DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `).run(); + console.log('[DATABASE] Initialization complete.'); } diff --git a/src/structures/ReminderManager.ts b/src/structures/ReminderManager.ts new file mode 100644 index 0000000..4bc7183 --- /dev/null +++ b/src/structures/ReminderManager.ts @@ -0,0 +1,62 @@ +import { TextChannel, EmbedBuilder } from 'discord.js'; + +export class ReminderManager { + static async checkReminders(client: any) { + const now = new Date().toISOString(); + + // Get all due reminders + const dueReminders: any[] = (client as any).DB?.all( + 'SELECT * FROM reminders WHERE target_time <= ?', + now + ) || []; + + if (dueReminders.length === 0) return; + + for (const reminder of dueReminders) { + try { + const guild = client.guilds.cache.get(reminder.guild_id); + if (!guild) { + // Remove reminder if guild doesn't exist + (client as any).DB?.run('DELETE FROM reminders WHERE id = ?', reminder.id); + continue; + } + + const channel = guild.channels.cache.get(reminder.channel_id) as TextChannel; + if (!channel || !channel.isTextBased()) { + // Remove reminder if channel doesn't exist or is not text-based + (client as any).DB?.run('DELETE FROM reminders WHERE id = ?', reminder.id); + continue; + } + + const user = await guild.members.fetch(reminder.user_id).catch(() => null); + const userMention = user ? `<@${user.id}>` : `<@${reminder.user_id}>`; + + const embed = new EmbedBuilder() + .setTitle('⏰ Erinnerung') + .setColor(0x3498db) + .setDescription(reminder.reminder_text) + .addFields({ + name: 'Gesetzt von', + value: userMention, + inline: true + }) + .setTimestamp(); + + await channel.send({ content: `${userMention} deine Erinnerung:`, embeds: [embed] }); + + // Delete the reminder after sending + (client as any).DB?.run('DELETE FROM reminders WHERE id = ?', reminder.id); + + console.log(`[REMINDER] Sent reminder ID ${reminder.id} in ${guild.name}`); + } catch (error) { + console.error(`[REMINDER] Error sending reminder ID ${reminder.id}:`, error); + } + } + } + + static startPolling(client: any) { + // Check every 30 seconds + setInterval(() => this.checkReminders(client), 30 * 1000); + console.log('[REMINDER] Polling started (30s interval).'); + } +} \ No newline at end of file