All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 7s
- Twitch add: multiple streamers via comma separation - Twitch add: optional discord channel (defaults to current) - Twitch remove: multiple streamers via comma separation - Twitch listall: new command showing all server streamers (admin) - Timer listall: fix SQL error by removing invalid JOIN Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
347 lines
14 KiB
TypeScript
347 lines
14 KiB
TypeScript
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 * FROM reminders WHERE guild_id = ? ORDER BY 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; |