fix & refactor: centralize event logging, add DB indexes, fix mute timeout, role menu & owner permission bugs
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 20s

This commit is contained in:
sarah
2026-08-02 22:53:32 +02:00
parent 0e1630f80b
commit 5c40faa5c9
20 changed files with 245 additions and 261 deletions

View File

@@ -17,8 +17,9 @@ The bot uses a **Modular Command & Event Loading** pattern with **ESM (ECMAScrip
8. **Reminder System:** Periodic reminder checking with `target_time` tracking.
9. **Auto-Response System:** Trigger word detection for automatic replies (cached).
10. **Welcome/Goodbye System:** Guild member add/remove events with customizable messages.
11. **Logging System:** Configurable event logging (messages, roles, moderation, etc.).
11. **Logging System:** Configurable event logging (messages, roles, moderation, etc.) via centralized `EventLogger`.
12. **Role Selection System:** Self-service role assignment via select menus with support for exclusive categories and max-role limits.
13. **Centralized Event Logger:** Kapsel event logs into `EventLogger.sendLog()` to eliminate duplicated logging logic across event files.
## 📁 Project Structure
@@ -34,7 +35,8 @@ pixelpoebel/
│ └── structures/
│ ├── ExtendedClient.ts # Typed client with DB property
│ ├── Command.ts
│ ├── Database.ts # Database with Transaction & Surgical Caching
│ ├── Database.ts # Database with Transaction, Indexes & Surgical Caching
│ ├── EventLogger.ts # Centralized event logging helper
│ ├── TwitchManager.ts # Batch Polling & Transaction logic
│ ├── TwitchMonitor.ts # IRC Monitoring & Webhook reuse
│ ├── TwitchCache.ts # IRC Message FIFO Cache
@@ -53,7 +55,7 @@ pixelpoebel/
### 1. Database with Surgical Caching
The database uses WAL mode and foreign keys. Caching is surgical (clears specific guild entries when possible).
The database uses WAL mode, foreign keys, and performance indexes for frequent lookups. Caching is surgical (clears specific guild entries when possible).
```typescript
// src/structures/Database.ts
@@ -63,7 +65,7 @@ export class DB {
static init() {
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
// ... Table creation ...
// ... Table creation & indexes ...
}
static run(query: string, ...params: any[]) {
@@ -144,6 +146,11 @@ if (process.env.AUTO_DEPLOY !== 'false') {
2. **Foreign Keys:** Always enable `foreign_keys = ON` to maintain data integrity.
3. **Type Safety:** Use `ExtendedClient` instead of `any` for the client instance.
4. **Webhook Reuse:** Map `WebhookClient` instances to their monitor keys to prevent leaks.
5. **Partial Message Handling:** In `messageDelete` and `messageUpdate` events, always check for null `author` property to avoid runtime crashes on uncached partial messages.
5. **Partial Message Handling:** In `messageDelete` and `messageUpdate` events, always handle partial messages safely (e.g. `message.author` or `message.content` may be missing for uncached messages).
6. **Centralized Settings Cache:** Always use `client.DB.getSettings(guildId)` instead of raw SQLite select queries for `guild_settings` to leverage central caching and automatic default generation.
7. **Category Role Selection Limits:** Do not count general guild roles when checking role selection limits; filter the user's role list using only the specific role IDs registered under that category.
8. **Event Logger Centralization:** Use `EventLogger.sendLog(client, guildId, event, embed)` for logging server events instead of inline/duplicated helper functions.
9. **Discord Timeout Limit:** Discord API limits timeouts to maximum 28 days (`2419200000 ms`). Never pass `null` to `member.timeout(durationMs)` when muting.
10. **Role Menu Error Handling:** Always wrap `member.roles.add` and `member.roles.remove` in `try/catch` blocks in interaction listeners to handle permission/hierarchy errors gracefully.
11. **Select Menu Emoji Handling:** Never pass an empty string `''` to `setEmoji()`. Only call `setEmoji()` when a valid, non-empty emoji string is provided.
12. **Team Application Owner Support:** When checking bot owner status, check `process.env.BOT_OWNER_ID` or test if `client.application.owner` is a `Team` object with `owner.members.has(userId)`.

View File

@@ -140,11 +140,13 @@ npm run deploy
## 📡 Features & Optimierungen
-**Modular Architecture:** TypeScript-basiert, ESM-Unterstützung, strikte Typisierung.
-**High Performance Database:** SQLite mit **WAL-Modus**, **Fremdschlüssel-Unterstützung** und **chirurgischem In-Memory Caching** für Guild-Settings und Trigger.
-**High Performance Database:** SQLite mit **WAL-Modus**, **Fremdschlüssel-Unterstützung**, **Datenbank-Indizes** (für schnelle Abfragen von Mod-Logs, Timern und Twitch-Monitoren) und **chirurgischem In-Memory Caching** für Guild-Settings und Trigger.
-**Centralized Event Logging:** Ein zentrales `EventLogger`-System zur strukturierten und verlässlichen Erfassung von Server-Ereignissen (Nachrichten, Rollen, Nicknames, Kanäle, Kicks, Bans, Warnungen).
-**Optimized Twitch Polling:** Nutzt **Batch-Requests** (100 Kanäle pro Request), Datenbank-Transaktionen und ein 2-Minuten-Intervall für zeitnahe Benachrichtigungen.
-**TwitchMonitor IRC:** Echtzeit IRC Chat-Logging (Mod-Events) via **wiederverwendbaren Webhooks** und FIFO-Cache.
-**Advanced Moderation:** Warn-System mit Auto-Actions, umfangreiches Purge-System.
-**Role Selection:** Self-Service Rollen-System über Discord Select Menus.
-**Advanced Moderation & Safety:** Warn-System mit Auto-Actions, umfangreiches Purge-System, abgesicherte Timeout-Limits (max. 28 Tage gemäß Discord-API) und vollständiges Exception handling.
-**Role Selection:** Self-Service Rollen-System über Discord Select Menus mit automatischer Berechtigungsprüfung und Fehlerabfangung.
-**Smart Auto-Response System:** Präzises Trigger-Wort-Matching mit Wortgrenzen-Regex (`\b`), um Falsch-Positive zu vermeiden.
-**Reminders:** Intelligentes Zeit-Parsing (z.B. "morgen 15:00", "30min").
-**Event Logging:** Detaillierte Erfassung von Server-Ereignissen.
-**Developer & Team Support:** Unterstützung von Discord Developer Teams und `BOT_OWNER_ID` Fallback für Owner-Befehle.
-**Docker Support:** Containerisiertes Deployment inklusive Volume-Mounts für Daten.

View File

@@ -1,27 +1,7 @@
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, GuildMember } from 'discord.js';
import { Command } from '../../structures/Command.js';
import { ExtendedClient } from '../../structures/ExtendedClient.js';
async function sendModerationLog(client: ExtendedClient, guildId: string, event: string, embed: EmbedBuilder) {
const DB = client.DB;
const settings = DB.getSettings(guildId);
if (!settings) return;
if (!settings.log_events) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
if (!settings.log_channel) return;
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as any).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending moderation log (${event}):`, error);
}
}
import { EventLogger } from '../../structures/EventLogger.js';
const command: Command = {
data: new SlashCommandBuilder()
@@ -256,7 +236,7 @@ const command: Command = {
{ name: 'Grund', value: reason, inline: false }
)
.setTimestamp();
await sendModerationLog(interaction.client, guildId, 'kicks', embed);
await EventLogger.sendLog(interaction.client, guildId, 'kicks', embed);
} catch (error) {
console.error(error);
await interaction.reply({ content: '❌ Fehler beim Kicken.', ephemeral: true });
@@ -330,7 +310,7 @@ const command: Command = {
{ name: 'Warnungen', value: `${warnCount}/${warnThreshold}`, inline: true }
)
.setTimestamp();
await sendModerationLog(interaction.client, guildId, 'warns', embed);
await EventLogger.sendLog(interaction.client, guildId, 'warns', embed);
return;
}
@@ -373,10 +353,11 @@ const command: Command = {
// Parse duration
let durationMs: number | null = null;
let durationText = '';
const MAX_TIMEOUT_MS = 28 * 24 * 60 * 60 * 1000; // 28 days max Discord timeout
if (unit === 'perm') {
durationMs = null; // Permanent = null
durationText = 'Permanent';
durationMs = MAX_TIMEOUT_MS;
durationText = '28 Tage (Maximaler Discord Timeout)';
} else if (duration) {
const unitSeconds: Record<string, number> = {
sec: 1,
@@ -395,18 +376,21 @@ const command: Command = {
}
durationMs = duration * multiplier * 1000;
// Human-readable duration
const unitNames: Record<string, string> = {
sec: 'Sekunden',
min: 'Minuten',
hour: 'Stunden',
day: 'Tage',
week: 'Wochen',
month: 'Monate',
year: 'Jahre'
};
durationText = `${duration} ${unitNames[unit!]}`;
if (durationMs > MAX_TIMEOUT_MS) {
durationMs = MAX_TIMEOUT_MS;
durationText = `${duration} ${unit} (auf 28 Tage beschränkt)`;
} else {
const unitNames: Record<string, string> = {
sec: 'Sekunden',
min: 'Minuten',
hour: 'Stunden',
day: 'Tage',
week: 'Wochen',
month: 'Monate',
year: 'Jahre'
};
durationText = `${duration} ${unitNames[unit!] || unit}`;
}
} else {
await interaction.reply({ content: '❌ Dauer oder Zeiteinheit angeben.', ephemeral: true });
return;
@@ -426,7 +410,7 @@ const command: Command = {
{ name: 'Grund', value: reason, inline: false }
)
.setTimestamp();
await sendModerationLog(interaction.client, guildId, 'mutes', embed);
await EventLogger.sendLog(interaction.client, guildId, 'mutes', embed);
} catch (error) {
console.error(error);
await interaction.reply({ content: '❌ Fehler beim Muten.', ephemeral: true });
@@ -481,7 +465,7 @@ const command: Command = {
{ name: 'Nachrichten gelöscht', value: `${deleteDays} Tage`, inline: true }
)
.setTimestamp();
await sendModerationLog(interaction.client, guildId, 'bans', embed);
await EventLogger.sendLog(interaction.client, guildId, 'bans', embed);
} catch (error) {
console.error(error);
await interaction.reply({ content: '❌ Fehler beim Bannen.', ephemeral: true });
@@ -659,7 +643,7 @@ const command: Command = {
purgeSubcommand === 'user' ? 'Nach Nutzer' : 'Alle', inline: true }
)
.setTimestamp();
await sendModerationLog(interaction.client, guildId, 'messages', embed);
await EventLogger.sendLog(interaction.client, guildId, 'messages', embed);
}
} catch (error) {
console.error('[ADMIN] Purge error:', error);

View File

@@ -10,8 +10,23 @@ const command: Command = {
async execute(interaction: any, client: any) {
if (!client.application?.owner) await client.application?.fetch();
const isOwner = interaction.user.id === client.application?.owner?.id;
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.Administrator);
const owner = client.application?.owner;
const envOwnerId = process.env.BOT_OWNER_ID;
let isOwner = false;
if (envOwnerId && interaction.user.id === envOwnerId) {
isOwner = true;
} else if (owner) {
if ('members' in owner && owner.members) {
isOwner = owner.members.has(interaction.user.id);
} else {
isOwner = interaction.user.id === owner.id;
}
}
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers) ||
interaction.memberPermissions?.has(PermissionFlagsBits.Administrator) ||
interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild);
const embed = new EmbedBuilder()
.setTitle('📖 Hilfe-Menü')

