fix: Enhance temporary voice channel management with improved Discord API error handling for ghost channels and detailed logging.

This commit is contained in:
이정수 2026-03-27 14:39:38 +09:00
parent b93255a2be
commit cb80d9dffe
2 changed files with 91 additions and 72 deletions

View File

@ -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
}

View File

@ -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) {