Add events module with dashboard UI, scheduling, signups, and settings updates; extend env/readme.

This commit is contained in:
Pascal Prießnitz
2025-12-02 23:52:10 +01:00
parent 874b01c999
commit 829d160164
578 changed files with 37647 additions and 11590 deletions

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,5 +1,5 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { SlashCommand } from '../../utils/types';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,7 +1,7 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { LoopMode } from '../../services/musicService.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
import { LoopMode } from '../../services/musicService';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,
@@ -15,3 +15,4 @@ const command: SlashCommand = {
};
export default command;

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, EmbedBuilder } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, PermissionFlagsBits, ChannelType, TextChannel } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { prisma } from '../../database/index.js';
import { SlashCommand } from '../../utils/types';
import { prisma } from '../../database/index';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { prisma } from '../../database/index.js';
import { SlashCommand } from '../../utils/types';
import { prisma } from '../../database/index';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,7 +1,7 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { prisma } from '../../database/index.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
import { prisma } from '../../database/index';
import path from 'path';
import fs from 'fs';

View File

@@ -0,0 +1,35 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types';
import { normalizeBirthdayInput } from '../../services/birthdayService';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('birthday')
.setDescription('Speichert dein Geburtsdatum.')
.addStringOption((opt) => opt.setName('datum').setDescription('Format: DD.MM.YYYY, DD.MM oder YYYY-MM-DD').setRequired(true)),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId) {
await interaction.reply({ content: 'Dieser Befehl funktioniert nur auf Servern.', ephemeral: true });
return;
}
const raw = interaction.options.getString('datum', true);
const normalized = normalizeBirthdayInput(raw);
if (!normalized) {
await interaction.reply({ content: 'Bitte gib ein gueltiges Datum an (DD.MM.YYYY, DD.MM oder YYYY-MM-DD).', ephemeral: true });
return;
}
await context.birthdays.setBirthday(interaction.guildId, interaction.user.id, normalized);
const formatted = normalized.startsWith('--')
? normalized
.replace('--', '')
.split('-')
.reverse()
.join('.') + '.'
: normalized.split('-').reverse().join('.');
await interaction.reply({ content: `Geburtstag gespeichert: ${formatted}`, ephemeral: true });
}
};
export default command;

View File

