From cb80d9dffe2a8b7e90ac870b7df71346ce736481 Mon Sep 17 00:00:00 2001 From: artbiit Date: Fri, 27 Mar 2026 14:39:38 +0900 Subject: [PATCH] fix: Enhance temporary voice channel management with improved Discord API error handling for ghost channels and detailed logging. --- prisma/schema.prisma | 129 +++++++++++++++++------------------ src/services/VoiceService.ts | 34 +++++++-- 2 files changed, 91 insertions(+), 72 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a9c985c..4f7c1db 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,24 +8,77 @@ datasource db { } model GuildConfig { - guildId String @id - prefix String @default("!") - mimicEnabled Boolean @default(true) - locale String? // Server locale override (e.g. 'ko', 'en') - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + guildId String @id + prefix String @default("!") + mimicEnabled Boolean @default(true) + locale String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model InviteRole { - id String @id @default(uuid()) - guildId String + id String @id @default(uuid()) + guildId String inviteCode String - roleId String - createdAt DateTime @default(now()) - + roleId String + createdAt DateTime @default(now()) + @@unique([guildId, inviteCode]) } +model UserSubscription { + userId String @id + tier SubscriptionTier @default(FREE) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + guilds GuildOwnership[] +} + +model GuildOwnership { + guildId String @id + ownerId String + createdAt DateTime @default(now()) + owner UserSubscription @relation(fields: [ownerId], references: [userId], onDelete: Cascade) + + @@index([ownerId]) +} + +model VoiceGenerator { + channelId String @id + guildId String + categoryId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([guildId]) +} + +model TempVoiceChannel { + channelId String @id + guildId String + ownerId String + deleteWhen DeleteCondition @default(EMPTY) + createdAt DateTime @default(now()) + + @@index([guildId]) + @@index([ownerId]) +} + +model UserVoiceProfile { + userId String @id + customName String? + userLimit Int? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model UserLocale { + userId String @id + locale String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + enum SubscriptionTier { FREE STANDARD @@ -37,57 +90,3 @@ enum DeleteCondition { OWNER_LEAVE EMPTY } - -model UserSubscription { - userId String @id - tier SubscriptionTier @default(FREE) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - guilds GuildOwnership[] -} - -model GuildOwnership { - guildId String @id - ownerId String - owner UserSubscription @relation(fields: [ownerId], references: [userId], onDelete: Cascade) - createdAt DateTime @default(now()) - - @@index([ownerId]) -} - -model VoiceGenerator { - channelId String @id - guildId String - categoryId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([guildId]) -} - -model TempVoiceChannel { - channelId String @id - guildId String - ownerId String - deleteWhen DeleteCondition @default(EMPTY) - createdAt DateTime @default(now()) - - @@index([guildId]) - @@index([ownerId]) -} - -model UserVoiceProfile { - userId String @id - customName String? - userLimit Int? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model UserLocale { - userId String @id - locale String // User's personal locale (e.g. 'ko', 'en') - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - diff --git a/src/services/VoiceService.ts b/src/services/VoiceService.ts index 3cfd498..2ddeb83 100644 --- a/src/services/VoiceService.ts +++ b/src/services/VoiceService.ts @@ -63,6 +63,8 @@ export class VoiceService { const member = newState.member; if (!member) return; + logger.debug(`VoiceService: handleVoiceStateUpdate - old: ${oldState.channelId}, new: ${newState.channelId}`); + if (!oldState.channelId && newState.channelId) { await this.handleJoin(newState); } else if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId) { @@ -82,7 +84,12 @@ export class VoiceService { where: { channelId } }); - if (!generator) return; + if (!generator) { + logger.debug(`VoiceService: handleJoin - channel ${channelId} is NOT a generator`); + return; + } + + logger.info(`VoiceService: handleJoin - detected generator join for ${member.user.tag} in ${channelId}`); const botMember = guild.members.me; if (!botMember?.permissions.has([ @@ -101,10 +108,18 @@ export class VoiceService { if (existingTemp) { try { await member.voice.setChannel(existingTemp.channelId); - } catch (e) { - logger.error(`${ErrorDefs.DISCORD_API_ERROR.code}: Could not move user to existing voice channel`, e); + return; // Success, moved to existing channel + } catch (e: any) { + // If the channel no longer exists in Discord, clean up DB and proceed to create new one + if (e.code === 10003 || e.status === 404) { + logger.warn(`VoiceService: Found ghost channel ${existingTemp.channelId} in DB. Cleaning up and creating fresh one.`); + await prisma.tempVoiceChannel.delete({ where: { channelId: existingTemp.channelId } }).catch(() => {}); + // FALL THROUGH to channel creation logic below + } else { + logger.error(`${ErrorDefs.DISCORD_API_ERROR.code}: Unexpected error moving user to existing voice channel`, e); + return; // Stop for other errors to prevent spamming creations + } } - return; } // Resolve locale for this context @@ -202,9 +217,14 @@ export class VoiceService { await channel.delete(); await prisma.tempVoiceChannel.delete({ where: { channelId } }); logger.info(`VoiceService: Successfully deleted temp channel ${channel.name}`); - } catch (error) { - logger.error(`${ErrorDefs.DISCORD_MISSING_PERMISSIONS.code}: Failed to delete channel ${channel.name}`, error); - // deliberately NOT deleting the database entry so it remains synchronized with Discord's state. + } catch (error: any) { + // If already deleted in Discord, just clean up DB + if (error.code === 10003 || error.status === 404) { + await prisma.tempVoiceChannel.delete({ where: { channelId } }).catch(() => {}); + logger.info(`VoiceService: Purged ghost channel ${channelId} from DB`); + } else { + logger.error(`${ErrorDefs.DISCORD_MISSING_PERMISSIONS.code}: Failed to delete channel ${channelId}`, error); + } } } else if (tempChannel.deleteWhen === 'EMPTY' && tempChannel.ownerId === member.id && channel.members.filter(m => !m.user.bot).size > 0) {