Add: Timer and Trigger modules with help subcommands
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 6s
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 6s
- 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 <noreply@anthropic.com>
This commit is contained in:
102
AGENTS-BOT.md
102
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<string, number> = {
|
||||
'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;
|
||||
// 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
|
||||
}
|
||||
} catch (error) {
|
||||
// Commands don't exist yet
|
||||
}
|
||||
|
||||
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<string, number> = {
|
||||
@@ -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 |
|
||||
| `/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 |
|
||||
18
README.md
18
README.md
@@ -58,6 +58,20 @@ npm run deploy
|
||||
- `/help` – Alle Befehle
|
||||
- `/twitch online <channel>` – Twitch-Status
|
||||
- `/twitch list` – Überwachte Kanäle
|
||||
- `/twitch help` – Twitch-Hilfe
|
||||
|
||||
### Trigger (Auto-Antworten)
|
||||
- `/trigger add <wort> <antwort>` – Trigger hinzufügen (Mod)
|
||||
- `/trigger remove <wort>` – Trigger entfernen (Mod)
|
||||
- `/trigger list` – Alle Trigger anzeigen
|
||||
- `/trigger help` – Trigger-Hilfe
|
||||
|
||||
### Timer (Erinnerungen)
|
||||
- `/timer add <zeit> <nachricht>` – Timer hinzufügen
|
||||
- `/timer remove <id>` – Timer entfernen (Mod)
|
||||
- `/timer list` – Timer dieses Kanals
|
||||
- `/timer listall` – Alle Timer (Mod)
|
||||
- `/timer help` – Timer-Hilfe
|
||||
|
||||
### Admin
|
||||
- `/twitch add <channel>` – Kanal überwachen
|
||||
@@ -70,11 +84,13 @@ npm run deploy
|
||||
- `/admin ban <user>` – Nutzer bannen
|
||||
- `/admin unban <id>` – 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
|
||||
@@ -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 <nutzer> [grund]`', value: 'Kickt einen Nutzer vom Server.', inline: false },
|
||||
{ name: '`/admin warn <nutzer> <grund>`', value: 'Verwarnt einen Nutzer. Bei X Warnungen wird automatisch eine Aktion ausgeführt.', inline: false },
|
||||
{ name: '`/admin unwarn <nutzer>`', value: 'Entfernt die letzte Warnung eines Nutzers.', inline: false },
|
||||
{ name: '`/admin mute <nutzer> <dauer> <einheit> [grund]`', value: 'Muted einen Nutzer für eine bestimmte Zeit.\nEinheiten: sec, min, hour, day, week, month, year, perm', inline: false },
|
||||
{ name: '`/admin unmute <nutzer>`', value: 'Entmutet einen Nutzer.', inline: false },
|
||||
{ name: '`/admin ban <nutzer> [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 <id>`', value: 'Entbannt einen Nutzer (via ID oder @username).', inline: false },
|
||||
{ name: '`/admin config <setting> [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) {
|
||||
|
||||
@@ -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] });
|
||||
|
||||
|
||||
347
src/commands/utility/timer.ts
Normal file
347
src/commands/utility/timer.ts
Normal file
@@ -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<string, number> = {
|
||||
'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 <zeit> <nachricht>`', value: 'Fügt einen neuen Timer hinzu.\nJeder kann Timer setzen.', inline: false },
|
||||
{ name: '`/timer remove <id>`', 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;
|
||||
131
src/commands/utility/trigger.ts
Normal file
131
src/commands/utility/trigger.ts
Normal file
@@ -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 <wort> <antwort>`', value: 'Fügt einen neuen Trigger hinzu (Mod).\nTrigger-Wörter werden automatisch klein geschrieben.', inline: false },
|
||||
{ name: '`/trigger remove <wort>`', 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;
|
||||
@@ -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 <kanal>`', value: 'Prüft, ob ein Kanal gerade online ist.', inline: false },
|
||||
{ name: '`/twitch add <kanal> [nachricht]`', value: 'Überwacht einen Kanal in diesem Kanal (Mod).\nWenn der Streamer live geht, wird benachrichtigt.', inline: false },
|
||||
{ name: '`/twitch remove <kanal>`', 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();
|
||||
|
||||
@@ -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);
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
|
||||
62
src/structures/ReminderManager.ts
Normal file
62
src/structures/ReminderManager.ts
Normal file
@@ -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).');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user