@@ -1,33 +1,82 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, PermissionFlagsBits, ChannelType } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { settings } from '../../config/state.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { settingsStore } from '../../config/state';
import { context } from '../../config/context';
import { LoggingService } from '../../services/loggingService';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('configure')
.setDescription('Setzt Basis-Einstellungen des Bots (gildenspezifisch).')
.addChannelOption((opt) => opt.setName('welcome_channel').setDescription('Kanal für Willkommensnachrichten').addChannelTypes(ChannelType.GuildText))
.addChannelOption((opt) => opt.setName('log_channel').setDescription('Kanal für Logs').addChannelTypes(ChannelType.GuildText))
.addChannelOption((opt) =>
opt.setName('welcome_channel').setDescription('Kanal fuer Willkommensnachrichten').addChannelTypes(ChannelType.GuildText)
)
.addChannelOption((opt) => opt.setName('log_channel').setDescription('Kanal fuer Logs').addChannelTypes(ChannelType.GuildText))
.addBooleanOption((opt) => opt.setName('automod').setDescription('Automod an/aus'))
.addNumberOption((opt) => opt.setName('spam_threshold').setDescription('Spam-Schwelle (Nachrichten im Zeitfenster)'))
.addNumberOption((opt) => opt.setName('spam_window_ms').setDescription('Spam-Zeitfenster in Millisekunden'))
.addNumberOption((opt) => opt.setName('spam_timeout_minutes').setDescription('Timeout-Dauer (Minuten) bei Spam'))
.addStringOption((opt) => opt.setName('link_whitelist').setDescription('Kommagetrennte Link-Whitelist (Domains)'))
.addBooleanOption((opt) => opt.setName('leveling').setDescription('Level-System an/aus'))
.addBooleanOption((opt) => opt.setName('dynamic_voice').setDescription('Dynamische Voice Channels an/aus'))
.addChannelOption((opt) =>
opt.setName('voice_lobby').setDescription('Voice-Lobby fuer dynamische Channels').addChannelTypes(ChannelType.GuildVoice)
)
.addStringOption((opt) => opt.setName('voice_template').setDescription('Name-Template, z.B. {user}s Channel'))
.addIntegerOption((opt) => opt.setName('voice_user_limit').setDescription('Userlimit fuer dynamische Channels'))
.addRoleOption((opt) => opt.setName('support_role').setDescription('Support-Rolle fuer Tickets'))
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId) return;
const guildSetting = settings.get(interaction.guildId) ?? {};
const guildSetting: any = settingsStore.get(interaction.guildId) ?? {};
const welcome = interaction.options.getChannel('welcome_channel');
const logChannel = interaction.options.getChannel('log_channel');
const automod = interaction.options.getBoolean('automod');
const spamThreshold = interaction.options.getNumber('spam_threshold');
const spamWindowMs = interaction.options.getNumber('spam_window_ms');
const spamTimeoutMinutes = interaction.options.getNumber('spam_timeout_minutes');
const linkWhitelist = interaction.options.getString('link_whitelist');
const leveling = interaction.options.getBoolean('leveling');
const dynamicVoice = interaction.options.getBoolean('dynamic_voice');
const voiceLobby = interaction.options.getChannel('voice_lobby');
const voiceTemplate = interaction.options.getString('voice_template');
const voiceUserLimit = interaction.options.getInteger('voice_user_limit');
const supportRole = interaction.options.getRole('support_role');
if (welcome) guildSetting.welcomeChannelId = welcome.id;
if (logChannel) guildSetting.logChannelId = logChannel.id;
if (automod !== null) guildSetting.automodEnabled = automod;
if (spamThreshold !== null || spamWindowMs !== null || spamTimeoutMinutes !== null || linkWhitelist !== null) {
guildSetting.automodConfig = {
...(guildSetting.automodConfig ?? {}),
spamThreshold: spamThreshold ?? guildSetting.automodConfig?.spamThreshold,
windowMs: spamWindowMs ?? guildSetting.automodConfig?.windowMs,
spamTimeoutMinutes: spamTimeoutMinutes ?? guildSetting.automodConfig?.spamTimeoutMinutes,
linkWhitelist:
linkWhitelist !== null
? linkWhitelist
.split(',')
.map((s) => s.trim())
.filter(Boolean)
: guildSetting.automodConfig?.linkWhitelist
};
}
if (leveling !== null) guildSetting.levelingEnabled = leveling;
if (dynamicVoice !== null) guildSetting.dynamicVoiceEnabled = dynamicVoice;
if (voiceLobby || voiceTemplate || voiceUserLimit !== null) {
guildSetting.dynamicVoiceConfig = {
...(guildSetting.dynamicVoiceConfig ?? {}),
lobbyChannelId: voiceLobby?.id ?? guildSetting.dynamicVoiceConfig?.lobbyChannelId,
categoryId: voiceLobby?.parentId ?? guildSetting.dynamicVoiceConfig?.categoryId,
template: voiceTemplate ?? guildSetting.dynamicVoiceConfig?.template,
userLimit: voiceUserLimit ?? guildSetting.dynamicVoiceConfig?.userLimit
};
}
if (supportRole) guildSetting.supportRoleId = supportRole.id;
settings.set(interaction.guildId, guildSetting);
context.logging = new (context.logging.constructor as any)(guildSetting.logChannelId);
const updated = await settingsStore.set(interaction.guildId, guildSetting);
context.logging = new LoggingService(updated.logChannelId);
await interaction.reply({ content: 'Einstellungen gespeichert.', ephemeral: true });
}

View File

@@ -0,0 +1,29 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
import { settingsStore } from '../../config/state';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('event')
.setDescription('Zeigt Events an.')
.addStringOption((opt) => opt.setName('list').setDescription('Zeigt naechste Events').setChoices({ name: 'show', value: 'show' })),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId) return;
const cfg = settingsStore.get(interaction.guildId);
if ((cfg as any)?.eventsEnabled === false) {
await interaction.reply({ content: 'Events sind deaktiviert.', ephemeral: true });
return;
}
const events = await (context as any).events.listEvents(interaction.guildId);
if (!events?.length) {
await interaction.reply({ content: 'Keine Events vorhanden.', ephemeral: true });
return;
}
const lines = events.slice(0, 5).map((ev: any) => `${ev.title} - <t:${Math.floor(new Date(ev.startTime).getTime() / 1000)}:f>`);
await interaction.reply({ content: lines.join('\n'), ephemeral: true });
}
};
export default command;

View File