View File

@@ -50,7 +50,8 @@ const command: Command = {
.addSubcommand(subcommand =>
subcommand
.setName('help')
.setDescription('Hilfe zu Log-Befehlen (Mod).')),
.setDescription('Hilfe zu Log-Befehlen (Mod).'))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
category: 'Admin',
async execute(interaction: any) {
const subcommand = interaction.options.getSubcommand();

View File

@@ -27,8 +27,22 @@ const command: Command = {
async execute(interaction: any, client: any) {
if (!client.application?.owner) await client.application?.fetch();
if (interaction.user.id !== client.application?.owner?.id) {
await interaction.reply({ content: 'Keine Berechtigung.', ephemeral: true });
const owner = client.application?.owner;
const envOwnerId = process.env.BOT_OWNER_ID;
let isOwner = false;
if (envOwnerId && interaction.user.id === envOwnerId) {
isOwner = true;
} else if (owner) {
if ('members' in owner && owner.members) {
isOwner = owner.members.has(interaction.user.id);
} else {
isOwner = interaction.user.id === owner.id;
}
}
if (!isOwner) {
await interaction.reply({ content: 'Keine Berechtigung. Nur für den Bot-Owner.', ephemeral: true });
return;
}

View File

@@ -19,12 +19,15 @@ export async function updateRoleMessage(client: any, category: any) {
return;
}
const roleOptions = options.map((opt: any) =>
new StringSelectMenuOptionBuilder()
const roleOptions = options.map((opt: any) => {
const menuOption = new StringSelectMenuOptionBuilder()
.setLabel(opt.label.replace(/^[^\s]+\s/, ''))
.setValue(opt.role_id)
.setEmoji(opt.emoji || '')
);
.setValue(opt.role_id);
if (opt.emoji && opt.emoji.trim().length > 0) {
menuOption.setEmoji(opt.emoji.trim());
}
return menuOption;
});
roleOptions.push(
new StringSelectMenuOptionBuilder()
@@ -144,7 +147,8 @@ const command: Command = {
.addSubcommand(subcommand =>
subcommand
.setName('list')
.setDescription('Listet alle Rollen-Kategorien auf')),
.setDescription('Listet alle Rollen-Kategorien auf'))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
category: 'Admin',
async execute(interaction: any) {
const subcommand = interaction.options.getSubcommand();

View File

@@ -32,7 +32,8 @@ const command: Command = {
.addSubcommand(subcommand =>
subcommand
.setName('help')
.setDescription('Zeigt Hilfe zu den Trigger-Befehlen an.')),
.setDescription('Zeigt Hilfe zu den Trigger-Befehlen an.'))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
category: 'Admin',
async execute(interaction: any) {
const subcommand = interaction.options.getSubcommand();

View File

@@ -34,7 +34,8 @@ const command: Command = {
.addSubcommand(subcommand =>
subcommand
.setName('help')
.setDescription('Zeigt Hilfe zu den TwitchMonitor-Befehlen')),
.setDescription('Zeigt Hilfe zu den TwitchMonitor-Befehlen'))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
category: 'Admin',
async execute(interaction: any) {
const subcommand = interaction.options.getSubcommand();

View File

@@ -48,7 +48,8 @@ const command: Command = {
.addSubcommand(subcommand =>
subcommand
.setName('help')
.setDescription('Hilfe zu Welcome-Befehlen.')),
.setDescription('Hilfe zu Welcome-Befehlen.'))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
category: 'Admin',
async execute(interaction: any) {
const subcommand = interaction.options.getSubcommand();

View File

@@ -1,21 +1,5 @@
import { Events, GuildChannel, EmbedBuilder } from 'discord.js';
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
const settings = client.DB.getSettings(guildId);
if (!settings?.log_events || !settings?.log_channel) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as any).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending log (${event}):`, error);
}
}
import { EventLogger } from '../structures/EventLogger.js';
export default {
name: Events.ChannelCreate,
@@ -31,6 +15,6 @@ export default {
)
.setTimestamp();
await sendLog(channel.client, channel.guild.id, 'channels', embed);
await EventLogger.sendLog(channel.client as any, channel.guild.id, 'channels', embed);
},
};

View File

@@ -1,21 +1,5 @@
import { Events, GuildChannel, EmbedBuilder } from 'discord.js';
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
const settings = client.DB.getSettings(guildId);
if (!settings?.log_events || !settings?.log_channel) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as any).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending log (${event}):`, error);
}
}
import { EventLogger } from '../structures/EventLogger.js';
export default {
name: Events.ChannelDelete,
@@ -31,6 +15,6 @@ export default {
)
.setTimestamp();
await sendLog(channel.client, channel.guild.id, 'channels', embed);
await EventLogger.sendLog(channel.client as any, channel.guild.id, 'channels', embed);
},
};

View File

@@ -1,24 +1,5 @@
import { Events, GuildMember, TextChannel, EmbedBuilder } from 'discord.js';
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
const DB = client.DB;
const settings = DB.getSettings(guildId);
if (!settings?.log_events) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
if (!settings?.log_channel) return;
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as any).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending log (${event}):`, error);
}
}
import { EventLogger } from '../structures/EventLogger.js';
export default {
name: Events.GuildMemberRemove,
@@ -36,7 +17,7 @@ export default {
)
.setTimestamp();
await sendLog(member.client, guildId, 'leaves', embed);
await EventLogger.sendLog(member.client as any, guildId, 'leaves', embed);
// Send goodbye message if configured
const settings = DB.getSettings(guildId);

View File

@@ -1,21 +1,5 @@
import { Events, GuildMember, EmbedBuilder } from 'discord.js';
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
const settings = client.DB.getSettings(guildId);
if (!settings?.log_events || !settings?.log_channel) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as any).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending log (${event}):`, error);
}
}
import { EventLogger } from '../structures/EventLogger.js';
export default {
name: Events.GuildMemberUpdate,
@@ -36,7 +20,7 @@ export default {
)
.setTimestamp();
await sendLog(newMember.client, guildId, 'nicks', embed);
await EventLogger.sendLog(newMember.client as any, guildId, 'nicks', embed);
}
// Role changes
@@ -46,30 +30,30 @@ export default {
const addedRoles = newRoles.filter(r => !oldRoles.has(r.id));
const removedRoles = oldRoles.filter(r => !newRoles.has(r.id));
for (const role of addedRoles) {
for (const role of addedRoles.values()) {
const embed = new EmbedBuilder()
.setTitle('🎭 Rolle hinzugefügt')
.setColor(0x27ae60)
.addFields(
{ name: 'Nutzer', value: `${newMember.user.tag} (<@${newMember.id}>)`, inline: true },
{ name: 'Rolle', value: `${role[1].name} (<@&${role[1].id}>)`, inline: true }
{ name: 'Rolle', value: `${role.name} (<@&${role.id}>)`, inline: true }
)
.setTimestamp();
await sendLog(newMember.client, guildId, 'roles', embed);
await EventLogger.sendLog(newMember.client as any, guildId, 'roles', embed);
}
for (const role of removedRoles) {
for (const role of removedRoles.values()) {
const embed = new EmbedBuilder()
.setTitle('🎭 Rolle entfernt')
.setColor(0xe74c3c)
.addFields(
{ name: 'Nutzer', value: `${newMember.user.tag} (<@${newMember.id}>)`, inline: true },
{ name: 'Rolle', value: `${role[1].name} (<@&${role[1].id}>)`, inline: true }
{ name: 'Rolle', value: `${role.name} (<@&${role.id}>)`, inline: true }
)
.setTimestamp();
await sendLog(newMember.client, guildId, 'roles', embed);
await EventLogger.sendLog(newMember.client as any, guildId, 'roles', embed);
}
},
};

View File

@@ -29,93 +29,104 @@ export default {
return;
}
if (selectedValue.startsWith('remove_')) {
const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id);
let removedCount = 0;
for (const opt of options) {
if (member.roles.cache.has(opt.role_id)) {
await member.roles.remove(opt.role_id);
removedCount++;
}
}
if (removedCount > 0) {
await interaction.reply({
content: `Alle Rollen aus "${categoryName}" wurden entfernt.`,
ephemeral: true
});
} else {
await interaction.reply({
content: `Du hast keine Rollen aus "${categoryName}".`,
ephemeral: true
});
}
return;
}
const option: any = client.DB.get(
'SELECT * FROM role_options WHERE category_id = ? AND role_id = ?',
category.id,
selectedValue
);
if (!option) {
await interaction.reply({ content: 'Rolle nicht gefunden.', ephemeral: true });
return;
}
const discordRole = guild.roles.cache.get(selectedValue);
if (!discordRole) {
await interaction.reply({ content: 'Diese Rolle existiert nicht mehr.', ephemeral: true });
return;
}
const hasRole = member.roles.cache.has(selectedValue);
if (category.exclusive) {
if (hasRole) {
await member.roles.remove(selectedValue);
await interaction.reply({
content: `Die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name} wurde entfernt.`,
ephemeral: true
});
} else {
try {
if (selectedValue.startsWith('remove_')) {
const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id);
let removedCount = 0;
for (const opt of options) {
if (opt.role_id !== selectedValue && member.roles.cache.has(opt.role_id)) {
if (member.roles.cache.has(opt.role_id)) {
await member.roles.remove(opt.role_id);
removedCount++;
}
}
await member.roles.add(selectedValue);
await interaction.reply({
content: `Du hast jetzt die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name}!`,
ephemeral: true
});
}
} else {
if (hasRole) {
await member.roles.remove(selectedValue);
await interaction.reply({
content: `Die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name} wurde entfernt.`,
ephemeral: true
});
} else {
const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id);
const userCategoryRoles = options.filter((opt: any) => member.roles.cache.has(opt.role_id)).length;
if (category.max_roles > 0 && userCategoryRoles >= category.max_roles) {
if (removedCount > 0) {
await interaction.reply({
content: `Du kannst maximal ${category.max_roles} Rollen aus dieser Kategorie haben.`,
content: `Alle Rollen aus "${categoryName}" wurden entfernt.`,
ephemeral: true
});
} else {
await interaction.reply({
content: `Du hast keine Rollen aus "${categoryName}".`,
ephemeral: true
});
return;
}
await member.roles.add(selectedValue);
await interaction.reply({
content: `Du hast jetzt die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name}!`,
ephemeral: true
});
return;
}
const option: any = client.DB.get(
'SELECT * FROM role_options WHERE category_id = ? AND role_id = ?',
category.id,
selectedValue
);
if (!option) {
await interaction.reply({ content: 'Rolle nicht gefunden.', ephemeral: true });
return;
}
const discordRole = guild.roles.cache.get(selectedValue) || await guild.roles.fetch(selectedValue).catch(() => null);
if (!discordRole) {
await interaction.reply({ content: 'Diese Rolle existiert nicht mehr.', ephemeral: true });
return;
}
const hasRole = member.roles.cache.has(selectedValue);
if (category.exclusive) {
if (hasRole) {
await member.roles.remove(selectedValue);
await interaction.reply({
content: `Die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name} wurde entfernt.`,
ephemeral: true
});
} else {
const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id);
for (const opt of options) {
if (opt.role_id !== selectedValue && member.roles.cache.has(opt.role_id)) {
await member.roles.remove(opt.role_id);
}
}
await member.roles.add(selectedValue);
await interaction.reply({
content: `Du hast jetzt die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name}!`,
ephemeral: true
});
}
} else {
if (hasRole) {
await member.roles.remove(selectedValue);
await interaction.reply({
content: `Die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name} wurde entfernt.`,
ephemeral: true
});
} else {
const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id);
const userCategoryRoles = options.filter((opt: any) => member.roles.cache.has(opt.role_id)).length;
if (category.max_roles > 0 && userCategoryRoles >= category.max_roles) {
await interaction.reply({
content: `Du kannst maximal ${category.max_roles} Rollen aus dieser Kategorie haben.`,
ephemeral: true
});
return;
}
await member.roles.add(selectedValue);
await interaction.reply({
content: `Du hast jetzt die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name}!`,
ephemeral: true
});
}
}
} catch (error) {
console.error('[ROLESETUP] Role assignment error:', error);
const errorMsg = '❌ Fehler beim Ändern der Rolle. Mir fehlen eventuell die Rechte dazu.';
if (interaction.replied || interaction.deferred) {
await interaction.followUp({ content: errorMsg, ephemeral: true });
} else {
await interaction.reply({ content: errorMsg, ephemeral: true });
}
}
return;
return;
}
}

