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
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 20s
This commit is contained in:
@@ -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)`.
|
||||
|
||||
10
README.md
10
README.md
@@ -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.
|
||||
|
||||
@@ -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,8 +376,10 @@ const command: Command = {
|
||||
}
|
||||
|
||||
durationMs = duration * multiplier * 1000;
|
||||
|
||||
// Human-readable duration
|
||||
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',
|
||||
@@ -406,7 +389,8 @@ const command: Command = {
|
||||
month: 'Monate',
|
||||
year: 'Jahre'
|
||||
};
|
||||
durationText = `${duration} ${unitNames[unit!]}`;
|
||||
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);
|
||||
|
||||
@@ -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ü')
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -29,6 +29,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (selectedValue.startsWith('remove_')) {
|
||||
const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id);
|
||||
let removedCount = 0;
|
||||
@@ -63,7 +64,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
const discordRole = guild.roles.cache.get(selectedValue);
|
||||
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;
|
||||
@@ -115,6 +116,16 @@ export default {
|
||||
});
|
||||
}
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
26
src/structures/EventLogger.ts
Normal file
26
src/structures/EventLogger.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user