@@ -1,5 +1,5 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, EmbedBuilder } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { SlashCommand } from '../../utils/types';
const command: SlashCommand = {
data: new SlashCommandBuilder().setName('help').setDescription('Zeigt Befehle und Module.'),

View File

@@ -1,5 +1,5 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { SlashCommand } from '../../utils/types';
const command: SlashCommand = {
data: new SlashCommandBuilder().setName('ping').setDescription('Antwortet mit Pong!'),

View File

@@ -1,6 +1,6 @@
import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { context } from '../../config/context.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -1,5 +1,5 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, EmbedBuilder } from 'discord.js';
import { SlashCommand } from '../../utils/types.js';
import { SlashCommand } from '../../utils/types';
const command: SlashCommand = {
guildOnly: true,

View File

@@ -0,0 +1,33 @@
import { SlashCommandBuilder, ChatInputCommandInteraction, EmbedBuilder } from 'discord.js';
import { context } from '../../config/context';
export default {
data: new SlashCommandBuilder().setName('status').setDescription('Zeigt Statuspage-Services.'),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId) {
await interaction.reply({ content: 'Nur in Guilds verfuegbar.', ephemeral: true });
return;
}
const cfg = await context.statuspage.getConfig(interaction.guildId);
const services = cfg.services ?? [];
const embed = new EmbedBuilder()
.setTitle('Service Status')
.setColor(services.some((s) => s.status === 'down') ? 0xef4444 : 0x22c55e)
.setDescription(services.length ? '' : 'Keine Services konfiguriert.')
.setTimestamp(new Date());
services.forEach((s) => {
const icon = s.status === 'up' ? '✅' : s.status === 'down' ? '❌' : '⚪';
const upPct =
s.upChecks && s.totalChecks
? Math.round(((s.upChecks ?? 0) / Math.max(1, s.totalChecks ?? 1)) * 100)
: 0;
const last = s.lastChecked ? new Date(s.lastChecked).toLocaleString() : 'n/a';
embed.addFields({
name: `${icon} ${s.name}`,
value: `Status: ${s.status ?? 'unknown'} | Uptime: ${upPct}% | Letzter Check: ${last}`,
inline: false
});
});
await interaction.reply({ embeds: [embed], ephemeral: true });
}
};

View File

@@ -0,0 +1,63 @@
import { ChatInputCommandInteraction, EmbedBuilder, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { settingsStore } from '../../config/state';
import { SlashCommand } from '../../utils/types';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('welcome')
.setDescription('Loest die konfigurierte Willkommensnachricht aus.')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId || !interaction.guild) {
await interaction.reply({ content: 'Dieser Befehl ist nur in Guilds verfuegbar.', ephemeral: true });
return;
}
const cfg = settingsStore.get(interaction.guildId) ?? {};
const welcome = cfg.welcomeConfig || cfg.automodConfig?.welcomeConfig;
if (!welcome?.enabled || !welcome.channelId) {
await interaction.reply({ content: 'Willkommensnachrichten sind fuer diesen Server nicht konfiguriert.', ephemeral: true });
return;
}
const channel = interaction.guild.channels.cache.get(welcome.channelId) || (await interaction.guild.channels.fetch(welcome.channelId).catch(() => null));
if (!channel || !channel.isTextBased()) {
await interaction.reply({ content: 'Ziel-Channel fuer Willkommensnachrichten ist ungueltig.', ephemeral: true });
return;
}
const colorVal = parseInt((welcome.embedColor || '00ff99').replace('#', ''), 16);
const embed = new EmbedBuilder()
.setTitle(welcome.embedTitle || 'Willkommen!')
.setDescription(welcome.embedDescription || 'Schön, dass du da bist.')
.setColor(isNaN(colorVal) ? 0x00ff99 : colorVal)
.setFooter({ text: welcome.embedFooter || '' });
const files: any[] = [];
if (welcome.embedThumbnailData && welcome.embedThumbnailData.startsWith('data:')) {
const [meta, b64] = welcome.embedThumbnailData.split(',');
const ext = meta.includes('gif') ? 'gif' : 'png';
const buf = Buffer.from(b64, 'base64');
const name = `welcome-thumb.${ext}`;
files.push({ attachment: buf, name });
embed.setThumbnail(`attachment://${name}`);
} else if (welcome.embedThumbnail) {
embed.setThumbnail(welcome.embedThumbnail);
}
if (welcome.embedImageData && welcome.embedImageData.startsWith('data:')) {
const [meta, b64] = welcome.embedImageData.split(',');
const ext = meta.includes('gif') ? 'gif' : 'png';
const buf = Buffer.from(b64, 'base64');
const name = `welcome-image.${ext}`;
files.push({ attachment: buf, name });
embed.setImage(`attachment://${name}`);
} else if (welcome.embedImage) {
embed.setImage(welcome.embedImage);
}
await channel.send({ embeds: [embed], files }).catch(async () => {
await interaction.reply({ content: 'Senden der Willkommensnachricht ist fehlgeschlagen.', ephemeral: true });
});
if (!interaction.replied && !interaction.deferred) {
await interaction.reply({ content: 'Willkommensnachricht gesendet.', ephemeral: true });
}
}
};
export default command;