fix: make trigger list public and ephemeral, fix rolesetup types, and improve bot safety
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 28s
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 28s
- Make /trigger list and help public subcommands with ephemeral replies - Restrict /trigger add and remove to moderators/admins - Use Unicode-aware regex and strip URLs before trigger word matching - Ensure guild_settings exist before UPDATE queries in welcome and log commands - Fix rolesetup edit neue_rolle type to addRoleOption and prevent label truncation - Reset select menu components after selection so users can re-click same options - Validate Twitch streamers via Helix Users API and prevent interaction timeouts with deferReply - Connect TwitchMonitor automatically on startup and when first channel is added - Add moderation role-hierarchy checks to admin commands and enforce timeout limits - Disable DM execution on server-bound commands to prevent SQLite null constraint errors - Update README.md and AGENTS-BOT.md with new constraints and command documentation
This commit is contained in:
@@ -158,3 +158,9 @@ if (process.env.AUTO_DEPLOY !== 'false') {
|
|||||||
11. **Select Menu Emoji Handling:** Never pass an empty string `''` to `setEmoji()`. Only call `setEmoji()` when a valid, non-empty emoji string is provided.
|
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)`.
|
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)`.
|
||||||
13. **Stopwatch Duplicate Prevention & Custom Start Time:** When creating stopwatches, perform a case-insensitive name uniqueness check per user/guild (`LOWER(name)`). Validate custom start times (`startzeit`) to ensure they do not lie in the future.
|
13. **Stopwatch Duplicate Prevention & Custom Start Time:** When creating stopwatches, perform a case-insensitive name uniqueness check per user/guild (`LOWER(name)`). Validate custom start times (`startzeit`) to ensure they do not lie in the future.
|
||||||
|
14. **Public vs. Mod Subcommands & Ephemeral Output:** For commands offering both public query options and mod-only actions (e.g. `/trigger`, `/timer`, `/twitch`), do not set `defaultMemberPermissions` on the root SlashCommandBuilder if subcommands like `list` or `help` should be public. Check `isAdmin` inside mutating subcommands (`add`, `remove`), and deliver `list`/`help` subcommands with `ephemeral: true`.
|
||||||
|
15. **Server-Bound Commands & DM Isolation:** Commands relying on guild context (`guildId`, roles, settings) must enforce `.setDMPermission(false)` and validate `if (!interaction.guildId) return;` to prevent SQLite null constraint errors in direct messages.
|
||||||
|
16. **Guild Settings Provisioning:** Before issuing raw SQL updates on `guild_settings`, call `client.DB.getSettings(guildId)` to ensure the guild settings record exists in SQLite.
|
||||||
|
17. **Unicode-Safe Trigger Matching & URL Filtering:** In `messageCreate`, strip URLs before matching auto-responses and use Unicode property escapes (`(?<![\p{L}\p{N}_])...(?![\p{L}\p{N}_])` with `iu`) to handle umlauts and avoid URL false positives.
|
||||||
|
18. **Role Option Select Menus & Typing:** Subcommand options taking a Discord role to edit must use `.addRoleOption()`. After role select interactions, re-render/reset components so users can click the same option again to toggle or remove it.
|
||||||
|
19. **Twitch Helix User Validation & Interaction Deferral:** Commands validating Twitch streamers must use `interaction.deferReply()` to avoid 3-second interaction timeouts and validate user accounts via the Helix Users API (`/helix/users`).
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -79,8 +79,8 @@ npm run deploy
|
|||||||
- `/twitch help` – Twitch-Hilfe
|
- `/twitch help` – Twitch-Hilfe
|
||||||
- `/timer list` – Timer dieses Kanals
|
- `/timer list` – Timer dieses Kanals
|
||||||
- `/timer help` – Timer-Hilfe
|
- `/timer help` – Timer-Hilfe
|
||||||
- `/trigger list` – Alle Trigger anzeigen
|
- `/trigger list` – Alle Trigger anzeigen (ephemeral)
|
||||||
- `/trigger help` – Trigger-Hilfe
|
- `/trigger help` – Trigger-Hilfe (ephemeral)
|
||||||
|
|
||||||
### Timer (Erinnerungen)
|
### Timer (Erinnerungen)
|
||||||
- `/timer add <zeit> <nachricht>` – Timer hinzufügen
|
- `/timer add <zeit> <nachricht>` – Timer hinzufügen
|
||||||
@@ -99,8 +99,8 @@ npm run deploy
|
|||||||
### Trigger (Auto-Antworten)
|
### Trigger (Auto-Antworten)
|
||||||
- `/trigger add <wort> <antwort>` – Trigger hinzufügen (Mod)
|
- `/trigger add <wort> <antwort>` – Trigger hinzufügen (Mod)
|
||||||
- `/trigger remove <wort>` – Trigger entfernen (Mod)
|
- `/trigger remove <wort>` – Trigger entfernen (Mod)
|
||||||
- `/trigger list` – Alle Trigger anzeigen
|
- `/trigger list` – Alle Trigger anzeigen (Für alle User, ephemeral)
|
||||||
- `/trigger help` – Trigger-Hilfe
|
- `/trigger help` – Trigger-Hilfe (Für alle User, ephemeral)
|
||||||
|
|
||||||
### Twitch
|
### Twitch
|
||||||
- `/twitch online <kanal>` – Prüft ob Kanal online ist
|
- `/twitch online <kanal>` – Prüft ob Kanal online ist
|
||||||
@@ -119,12 +119,26 @@ npm run deploy
|
|||||||
### Rollen-Auswahl
|
### Rollen-Auswahl
|
||||||
- `/rolesetup create <#kanal> <name>` – Rollen-Auswahl Message erstellen
|
- `/rolesetup create <#kanal> <name>` – Rollen-Auswahl Message erstellen
|
||||||
- `/rolesetup add <kategorie> <@rolle> [emoji]` – Rolle zur Auswahl hinzufügen
|
- `/rolesetup add <kategorie> <@rolle> [emoji]` – Rolle zur Auswahl hinzufügen
|
||||||
- `/rolesetup edit <kategorie> <rolle> [neues_emoji]` – Emoji ändern
|
- `/rolesetup edit <kategorie> <rolle> [neue_rolle] [neues_emoji]` – Rolle oder Emoji ändern
|
||||||
- `/rolesetup remove <kategorie> <rolle>` – Rolle entfernen
|
- `/rolesetup remove <kategorie> <rolle>` – Rolle entfernen
|
||||||
- `/rolesetup delete <kategorie>` – Kategorie löschen
|
- `/rolesetup delete <kategorie>` – Kategorie löschen
|
||||||
- `/rolesetup config <kategorie> max:1 exclusive:true` – Einstellungen
|
- `/rolesetup config <kategorie> max:1 exclusive:true` – Einstellungen
|
||||||
- `/rolesetup list` – Alle Kategorien anzeigen
|
- `/rolesetup list` – Alle Kategorien anzeigen
|
||||||
|
|
||||||
|
### Welcome & Goodbye
|
||||||
|
- `/welcome setchannel <#kanal>` – Kanal für Willkommen/Abschied setzen (Mod)
|
||||||
|
- `/welcome add <welcome|goodbye> <nachricht>` – Nachricht setzen (Mod)
|
||||||
|
- `/welcome remove <welcome|goodbye>` – Nachricht entfernen (Mod)
|
||||||
|
- `/welcome list` – Aktuelle Einstellungen anzeigen (Mod)
|
||||||
|
- `/welcome help` – Welcome-Hilfe
|
||||||
|
|
||||||
|
### Log (Server-Logs)
|
||||||
|
- `/log setchannel <#kanal>` – Log-Kanal setzen (Mod)
|
||||||
|
- `/log enable <event>` – Event zum Loggen aktivieren (Mod)
|
||||||
|
- `/log disable <event>` – Event deaktivieren (Mod)
|
||||||
|
- `/log list` – Aktive Log-Events anzeigen (Mod)
|
||||||
|
- `/log help` – Log-Hilfe
|
||||||
|
|
||||||
### Admin
|
### Admin
|
||||||
- `/admin kick <user>` – Nutzer kicken
|
- `/admin kick <user>` – Nutzer kicken
|
||||||
- `/admin warn <user> <reason>` – Nutzer verwarnen
|
- `/admin warn <user> <reason>` – Nutzer verwarnen
|
||||||
@@ -153,9 +167,9 @@ npm run deploy
|
|||||||
- ✅ **Centralized Event Logging:** Ein zentrales `EventLogger`-System zur strukturierten und verlässlichen Erfassung von Server-Ereignissen (Nachrichten, Rollen, Nicknames, Kanäle, Kicks, Bans, Warnungen).
|
- ✅ **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.
|
- ✅ **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.
|
- ✅ **TwitchMonitor IRC:** Echtzeit IRC Chat-Logging (Mod-Events) via **wiederverwendbaren Webhooks** und FIFO-Cache.
|
||||||
- ✅ **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.
|
- ✅ **Advanced Moderation & Safety:** Warn-System mit Auto-Actions, umfangreiches Purge-System, Rollen-Hierarchie-Schutz (Moderatoren können sich oder höherrangige Rollen nicht kicken/bannen/muten), 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.
|
- ✅ **Role Selection:** Self-Service Rollen-System über Discord Select Menus mit automatischer Berechtigungsprüfung, Menü-Reset nach Auswahl und Fehlerabfangung.
|
||||||
- ✅ **Smart Auto-Response System:** Präzises Trigger-Wort-Matching mit Wortgrenzen-Regex (`\b`), um Falsch-Positive zu vermeiden.
|
- ✅ **Smart Auto-Response System:** Präzises Trigger-Wort-Matching mit Unicode-Wortgrenzen (`\p{L}`), URL-Filterung gegen False Positives und ephemeraler Trigger-Liste für alle Server-Mitglieder.
|
||||||
- ✅ **Reminders:** Intelligentes Zeit-Parsing (z.B. "morgen 15:00", "30min").
|
- ✅ **Reminders:** Intelligentes Zeit-Parsing (z.B. "morgen 15:00", "30min").
|
||||||
- ✅ **Persistent Stopwatches:** Namentliche hochzählende Stoppuhren mit SQLite-Speicherung, Unterstützung für optionale vergangene Startzeiten (z.B. "14:30", "2h"), Duplikat-Schutz und relativer Discord-Zeitdarstellung.
|
- ✅ **Persistent Stopwatches:** Namentliche hochzählende Stoppuhren mit SQLite-Speicherung, Unterstützung für optionale vergangene Startzeiten (z.B. "14:30", "2h"), Duplikat-Schutz und relativer Discord-Zeitdarstellung.
|
||||||
- ✅ **Developer & Team Support:** Unterstützung von Discord Developer Teams und `BOT_OWNER_ID` Fallback für Owner-Befehle.
|
- ✅ **Developer & Team Support:** Unterstützung von Discord Developer Teams und `BOT_OWNER_ID` Fallback für Owner-Befehle.
|
||||||
|
|||||||
@@ -116,8 +116,9 @@ const command: Command = {
|
|||||||
.setDescription('Neuer Wert'))
|
.setDescription('Neuer Wert'))
|
||||||
.addIntegerOption(option =>
|
.addIntegerOption(option =>
|
||||||
option.setName('duration_value')
|
option.setName('duration_value')
|
||||||
.setDescription('Dauer in Sekunden')
|
.setDescription('Dauer in Sekunden (1 - 2419200)')
|
||||||
.setMinValue(0)))
|
.setMinValue(1)
|
||||||
|
.setMaxValue(2419200)))
|
||||||
.addSubcommandGroup(group =>
|
.addSubcommandGroup(group =>
|
||||||
group
|
group
|
||||||
.setName('purge')
|
.setName('purge')
|
||||||
@@ -171,7 +172,8 @@ const command: Command = {
|
|||||||
subcommand
|
subcommand
|
||||||
.setName('help')
|
.setName('help')
|
||||||
.setDescription('Zeigt Hilfe zu den Admin-Befehlen an.'))
|
.setDescription('Zeigt Hilfe zu den Admin-Befehlen an.'))
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
|
||||||
|
.setDMPermission(false),
|
||||||
category: 'Admin',
|
category: 'Admin',
|
||||||
async execute(interaction: any) {
|
async execute(interaction: any) {
|
||||||
const client = interaction.client as ExtendedClient;
|
const client = interaction.client as ExtendedClient;
|
||||||
@@ -179,6 +181,14 @@ const command: Command = {
|
|||||||
const DB = client.DB;
|
const DB = client.DB;
|
||||||
const guild = interaction.guild;
|
const guild = interaction.guild;
|
||||||
const guildId = interaction.guildId;
|
const guildId = interaction.guildId;
|
||||||
|
if (!guildId || !guild) {
|
||||||
|
await interaction.reply({ content: '❌ Dieser Befehl kann nur auf einem Server verwendet werden.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.Administrator);
|
||||||
|
|
||||||
if (subcommand === 'help') {
|
if (subcommand === 'help') {
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
@@ -206,6 +216,25 @@ const command: Command = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkTargetHierarchy = (target: GuildMember): string | null => {
|
||||||
|
if (target.id === interaction.user.id) {
|
||||||
|
return '❌ Du kannst diese Aktion nicht auf dich selbst anwenden.';
|
||||||
|
}
|
||||||
|
if (target.id === guild.ownerId) {
|
||||||
|
return '❌ Diese Aktion kann nicht auf den Server-Owner angewendet werden.';
|
||||||
|
}
|
||||||
|
const modMember = interaction.member as GuildMember | null;
|
||||||
|
if (modMember && interaction.user.id !== guild.ownerId && target.roles.highest.position >= modMember.roles.highest.position) {
|
||||||
|
return '❌ Du kannst keinen Nutzer moderieren, der eine gleich hohe oder höhere Rolle als du hat.';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
// Get or create guild settings
|
// Get or create guild settings
|
||||||
const settings = DB.getSettings(guildId);
|
const settings = DB.getSettings(guildId);
|
||||||
|
|
||||||
@@ -218,6 +247,12 @@ const command: Command = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hierarchyError = checkTargetHierarchy(target);
|
||||||
|
if (hierarchyError) {
|
||||||
|
await interaction.reply({ content: hierarchyError, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!target.kickable) {
|
if (!target.kickable) {
|
||||||
await interaction.reply({ content: 'Ich kann diesen Nutzer nicht kicken.', ephemeral: true });
|
await interaction.reply({ content: 'Ich kann diesen Nutzer nicht kicken.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
@@ -239,7 +274,9 @@ const command: Command = {
|
|||||||
await EventLogger.sendLog(interaction.client, guildId, 'kicks', embed);
|
await EventLogger.sendLog(interaction.client, guildId, 'kicks', embed);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
await interaction.reply({ content: '❌ Fehler beim Kicken.', ephemeral: true });
|
if (!interaction.replied && !interaction.deferred) {
|
||||||
|
await interaction.reply({ content: '❌ Fehler beim Kicken.', ephemeral: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -253,6 +290,12 @@ const command: Command = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hierarchyError = checkTargetHierarchy(target);
|
||||||
|
if (hierarchyError) {
|
||||||
|
await interaction.reply({ content: hierarchyError, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
DB.run(
|
DB.run(
|
||||||
'INSERT INTO mod_logs (guild_id, user_id, moderator_id, action, reason) VALUES (?, ?, ?, ?, ?)',
|
'INSERT INTO mod_logs (guild_id, user_id, moderator_id, action, reason) VALUES (?, ?, ?, ?, ?)',
|
||||||
guildId,
|
guildId,
|
||||||
@@ -284,9 +327,11 @@ const command: Command = {
|
|||||||
await target.kick(`Auto-Kick: ${warnThreshold} Warnungen`);
|
await target.kick(`Auto-Kick: ${warnThreshold} Warnungen`);
|
||||||
message += `\n👢 **Automatisch gekickt!**`;
|
message += `\n👢 **Automatisch gekickt!**`;
|
||||||
} else if (warnAction === 'mute') {
|
} else if (warnAction === 'mute') {
|
||||||
const muteDuration = settings?.warn_mute_duration ?? 1800; // 30 min default
|
const MAX_TIMEOUT_SEC = 28 * 24 * 60 * 60;
|
||||||
|
const rawDuration = settings?.warn_mute_duration ?? 1800; // 30 min default
|
||||||
|
const muteDuration = Math.min(Math.max(rawDuration, 1), MAX_TIMEOUT_SEC);
|
||||||
await target.timeout(muteDuration * 1000, `Auto-Mute: ${warnThreshold} Warnungen`);
|
await target.timeout(muteDuration * 1000, `Auto-Mute: ${warnThreshold} Warnungen`);
|
||||||
const durationMins = Math.floor(muteDuration / 60);
|
const durationMins = Math.max(1, Math.floor(muteDuration / 60));
|
||||||
message += `\n🔇 **Automatisch gemuted (${durationMins} Min)!**`;
|
message += `\n🔇 **Automatisch gemuted (${durationMins} Min)!**`;
|
||||||
} else if (warnAction === 'ban') {
|
} else if (warnAction === 'ban') {
|
||||||
await target.ban({ reason: `Auto-Ban: ${warnThreshold} Warnungen` });
|
await target.ban({ reason: `Auto-Ban: ${warnThreshold} Warnungen` });
|
||||||
@@ -350,6 +395,12 @@ const command: Command = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hierarchyError = checkTargetHierarchy(target);
|
||||||
|
if (hierarchyError) {
|
||||||
|
await interaction.reply({ content: hierarchyError, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse duration
|
// Parse duration
|
||||||
let durationMs: number | null = null;
|
let durationMs: number | null = null;
|
||||||
let durationText = '';
|
let durationText = '';
|
||||||
@@ -413,7 +464,9 @@ const command: Command = {
|
|||||||
await EventLogger.sendLog(interaction.client, guildId, 'mutes', embed);
|
await EventLogger.sendLog(interaction.client, guildId, 'mutes', embed);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
await interaction.reply({ content: '❌ Fehler beim Muten.', ephemeral: true });
|
if (!interaction.replied && !interaction.deferred) {
|
||||||
|
await interaction.reply({ content: '❌ Fehler beim Muten.', ephemeral: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -446,6 +499,12 @@ const command: Command = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hierarchyError = checkTargetHierarchy(target);
|
||||||
|
if (hierarchyError) {
|
||||||
|
await interaction.reply({ content: hierarchyError, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!target.bannable) {
|
if (!target.bannable) {
|
||||||
await interaction.reply({ content: 'Ich kann diesen Nutzer nicht bannen.', ephemeral: true });
|
await interaction.reply({ content: 'Ich kann diesen Nutzer nicht bannen.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
@@ -468,7 +527,9 @@ const command: Command = {
|
|||||||
await EventLogger.sendLog(interaction.client, guildId, 'bans', embed);
|
await EventLogger.sendLog(interaction.client, guildId, 'bans', embed);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
await interaction.reply({ content: '❌ Fehler beim Bannen.', ephemeral: true });
|
if (!interaction.replied && !interaction.deferred) {
|
||||||
|
await interaction.reply({ content: '❌ Fehler beim Bannen.', ephemeral: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -510,35 +571,37 @@ const command: Command = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (setting === 'warn_threshold') {
|
if (setting === 'warn_threshold') {
|
||||||
const threshold = parseInt(value!);
|
const threshold = value !== null ? parseInt(value) : (durationValue ?? NaN);
|
||||||
if (isNaN(threshold) || threshold < 1) {
|
if (isNaN(threshold) || threshold < 1) {
|
||||||
await interaction.reply({ content: '❌ Ungültiger Wert. Mindestens 1.', ephemeral: true });
|
await interaction.reply({ content: '❌ Ungültiger Wert. Mindestens 1.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DB.run('UPDATE guild_settings SET warn_threshold = ? WHERE guild_id = ?', threshold, guildId);
|
DB.run('UPDATE guild_settings SET warn_threshold = ? WHERE guild_id = ?', threshold, guildId);
|
||||||
await interaction.reply(`✅ Warn-Limit auf ${threshold} gesetzt.`);
|
await interaction.reply({ content: `✅ Warn-Limit auf ${threshold} gesetzt.`, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setting === 'warn_action') {
|
if (setting === 'warn_action') {
|
||||||
const validActions = ['none', 'kick', 'mute', 'ban'];
|
const validActions = ['none', 'kick', 'mute', 'ban'];
|
||||||
if (!validActions.includes(value!)) {
|
if (!value || !validActions.includes(value)) {
|
||||||
await interaction.reply({ content: `❌ Ungültige Aktion. Möglich: ${validActions.join(', ')}`, ephemeral: true });
|
await interaction.reply({ content: `❌ Ungültige Aktion. Möglich: ${validActions.join(', ')}`, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DB.run('UPDATE guild_settings SET warn_action = ? WHERE guild_id = ?', value, guildId);
|
DB.run('UPDATE guild_settings SET warn_action = ? WHERE guild_id = ?', value, guildId);
|
||||||
await interaction.reply(`✅ Auto-Aktion auf ${value} gesetzt.`);
|
await interaction.reply({ content: `✅ Auto-Aktion auf ${value} gesetzt.`, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setting === 'warn_mute_duration') {
|
if (setting === 'warn_mute_duration') {
|
||||||
const duration = durationValue ?? 1800;
|
const parsedValue = value !== null ? parseInt(value) : NaN;
|
||||||
if (duration < 0) {
|
const duration = durationValue ?? (!isNaN(parsedValue) ? parsedValue : 1800);
|
||||||
await interaction.reply({ content: '❌ Ungültige Dauer.', ephemeral: true });
|
const MAX_TIMEOUT_SEC = 28 * 24 * 60 * 60;
|
||||||
|
if (duration < 1 || duration > MAX_TIMEOUT_SEC) {
|
||||||
|
await interaction.reply({ content: `❌ Ungültige Dauer. Erlaubt sind 1 bis ${MAX_TIMEOUT_SEC} Sekunden (max. 28 Tage).`, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DB.run('UPDATE guild_settings SET warn_mute_duration = ? WHERE guild_id = ?', duration, guildId);
|
DB.run('UPDATE guild_settings SET warn_mute_duration = ? WHERE guild_id = ?', duration, guildId);
|
||||||
await interaction.reply(`✅ Mute-Dauer auf ${duration} Sekunden (${Math.floor(duration/60)} Min) gesetzt.`);
|
await interaction.reply({ content: `✅ Mute-Dauer auf ${duration} Sekunden (${Math.floor(duration/60)} Min) gesetzt.`, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,12 +51,19 @@ const command: Command = {
|
|||||||
subcommand
|
subcommand
|
||||||
.setName('help')
|
.setName('help')
|
||||||
.setDescription('Hilfe zu Log-Befehlen (Mod).'))
|
.setDescription('Hilfe zu Log-Befehlen (Mod).'))
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
|
||||||
|
.setDMPermission(false),
|
||||||
category: 'Admin',
|
category: 'Admin',
|
||||||
async execute(interaction: any) {
|
async execute(interaction: any) {
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
const guildId = interaction.guildId;
|
const guildId = interaction.guildId;
|
||||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
if (!guildId) {
|
||||||
|
await interaction.reply({ content: '❌ Dieser Befehl kann nur auf einem Server verwendet werden.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.Administrator);
|
||||||
const DB = interaction.client.DB;
|
const DB = interaction.client.DB;
|
||||||
|
|
||||||
|
|
||||||
@@ -84,6 +91,9 @@ const command: Command = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure guild settings row exists before reading or updating
|
||||||
|
const settings = DB.getSettings(guildId);
|
||||||
|
|
||||||
if (subcommand === 'setchannel') {
|
if (subcommand === 'setchannel') {
|
||||||
const channel = interaction.options.getChannel('channel');
|
const channel = interaction.options.getChannel('channel');
|
||||||
|
|
||||||
@@ -98,8 +108,6 @@ const command: Command = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'list') {
|
if (subcommand === 'list') {
|
||||||
const settings = DB.getSettings(guildId);
|
|
||||||
|
|
||||||
const channel = settings?.log_channel
|
const channel = settings?.log_channel
|
||||||
? `<#${settings.log_channel}>`
|
? `<#${settings.log_channel}>`
|
||||||
: 'Nicht gesetzt';
|
: 'Nicht gesetzt';
|
||||||
|
|||||||
@@ -20,8 +20,11 @@ export async function updateRoleMessage(client: any, category: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const roleOptions = options.map((opt: any) => {
|
const roleOptions = options.map((opt: any) => {
|
||||||
|
const cleanLabel = opt.emoji && opt.label.startsWith(`${opt.emoji} `)
|
||||||
|
? opt.label.slice(opt.emoji.length + 1)
|
||||||
|
: opt.label;
|
||||||
const menuOption = new StringSelectMenuOptionBuilder()
|
const menuOption = new StringSelectMenuOptionBuilder()
|
||||||
.setLabel(opt.label.replace(/^[^\s]+\s/, ''))
|
.setLabel(cleanLabel)
|
||||||
.setValue(opt.role_id);
|
.setValue(opt.role_id);
|
||||||
if (opt.emoji && opt.emoji.trim().length > 0) {
|
if (opt.emoji && opt.emoji.trim().length > 0) {
|
||||||
menuOption.setEmoji(opt.emoji.trim());
|
menuOption.setEmoji(opt.emoji.trim());
|
||||||
@@ -95,7 +98,7 @@ const command: Command = {
|
|||||||
option.setName('rolle')
|
option.setName('rolle')
|
||||||
.setDescription('Die Discord-Rolle zum Ändern')
|
.setDescription('Die Discord-Rolle zum Ändern')
|
||||||
.setRequired(true))
|
.setRequired(true))
|
||||||
.addStringOption(option =>
|
.addRoleOption(option =>
|
||||||
option.setName('neue_rolle')
|
option.setName('neue_rolle')
|
||||||
.setDescription('Die neue Discord-Rolle')
|
.setDescription('Die neue Discord-Rolle')
|
||||||
.setRequired(false))
|
.setRequired(false))
|
||||||
@@ -148,14 +151,26 @@ const command: Command = {
|
|||||||
subcommand
|
subcommand
|
||||||
.setName('list')
|
.setName('list')
|
||||||
.setDescription('Listet alle Rollen-Kategorien auf'))
|
.setDescription('Listet alle Rollen-Kategorien auf'))
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
|
||||||
|
.setDMPermission(false),
|
||||||
category: 'Admin',
|
category: 'Admin',
|
||||||
async execute(interaction: any) {
|
async execute(interaction: any) {
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
const guildId = interaction.guildId;
|
const guildId = interaction.guildId;
|
||||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
if (!guildId) {
|
||||||
|
await interaction.reply({ content: '❌ Dieser Befehl kann nur auf einem Server verwendet werden.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.Administrator);
|
||||||
const DB = interaction.client.DB;
|
const DB = interaction.client.DB;
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
await interaction.reply({ content: 'Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (subcommand === 'list') {
|
if (subcommand === 'list') {
|
||||||
const categories: any[] = DB.all('SELECT * FROM role_categories WHERE guild_id = ? ORDER BY category_name ASC', guildId);
|
const categories: any[] = DB.all('SELECT * FROM role_categories WHERE guild_id = ? ORDER BY category_name ASC', guildId);
|
||||||
|
|
||||||
@@ -323,7 +338,7 @@ const command: Command = {
|
|||||||
if (discordRole) {
|
if (discordRole) {
|
||||||
const members = guild.members.cache.filter((m: any) => m.roles.cache.has(role.id));
|
const members = guild.members.cache.filter((m: any) => m.roles.cache.has(role.id));
|
||||||
for (const member of members.values()) {
|
for (const member of members.values()) {
|
||||||
await member.roles.remove(role.id);
|
await member.roles.remove(role.id).catch(() => null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,7 +371,7 @@ const command: Command = {
|
|||||||
if (discordRole) {
|
if (discordRole) {
|
||||||
const members = guild.members.cache.filter((m: any) => m.roles.cache.has(opt.role_id));
|
const members = guild.members.cache.filter((m: any) => m.roles.cache.has(opt.role_id));
|
||||||
for (const member of members.values()) {
|
for (const member of members.values()) {
|
||||||
await member.roles.remove(opt.role_id);
|
await member.roles.remove(opt.role_id).catch(() => null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,12 +125,16 @@ const command: Command = {
|
|||||||
subcommand
|
subcommand
|
||||||
.setName('help')
|
.setName('help')
|
||||||
.setDescription('Zeigt Hilfe zu den Stoppuhr-Befehlen an.'))
|
.setDescription('Zeigt Hilfe zu den Stoppuhr-Befehlen an.'))
|
||||||
.setDMPermission(true),
|
.setDMPermission(false),
|
||||||
category: 'Public',
|
category: 'Public',
|
||||||
async execute(interaction: any) {
|
async execute(interaction: any) {
|
||||||
const client = interaction.client as ExtendedClient;
|
const client = interaction.client as ExtendedClient;
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
const guildId = interaction.guildId;
|
const guildId = interaction.guildId;
|
||||||
|
if (!guildId) {
|
||||||
|
await interaction.reply({ content: '❌ Dieser Befehl kann nur auf einem Server verwendet werden.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const userId = interaction.user.id;
|
const userId = interaction.user.id;
|
||||||
const channelId = interaction.channelId;
|
const channelId = interaction.channelId;
|
||||||
const DB = client.DB;
|
const DB = client.DB;
|
||||||
|
|||||||
@@ -134,14 +134,21 @@ const command: Command = {
|
|||||||
.addSubcommand(subcommand =>
|
.addSubcommand(subcommand =>
|
||||||
subcommand
|
subcommand
|
||||||
.setName('help')
|
.setName('help')
|
||||||
.setDescription('Zeigt Hilfe zu den Timer-Befehlen an.')),
|
.setDescription('Zeigt Hilfe zu den Timer-Befehlen an.'))
|
||||||
|
.setDMPermission(false),
|
||||||
category: 'Public',
|
category: 'Public',
|
||||||
async execute(interaction: any) {
|
async execute(interaction: any) {
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
const guildId = interaction.guildId;
|
const guildId = interaction.guildId;
|
||||||
|
if (!guildId) {
|
||||||
|
await interaction.reply({ content: '❌ Dieser Befehl kann nur auf einem Server verwendet werden.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const channelId = interaction.channelId;
|
const channelId = interaction.channelId;
|
||||||
const userId = interaction.user.id;
|
const userId = interaction.user.id;
|
||||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.Administrator);
|
||||||
const DB = interaction.client.DB;
|
const DB = interaction.client.DB;
|
||||||
|
|
||||||
if (subcommand === 'help') {
|
if (subcommand === 'help') {
|
||||||
|
|||||||
@@ -33,12 +33,18 @@ const command: Command = {
|
|||||||
subcommand
|
subcommand
|
||||||
.setName('help')
|
.setName('help')
|
||||||
.setDescription('Zeigt Hilfe zu den Trigger-Befehlen an.'))
|
.setDescription('Zeigt Hilfe zu den Trigger-Befehlen an.'))
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
.setDMPermission(false),
|
||||||
category: 'Admin',
|
category: 'Public',
|
||||||
async execute(interaction: any) {
|
async execute(interaction: any) {
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
const guildId = interaction.guildId;
|
const guildId = interaction.guildId;
|
||||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
if (!guildId) {
|
||||||
|
await interaction.reply({ content: '❌ Dieser Befehl kann nur auf einem Server verwendet werden.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.Administrator);
|
||||||
const DB = interaction.client.DB;
|
const DB = interaction.client.DB;
|
||||||
|
|
||||||
if (subcommand === 'help') {
|
if (subcommand === 'help') {
|
||||||
@@ -99,9 +105,14 @@ const command: Command = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'add') {
|
if (subcommand === 'add') {
|
||||||
const triggerWord = interaction.options.getString('trigger')!.toLowerCase();
|
const triggerWord = interaction.options.getString('trigger')!.trim().toLowerCase();
|
||||||
const responseText = interaction.options.getString('antwort')!;
|
const responseText = interaction.options.getString('antwort')!;
|
||||||
|
|
||||||
|
if (!triggerWord) {
|
||||||
|
await interaction.reply({ content: '❌ Das Trigger-Wort darf nicht leer sein.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
DB.run(
|
DB.run(
|
||||||
'INSERT INTO auto_responses (guild_id, trigger_word, response_text) VALUES (?, ?, ?) ON CONFLICT(guild_id, trigger_word) DO UPDATE SET response_text = ?',
|
'INSERT INTO auto_responses (guild_id, trigger_word, response_text) VALUES (?, ?, ?) ON CONFLICT(guild_id, trigger_word) DO UPDATE SET response_text = ?',
|
||||||
guildId,
|
guildId,
|
||||||
@@ -115,7 +126,7 @@ const command: Command = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'remove') {
|
if (subcommand === 'remove') {
|
||||||
const triggerWord = interaction.options.getString('trigger')!.toLowerCase();
|
const triggerWord = interaction.options.getString('trigger')!.trim().toLowerCase();
|
||||||
|
|
||||||
const result = DB.run('DELETE FROM auto_responses WHERE guild_id = ? AND trigger_word = ?', guildId, triggerWord);
|
const result = DB.run('DELETE FROM auto_responses WHERE guild_id = ? AND trigger_word = ?', guildId, triggerWord);
|
||||||
|
|
||||||
|
|||||||
@@ -45,13 +45,20 @@ const command: Command = {
|
|||||||
.addSubcommand(subcommand =>
|
.addSubcommand(subcommand =>
|
||||||
subcommand
|
subcommand
|
||||||
.setName('help')
|
.setName('help')
|
||||||
.setDescription('Zeigt Hilfe zu den Twitch-Befehlen an.')),
|
.setDescription('Zeigt Hilfe zu den Twitch-Befehlen an.'))
|
||||||
|
.setDMPermission(false),
|
||||||
category: 'Public',
|
category: 'Public',
|
||||||
async execute(interaction: any) {
|
async execute(interaction: any) {
|
||||||
const client = interaction.client as ExtendedClient;
|
const client = interaction.client as ExtendedClient;
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
const guildId = interaction.guildId;
|
const guildId = interaction.guildId;
|
||||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
if (!guildId) {
|
||||||
|
await interaction.reply({ content: '❌ Dieser Befehl kann nur auf einem Server verwendet werden.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.Administrator);
|
||||||
const DB = client.DB;
|
const DB = client.DB;
|
||||||
|
|
||||||
if (subcommand === 'help') {
|
if (subcommand === 'help') {
|
||||||
@@ -65,7 +72,7 @@ const command: Command = {
|
|||||||
{ name: '`/twitch remove streamer1, streamer2, ...`', value: 'Entfernt Twitch-Kanäle (Mod).', inline: false },
|
{ name: '`/twitch remove streamer1, streamer2, ...`', value: 'Entfernt Twitch-Kanäle (Mod).', inline: false },
|
||||||
{ name: '`/twitch list`', value: 'Zeigt überwachte Kanäle in diesem Kanal.', inline: false },
|
{ name: '`/twitch list`', value: 'Zeigt überwachte Kanäle in diesem Kanal.', inline: false },
|
||||||
{ name: '`/twitch listall`', value: 'Zeigt alle überwachten Kanäle des Servers (Mod).', inline: false },
|
{ name: '`/twitch listall`', value: 'Zeigt alle überwachten Kanäle des Servers (Mod).', inline: false },
|
||||||
{ name: 'Beispiele', value: '`/twitch add shroud, ninja`\n`/twitch add #general shroud, ninja "Live!"`\n`/twitch remove shroud, ninja`', inline: false }
|
{ name: 'Beispiele', value: '`/twitch add streamers: shroud, ninja`\n`/twitch add streamers: shroud, ninja message: Live!`\n`/twitch remove streamers: shroud, ninja`', inline: false }
|
||||||
)
|
)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
@@ -184,13 +191,19 @@ const command: Command = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
const results = { added: 0, updated: 0, errors: 0 };
|
const results = { added: 0, updated: 0, errors: 0 };
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
for (const streamer of streamers) {
|
for (const streamer of streamers) {
|
||||||
try {
|
try {
|
||||||
// Check if stream exists
|
const isValid = await TwitchManager.validateUser(streamer);
|
||||||
await TwitchManager.fetchStreamData(streamer);
|
if (!isValid) {
|
||||||
|
errors.push(streamer);
|
||||||
|
results.errors++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if already exists
|
// Check if already exists
|
||||||
const existing = DB.get('SELECT id FROM twitch_monitors WHERE guild_id = ? AND channel_name = ?', guildId, streamer);
|
const existing = DB.get('SELECT id FROM twitch_monitors WHERE guild_id = ? AND channel_name = ?', guildId, streamer);
|
||||||
@@ -211,12 +224,12 @@ const command: Command = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let reply = `✅ **${streamers.length}** Twitch-Kanal/Kanäle für <#${discordChannelId}> konfiguriert.`;
|
let reply = `✅ **${results.added + results.updated}** Twitch-Kanal/Kanäle für <#${discordChannelId}> konfiguriert.`;
|
||||||
if (results.added > 0) reply += `\n🆕 Neu: ${results.added}`;
|
if (results.added > 0) reply += `\n🆕 Neu: ${results.added}`;
|
||||||
if (results.updated > 0) reply += `\n🔄 Aktualisiert: ${results.updated}`;
|
if (results.updated > 0) reply += `\n🔄 Aktualisiert: ${results.updated}`;
|
||||||
if (results.errors > 0) reply += `\n❌ Fehler: ${results.errors} (${errors.join(', ')})`;
|
if (results.errors > 0) reply += `\n❌ Fehler: ${results.errors} (${errors.join(', ')})`;
|
||||||
|
|
||||||
await interaction.reply({ content: reply, ephemeral: true });
|
await interaction.editReply({ content: reply });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,12 +35,19 @@ const command: Command = {
|
|||||||
subcommand
|
subcommand
|
||||||
.setName('help')
|
.setName('help')
|
||||||
.setDescription('Zeigt Hilfe zu den TwitchMonitor-Befehlen'))
|
.setDescription('Zeigt Hilfe zu den TwitchMonitor-Befehlen'))
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
|
||||||
|
.setDMPermission(false),
|
||||||
category: 'Admin',
|
category: 'Admin',
|
||||||
async execute(interaction: any) {
|
async execute(interaction: any) {
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
const guildId = interaction.guildId;
|
const guildId = interaction.guildId;
|
||||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
if (!guildId) {
|
||||||
|
await interaction.reply({ content: '❌ Dieser Befehl kann nur auf einem Server verwendet werden.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.Administrator);
|
||||||
const DB = interaction.client.DB;
|
const DB = interaction.client.DB;
|
||||||
|
|
||||||
if (subcommand === 'help') {
|
if (subcommand === 'help') {
|
||||||
@@ -59,6 +66,11 @@ const command: Command = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
await interaction.reply({ content: 'Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (subcommand === 'list') {
|
if (subcommand === 'list') {
|
||||||
const monitors: any[] = DB.all('SELECT * FROM twitch_monitor_channels WHERE guild_id = ? ORDER BY twitch_channel ASC', guildId);
|
const monitors: any[] = DB.all('SELECT * FROM twitch_monitor_channels WHERE guild_id = ? ORDER BY twitch_channel ASC', guildId);
|
||||||
|
|
||||||
@@ -79,13 +91,8 @@ const command: Command = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAdmin) {
|
|
||||||
await interaction.reply({ content: 'Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subcommand === 'add') {
|
if (subcommand === 'add') {
|
||||||
const twitchChannel = interaction.options.getString('twitch').toLowerCase().replace(/^@/, '');
|
const twitchChannel = interaction.options.getString('twitch').trim().toLowerCase().replace(/^[@#]/, '');
|
||||||
const discordChannel = interaction.options.getChannel('discord');
|
const discordChannel = interaction.options.getChannel('discord');
|
||||||
|
|
||||||
const existing = DB.get('SELECT * FROM twitch_monitor_channels WHERE guild_id = ? AND twitch_channel = ?', guildId, twitchChannel);
|
const existing = DB.get('SELECT * FROM twitch_monitor_channels WHERE guild_id = ? AND twitch_channel = ?', guildId, twitchChannel);
|
||||||
@@ -94,34 +101,41 @@ const command: Command = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const webhook = await discordChannel.createWebhook({
|
await interaction.deferReply({ ephemeral: true });
|
||||||
name: `${twitchChannel} | Mod-Log`,
|
|
||||||
avatar: undefined,
|
try {
|
||||||
}).catch(async () => {
|
const webhook = await discordChannel.createWebhook({
|
||||||
const existing_webhook = await discordChannel.fetchWebhooks().then((webhooks: any) =>
|
name: `${twitchChannel} | Mod-Log`,
|
||||||
webhooks.find((w: any) => w.name?.includes(twitchChannel))
|
avatar: undefined,
|
||||||
|
}).catch(async () => {
|
||||||
|
const existing_webhook = await discordChannel.fetchWebhooks().then((webhooks: any) =>
|
||||||
|
webhooks.find((w: any) => w.name?.includes(twitchChannel))
|
||||||
|
);
|
||||||
|
if (existing_webhook) return existing_webhook;
|
||||||
|
throw new Error('Could not create webhook');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB.run(
|
||||||
|
'INSERT INTO twitch_monitor_channels (guild_id, twitch_channel, discord_channel_id, webhook_url) VALUES (?, ?, ?, ?)',
|
||||||
|
guildId,
|
||||||
|
twitchChannel,
|
||||||
|
discordChannel.id,
|
||||||
|
webhook.url
|
||||||
);
|
);
|
||||||
if (existing_webhook) return existing_webhook;
|
|
||||||
throw new Error('Could not create webhook');
|
|
||||||
});
|
|
||||||
|
|
||||||
DB.run(
|
const monitor = TwitchMonitor.getInstance();
|
||||||
'INSERT INTO twitch_monitor_channels (guild_id, twitch_channel, discord_channel_id, webhook_url) VALUES (?, ?, ?, ?)',
|
await monitor.addMonitor(guildId, twitchChannel, discordChannel.id, webhook.url);
|
||||||
guildId,
|
|
||||||
twitchChannel,
|
|
||||||
discordChannel.id,
|
|
||||||
webhook.url
|
|
||||||
);
|
|
||||||
|
|
||||||
const monitor = TwitchMonitor.getInstance();
|
await interaction.editReply({ content: `✅ **${twitchChannel}** wird jetzt in ${discordChannel} überwacht.` });
|
||||||
await monitor.addMonitor(guildId, twitchChannel, discordChannel.id, webhook.url);
|
} catch (error) {
|
||||||
|
console.error('[TWITCHMONITOR] Error adding monitor:', error);
|
||||||
await interaction.reply({ content: `${twitchChannel} wird jetzt überwacht.`, ephemeral: true });
|
await interaction.editReply({ content: '❌ Konnte keinen Webhook erstellen. Bitte prüfe, ob der Bot die Berechtigung "Webhooks verwalten" im Ziel-Kanal hat.' });
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'remove') {
|
if (subcommand === 'remove') {
|
||||||
const twitchChannel = interaction.options.getString('twitch').toLowerCase().replace(/^@/, '');
|
const twitchChannel = interaction.options.getString('twitch').trim().toLowerCase().replace(/^[@#]/, '');
|
||||||
|
|
||||||
const monitor: any = DB.get('SELECT * FROM twitch_monitor_channels WHERE guild_id = ? AND twitch_channel = ?', guildId, twitchChannel);
|
const monitor: any = DB.get('SELECT * FROM twitch_monitor_channels WHERE guild_id = ? AND twitch_channel = ?', guildId, twitchChannel);
|
||||||
if (!monitor) {
|
if (!monitor) {
|
||||||
|
|||||||
@@ -49,15 +49,21 @@ const command: Command = {
|
|||||||
subcommand
|
subcommand
|
||||||
.setName('help')
|
.setName('help')
|
||||||
.setDescription('Hilfe zu Welcome-Befehlen.'))
|
.setDescription('Hilfe zu Welcome-Befehlen.'))
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
|
||||||
|
.setDMPermission(false),
|
||||||
category: 'Admin',
|
category: 'Admin',
|
||||||
async execute(interaction: any) {
|
async execute(interaction: any) {
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
const guildId = interaction.guildId;
|
const guildId = interaction.guildId;
|
||||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
if (!guildId) {
|
||||||
|
await interaction.reply({ content: '❌ Dieser Befehl kann nur auf einem Server verwendet werden.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild) ||
|
||||||
|
interaction.memberPermissions?.has(PermissionFlagsBits.Administrator);
|
||||||
const DB = interaction.client.DB;
|
const DB = interaction.client.DB;
|
||||||
|
|
||||||
|
|
||||||
if (subcommand === 'help') {
|
if (subcommand === 'help') {
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle('👋 Welcome Hilfe')
|
.setTitle('👋 Welcome Hilfe')
|
||||||
@@ -78,9 +84,16 @@ const command: Command = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'list') {
|
// Admin only for all commands
|
||||||
const settings = DB.getSettings(guildId);
|
if (!isAdmin) {
|
||||||
|
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure guild settings row exists before reading or updating
|
||||||
|
const settings = DB.getSettings(guildId);
|
||||||
|
|
||||||
|
if (subcommand === 'list') {
|
||||||
const channel = settings?.welcome_channel
|
const channel = settings?.welcome_channel
|
||||||
? `<#${settings.welcome_channel}>`
|
? `<#${settings.welcome_channel}>`
|
||||||
: 'Nicht gesetzt';
|
: 'Nicht gesetzt';
|
||||||
@@ -103,12 +116,6 @@ const command: Command = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin only for modify commands
|
|
||||||
if (!isAdmin) {
|
|
||||||
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subcommand === 'setchannel') {
|
if (subcommand === 'setchannel') {
|
||||||
const channel = interaction.options.getChannel('channel');
|
const channel = interaction.options.getChannel('channel');
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export default {
|
|||||||
ephemeral: true
|
ephemeral: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await interaction.message.edit({ components: interaction.message.components }).catch(() => null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +117,7 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await interaction.message.edit({ components: interaction.message.components }).catch(() => null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ROLESETUP] Role assignment error:', error);
|
console.error('[ROLESETUP] Role assignment error:', error);
|
||||||
const errorMsg = '❌ Fehler beim Ändern der Rolle. Mir fehlen eventuell die Rechte dazu.';
|
const errorMsg = '❌ Fehler beim Ändern der Rolle. Mir fehlen eventuell die Rechte dazu.';
|
||||||
@@ -126,7 +128,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,20 @@ export default {
|
|||||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strip URLs before checking triggers so domain names don't trigger auto-responses
|
||||||
|
const contentWithoutUrls = message.content.replace(/https?:\/\/\S+/gi, ' ');
|
||||||
|
|
||||||
for (const trigger of triggers) {
|
for (const trigger of triggers) {
|
||||||
const word = trigger.trigger_word.trim().toLowerCase();
|
const word = trigger.trigger_word.trim().toLowerCase();
|
||||||
if (!word) continue;
|
if (!word) continue;
|
||||||
|
|
||||||
const pattern = new RegExp(`(?:^|\\s|\\b)${escapeRegex(word)}(?:$|\\s|\\b)`, 'i');
|
const pattern = new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegex(word)}(?![\\p{L}\\p{N}_])`, 'iu');
|
||||||
if (pattern.test(message.content)) {
|
if (pattern.test(contentWithoutUrls)) {
|
||||||
await message.channel.send(trigger.response_text);
|
try {
|
||||||
|
await message.channel.send(trigger.response_text);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[TRIGGER] Failed to send auto-response in guild ${message.guildId}:`, error);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ async function validateRoles(client: ExtendedClient) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function initTwitchMonitor(client: ExtendedClient) {
|
async function initTwitchMonitor(client: ExtendedClient) {
|
||||||
|
const monitor = TwitchMonitor.getInstance();
|
||||||
|
monitor.setDiscordClient(client);
|
||||||
|
|
||||||
if (!process.env.TWITCH_OAUTH_TOKEN || !process.env.TWITCH_USERNAME) {
|
if (!process.env.TWITCH_OAUTH_TOKEN || !process.env.TWITCH_USERNAME) {
|
||||||
console.warn('[TWITCH] Missing TWITCH_OAUTH_TOKEN or TWITCH_USERNAME - moderation events will not be received');
|
console.warn('[TWITCH] Missing TWITCH_OAUTH_TOKEN or TWITCH_USERNAME - moderation events will not be received');
|
||||||
return;
|
return;
|
||||||
@@ -59,9 +62,6 @@ async function initTwitchMonitor(client: ExtendedClient) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const monitor = TwitchMonitor.getInstance();
|
|
||||||
monitor.setDiscordClient(client);
|
|
||||||
|
|
||||||
for (const m of monitors) {
|
for (const m of monitors) {
|
||||||
const webhookExists = m.webhook_url && m.webhook_url.length > 0;
|
const webhookExists = m.webhook_url && m.webhook_url.length > 0;
|
||||||
if (webhookExists) {
|
if (webhookExists) {
|
||||||
|
|||||||
@@ -63,6 +63,31 @@ export class TwitchManager {
|
|||||||
return streams[0] || null;
|
return streams[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates whether a Twitch user/channel exists via the Helix Users API.
|
||||||
|
*/
|
||||||
|
static async validateUser(channelName: string): Promise<boolean> {
|
||||||
|
if (!/^[a-z0-9_]{1,25}$/i.test(channelName)) return false;
|
||||||
|
|
||||||
|
const token = await this.getAccessToken();
|
||||||
|
if (!token) return true; // Fallback if Twitch API credentials are not configured locally
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://api.twitch.tv/helix/users?login=${encodeURIComponent(channelName)}`, {
|
||||||
|
headers: {
|
||||||
|
'Client-ID': process.env.TWITCH_CLIENT_ID!,
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response.ok) return false;
|
||||||
|
const data: any = await response.json();
|
||||||
|
return Array.isArray(data.data) && data.data.length > 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[TWITCH] Error validating user ${channelName}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async checkStreams(client: ExtendedClient) {
|
static async checkStreams(client: ExtendedClient) {
|
||||||
const monitors: any[] = client.DB?.all('SELECT * FROM twitch_monitors') || [];
|
const monitors: any[] = client.DB?.all('SELECT * FROM twitch_monitors') || [];
|
||||||
if (monitors.length === 0) return;
|
if (monitors.length === 0) return;
|
||||||
|
|||||||
@@ -121,87 +121,89 @@ export class TwitchMonitor {
|
|||||||
this.client.on('messagedeleted', (channel: string, username: string, deletedMessage: string, userstate: DeleteUserstate) => {
|
this.client.on('messagedeleted', (channel: string, username: string, deletedMessage: string, userstate: DeleteUserstate) => {
|
||||||
const msgId = userstate['target-msg-id'] || '';
|
const msgId = userstate['target-msg-id'] || '';
|
||||||
const cached = this.cache.getByMsgId(msgId);
|
const cached = this.cache.getByMsgId(msgId);
|
||||||
const result = this.findMonitor(channel);
|
const results = this.findMonitors(channel);
|
||||||
if (!result) return;
|
for (const result of results) {
|
||||||
|
this.sendWebhook(result.monitor.webhookUrl, {
|
||||||
this.sendWebhook(result.monitor.webhookUrl, {
|
title: '🗑️ Nachricht gelöscht',
|
||||||
title: '🗑️ Nachricht gelöscht',
|
description: `**${username}** hat eine Nachricht gelöscht.\n\n${cached ? `~~${cached.text}~~` : `(Nachricht nicht im Cache)`}`,
|
||||||
description: `**${username}** hat eine Nachricht gelöscht.\n\n${cached ? `~~${cached.text}~~` : `(Nachricht nicht im Cache)`}`,
|
color: 0xff0000,
|
||||||
color: 0xff0000,
|
}, result.key);
|
||||||
}, result.key);
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.on('ban', (channel: string, username: string, reason: string) => {
|
this.client.on('ban', (channel: string, username: string, reason: string) => {
|
||||||
const result = this.findMonitor(channel);
|
const results = this.findMonitors(channel);
|
||||||
if (!result) return;
|
if (results.length === 0) return;
|
||||||
|
|
||||||
const lastMessages = this.cache.getLastN(username, channel, 5);
|
const lastMessages = this.cache.getLastN(username, channel, 5);
|
||||||
const context = lastMessages.length > 0
|
const context = lastMessages.length > 0
|
||||||
? `\n\n**Letzte Nachrichten:**\n${lastMessages.map(m => `• ${m.text}`).join('\n')}`
|
? `\n\n**Letzte Nachrichten:**\n${lastMessages.map(m => `• ${m.text}`).join('\n')}`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
this.sendWebhook(result.monitor.webhookUrl, {
|
for (const result of results) {
|
||||||
title: '🔨 User gebannt',
|
this.sendWebhook(result.monitor.webhookUrl, {
|
||||||
description: `**${username}** wurde gebannt.\n${reason ? `Grund: *${reason}*` : ''}${context}`,
|
title: '🔨 User gebannt',
|
||||||
color: 0x8b0000,
|
description: `**${username}** wurde gebannt.\n${reason ? `Grund: *${reason}*` : ''}${context}`,
|
||||||
}, result.key);
|
color: 0x8b0000,
|
||||||
|
}, result.key);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.on('timeout', (channel: string, username: string, reason: string) => {
|
this.client.on('timeout', (channel: string, username: string, reason: string) => {
|
||||||
const result = this.findMonitor(channel);
|
const results = this.findMonitors(channel);
|
||||||
if (!result) return;
|
for (const result of results) {
|
||||||
|
this.sendWebhook(result.monitor.webhookUrl, {
|
||||||
this.sendWebhook(result.monitor.webhookUrl, {
|
title: '⏱️ User getimeouted',
|
||||||
title: '⏱️ User getimeouted',
|
description: `**${username}** wurde getimeouted.\n${reason ? `Grund: *${reason}*` : ''}`,
|
||||||
description: `**${username}** wurde getimeouted.\n${reason ? `Grund: *${reason}*` : ''}`,
|
color: 0xffa500,
|
||||||
color: 0xffa500,
|
}, result.key);
|
||||||
}, result.key);
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.on('clearchat', (channel: string) => {
|
this.client.on('clearchat', (channel: string) => {
|
||||||
const result = this.findMonitor(channel);
|
const results = this.findMonitors(channel);
|
||||||
if (!result) return;
|
for (const result of results) {
|
||||||
|
this.sendWebhook(result.monitor.webhookUrl, {
|
||||||
this.sendWebhook(result.monitor.webhookUrl, {
|
title: '🧹 Chat geleert',
|
||||||
title: '🧹 Chat geleert',
|
description: `Der Chat in **${channel}** wurde geleert.`,
|
||||||
description: `Der Chat in **${channel}** wurde geleert.`,
|
color: 0xffa500,
|
||||||
color: 0xffa500,
|
}, result.key);
|
||||||
}, result.key);
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.on('cheer', (channel: string, userstate: ChatUserstate, message: string) => {
|
this.client.on('cheer', (channel: string, userstate: ChatUserstate, message: string) => {
|
||||||
const result = this.findMonitor(channel);
|
const results = this.findMonitors(channel);
|
||||||
if (!result) return;
|
|
||||||
|
|
||||||
const amount = userstate.bits || 0;
|
const amount = userstate.bits || 0;
|
||||||
|
|
||||||
this.sendWebhook(result.monitor.webhookUrl, {
|
for (const result of results) {
|
||||||
title: '💰 Bit-Cheer',
|
this.sendWebhook(result.monitor.webhookUrl, {
|
||||||
description: `**${userstate['display-name'] || userstate.username}** hat ${amount} Bits gespendet!\n${message || ''}`,
|
title: '💰 Bit-Cheer',
|
||||||
color: 0x9b59b6,
|
description: `**${userstate['display-name'] || userstate.username}** hat ${amount} Bits gespendet!\n${message || ''}`,
|
||||||
}, result.key);
|
color: 0x9b59b6,
|
||||||
|
}, result.key);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.on('subscription', (channel: string, username: string, methods: SubMethods, message: string, userstate: SubUserstate) => {
|
this.client.on('subscription', (channel: string, username: string, methods: SubMethods, message: string, userstate: SubUserstate) => {
|
||||||
const result = this.findMonitor(channel);
|
const results = this.findMonitors(channel);
|
||||||
if (!result) return;
|
for (const result of results) {
|
||||||
|
this.sendWebhook(result.monitor.webhookUrl, {
|
||||||
this.sendWebhook(result.monitor.webhookUrl, {
|
title: '🅿️ Subscription',
|
||||||
title: '🅿️ Subscription',
|
description: `**${username}** hat subscribed!${methods?.prime ? ' (Prime)' : ''}${message ? `\nNachricht: ${message}` : ''}`,
|
||||||
description: `**${username}** hat subscribed!${methods?.prime ? ' (Prime)' : ''}${message ? `\nNachricht: ${message}` : ''}`,
|
color: 0x9146ff,
|
||||||
color: 0x9146ff,
|
}, result.key);
|
||||||
}, result.key);
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.on('resub', (channel: string, username: string, months: number, message: string, userstate: SubUserstate, methods: SubMethods) => {
|
this.client.on('resub', (channel: string, username: string, months: number, message: string, userstate: SubUserstate, methods: SubMethods) => {
|
||||||
const result = this.findMonitor(channel);
|
const results = this.findMonitors(channel);
|
||||||
if (!result) return;
|
for (const result of results) {
|
||||||
|
this.sendWebhook(result.monitor.webhookUrl, {
|
||||||
this.sendWebhook(result.monitor.webhookUrl, {
|
title: '🅿️ Resubscription',
|
||||||
title: '🅿️ Resubscription',
|
description: `**${username}** resubbed für ${months} Monate!${methods?.prime ? ' (Prime)' : ''}\n${message || ''}`,
|
||||||
description: `**${username}** resubbed für ${months} Monate!${methods?.prime ? ' (Prime)' : ''}\n${message || ''}`,
|
color: 0x9146ff,
|
||||||
color: 0x9146ff,
|
}, result.key);
|
||||||
}, result.key);
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.on('disconnected', async () => {
|
this.client.on('disconnected', async () => {
|
||||||
@@ -231,13 +233,14 @@ export class TwitchMonitor {
|
|||||||
(this as any).discordClient = discordClient;
|
(this as any).discordClient = discordClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
private findMonitor(channel: string): { monitor: MonitorChannel; key: string } | undefined {
|
private findMonitors(channel: string): { monitor: MonitorChannel; key: string }[] {
|
||||||
const normalizedChannel = channel.startsWith('#') ? channel : `#${channel}`;
|
const normalizedChannel = channel.startsWith('#') ? channel : `#${channel}`;
|
||||||
const entry = Array.from(this.monitors.entries()).find(
|
return Array.from(this.monitors.entries())
|
||||||
([_, m]) => m.twitchChannel.toLowerCase() === normalizedChannel.toLowerCase() ||
|
.filter(
|
||||||
m.twitchChannel.toLowerCase() === channel.toLowerCase()
|
([_, m]) => m.twitchChannel.toLowerCase() === normalizedChannel.toLowerCase() ||
|
||||||
);
|
m.twitchChannel.toLowerCase() === channel.toLowerCase()
|
||||||
return entry ? { key: entry[0], monitor: entry[1] } : undefined;
|
)
|
||||||
|
.map(([key, monitor]) => ({ key, monitor }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async addMonitor(guildId: string, twitchChannel: string, discordChannelId: string, webhookUrl: string): Promise<void> {
|
async addMonitor(guildId: string, twitchChannel: string, discordChannelId: string, webhookUrl: string): Promise<void> {
|
||||||
@@ -254,21 +257,26 @@ export class TwitchMonitor {
|
|||||||
this.webhookClients.set(key, new WebhookClient({ url: webhookUrl }));
|
this.webhookClients.set(key, new WebhookClient({ url: webhookUrl }));
|
||||||
|
|
||||||
if (this.client) {
|
if (this.client) {
|
||||||
await this.client.join(normalizedChannel);
|
await this.client.join(normalizedChannel).catch(() => null);
|
||||||
|
} else if (process.env.TWITCH_OAUTH_TOKEN && process.env.TWITCH_USERNAME && !this.isTokenInvalid) {
|
||||||
|
await this.connect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeMonitor(guildId: string, twitchChannel: string): Promise<void> {
|
async removeMonitor(guildId: string, twitchChannel: string): Promise<void> {
|
||||||
const normalizedChannel = twitchChannel.startsWith('#') ? twitchChannel : `#${twitchChannel}`;
|
const normalizedChannel = twitchChannel.startsWith('#') ? twitchChannel : `#${twitchChannel}`;
|
||||||
const key = `${guildId}:${normalizedChannel}`;
|
const key = `${guildId}:${normalizedChannel}`;
|
||||||
const monitor = this.monitors.get(key);
|
|
||||||
|
|
||||||
if (monitor && this.client) {
|
|
||||||
await this.client.part(normalizedChannel);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.monitors.delete(key);
|
this.monitors.delete(key);
|
||||||
this.webhookClients.delete(key);
|
this.webhookClients.delete(key);
|
||||||
|
|
||||||
|
const stillMonitored = Array.from(this.monitors.values()).some(
|
||||||
|
m => m.twitchChannel.toLowerCase() === normalizedChannel.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!stillMonitored && this.client) {
|
||||||
|
await this.client.part(normalizedChannel).catch(() => null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getMonitors(guildId?: string): MonitorChannel[] {
|
getMonitors(guildId?: string): MonitorChannel[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user