View File

@@ -10,10 +10,16 @@ export default {
if (triggers.length === 0) return;
const contentLower = message.content.toLowerCase();
function escapeRegex(str: string) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
for (const trigger of triggers) {
if (contentLower.includes(trigger.trigger_word.toLowerCase())) {
const word = trigger.trigger_word.trim().toLowerCase();
if (!word) continue;
const pattern = new RegExp(`(?:^|\\s|\\b)${escapeRegex(word)}(?:$|\\s|\\b)`, 'i');
if (pattern.test(message.content)) {
await message.channel.send(trigger.response_text);
return;
}

View File

@@ -1,44 +1,33 @@
import { Events, Message, TextChannel, EmbedBuilder } from 'discord.js';
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
const settings = client.DB.getSettings(guildId);
if (!settings?.log_events || !settings?.log_channel) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as any).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending log (${event}):`, error);
}
}
import { Events, Message, EmbedBuilder } from 'discord.js';
import { EventLogger } from '../structures/EventLogger.js';
export default {
name: Events.MessageDelete,
async execute(message: Message) {
if (!message.guild || !message.author || message.author.bot) return;
if (!message.guild) return;
if (message.author?.bot) return;
const authorTag = message.author ? `${message.author.tag} (<@${message.author.id}>)` : 'Unbekannter Nutzer (Nicht im Cache)';
const embed = new EmbedBuilder()
.setTitle('🗑️ Nachricht gelöscht')
.setColor(0xe74c3c)
.addFields(
{ name: 'Autor', value: `${message.author.tag} (<@${message.author.id}>)`, inline: true },
{ name: 'Autor', value: authorTag, inline: true },
{ name: 'Kanal', value: `<#${message.channelId}>`, inline: true }
)
.setTimestamp();
if (message.content) {
embed.addFields({ name: 'Inhalt', value: message.content.substring(0, 1024), inline: false });
} else {
embed.addFields({ name: 'Inhalt', value: '*(Nachricht nicht im Cache oder enthielt nur Medien)*', inline: false });
}
if (message.attachments.size > 0) {
if (message.attachments && message.attachments.size > 0) {
embed.addFields({ name: 'Anhänge', value: `${message.attachments.size} Datei(en)`, inline: true });
}
await sendLog(message.client, message.guildId!, 'messages', embed);
await EventLogger.sendLog(message.client as any, message.guildId!, 'messages', embed);
},
};

