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:
@@ -116,8 +116,9 @@ const command: Command = {
|
||||
.setDescription('Neuer Wert'))
|
||||
.addIntegerOption(option =>
|
||||
option.setName('duration_value')
|
||||
.setDescription('Dauer in Sekunden')
|
||||
.setMinValue(0)))
|
||||
.setDescription('Dauer in Sekunden (1 - 2419200)')
|
||||
.setMinValue(1)
|
||||
.setMaxValue(2419200)))
|
||||
.addSubcommandGroup(group =>
|
||||
group
|
||||
.setName('purge')
|
||||
@@ -171,7 +172,8 @@ const command: Command = {
|
||||
subcommand
|
||||
.setName('help')
|
||||
.setDescription('Zeigt Hilfe zu den Admin-Befehlen an.'))
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
|
||||
.setDMPermission(false),
|
||||
category: 'Admin',
|
||||
async execute(interaction: any) {
|
||||
const client = interaction.client as ExtendedClient;
|
||||
@@ -179,6 +181,14 @@ const command: Command = {
|
||||
const DB = client.DB;
|
||||
const guild = interaction.guild;
|
||||
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') {
|
||||
const embed = new EmbedBuilder()
|
||||
@@ -206,6 +216,25 @@ const command: Command = {
|
||||
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
|
||||
const settings = DB.getSettings(guildId);
|
||||
|
||||
@@ -218,6 +247,12 @@ const command: Command = {
|
||||
return;
|
||||
}
|
||||
|
||||
const hierarchyError = checkTargetHierarchy(target);
|
||||
if (hierarchyError) {
|
||||
await interaction.reply({ content: hierarchyError, ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.kickable) {
|
||||
await interaction.reply({ content: 'Ich kann diesen Nutzer nicht kicken.', ephemeral: true });
|
||||
return;
|
||||
@@ -239,7 +274,9 @@ const command: Command = {
|
||||
await EventLogger.sendLog(interaction.client, guildId, 'kicks', embed);
|
||||
} catch (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;
|
||||
}
|
||||
@@ -253,6 +290,12 @@ const command: Command = {
|
||||
return;
|
||||
}
|
||||
|
||||
const hierarchyError = checkTargetHierarchy(target);
|
||||
if (hierarchyError) {
|
||||
await interaction.reply({ content: hierarchyError, ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
DB.run(
|
||||
'INSERT INTO mod_logs (guild_id, user_id, moderator_id, action, reason) VALUES (?, ?, ?, ?, ?)',
|
||||
guildId,
|
||||
@@ -284,9 +327,11 @@ const command: Command = {
|
||||
await target.kick(`Auto-Kick: ${warnThreshold} Warnungen`);
|
||||
message += `\n👢 **Automatisch gekickt!**`;
|
||||
} 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`);
|
||||
const durationMins = Math.floor(muteDuration / 60);
|
||||
const durationMins = Math.max(1, Math.floor(muteDuration / 60));
|
||||
message += `\n🔇 **Automatisch gemuted (${durationMins} Min)!**`;
|
||||
} else if (warnAction === 'ban') {
|
||||
await target.ban({ reason: `Auto-Ban: ${warnThreshold} Warnungen` });
|
||||
@@ -350,6 +395,12 @@ const command: Command = {
|
||||
return;
|
||||
}
|
||||
|
||||
const hierarchyError = checkTargetHierarchy(target);
|
||||
if (hierarchyError) {
|
||||
await interaction.reply({ content: hierarchyError, ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse duration
|
||||
let durationMs: number | null = null;
|
||||
let durationText = '';
|
||||
@@ -413,7 +464,9 @@ const command: Command = {
|
||||
await EventLogger.sendLog(interaction.client, guildId, 'mutes', embed);
|
||||
} catch (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;
|
||||
}
|
||||
@@ -446,6 +499,12 @@ const command: Command = {
|
||||
return;
|
||||
}
|
||||
|
||||
const hierarchyError = checkTargetHierarchy(target);
|
||||
if (hierarchyError) {
|
||||
await interaction.reply({ content: hierarchyError, ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.bannable) {
|
||||
await interaction.reply({ content: 'Ich kann diesen Nutzer nicht bannen.', ephemeral: true });
|
||||
return;
|
||||
@@ -468,7 +527,9 @@ const command: Command = {
|
||||
await EventLogger.sendLog(interaction.client, guildId, 'bans', embed);
|
||||
} catch (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;
|
||||
}
|
||||
@@ -510,35 +571,37 @@ const command: Command = {
|
||||
}
|
||||
|
||||
if (setting === 'warn_threshold') {
|
||||
const threshold = parseInt(value!);
|
||||
const threshold = value !== null ? parseInt(value) : (durationValue ?? NaN);
|
||||
if (isNaN(threshold) || threshold < 1) {
|
||||
await interaction.reply({ content: '❌ Ungültiger Wert. Mindestens 1.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
if (setting === 'warn_action') {
|
||||
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 });
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
if (setting === 'warn_mute_duration') {
|
||||
const duration = durationValue ?? 1800;
|
||||
if (duration < 0) {
|
||||
await interaction.reply({ content: '❌ Ungültige Dauer.', ephemeral: true });
|
||||
const parsedValue = value !== null ? parseInt(value) : NaN;
|
||||
const duration = durationValue ?? (!isNaN(parsedValue) ? parsedValue : 1800);
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,12 +51,19 @@ const command: Command = {
|
||||
subcommand
|
||||
.setName('help')
|
||||
.setDescription('Hilfe zu Log-Befehlen (Mod).'))
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
|
||||
.setDMPermission(false),
|
||||
category: 'Admin',
|
||||
async execute(interaction: any) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
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;
|
||||
|
||||
|
||||
@@ -84,6 +91,9 @@ const command: Command = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure guild settings row exists before reading or updating
|
||||
const settings = DB.getSettings(guildId);
|
||||
|
||||
if (subcommand === 'setchannel') {
|
||||
const channel = interaction.options.getChannel('channel');
|
||||
|
||||
@@ -98,8 +108,6 @@ const command: Command = {
|
||||
}
|
||||
|
||||
if (subcommand === 'list') {
|
||||
const settings = DB.getSettings(guildId);
|
||||
|
||||
const channel = settings?.log_channel
|
||||
? `<#${settings.log_channel}>`
|
||||
: 'Nicht gesetzt';
|
||||
|
||||
@@ -20,8 +20,11 @@ export async function updateRoleMessage(client: any, category: 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()
|
||||
.setLabel(opt.label.replace(/^[^\s]+\s/, ''))
|
||||
.setLabel(cleanLabel)
|
||||
.setValue(opt.role_id);
|
||||
if (opt.emoji && opt.emoji.trim().length > 0) {
|
||||
menuOption.setEmoji(opt.emoji.trim());
|
||||
@@ -95,7 +98,7 @@ const command: Command = {
|
||||
option.setName('rolle')
|
||||
.setDescription('Die Discord-Rolle zum Ändern')
|
||||
.setRequired(true))
|
||||
.addStringOption(option =>
|
||||
.addRoleOption(option =>
|
||||
option.setName('neue_rolle')
|
||||
.setDescription('Die neue Discord-Rolle')
|
||||
.setRequired(false))
|
||||
@@ -148,14 +151,26 @@ const command: Command = {
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('Listet alle Rollen-Kategorien auf'))
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
|
||||
.setDMPermission(false),
|
||||
category: 'Admin',
|
||||
async execute(interaction: any) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
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;
|
||||
|
||||
if (!isAdmin) {
|
||||
await interaction.reply({ content: 'Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'list') {
|
||||
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) {
|
||||
const members = guild.members.cache.filter((m: any) => m.roles.cache.has(role.id));
|
||||
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) {
|
||||
const members = guild.members.cache.filter((m: any) => m.roles.cache.has(opt.role_id));
|
||||
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
|
||||
.setName('help')
|
||||
.setDescription('Zeigt Hilfe zu den Stoppuhr-Befehlen an.'))
|
||||
.setDMPermission(true),
|
||||
.setDMPermission(false),
|
||||
category: 'Public',
|
||||
async execute(interaction: any) {
|
||||
const client = interaction.client as ExtendedClient;
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
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 channelId = interaction.channelId;
|
||||
const DB = client.DB;
|
||||
|
||||
@@ -134,14 +134,21 @@ const command: Command = {
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('help')
|
||||
.setDescription('Zeigt Hilfe zu den Timer-Befehlen an.')),
|
||||
.setDescription('Zeigt Hilfe zu den Timer-Befehlen an.'))
|
||||
.setDMPermission(false),
|
||||
category: 'Public',
|
||||
async execute(interaction: any) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
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 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;
|
||||
|
||||
if (subcommand === 'help') {
|
||||
|
||||
@@ -33,12 +33,18 @@ const command: Command = {
|
||||
subcommand
|
||||
.setName('help')
|
||||
.setDescription('Zeigt Hilfe zu den Trigger-Befehlen an.'))
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
||||
category: 'Admin',
|
||||
.setDMPermission(false),
|
||||
category: 'Public',
|
||||
async execute(interaction: any) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
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;
|
||||
|
||||
if (subcommand === 'help') {
|
||||
@@ -99,9 +105,14 @@ const command: Command = {
|
||||
}
|
||||
|
||||
if (subcommand === 'add') {
|
||||
const triggerWord = interaction.options.getString('trigger')!.toLowerCase();
|
||||
const triggerWord = interaction.options.getString('trigger')!.trim().toLowerCase();
|
||||
const responseText = interaction.options.getString('antwort')!;
|
||||
|
||||
if (!triggerWord) {
|
||||
await interaction.reply({ content: '❌ Das Trigger-Wort darf nicht leer sein.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
DB.run(
|
||||
'INSERT INTO auto_responses (guild_id, trigger_word, response_text) VALUES (?, ?, ?) ON CONFLICT(guild_id, trigger_word) DO UPDATE SET response_text = ?',
|
||||
guildId,
|
||||
@@ -115,7 +126,7 @@ const command: Command = {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -45,13 +45,20 @@ const command: Command = {
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('help')
|
||||
.setDescription('Zeigt Hilfe zu den Twitch-Befehlen an.')),
|
||||
.setDescription('Zeigt Hilfe zu den Twitch-Befehlen an.'))
|
||||
.setDMPermission(false),
|
||||
category: 'Public',
|
||||
async execute(interaction: any) {
|
||||
const client = interaction.client as ExtendedClient;
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
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;
|
||||
|
||||
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 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: '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();
|
||||
|
||||
@@ -184,13 +191,19 @@ const command: Command = {
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const results = { added: 0, updated: 0, errors: 0 };
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const streamer of streamers) {
|
||||
try {
|
||||
// Check if stream exists
|
||||
await TwitchManager.fetchStreamData(streamer);
|
||||
const isValid = await TwitchManager.validateUser(streamer);
|
||||
if (!isValid) {
|
||||
errors.push(streamer);
|
||||
results.errors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if already exists
|
||||
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.updated > 0) reply += `\n🔄 Aktualisiert: ${results.updated}`;
|
||||
if (results.errors > 0) reply += `\n❌ Fehler: ${results.errors} (${errors.join(', ')})`;
|
||||
|
||||
await interaction.reply({ content: reply, ephemeral: true });
|
||||
await interaction.editReply({ content: reply });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,12 +35,19 @@ const command: Command = {
|
||||
subcommand
|
||||
.setName('help')
|
||||
.setDescription('Zeigt Hilfe zu den TwitchMonitor-Befehlen'))
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
|
||||
.setDMPermission(false),
|
||||
category: 'Admin',
|
||||
async execute(interaction: any) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
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;
|
||||
|
||||
if (subcommand === 'help') {
|
||||
@@ -59,6 +66,11 @@ const command: Command = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAdmin) {
|
||||
await interaction.reply({ content: 'Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'list') {
|
||||
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;
|
||||
}
|
||||
|
||||
if (!isAdmin) {
|
||||
await interaction.reply({ content: 'Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
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 existing = DB.get('SELECT * FROM twitch_monitor_channels WHERE guild_id = ? AND twitch_channel = ?', guildId, twitchChannel);
|
||||
@@ -94,34 +101,41 @@ const command: Command = {
|
||||
return;
|
||||
}
|
||||
|
||||
const webhook = await discordChannel.createWebhook({
|
||||
name: `${twitchChannel} | Mod-Log`,
|
||||
avatar: undefined,
|
||||
}).catch(async () => {
|
||||
const existing_webhook = await discordChannel.fetchWebhooks().then((webhooks: any) =>
|
||||
webhooks.find((w: any) => w.name?.includes(twitchChannel))
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
try {
|
||||
const webhook = await discordChannel.createWebhook({
|
||||
name: `${twitchChannel} | Mod-Log`,
|
||||
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(
|
||||
'INSERT INTO twitch_monitor_channels (guild_id, twitch_channel, discord_channel_id, webhook_url) VALUES (?, ?, ?, ?)',
|
||||
guildId,
|
||||
twitchChannel,
|
||||
discordChannel.id,
|
||||
webhook.url
|
||||
);
|
||||
const monitor = TwitchMonitor.getInstance();
|
||||
await monitor.addMonitor(guildId, twitchChannel, discordChannel.id, webhook.url);
|
||||
|
||||
const monitor = TwitchMonitor.getInstance();
|
||||
await monitor.addMonitor(guildId, twitchChannel, discordChannel.id, webhook.url);
|
||||
|
||||
await interaction.reply({ content: `${twitchChannel} wird jetzt überwacht.`, ephemeral: true });
|
||||
await interaction.editReply({ content: `✅ **${twitchChannel}** wird jetzt in ${discordChannel} überwacht.` });
|
||||
} catch (error) {
|
||||
console.error('[TWITCHMONITOR] Error adding monitor:', error);
|
||||
await interaction.editReply({ content: '❌ Konnte keinen Webhook erstellen. Bitte prüfe, ob der Bot die Berechtigung "Webhooks verwalten" im Ziel-Kanal hat.' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
if (!monitor) {
|
||||
|
||||
@@ -49,15 +49,21 @@ const command: Command = {
|
||||
subcommand
|
||||
.setName('help')
|
||||
.setDescription('Hilfe zu Welcome-Befehlen.'))
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers)
|
||||
.setDMPermission(false),
|
||||
category: 'Admin',
|
||||
async execute(interaction: any) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
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;
|
||||
|
||||
|
||||
if (subcommand === 'help') {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('👋 Welcome Hilfe')
|
||||
@@ -78,9 +84,16 @@ const command: Command = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'list') {
|
||||
const settings = DB.getSettings(guildId);
|
||||
// Admin only for all commands
|
||||
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
|
||||
? `<#${settings.welcome_channel}>`
|
||||
: 'Nicht gesetzt';
|
||||
@@ -103,12 +116,6 @@ const command: Command = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin only for modify commands
|
||||
if (!isAdmin) {
|
||||
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'setchannel') {
|
||||
const channel = interaction.options.getChannel('channel');
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export default {
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
await interaction.message.edit({ components: interaction.message.components }).catch(() => null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,6 +117,7 @@ export default {
|
||||
});
|
||||
}
|
||||
}
|
||||
await interaction.message.edit({ components: interaction.message.components }).catch(() => null);
|
||||
} catch (error) {
|
||||
console.error('[ROLESETUP] Role assignment error:', error);
|
||||
const errorMsg = '❌ Fehler beim Ändern der Rolle. Mir fehlen eventuell die Rechte dazu.';
|
||||
@@ -126,7 +128,6 @@ export default {
|
||||
}
|
||||
}
|
||||
return;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,20 @@ export default {
|
||||
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) {
|
||||
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);
|
||||
const pattern = new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegex(word)}(?![\\p{L}\\p{N}_])`, 'iu');
|
||||
if (pattern.test(contentWithoutUrls)) {
|
||||
try {
|
||||
await message.channel.send(trigger.response_text);
|
||||
} catch (error) {
|
||||
console.error(`[TRIGGER] Failed to send auto-response in guild ${message.guildId}:`, error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ async function validateRoles(client: ExtendedClient) {
|
||||
}
|
||||
|
||||
async function initTwitchMonitor(client: ExtendedClient) {
|
||||
const monitor = TwitchMonitor.getInstance();
|
||||
monitor.setDiscordClient(client);
|
||||
|
||||
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');
|
||||
return;
|
||||
@@ -59,9 +62,6 @@ async function initTwitchMonitor(client: ExtendedClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
const monitor = TwitchMonitor.getInstance();
|
||||
monitor.setDiscordClient(client);
|
||||
|
||||
for (const m of monitors) {
|
||||
const webhookExists = m.webhook_url && m.webhook_url.length > 0;
|
||||
if (webhookExists) {
|
||||
|
||||
@@ -63,6 +63,31 @@ export class TwitchManager {
|
||||
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) {
|
||||
const monitors: any[] = client.DB?.all('SELECT * FROM twitch_monitors') || [];
|
||||
if (monitors.length === 0) return;
|
||||
|
||||
@@ -121,87 +121,89 @@ export class TwitchMonitor {
|
||||
this.client.on('messagedeleted', (channel: string, username: string, deletedMessage: string, userstate: DeleteUserstate) => {
|
||||
const msgId = userstate['target-msg-id'] || '';
|
||||
const cached = this.cache.getByMsgId(msgId);
|
||||
const result = this.findMonitor(channel);
|
||||
if (!result) return;
|
||||
|
||||
this.sendWebhook(result.monitor.webhookUrl, {
|
||||
title: '🗑️ Nachricht gelöscht',
|
||||
description: `**${username}** hat eine Nachricht gelöscht.\n\n${cached ? `~~${cached.text}~~` : `(Nachricht nicht im Cache)`}`,
|
||||
color: 0xff0000,
|
||||
}, result.key);
|
||||
const results = this.findMonitors(channel);
|
||||
for (const result of results) {
|
||||
this.sendWebhook(result.monitor.webhookUrl, {
|
||||
title: '🗑️ Nachricht gelöscht',
|
||||
description: `**${username}** hat eine Nachricht gelöscht.\n\n${cached ? `~~${cached.text}~~` : `(Nachricht nicht im Cache)`}`,
|
||||
color: 0xff0000,
|
||||
}, result.key);
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on('ban', (channel: string, username: string, reason: string) => {
|
||||
const result = this.findMonitor(channel);
|
||||
if (!result) return;
|
||||
const results = this.findMonitors(channel);
|
||||
if (results.length === 0) return;
|
||||
|
||||
const lastMessages = this.cache.getLastN(username, channel, 5);
|
||||
const context = lastMessages.length > 0
|
||||
? `\n\n**Letzte Nachrichten:**\n${lastMessages.map(m => `• ${m.text}`).join('\n')}`
|
||||
: '';
|
||||
|
||||
this.sendWebhook(result.monitor.webhookUrl, {
|
||||
title: '🔨 User gebannt',
|
||||
description: `**${username}** wurde gebannt.\n${reason ? `Grund: *${reason}*` : ''}${context}`,
|
||||
color: 0x8b0000,
|
||||
}, result.key);
|
||||
for (const result of results) {
|
||||
this.sendWebhook(result.monitor.webhookUrl, {
|
||||
title: '🔨 User gebannt',
|
||||
description: `**${username}** wurde gebannt.\n${reason ? `Grund: *${reason}*` : ''}${context}`,
|
||||
color: 0x8b0000,
|
||||
}, result.key);
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on('timeout', (channel: string, username: string, reason: string) => {
|
||||
const result = this.findMonitor(channel);
|
||||
if (!result) return;
|
||||
|
||||
this.sendWebhook(result.monitor.webhookUrl, {
|
||||
title: '⏱️ User getimeouted',
|
||||
description: `**${username}** wurde getimeouted.\n${reason ? `Grund: *${reason}*` : ''}`,
|
||||
color: 0xffa500,
|
||||
}, result.key);
|
||||
const results = this.findMonitors(channel);
|
||||
for (const result of results) {
|
||||
this.sendWebhook(result.monitor.webhookUrl, {
|
||||
title: '⏱️ User getimeouted',
|
||||
description: `**${username}** wurde getimeouted.\n${reason ? `Grund: *${reason}*` : ''}`,
|
||||
color: 0xffa500,
|
||||
}, result.key);
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on('clearchat', (channel: string) => {
|
||||
const result = this.findMonitor(channel);
|
||||
if (!result) return;
|
||||
|
||||
this.sendWebhook(result.monitor.webhookUrl, {
|
||||
title: '🧹 Chat geleert',
|
||||
description: `Der Chat in **${channel}** wurde geleert.`,
|
||||
color: 0xffa500,
|
||||
}, result.key);
|
||||
const results = this.findMonitors(channel);
|
||||
for (const result of results) {
|
||||
this.sendWebhook(result.monitor.webhookUrl, {
|
||||
title: '🧹 Chat geleert',
|
||||
description: `Der Chat in **${channel}** wurde geleert.`,
|
||||
color: 0xffa500,
|
||||
}, result.key);
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on('cheer', (channel: string, userstate: ChatUserstate, message: string) => {
|
||||
const result = this.findMonitor(channel);
|
||||
if (!result) return;
|
||||
|
||||
const results = this.findMonitors(channel);
|
||||
const amount = userstate.bits || 0;
|
||||
|
||||
this.sendWebhook(result.monitor.webhookUrl, {
|
||||
title: '💰 Bit-Cheer',
|
||||
description: `**${userstate['display-name'] || userstate.username}** hat ${amount} Bits gespendet!\n${message || ''}`,
|
||||
color: 0x9b59b6,
|
||||
}, result.key);
|
||||
for (const result of results) {
|
||||
this.sendWebhook(result.monitor.webhookUrl, {
|
||||
title: '💰 Bit-Cheer',
|
||||
description: `**${userstate['display-name'] || userstate.username}** hat ${amount} Bits gespendet!\n${message || ''}`,
|
||||
color: 0x9b59b6,
|
||||
}, result.key);
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on('subscription', (channel: string, username: string, methods: SubMethods, message: string, userstate: SubUserstate) => {
|
||||
const result = this.findMonitor(channel);
|
||||
if (!result) return;
|
||||
|
||||
this.sendWebhook(result.monitor.webhookUrl, {
|
||||
title: '🅿️ Subscription',
|
||||
description: `**${username}** hat subscribed!${methods?.prime ? ' (Prime)' : ''}${message ? `\nNachricht: ${message}` : ''}`,
|
||||
color: 0x9146ff,
|
||||
}, result.key);
|
||||
const results = this.findMonitors(channel);
|
||||
for (const result of results) {
|
||||
this.sendWebhook(result.monitor.webhookUrl, {
|
||||
title: '🅿️ Subscription',
|
||||
description: `**${username}** hat subscribed!${methods?.prime ? ' (Prime)' : ''}${message ? `\nNachricht: ${message}` : ''}`,
|
||||
color: 0x9146ff,
|
||||
}, result.key);
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on('resub', (channel: string, username: string, months: number, message: string, userstate: SubUserstate, methods: SubMethods) => {
|
||||
const result = this.findMonitor(channel);
|
||||
if (!result) return;
|
||||
|
||||
this.sendWebhook(result.monitor.webhookUrl, {
|
||||
title: '🅿️ Resubscription',
|
||||
description: `**${username}** resubbed für ${months} Monate!${methods?.prime ? ' (Prime)' : ''}\n${message || ''}`,
|
||||
color: 0x9146ff,
|
||||
}, result.key);
|
||||
const results = this.findMonitors(channel);
|
||||
for (const result of results) {
|
||||
this.sendWebhook(result.monitor.webhookUrl, {
|
||||
title: '🅿️ Resubscription',
|
||||
description: `**${username}** resubbed für ${months} Monate!${methods?.prime ? ' (Prime)' : ''}\n${message || ''}`,
|
||||
color: 0x9146ff,
|
||||
}, result.key);
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on('disconnected', async () => {
|
||||
@@ -231,13 +233,14 @@ export class TwitchMonitor {
|
||||
(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 entry = Array.from(this.monitors.entries()).find(
|
||||
([_, m]) => m.twitchChannel.toLowerCase() === normalizedChannel.toLowerCase() ||
|
||||
m.twitchChannel.toLowerCase() === channel.toLowerCase()
|
||||
);
|
||||
return entry ? { key: entry[0], monitor: entry[1] } : undefined;
|
||||
return Array.from(this.monitors.entries())
|
||||
.filter(
|
||||
([_, m]) => m.twitchChannel.toLowerCase() === normalizedChannel.toLowerCase() ||
|
||||
m.twitchChannel.toLowerCase() === channel.toLowerCase()
|
||||
)
|
||||
.map(([key, monitor]) => ({ key, monitor }));
|
||||
}
|
||||
|
||||
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 }));
|
||||
|
||||
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> {
|
||||
const normalizedChannel = twitchChannel.startsWith('#') ? twitchChannel : `#${twitchChannel}`;
|
||||
const key = `${guildId}:${normalizedChannel}`;
|
||||
const monitor = this.monitors.get(key);
|
||||
|
||||
if (monitor && this.client) {
|
||||
await this.client.part(normalizedChannel);
|
||||
}
|
||||
|
||||
this.monitors.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[] {
|
||||
|
||||
Reference in New Issue
Block a user