From 62a4a6a55c2c37a4bd864d3b42b66fdf8c549de2 Mon Sep 17 00:00:00 2001 From: vibe-bot Date: Sun, 10 Aug 2025 23:10:51 +0200 Subject: [PATCH 1/8] feat(entrance-exit): Entrance/Exit-Sounds pro Nutzer via DM (?entrance/?exit); Playback bei Join/Leave wenn Bot im Channel; ?help aktualisiert; ?restart entfernt --- server/src/index.ts | 103 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 89 insertions(+), 14 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 99d67ad..f0de27d 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -5,7 +5,7 @@ import express, { Request, Response } from 'express'; // import multer from 'multer'; import cors from 'cors'; import crypto from 'node:crypto'; -import { Client, GatewayIntentBits, Partials, ChannelType, Events, type Message } from 'discord.js'; +import { Client, GatewayIntentBits, Partials, ChannelType, Events, type Message, VoiceState } from 'discord.js'; import { joinVoiceChannel, createAudioPlayer, @@ -55,6 +55,8 @@ type Category = { id: string; name: string; color?: string; sort?: number }; fileCategories?: Record; // relPath or fileName -> categoryIds[] fileBadges?: Record; // relPath or fileName -> custom badges (emoji/text) selectedChannels?: Record; // guildId -> channelId (serverweite Auswahl) + entranceSounds?: Record; // userId -> relativePath or fileName + exitSounds?: Record; // userId -> relativePath or fileName }; // Neuer, persistenter Speicherort direkt im Sounds-Volume const STATE_FILE_NEW = path.join(SOUNDS_DIR, 'state.json'); @@ -74,7 +76,9 @@ function readPersistedState(): PersistedState { categories: Array.isArray(parsed.categories) ? parsed.categories : [], fileCategories: parsed.fileCategories ?? {}, fileBadges: parsed.fileBadges ?? {}, - selectedChannels: parsed.selectedChannels ?? {} + selectedChannels: parsed.selectedChannels ?? {}, + entranceSounds: parsed.entranceSounds ?? {}, + exitSounds: parsed.exitSounds ?? {} } as PersistedState; } // 2) Fallback: alten Speicherort lesen und sofort nach NEW migrieren @@ -88,7 +92,9 @@ function readPersistedState(): PersistedState { categories: Array.isArray(parsed.categories) ? parsed.categories : [], fileCategories: parsed.fileCategories ?? {}, fileBadges: parsed.fileBadges ?? {}, - selectedChannels: parsed.selectedChannels ?? {} + selectedChannels: parsed.selectedChannels ?? {}, + entranceSounds: parsed.entranceSounds ?? {}, + exitSounds: parsed.exitSounds ?? {} }; try { fs.mkdirSync(path.dirname(STATE_FILE_NEW), { recursive: true }); @@ -234,7 +240,8 @@ async function handleCommand(message: Message, content: string) { 'Available commands\n' + '?help - zeigt diese Hilfe\n' + '?list - listet alle Audio-Dateien (mp3/wav)\n' + - '?restart - startet den Container neu (Bestätigung erforderlich)\n' + '?entrance - setze deinen Entrance-Sound\n' + + '?exit - setze deinen Exit-Sound (optional)\n' ); return; } @@ -245,16 +252,41 @@ async function handleCommand(message: Message, content: string) { await reply(files.length ? files.join('\n') : 'Keine Dateien gefunden.'); return; } - if (cmd === '?restart') { - const confirm = (parts[1] || '').toLowerCase(); - if (confirm === 'y' || confirm === 'yes' || confirm === 'ja' || confirm === 'confirm') { - await reply('Neustart wird ausgeführt...'); - try { await fetch('http://127.0.0.1:9001/_restart').catch(() => {}); } catch {} - setTimeout(() => process.exit(0), 500); - } else { - await reply('Bitte mit "?restart y" bestätigen.'); - } - return; + if (cmd === '?entrance') { + const [, userName, fileName] = parts; + if (!userName || !fileName) { await reply('Verwendung: ?entrance '); return; } + const lower = fileName.toLowerCase(); + if (!(lower.endsWith('.mp3') || lower.endsWith('.wav'))) { await reply('Nur .mp3 oder .wav Dateien sind erlaubt'); return; } + const resolve = (() => { + try { + const direct = path.join(SOUNDS_DIR, fileName); if (fs.existsSync(direct)) return fileName; + const dirs = fs.readdirSync(SOUNDS_DIR, { withFileTypes: true }); + for (const d of dirs) { if (!d.isDirectory()) continue; const cand = path.join(SOUNDS_DIR, d.name, fileName); if (fs.existsSync(cand)) return path.join(d.name, fileName).replace(/\\/g, '/'); } + return ''; + } catch { return ''; } + })(); + if (!resolve) { await reply('Datei nicht gefunden. Nutze ?list.'); return; } + const userId = message.author?.id ?? ''; if (!userId) { await reply('Kein Benutzer erkannt.'); return; } + persistedState.entranceSounds = persistedState.entranceSounds ?? {}; persistedState.entranceSounds[userId] = resolve; writePersistedState(persistedState); + await reply(`Entrance-Sound gesetzt: ${resolve}`); return; + } + if (cmd === '?exit') { + const [, userName, fileName] = parts; + if (!userName || !fileName) { await reply('Verwendung: ?exit '); return; } + const lower = fileName.toLowerCase(); + if (!(lower.endsWith('.mp3') || lower.endsWith('.wav'))) { await reply('Nur .mp3 oder .wav Dateien sind erlaubt'); return; } + const resolve = (() => { + try { + const direct = path.join(SOUNDS_DIR, fileName); if (fs.existsSync(direct)) return fileName; + const dirs = fs.readdirSync(SOUNDS_DIR, { withFileTypes: true }); + for (const d of dirs) { if (!d.isDirectory()) continue; const cand = path.join(SOUNDS_DIR, d.name, fileName); if (fs.existsSync(cand)) return path.join(d.name, fileName).replace(/\\/g, '/'); } + return ''; + } catch { return ''; } + })(); + if (!resolve) { await reply('Datei nicht gefunden. Nutze ?list.'); return; } + const userId = message.author?.id ?? ''; if (!userId) { await reply('Kein Benutzer erkannt.'); return; } + persistedState.exitSounds = persistedState.exitSounds ?? {}; persistedState.exitSounds[userId] = resolve; writePersistedState(persistedState); + await reply(`Exit-Sound gesetzt: ${resolve}`); return; } await reply('Unbekannter Command. Nutze ?help.'); } @@ -338,6 +370,49 @@ client.once(Events.ClientReady, () => { console.log(`Bot eingeloggt als ${client.user?.tag}`); }); +// Voice State Updates: Entrance/Exit +client.on(Events.VoiceStateUpdate, async (oldState: VoiceState, newState: VoiceState) => { + try { + const userId = newState.member?.user?.id || oldState.member?.user?.id; + if (!userId) return; + const guildId = (newState.guild?.id || oldState.guild?.id) as string; + if (!guildId) return; + + const before = oldState.channelId; + const after = newState.channelId; + + // Bot muss bereits im Ziel-Channel sein, sonst nichts tun + const connection = getVoiceConnection(guildId); + const botChannelId = connection?.joinConfig?.channelId; + + if (!before && after && botChannelId && botChannelId === after) { + // User joined bot channel → Entrance + const mapping = persistedState.entranceSounds ?? {}; + const file = mapping[userId]; + if (file) { + const rel = file.replace(/\\/g, '/'); + const abs = path.join(SOUNDS_DIR, rel); + if (fs.existsSync(abs)) { + await playFilePath(guildId, after, abs, undefined, rel); + } + } + } else if (before && !after && botChannelId && botChannelId === before) { + // User left bot channel → Exit + const mapping = persistedState.exitSounds ?? {}; + const file = mapping[userId]; + if (file) { + const rel = file.replace(/\\/g, '/'); + const abs = path.join(SOUNDS_DIR, rel); + if (fs.existsSync(abs)) { + await playFilePath(guildId, before, abs, undefined, rel); + } + } + } + } catch (e) { + console.warn('VoiceStateUpdate entrance/exit handling error', e); + } +}); + client.on(Events.MessageCreate, async (message: Message) => { try { if (message.author?.bot) return; From 8604e5591ddab6bb20077e46a2f3a7d443c2f3e4 Mon Sep 17 00:00:00 2001 From: vibe-bot Date: Sun, 10 Aug 2025 23:18:43 +0200 Subject: [PATCH 2/8] feat(entrance-exit): Bot joint dem Nutzer nach (Entrance) und spielt Sound; Exit-Sound beim Verlassen; ?entrance/?exit nutzen Discord-User statt Namen --- server/src/index.ts | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index f0de27d..52b5e97 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -240,8 +240,8 @@ async function handleCommand(message: Message, content: string) { 'Available commands\n' + '?help - zeigt diese Hilfe\n' + '?list - listet alle Audio-Dateien (mp3/wav)\n' + - '?entrance - setze deinen Entrance-Sound\n' + - '?exit - setze deinen Exit-Sound (optional)\n' + '?entrance - setze deinen Entrance-Sound\n' + + '?exit - setze deinen Exit-Sound (optional)\n' ); return; } @@ -253,8 +253,8 @@ async function handleCommand(message: Message, content: string) { return; } if (cmd === '?entrance') { - const [, userName, fileName] = parts; - if (!userName || !fileName) { await reply('Verwendung: ?entrance '); return; } + const [, fileName] = parts; + if (!fileName) { await reply('Verwendung: ?entrance '); return; } const lower = fileName.toLowerCase(); if (!(lower.endsWith('.mp3') || lower.endsWith('.wav'))) { await reply('Nur .mp3 oder .wav Dateien sind erlaubt'); return; } const resolve = (() => { @@ -271,8 +271,8 @@ async function handleCommand(message: Message, content: string) { await reply(`Entrance-Sound gesetzt: ${resolve}`); return; } if (cmd === '?exit') { - const [, userName, fileName] = parts; - if (!userName || !fileName) { await reply('Verwendung: ?exit '); return; } + const [, fileName] = parts; + if (!fileName) { await reply('Verwendung: ?exit '); return; } const lower = fileName.toLowerCase(); if (!(lower.endsWith('.mp3') || lower.endsWith('.wav'))) { await reply('Nur .mp3 oder .wav Dateien sind erlaubt'); return; } const resolve = (() => { @@ -381,30 +381,32 @@ client.on(Events.VoiceStateUpdate, async (oldState: VoiceState, newState: VoiceS const before = oldState.channelId; const after = newState.channelId; - // Bot muss bereits im Ziel-Channel sein, sonst nichts tun - const connection = getVoiceConnection(guildId); - const botChannelId = connection?.joinConfig?.channelId; - - if (!before && after && botChannelId && botChannelId === after) { - // User joined bot channel → Entrance + // Entrance: Nutzer joint einem Channel + if (!before && after) { const mapping = persistedState.entranceSounds ?? {}; const file = mapping[userId]; if (file) { const rel = file.replace(/\\/g, '/'); const abs = path.join(SOUNDS_DIR, rel); if (fs.existsSync(abs)) { - await playFilePath(guildId, after, abs, undefined, rel); + try { + // Dem Channel beitreten und Sound spielen + await playFilePath(guildId, after, abs, undefined, rel); + } catch (e) { console.warn('Entrance play error', e); } } } - } else if (before && !after && botChannelId && botChannelId === before) { - // User left bot channel → Exit + } + // Exit: Nutzer verlässt einen Channel – spiele im vorherigen Channel + if (before && !after) { const mapping = persistedState.exitSounds ?? {}; const file = mapping[userId]; if (file) { const rel = file.replace(/\\/g, '/'); const abs = path.join(SOUNDS_DIR, rel); if (fs.existsSync(abs)) { - await playFilePath(guildId, before, abs, undefined, rel); + try { + await playFilePath(guildId, before, abs, undefined, rel); + } catch (e) { console.warn('Exit play error', e); } } } } From 0fc533bbd5e76f87520723bba587ea5673587ddd Mon Sep 17 00:00:00 2001 From: vibe-bot Date: Sun, 10 Aug 2025 23:26:00 +0200 Subject: [PATCH 3/8] fix(entrance-exit): Stelle sicher, dass VoiceStateUpdate/DM-Commands via expliziten Intents (Guilds, GuildVoiceStates, DirectMessages, MessageContent) aktiv sind --- server/src/index.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 52b5e97..34ae6bc 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -143,10 +143,12 @@ console.log(generateDependencyReport()); // --- Discord Client --- const client = new Client({ - // 32385 = Guilds + GuildVoiceStates + GuildMessages + GuildMessageReactions + GuildMessageTyping - // + DirectMessages + DirectMessageReactions + DirectMessageTyping - // (ohne privilegierte Intents wie MessageContent/GuildMembers/Presences) - intents: 32385, + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildVoiceStates, + GatewayIntentBits.DirectMessages, + GatewayIntentBits.MessageContent, + ], partials: [Partials.Channel] }); @@ -418,7 +420,7 @@ client.on(Events.VoiceStateUpdate, async (oldState: VoiceState, newState: VoiceS client.on(Events.MessageCreate, async (message: Message) => { try { if (message.author?.bot) return; - // Commands überall annehmen + // Commands überall annehmen (inkl. DMs) const content = (message.content || '').trim(); if (content.startsWith('?')) { await handleCommand(message, content); From 620608736201d8139dd17a0169a06703a5e3e35d Mon Sep 17 00:00:00 2001 From: vibe-bot Date: Sun, 10 Aug 2025 23:35:58 +0200 Subject: [PATCH 4/8] fix(entrance-exit): Log-Ausgaben + robustes Rejoin auf Ziel-Channel vor Playback; ignore self-events --- server/src/index.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/server/src/index.ts b/server/src/index.ts index 34ae6bc..667ad3f 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -209,6 +209,26 @@ async function playFilePath(guildId: string, channelId: string, filePath: string state.connection = await ensureConnectionReady(connection, channelId, guildId, guild); attachVoiceLifecycle(state, guild); } + // Wenn der Bot in einer anderen ChannelId ist, sauber rüberwechseln + try { + const current = getVoiceConnection(guildId); + if (current && current.joinConfig?.channelId !== channelId) { + current.destroy(); + const connection = joinVoiceChannel({ + channelId, + guildId, + adapterCreator: guild.voiceAdapterCreator as any, + selfMute: false, + selfDeaf: false + }); + const player = state.player ?? createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } }); + connection.subscribe(player); + state = { connection, player, guildId, channelId, currentVolume: state.currentVolume ?? getPersistedVolume(guildId) }; + guildAudioState.set(guildId, state); + state.connection = await ensureConnectionReady(connection, channelId, guildId, guild); + attachVoiceLifecycle(state, guild); + } + } catch {} const useVolume = typeof volume === 'number' && Number.isFinite(volume) ? Math.max(0, Math.min(1, volume)) : (state.currentVolume ?? 1); @@ -375,13 +395,16 @@ client.once(Events.ClientReady, () => { // Voice State Updates: Entrance/Exit client.on(Events.VoiceStateUpdate, async (oldState: VoiceState, newState: VoiceState) => { try { - const userId = newState.member?.user?.id || oldState.member?.user?.id; + const userId = (newState.id || oldState.id) as string; if (!userId) return; + // Eigene Events ignorieren + if (userId === client.user?.id) return; const guildId = (newState.guild?.id || oldState.guild?.id) as string; if (!guildId) return; const before = oldState.channelId; const after = newState.channelId; + console.log(`${new Date().toISOString()} | VoiceStateUpdate user=${userId} before=${before ?? '-'} after=${after ?? '-'}`); // Entrance: Nutzer joint einem Channel if (!before && after) { @@ -394,6 +417,7 @@ client.on(Events.VoiceStateUpdate, async (oldState: VoiceState, newState: VoiceS try { // Dem Channel beitreten und Sound spielen await playFilePath(guildId, after, abs, undefined, rel); + console.log(`${new Date().toISOString()} | Entrance played for ${userId}: ${rel}`); } catch (e) { console.warn('Entrance play error', e); } } } @@ -408,6 +432,7 @@ client.on(Events.VoiceStateUpdate, async (oldState: VoiceState, newState: VoiceS if (fs.existsSync(abs)) { try { await playFilePath(guildId, before, abs, undefined, rel); + console.log(`${new Date().toISOString()} | Exit played for ${userId}: ${rel}`); } catch (e) { console.warn('Exit play error', e); } } } From 64d2e91efa5b11de5caa718731f0e7079f620d79 Mon Sep 17 00:00:00 2001 From: vibe-bot Date: Sun, 10 Aug 2025 23:43:40 +0200 Subject: [PATCH 5/8] chore(logging): Logge gesetzte ?entrance/?exit Zuordnungen (user tag + file) --- server/src/index.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 667ad3f..692bd2c 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -289,7 +289,12 @@ async function handleCommand(message: Message, content: string) { })(); if (!resolve) { await reply('Datei nicht gefunden. Nutze ?list.'); return; } const userId = message.author?.id ?? ''; if (!userId) { await reply('Kein Benutzer erkannt.'); return; } - persistedState.entranceSounds = persistedState.entranceSounds ?? {}; persistedState.entranceSounds[userId] = resolve; writePersistedState(persistedState); + persistedState.entranceSounds = persistedState.entranceSounds ?? {}; + persistedState.entranceSounds[userId] = resolve; + writePersistedState(persistedState); + try { + console.log(`${new Date().toISOString()} | Entrance set: user=${userId} (${message.author?.tag || 'unknown'}) file=${resolve}`); + } catch {} await reply(`Entrance-Sound gesetzt: ${resolve}`); return; } if (cmd === '?exit') { @@ -307,7 +312,12 @@ async function handleCommand(message: Message, content: string) { })(); if (!resolve) { await reply('Datei nicht gefunden. Nutze ?list.'); return; } const userId = message.author?.id ?? ''; if (!userId) { await reply('Kein Benutzer erkannt.'); return; } - persistedState.exitSounds = persistedState.exitSounds ?? {}; persistedState.exitSounds[userId] = resolve; writePersistedState(persistedState); + persistedState.exitSounds = persistedState.exitSounds ?? {}; + persistedState.exitSounds[userId] = resolve; + writePersistedState(persistedState); + try { + console.log(`${new Date().toISOString()} | Exit set: user=${userId} (${message.author?.tag || 'unknown'}) file=${resolve}`); + } catch {} await reply(`Exit-Sound gesetzt: ${resolve}`); return; } await reply('Unbekannter Command. Nutze ?help.'); From 9f7aa5fc9444478b11663f969505c4310bfaaf59 Mon Sep 17 00:00:00 2001 From: vibe-bot Date: Sun, 10 Aug 2025 23:50:51 +0200 Subject: [PATCH 6/8] =?UTF-8?q?fix(entrance-exit):=20Trigger=20auch=20bei?= =?UTF-8?q?=20Channel-Wechsel;=20zus=C3=A4tzliche=20Logs;=20robustes=20Joi?= =?UTF-8?q?n=20falls=20keine=20aktive=20Verbindung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/index.ts | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 692bd2c..88bbef4 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -221,6 +221,7 @@ async function playFilePath(guildId: string, channelId: string, filePath: string selfMute: false, selfDeaf: false }); + // Reuse bestehenden Player falls vorhanden const player = state.player ?? createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } }); connection.subscribe(player); state = { connection, player, guildId, channelId, currentVolume: state.currentVolume ?? getPersistedVolume(guildId) }; @@ -229,6 +230,23 @@ async function playFilePath(guildId: string, channelId: string, filePath: string attachVoiceLifecycle(state, guild); } } catch {} + + // Falls keine aktive Verbindung existiert (oder nach destroy), sicherstellen + if (!getVoiceConnection(guildId)) { + const connection = joinVoiceChannel({ + channelId, + guildId, + adapterCreator: guild.voiceAdapterCreator as any, + selfMute: false, + selfDeaf: false + }); + const player = state?.player ?? createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } }); + connection.subscribe(player); + state = { connection, player, guildId, channelId, currentVolume: state?.currentVolume ?? getPersistedVolume(guildId) }; + guildAudioState.set(guildId, state); + state.connection = await ensureConnectionReady(connection, channelId, guildId, guild); + attachVoiceLifecycle(state, guild); + } const useVolume = typeof volume === 'number' && Number.isFinite(volume) ? Math.max(0, Math.min(1, volume)) : (state.currentVolume ?? 1); @@ -416,8 +434,9 @@ client.on(Events.VoiceStateUpdate, async (oldState: VoiceState, newState: VoiceS const after = newState.channelId; console.log(`${new Date().toISOString()} | VoiceStateUpdate user=${userId} before=${before ?? '-'} after=${after ?? '-'}`); - // Entrance: Nutzer joint einem Channel - if (!before && after) { + // Entrance: Nutzer betritt einen Channel (erstmaliger Join oder Wechsel) + if (after && before !== after) { + console.log(`${new Date().toISOString()} | Entrance condition met for user=${userId} before=${before ?? '-'} after=${after}`); const mapping = persistedState.entranceSounds ?? {}; const file = mapping[userId]; if (file) { @@ -432,8 +451,9 @@ client.on(Events.VoiceStateUpdate, async (oldState: VoiceState, newState: VoiceS } } } - // Exit: Nutzer verlässt einen Channel – spiele im vorherigen Channel - if (before && !after) { + // Exit: Nutzer verlässt einen Channel (vollständig oder wechselt zu anderem) + if (before && (!after || after !== before)) { + console.log(`${new Date().toISOString()} | Exit condition met for user=${userId} before=${before} after=${after ?? '-'}`); const mapping = persistedState.exitSounds ?? {}; const file = mapping[userId]; if (file) { From 0ae0817598be2c5664630e5b9449b13fffc910dd Mon Sep 17 00:00:00 2001 From: vibe-bot Date: Mon, 11 Aug 2025 00:00:08 +0200 Subject: [PATCH 7/8] feat(entrance-exit): Support ?entrance remove / ?exit remove (clear mapping); Help-Text aktualisiert --- server/src/index.ts | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 88bbef4..cb43e2d 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -280,8 +280,8 @@ async function handleCommand(message: Message, content: string) { 'Available commands\n' + '?help - zeigt diese Hilfe\n' + '?list - listet alle Audio-Dateien (mp3/wav)\n' + - '?entrance - setze deinen Entrance-Sound\n' + - '?exit - setze deinen Exit-Sound (optional)\n' + '?entrance | remove - setze oder entferne deinen Entrance-Sound\n' + + '?exit | remove - setze oder entferne deinen Exit-Sound\n' ); return; } @@ -293,8 +293,19 @@ async function handleCommand(message: Message, content: string) { return; } if (cmd === '?entrance') { - const [, fileName] = parts; - if (!fileName) { await reply('Verwendung: ?entrance '); return; } + const [, fileNameRaw] = parts; + const userId = message.author?.id ?? ''; + if (!userId) { await reply('Kein Benutzer erkannt.'); return; } + const fileName = fileNameRaw?.trim(); + if (!fileName) { await reply('Verwendung: ?entrance | remove'); return; } + if (/^(remove|clear|delete)$/i.test(fileName)) { + persistedState.entranceSounds = persistedState.entranceSounds ?? {}; + delete persistedState.entranceSounds[userId]; + writePersistedState(persistedState); + try { console.log(`${new Date().toISOString()} | Entrance removed: user=${userId} (${message.author?.tag || 'unknown'})`); } catch {} + await reply('Entrance-Sound entfernt.'); + return; + } const lower = fileName.toLowerCase(); if (!(lower.endsWith('.mp3') || lower.endsWith('.wav'))) { await reply('Nur .mp3 oder .wav Dateien sind erlaubt'); return; } const resolve = (() => { @@ -306,7 +317,6 @@ async function handleCommand(message: Message, content: string) { } catch { return ''; } })(); if (!resolve) { await reply('Datei nicht gefunden. Nutze ?list.'); return; } - const userId = message.author?.id ?? ''; if (!userId) { await reply('Kein Benutzer erkannt.'); return; } persistedState.entranceSounds = persistedState.entranceSounds ?? {}; persistedState.entranceSounds[userId] = resolve; writePersistedState(persistedState); @@ -316,8 +326,19 @@ async function handleCommand(message: Message, content: string) { await reply(`Entrance-Sound gesetzt: ${resolve}`); return; } if (cmd === '?exit') { - const [, fileName] = parts; - if (!fileName) { await reply('Verwendung: ?exit '); return; } + const [, fileNameRaw] = parts; + const userId = message.author?.id ?? ''; + if (!userId) { await reply('Kein Benutzer erkannt.'); return; } + const fileName = fileNameRaw?.trim(); + if (!fileName) { await reply('Verwendung: ?exit | remove'); return; } + if (/^(remove|clear|delete)$/i.test(fileName)) { + persistedState.exitSounds = persistedState.exitSounds ?? {}; + delete persistedState.exitSounds[userId]; + writePersistedState(persistedState); + try { console.log(`${new Date().toISOString()} | Exit removed: user=${userId} (${message.author?.tag || 'unknown'})`); } catch {} + await reply('Exit-Sound entfernt.'); + return; + } const lower = fileName.toLowerCase(); if (!(lower.endsWith('.mp3') || lower.endsWith('.wav'))) { await reply('Nur .mp3 oder .wav Dateien sind erlaubt'); return; } const resolve = (() => { @@ -329,7 +350,6 @@ async function handleCommand(message: Message, content: string) { } catch { return ''; } })(); if (!resolve) { await reply('Datei nicht gefunden. Nutze ?list.'); return; } - const userId = message.author?.id ?? ''; if (!userId) { await reply('Kein Benutzer erkannt.'); return; } persistedState.exitSounds = persistedState.exitSounds ?? {}; persistedState.exitSounds[userId] = resolve; writePersistedState(persistedState); From c727b445a4d62fd718f3851241532eb8e564769c Mon Sep 17 00:00:00 2001 From: vibe-bot Date: Mon, 11 Aug 2025 00:14:07 +0200 Subject: [PATCH 8/8] =?UTF-8?q?fix(exit-logic):=20Exit=20nur=20bei=20Disco?= =?UTF-8?q?nnect=20(after=20is=20null),=20bei=20Kanalwechsel=20unterdr?= =?UTF-8?q?=C3=BCcken;=20Lifecycle-Listener=20nur=20einmal=20registrieren?= =?UTF-8?q?=20und=20MaxListeners=20erh=C3=B6hen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/index.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index cb43e2d..e962928 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -397,6 +397,9 @@ async function ensureConnectionReady(connection: VoiceConnection, channelId: str function attachVoiceLifecycle(state: GuildAudioState, guild: any) { const { connection } = state; + // Mehrfach-Registrierung verhindern + if ((connection as any).__lifecycleAttached) return; + try { (connection as any).setMaxListeners?.(0); } catch {} connection.on('stateChange', async (oldS: any, newS: any) => { console.log(`${new Date().toISOString()} | VoiceConnection: ${oldS.status} -> ${newS.status}`); try { @@ -434,6 +437,7 @@ function attachVoiceLifecycle(state: GuildAudioState, guild: any) { console.error(`${new Date().toISOString()} | Voice lifecycle handler error`, e); } }); + (connection as any).__lifecycleAttached = true; } client.once(Events.ClientReady, () => { @@ -471,9 +475,9 @@ client.on(Events.VoiceStateUpdate, async (oldState: VoiceState, newState: VoiceS } } } - // Exit: Nutzer verlässt einen Channel (vollständig oder wechselt zu anderem) - if (before && (!after || after !== before)) { - console.log(`${new Date().toISOString()} | Exit condition met for user=${userId} before=${before} after=${after ?? '-'}`); + // Exit: Nur wenn Nutzer wirklich auflegt (after ist leer). Bei Wechsel KEIN Exit-Sound. + if (before && !after) { + console.log(`${new Date().toISOString()} | Exit condition met (disconnect) for user=${userId} before=${before}`); const mapping = persistedState.exitSounds ?? {}; const file = mapping[userId]; if (file) { @@ -486,6 +490,9 @@ client.on(Events.VoiceStateUpdate, async (oldState: VoiceState, newState: VoiceS } catch (e) { console.warn('Exit play error', e); } } } + } else if (before && after && before !== after) { + // Kanalwechsel: Exit-Sound unterdrücken + console.log(`${new Date().toISOString()} | Exit suppressed (move) for user=${userId} before=${before} after=${after}`); } } catch (e) { console.warn('VoiceStateUpdate entrance/exit handling error', e);