View File

@@ -1,21 +1,5 @@
import { Events, Message, EmbedBuilder } from 'discord.js';
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
const settings = client.DB.getSettings(guildId);
if (!settings?.log_events || !settings?.log_channel) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as any).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending log (${event}):`, error);
}
}
import { EventLogger } from '../structures/EventLogger.js';
export default {
name: Events.MessageUpdate,
@@ -37,6 +21,6 @@ export default {
)
.setTimestamp();
await sendLog(newMessage.client, newMessage.guildId!, 'messages', embed);
await EventLogger.sendLog(newMessage.client as any, newMessage.guildId!, 'messages', embed);
},
};

View File

@@ -151,6 +151,11 @@ export class DB {
)
`).run();
// Performance Indexes
db.prepare('CREATE INDEX IF NOT EXISTS idx_mod_logs_guild_user ON mod_logs(guild_id, user_id, action)').run();
db.prepare('CREATE INDEX IF NOT EXISTS idx_reminders_target ON reminders(target_time)').run();
db.prepare('CREATE INDEX IF NOT EXISTS idx_twitch_monitors_guild ON twitch_monitors(guild_id)').run();
// Migration logic
const columns = db.prepare("PRAGMA table_info(guild_settings)").all() as any[];
const columnNames = columns.map((c: any) => c.name);

View File

@@ -0,0 +1,26 @@
import { EmbedBuilder, TextChannel } from 'discord.js';
import { ExtendedClient } from './ExtendedClient.js';
export class EventLogger {
static async sendLog(client: ExtendedClient, guildId: string, event: string, embed: EmbedBuilder) {
if (!guildId) return;
const settings = client.DB?.getSettings(guildId);
if (!settings?.log_events || !settings?.log_channel) return;
const activeEvents = settings.log_events.split(',').filter(Boolean);
if (!activeEvents.includes(event)) return;
const guild = client.guilds.cache.get(guildId);
if (!guild) return;
const channel = guild.channels.cache.get(settings.log_channel);
if (!channel || !channel.isTextBased()) return;
try {
await (channel as TextChannel).send({ embeds: [embed] });
} catch (error) {
console.error(`[LOG] Error sending log (${event}) in guild ${guildId}:`, error);
}
}
}