From 6a829392eb179699ae8ed79d723cb87a4941c276 Mon Sep 17 00:00:00 2001 From: Elliott DuCharme Date: Fri, 14 Aug 2026 13:18:44 -0500 Subject: [PATCH 1/3] Forgot to merge these files in a past MR --- .../adapters/services/DiscordVoiceService.ts | 18 +++++++++++++++++- .../src/application/commands/Commands.ts | 2 +- .../src/application/commands/VoiceCommands.ts | 2 +- .../eventHandlers/DiscordVoiceService.ts | 2 +- .../core/services/SoundboardThreadService.ts | 2 +- .../core/services/VoiceEventSoundsService.ts | 3 ++- .../backend/src/core/services/VoiceService.ts | 19 ------------------- .../src/infrastructure/discord/DiscordBot.ts | 2 +- .../tests/utils/createMinimalTestBot.ts | 2 +- 9 files changed, 25 insertions(+), 27 deletions(-) delete mode 100644 packages/backend/src/core/services/VoiceService.ts diff --git a/packages/backend/src/adapters/services/DiscordVoiceService.ts b/packages/backend/src/adapters/services/DiscordVoiceService.ts index 4884c8d1..835e37b4 100644 --- a/packages/backend/src/adapters/services/DiscordVoiceService.ts +++ b/packages/backend/src/adapters/services/DiscordVoiceService.ts @@ -3,11 +3,27 @@ import type { PlayedSoundsRepository } from "@adapters/repositories/PlayedSounds import type { SoundRepository } from "@adapters/repositories/SoundRepository.js"; import { createPlayingSound } from "@core/entities/PlayingSound.js"; import type { SoundService } from "@core/services/SoundService.js"; -import type { VoiceService } from "@core/services/VoiceService.js"; import type { PlayedSoundSource } from "@db/types.js"; import { AudioPlayerStatus, type VoiceConnection, VoiceConnectionStatus, joinVoiceChannel } from "@discordjs/voice"; import type { Guild, VoiceChannel } from "discord.js"; +export type VoiceService = { + readonly connect: (guild: Guild, channelId: string) => Promise; + readonly disconnect: (serverId: string) => Promise; + readonly isConnected: (serverId: string) => boolean; + readonly playAudio: (serverId: string, nameOrSource: string, userId: string, playedSoundSource: PlayedSoundSource, volume?: number) => Promise; + readonly stopAudio: (serverId: string) => Promise; + readonly stopAudioById: (serverId: string, audioId: string) => Promise; + readonly stopAllAudio: (serverId: string) => Promise; + readonly isPlaying: (serverId: string) => boolean; + readonly getActiveAudioCount: (serverId: string) => number; + readonly getActiveAudioIds: (serverId: string) => string[]; + readonly getVolume: (serverId: string) => number; + readonly setVolume: (serverId: string, volume: number) => Promise; + readonly pause: (serverId: string) => Promise; + readonly resume: (serverId: string) => Promise; +}; + export type DiscordVoiceServiceDeps = { readonly soundRepository: SoundRepository; readonly playedSoundsRepository: PlayedSoundsRepository; diff --git a/packages/backend/src/application/commands/Commands.ts b/packages/backend/src/application/commands/Commands.ts index e523294e..9c12c1ad 100644 --- a/packages/backend/src/application/commands/Commands.ts +++ b/packages/backend/src/application/commands/Commands.ts @@ -3,6 +3,7 @@ import type { PlayedSoundsRepository } from "@adapters/repositories/PlayedSounds import type { ReactionRepository } from "@adapters/repositories/ReactionRepository.js"; import type { SoundRepository } from "@adapters/repositories/SoundRepository.js"; import type { VoiceEventSoundsRepository } from "@adapters/repositories/VoiceEventSoundsRepository.js"; +import type { VoiceService } from "@adapters/services/DiscordVoiceService.js"; import { createBannedFeaturesCommands } from "@application/commands/BannedFeaturesCommands.js"; import { createPlayedSoundsCommands } from "@application/commands/PlayedSoundsCommands.js"; import { createReactionCommands } from "@application/commands/ReactionCommands.js"; @@ -11,7 +12,6 @@ import type { CommandChoicesService } from "@core/services/CommandChoicesService import type { DiscordChatService } from "@core/services/DiscordChatService.js"; import type { SoundService } from "@core/services/SoundService.js"; import type { SoundTagService } from "@core/services/SoundTagService.js"; -import type { VoiceService } from "@core/services/VoiceService.js"; import { type ApplicationCommandOptionChoiceData, type AutocompleteFocusedOption, ChatInputCommandInteraction, Events, MessageFlags, REST, Routes, type SlashCommandOptionsOnlyBuilder } from "discord.js"; import type { DiscordBot } from "@/infrastructure/discord/DiscordBot.js"; diff --git a/packages/backend/src/application/commands/VoiceCommands.ts b/packages/backend/src/application/commands/VoiceCommands.ts index 120900b3..d86b755b 100644 --- a/packages/backend/src/application/commands/VoiceCommands.ts +++ b/packages/backend/src/application/commands/VoiceCommands.ts @@ -1,8 +1,8 @@ import type { BannedFeaturesRepository } from "@adapters/repositories/BannedFeaturesRepository.js"; +import type { VoiceService } from "@adapters/services/DiscordVoiceService.js"; import { parseAudioSource } from "@core/services/AudioFetcherService.js"; import type { CommandChoicesService } from "@core/services/CommandChoicesService.js"; import type { SoundService } from "@core/services/SoundService.js"; -import type { VoiceService } from "@core/services/VoiceService.js"; import { randomArrayItem, randomInt } from "@core/utils/probabilityUtils.js"; import { parseTimeSpan } from "@core/utils/timeUtils.js"; import { ChannelType, ChatInputCommandInteraction, GuildMember, MessageFlags, SlashCommandBuilder } from "discord.js"; diff --git a/packages/backend/src/application/eventHandlers/DiscordVoiceService.ts b/packages/backend/src/application/eventHandlers/DiscordVoiceService.ts index a44471a6..d3533c7c 100644 --- a/packages/backend/src/application/eventHandlers/DiscordVoiceService.ts +++ b/packages/backend/src/application/eventHandlers/DiscordVoiceService.ts @@ -1,5 +1,5 @@ +import type { VoiceService } from "@adapters/services/DiscordVoiceService.js"; import type { Config } from "@core/config/Config.js"; -import type { VoiceService } from "@core/services/VoiceService.js"; import type { DiscordBot } from "@infrastructure/discord/DiscordBot.js"; import { Events, VoiceChannel, VoiceState } from "discord.js"; diff --git a/packages/backend/src/core/services/SoundboardThreadService.ts b/packages/backend/src/core/services/SoundboardThreadService.ts index b36161b2..8b384ae8 100644 --- a/packages/backend/src/core/services/SoundboardThreadService.ts +++ b/packages/backend/src/core/services/SoundboardThreadService.ts @@ -1,7 +1,7 @@ import type { BannedFeaturesRepository } from "@adapters/repositories/BannedFeaturesRepository.js"; import type { SoundRepository } from "@adapters/repositories/SoundRepository.js"; +import type { VoiceService } from "@adapters/services/DiscordVoiceService.js"; import type { Config } from "@core/config/Config.js"; -import type { VoiceService } from "@core/services/VoiceService.js"; import { ChannelType, type Guild, type Message, MessageFlags, type TextChannel, ThreadAutoArchiveDuration, type ThreadChannel } from "discord.js"; export type SoundboardThreadService = { diff --git a/packages/backend/src/core/services/VoiceEventSoundsService.ts b/packages/backend/src/core/services/VoiceEventSoundsService.ts index bf477ed3..90299997 100644 --- a/packages/backend/src/core/services/VoiceEventSoundsService.ts +++ b/packages/backend/src/core/services/VoiceEventSoundsService.ts @@ -1,6 +1,6 @@ import type { VoiceEventSoundsRepository } from "@adapters/repositories/VoiceEventSoundsRepository.js"; +import type { VoiceService } from "@adapters/services/DiscordVoiceService.js"; import type { Config } from "@core/config/Config.js"; -import type { VoiceService } from "@core/services/VoiceService.js"; import { randomArrayItem } from "@core/utils/probabilityUtils.js"; import { sleep } from "@core/utils/timeUtils.js"; import type { VoiceEventSoundType } from "@db/types.js"; @@ -37,6 +37,7 @@ export const createVoiceEventSoundsService = ({ config, voiceEventSoundsReposito const guild = newState.guild; const userId = newState.member!.id; + const availableSounds = await voiceEventSoundsRepository.getVoiceEventSounds({ userId, type }); const sound = randomArrayItem(availableSounds); if (!sound) return; diff --git a/packages/backend/src/core/services/VoiceService.ts b/packages/backend/src/core/services/VoiceService.ts deleted file mode 100644 index 0cc06132..00000000 --- a/packages/backend/src/core/services/VoiceService.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { PlayedSoundSource } from "@db/types.js"; -import type { Guild } from "discord.js"; - -export type VoiceService = { - readonly connect: (guild: Guild, channelId: string) => Promise; - readonly disconnect: (serverId: string) => Promise; - readonly isConnected: (serverId: string) => boolean; - readonly playAudio: (serverId: string, nameOrSource: string, userId: string, playedSoundSource: PlayedSoundSource, volume?: number) => Promise; - readonly stopAudio: (serverId: string) => Promise; - readonly stopAudioById: (serverId: string, audioId: string) => Promise; - readonly stopAllAudio: (serverId: string) => Promise; - readonly isPlaying: (serverId: string) => boolean; - readonly getActiveAudioCount: (serverId: string) => number; - readonly getActiveAudioIds: (serverId: string) => string[]; - readonly getVolume: (serverId: string) => number; - readonly setVolume: (serverId: string, volume: number) => Promise; - readonly pause: (serverId: string) => Promise; - readonly resume: (serverId: string) => Promise; -}; diff --git a/packages/backend/src/infrastructure/discord/DiscordBot.ts b/packages/backend/src/infrastructure/discord/DiscordBot.ts index eb4a0142..52f1a56e 100644 --- a/packages/backend/src/infrastructure/discord/DiscordBot.ts +++ b/packages/backend/src/infrastructure/discord/DiscordBot.ts @@ -5,6 +5,7 @@ import type { ReactionEmoteRepository } from "@adapters/repositories/ReactionEmo import type { ReactionRepository } from "@adapters/repositories/ReactionRepository.js"; import type { SoundRepository } from "@adapters/repositories/SoundRepository.js"; import type { VoiceEventSoundsRepository } from "@adapters/repositories/VoiceEventSoundsRepository.js"; +import type { VoiceService } from "@adapters/services/DiscordVoiceService.js"; import { deployCommands, registerCommands } from "@application/commands/Commands.js"; import { registerAutoReactionEvents } from "@application/eventHandlers/AutoReaction.js"; import { registerDiscordUserSyncEvents } from "@application/eventHandlers/DiscordUserSyncService.js"; @@ -26,7 +27,6 @@ import type { SoundService } from "@core/services/SoundService.js"; import type { SoundTagService } from "@core/services/SoundTagService.js"; import type { SoundboardThreadService } from "@core/services/SoundboardThreadService.js"; import type { VoiceEventSoundsService } from "@core/services/VoiceEventSoundsService.js"; -import type { VoiceService } from "@core/services/VoiceService.js"; import { sleep } from "@core/utils/timeUtils.js"; import type { GeminiLlmService } from "@infrastructure/services/GeminiLlmService.js"; import { Client, type ClientEvents, Events, GatewayIntentBits, Partials, PresenceUpdateStatus, RESTEvents, type TextChannel } from "discord.js"; diff --git a/packages/backend/tests/utils/createMinimalTestBot.ts b/packages/backend/tests/utils/createMinimalTestBot.ts index 52d3017b..56b4c08d 100644 --- a/packages/backend/tests/utils/createMinimalTestBot.ts +++ b/packages/backend/tests/utils/createMinimalTestBot.ts @@ -8,6 +8,7 @@ import { createReactionRepository } from "@adapters/repositories/ReactionReposit import type { SoundRepository } from "@adapters/repositories/SoundRepository.js"; import { createUserRepository } from "@adapters/repositories/UserRepository.js"; import type { VoiceEventSoundsRepository } from "@adapters/repositories/VoiceEventSoundsRepository.js"; +import type { VoiceService } from "@adapters/services/DiscordVoiceService.js"; import type { Config } from "@core/config/Config.js"; import type { AutoReactionService } from "@core/services/AutoReactionService.js"; import { createAutoReactionService } from "@core/services/AutoReactionService.js"; @@ -24,7 +25,6 @@ import type { SoundService } from "@core/services/SoundService.js"; import type { SoundTagService } from "@core/services/SoundTagService.js"; import type { SoundboardThreadService } from "@core/services/SoundboardThreadService.js"; import type { VoiceEventSoundsService } from "@core/services/VoiceEventSoundsService.js"; -import type { VoiceService } from "@core/services/VoiceService.js"; import { runMigrations } from "@db/migrations.js"; import type { DB } from "@db/types.js"; import { createDatabaseConnection } from "@infrastructure/database/DatabaseConnection.js"; From 3fd67aa174a644f878e6916c94aeaf07f6108f07 Mon Sep 17 00:00:00 2001 From: Elliott DuCharme Date: Fri, 14 Aug 2026 14:36:09 -0500 Subject: [PATCH 2/3] Add getVoiceChannelId to DiscordVoiceService --- .../backend/src/adapters/services/DiscordVoiceService.ts | 7 +++++++ packages/backend/tests/utils/createMinimalTestBot.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/packages/backend/src/adapters/services/DiscordVoiceService.ts b/packages/backend/src/adapters/services/DiscordVoiceService.ts index 835e37b4..4f944d1d 100644 --- a/packages/backend/src/adapters/services/DiscordVoiceService.ts +++ b/packages/backend/src/adapters/services/DiscordVoiceService.ts @@ -11,6 +11,7 @@ export type VoiceService = { readonly connect: (guild: Guild, channelId: string) => Promise; readonly disconnect: (serverId: string) => Promise; readonly isConnected: (serverId: string) => boolean; + readonly getVoiceChannelId: (serverId: string) => string | null; readonly playAudio: (serverId: string, nameOrSource: string, userId: string, playedSoundSource: PlayedSoundSource, volume?: number) => Promise; readonly stopAudio: (serverId: string) => Promise; readonly stopAudioById: (serverId: string, audioId: string) => Promise; @@ -159,6 +160,11 @@ export const createDiscordVoiceService = ({ soundRepository, playedSoundsReposit return connected; }; + const getVoiceChannelId = (serverId: string): string | null => { + const state = voiceStates.get(serverId); + return state?.connection.joinConfig.channelId ?? null; + }; + const playAudio = async (serverId: string, nameOrSource: string, userId: string, playedSoundSource: PlayedSoundSource, volume?: number): Promise => { console.log(`[DiscordVoiceService] Playing audio: ${nameOrSource} in server ${serverId}`); @@ -311,6 +317,7 @@ export const createDiscordVoiceService = ({ soundRepository, playedSoundsReposit connect, disconnect, isConnected, + getVoiceChannelId, playAudio, stopAudio, stopAudioById, diff --git a/packages/backend/tests/utils/createMinimalTestBot.ts b/packages/backend/tests/utils/createMinimalTestBot.ts index 56b4c08d..45ee01f5 100644 --- a/packages/backend/tests/utils/createMinimalTestBot.ts +++ b/packages/backend/tests/utils/createMinimalTestBot.ts @@ -169,6 +169,7 @@ export async function createMinimalTestBot(config: Config, schemaName: string, o connect: async () => {}, disconnect: async () => {}, isConnected: () => false, + getVoiceChannelId: () => null, playAudio: async () => "", stopAudio: async () => {}, stopAudioById: async () => false, From 0690a2bcaab3ec054528750dab42c3c7bdce2c0a Mon Sep 17 00:00:00 2001 From: Elliott DuCharme Date: Fri, 14 Aug 2026 15:06:43 -0500 Subject: [PATCH 3/3] Now only plays a VoiceEventSound if the user and bot are in the same VC --- .../src/core/services/VoiceEventSoundsService.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/core/services/VoiceEventSoundsService.ts b/packages/backend/src/core/services/VoiceEventSoundsService.ts index 90299997..147362a6 100644 --- a/packages/backend/src/core/services/VoiceEventSoundsService.ts +++ b/packages/backend/src/core/services/VoiceEventSoundsService.ts @@ -38,14 +38,18 @@ export const createVoiceEventSoundsService = ({ config, voiceEventSoundsReposito const guild = newState.guild; const userId = newState.member!.id; - const availableSounds = await voiceEventSoundsRepository.getVoiceEventSounds({ userId, type }); - const sound = randomArrayItem(availableSounds); - if (!sound) return; - if (!voiceService.isConnected(guild.id)) { await voiceService.connect(guild, config.discord.defaultVoiceChannelId); } + const userChannelId = type === "UserJoin" ? newState.channelId : oldState.channelId; + const botChannelId = voiceService.getVoiceChannelId(guild.id) ?? config.discord.defaultVoiceChannelId; + if (userChannelId !== botChannelId) return; + + const availableSounds = await voiceEventSoundsRepository.getVoiceEventSounds({ userId, type }); + const sound = randomArrayItem(availableSounds); + if (!sound) return; + const delay = soundDelays[type]; if (delay) { await sleep(delay);