diff --git a/src/commands/audit.ts b/src/commands/audit.ts new file mode 100644 index 0000000..fd61d18 --- /dev/null +++ b/src/commands/audit.ts @@ -0,0 +1,237 @@ +import { + SlashCommandBuilder, + PermissionFlagsBits, + ChatInputCommandInteraction, + ChannelType, + EmbedBuilder, + Colors, + TextChannel +} from 'discord.js'; +import { auditLogService, AuditCategory } from '../services/AuditLogService'; +import { PermissionAuditService, AuditResult, AuditStatus } from '../services/PermissionAuditService'; +import { SupportedLocale, t } from '../i18n'; + +const STATUS_EMOJI: Record = { + SUCCESS: '✅', + WARNING: '⚠️', + FAIL: '❌', +}; + +function getOverallColor(results: AuditResult[]): number { + if (results.some((r) => r.status === 'FAIL')) return Colors.Red; + if (results.some((r) => r.status === 'WARNING')) return Colors.Yellow; + return Colors.Green; +} + +function buildResultLine(result: AuditResult, locale: SupportedLocale): string { + const emoji = STATUS_EMOJI[result.status]; + const featureName = t(locale, `commands.permissionAudit.features.${result.featureKey}`) || result.featureKey; + + let line = `${emoji} **${featureName}**`; + if (result.scope === 'channel' && result.channelId) line += ` (<#${result.channelId}>)`; + if (result.missingPermissions.length > 0) line += `\n> \`${result.missingPermissions.join('`, `')}\``; + + if (result.scope === 'hierarchy' && result.hierarchyInfo) { + const { targetRoleName, botRolePosition, targetRolePosition } = result.hierarchyInfo; + if (result.status === 'FAIL') { + line += `\n> ${t(locale, 'commands.permissionAudit.hierarchyWarning', { + role: targetRoleName, + botPos: String(botRolePosition), + targetPos: String(targetRolePosition), + })}`; + } + } + return line; +} + +export default { + data: new SlashCommandBuilder() + .setName('audit') + .setDescription('Manage audit logs and bot permissions.') + .setDescriptionLocalizations({ + ko: '감사 로그 및 봇 권한을 관리합니다.', + }) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + // --- Channel Subcommand Group --- + .addSubcommandGroup(group => + group + .setName('channel') + .setDescription('Manage audit log channel settings.') + .setDescriptionLocalizations({ ko: '감사 채널 설정을 관리합니다.' }) + .addSubcommand(subcommand => + subcommand + .setName('set') + .setDescription('Set the audit log channel.') + .setDescriptionLocalizations({ ko: '감사 채널을 설정합니다.' }) + .addChannelOption(option => + option.setName('channel') + .setDescription('The text channel to use for audit logs.') + .setDescriptionLocalizations({ ko: '감사 로그를 기록할 텍스트 채널' }) + .addChannelTypes(ChannelType.GuildText) + .setRequired(true) + ) + ) + .addSubcommand(subcommand => + subcommand + .setName('clear') + .setDescription('Clear the audit log channel.') + .setDescriptionLocalizations({ ko: '감사 채널 설정을 해제합니다.' }) + ) + .addSubcommand(subcommand => + subcommand + .setName('status') + .setDescription('Check current audit log channel status.') + .setDescriptionLocalizations({ ko: '현재 감사 채널 설정 상태를 확인합니다.' }) + ) + .addSubcommand(subcommand => + subcommand + .setName('filter') + .setDescription('Enable or disable specific audit log categories.') + .setDescriptionLocalizations({ ko: '특정 종류의 감사 로그 수신 여부를 설정합니다.' }) + .addStringOption(option => + option.setName('category') + .setDescription('The category to manage') + .setDescriptionLocalizations({ ko: '설정할 카테고리' }) + .setRequired(true) + .addChoices( + { name: 'SYSTEM (System Errors)', value: 'SYSTEM' }, + { name: 'BOOT (Bot Online Notifications)', value: 'BOOT' }, + { name: 'VOICE (Voice Channels)', value: 'VOICE' }, + { name: 'PERMISSION (Permission Issues)', value: 'PERMISSION' }, + { name: 'INVITE (Invite Tracking)', value: 'INVITE' }, + { name: 'MIMIC (Mimic Features)', value: 'MIMIC' } + ) + ) + .addBooleanOption(option => + option.setName('enable') + .setDescription('True to receive logs for this category, False to ignore.') + .setDescriptionLocalizations({ ko: '해당 카테고리 수신 (true) 또는 차단 (false)' }) + .setRequired(true) + ) + ) + ) + // --- Permissions Subcommand --- + .addSubcommand(subcommand => + subcommand + .setName('permissions') + .setDescription('Check if the bot has all required permissions for its features.') + .setDescriptionLocalizations({ ko: '봇이 필요한 권한을 가지고 있는지 진단합니다.' }) + ), + + async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) { + const group = interaction.options.getSubcommandGroup(); + const subcommand = interaction.options.getSubcommand(); + const guild = interaction.guild!; + + try { + // --- CHANNEL GROUP --- + if (group === 'channel') { + if (subcommand === 'set') { + await interaction.deferReply({ ephemeral: true }); + const channel = interaction.options.getChannel('channel', true) as TextChannel; + const botMember = guild.members.me; + if (!botMember) return; + const perms = channel.permissionsFor(botMember); + + if (!perms.has(PermissionFlagsBits.SendMessages) || !perms.has(PermissionFlagsBits.EmbedLinks)) { + return interaction.editReply({ content: `❌ 봇에게 <#${channel.id}> 채널의 \`메시지 보내기\` 및 \`링크 첨부\` 권한을 부여해주세요.` }); + } + + await auditLogService.setChannel(guild.id, channel.id); + await interaction.editReply({ content: `✅ 감사 채널이 <#${channel.id}>로 설정되었습니다.` }); + await auditLogService.log(guild, { + category: 'SYSTEM', + severity: 'INFO', + title: 'Audit Channel Configured', + description: `This channel has been configured as the audit log channel by <@${interaction.user.id}>.`, + }); + return; + } + + if (subcommand === 'clear') { + await interaction.deferReply({ ephemeral: true }); + await auditLogService.clearChannel(guild.id); + return interaction.editReply({ content: `✅ 감사 채널 설정이 해제되었습니다.` }); + } + + if (subcommand === 'status') { + await interaction.deferReply({ ephemeral: true }); + const config = await auditLogService.getChannel(guild.id); + if (!config) return interaction.editReply({ content: `설정된 감사 채널이 없습니다. 먼저 \`/audit channel set\` 명령어로 채널을 설정해주세요.` }); + + const disabled = config.disabledCategories.length > 0 ? config.disabledCategories.map((c: string) => `\`${c}\``).join(', ') : '없음 (모두 수신 중)'; + const embed = new EmbedBuilder() + .setTitle('🔍 감사 채널 설정 상태') + .setColor(Colors.Blue) + .addFields( + { name: '지정된 채널', value: `<#${config.channelId}>`, inline: false }, + { name: '수신 차단된 카테고리 (Muted)', value: disabled, inline: false } + ) + .setTimestamp(); + return interaction.editReply({ embeds: [embed] }); + } + + if (subcommand === 'filter') { + await interaction.deferReply({ ephemeral: true }); + const category = interaction.options.getString('category', true) as AuditCategory; + const enable = interaction.options.getBoolean('enable', true); + + const config = await auditLogService.getChannel(guild.id); + if (!config) return interaction.editReply({ content: `설정된 감사 채널이 없습니다. 먼저 \`/audit channel set\` 명령어로 채널을 설정해주세요.` }); + + await auditLogService.setFilter(guild.id, category, enable); + return interaction.editReply({ content: `✅ **${category}** 카테고리의 감사 로그 수신이 **${enable ? '활성화(ON)' : '비활성화(OFF)'}** 되었습니다.` }); + } + } + + // --- PERMISSIONS SUBCOMMAND --- + if (subcommand === 'permissions') { + await interaction.deferReply({ ephemeral: true }); + const results = await PermissionAuditService.auditGuild(guild); + if (results.length === 0) return interaction.editReply({ content: t(locale, 'commands.permissionAudit.noResults') }); + + const sorted = [...results].sort((a, b) => { + const order: Record = { FAIL: 0, WARNING: 1, SUCCESS: 2 }; + return order[a.status] - order[b.status]; + }); + + const lines = sorted.map((r) => buildResultLine(r, locale)); + const failCount = results.filter((r) => r.status === 'FAIL').length; + const warnCount = results.filter((r) => r.status === 'WARNING').length; + + let summary = failCount === 0 && warnCount === 0 ? t(locale, 'commands.permissionAudit.summaryOk') : t(locale, 'commands.permissionAudit.summaryIssue', { fail: String(failCount), warn: String(warnCount) }); + + const embed = new EmbedBuilder() + .setTitle(t(locale, 'commands.permissionAudit.title')) + .setDescription(lines.join('\n\n')) + .addFields({ name: t(locale, 'commands.permissionAudit.summaryLabel'), value: summary }) + .setColor(getOverallColor(results)) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + + if (failCount > 0 || warnCount > 0) { + const issues = sorted.filter(r => r.status !== 'SUCCESS'); + const descLines = issues.map(r => { + let text = `- **${r.featureKey}** [${r.status}]`; + if (r.missingPermissions.length > 0) text += `\n 누락: \`${r.missingPermissions.join('`, `')}\``; + if (r.scope === 'channel') text += `\n 채널: <#${r.channelId}>`; + return text; + }); + + await auditLogService.log(guild, { + category: 'PERMISSION', + severity: failCount > 0 ? 'ERROR' : 'WARN', + title: '권한 감사에서 문제 감지', + description: `\`/audit permissions\` 실행 중 권한 문제가 확인되었습니다.\n\n${descLines.join('\n')}` + }).catch(() => {}); + } + return; + } + } catch (error) { + console.error('Error in audit command', error); + const reply = interaction.deferred ? interaction.editReply : interaction.reply; + return (reply as any)({ content: '명령 실행 중 오류가 발생했습니다.', ephemeral: true }); + } + }, +}; diff --git a/src/commands/auditChannel.ts b/src/commands/auditChannel.ts deleted file mode 100644 index daad2eb..0000000 --- a/src/commands/auditChannel.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { - SlashCommandBuilder, - PermissionFlagsBits, - ChatInputCommandInteraction, - ChannelType, - EmbedBuilder, - Colors, - TextChannel, -} from 'discord.js'; -import { auditLogService, AuditCategory } from '../services/AuditLogService'; -import { SupportedLocale, t } from '../i18n'; - -export default { - data: new SlashCommandBuilder() - .setName('audit-channel') - .setDescription('Manage the audit log channel settings.') - .setDescriptionLocalizations({ - ko: '감사 채널 설정을 관리합니다.', - }) - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - .addSubcommand((subcommand) => - subcommand - .setName('set') - .setDescription('Set the audit log channel.') - .setDescriptionLocalizations({ ko: '감사 채널을 설정합니다.' }) - .addChannelOption((option) => - option - .setName('channel') - .setDescription('The text channel to use for audit logs.') - .setDescriptionLocalizations({ ko: '감사 로그를 기록할 텍스트 채널' }) - .addChannelTypes(ChannelType.GuildText) - .setRequired(true) - ) - ) - .addSubcommand((subcommand) => - subcommand - .setName('clear') - .setDescription('Clear the audit log channel.') - .setDescriptionLocalizations({ ko: '감사 채널 설정을 해제합니다.' }) - ) - .addSubcommand((subcommand) => - subcommand - .setName('status') - .setDescription('Check current audit log channel status.') - .setDescriptionLocalizations({ ko: '현재 감사 채널 설정 상태를 확인합니다.' }) - ) - .addSubcommand((subcommand) => - subcommand - .setName('filter') - .setDescription('Enable or disable specific audit log categories.') - .setDescriptionLocalizations({ ko: '특정 종류의 감사 로그 수신 여부를 설정합니다.' }) - .addStringOption((option) => - option - .setName('category') - .setDescription('The category to manage') - .setDescriptionLocalizations({ ko: '설정할 카테고리' }) - .setRequired(true) - .addChoices( - { name: 'SYSTEM (System Errors)', value: 'SYSTEM' }, - { name: 'BOOT (Bot Online Notifications)', value: 'BOOT' }, - { name: 'VOICE (Voice Channels)', value: 'VOICE' }, - { name: 'PERMISSION (Permission Issues)', value: 'PERMISSION' }, - { name: 'INVITE (Invite Tracking)', value: 'INVITE' }, - { name: 'MIMIC (Mimic Features)', value: 'MIMIC' } - ) - ) - .addBooleanOption((option) => - option - .setName('enable') - .setDescription('True to receive logs for this category, False to ignore.') - .setDescriptionLocalizations({ ko: '해당 카테고리 수신 (true) 또는 차단 (false)' }) - .setRequired(true) - ) - ), - - async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) { - if (!interaction.guild) return; - - const subcommand = interaction.options.getSubcommand(); - await interaction.deferReply({ ephemeral: true }); - - if (subcommand === 'set') { - const channel = interaction.options.getChannel('channel', true); - - // 권한 검증 - const botMember = interaction.guild.members.me; - if (!botMember) return; - - const textChannel = channel as TextChannel; - const perms = textChannel.permissionsFor(botMember); - - if (!perms.has(PermissionFlagsBits.SendMessages) || !perms.has(PermissionFlagsBits.EmbedLinks)) { - await interaction.editReply({ - content: `❌ 봇에게 <#${channel.id}> 채널의 \`메시지 보내기(Send Messages)\` 및 \`링크 첨부(Embed Links)\` 권한을 부여해주세요.`, - }); - return; - } - - await auditLogService.setChannel(interaction.guild.id, channel.id); - - await interaction.editReply({ - content: `✅ 감사 채널이 <#${channel.id}>로 설정되었습니다.`, - }); - - // 테스트 로그 전송 - await auditLogService.log(interaction.guild, { - category: 'SYSTEM', - severity: 'INFO', - title: 'Audit Channel Configured', - description: `This channel has been configured as the audit log channel by <@${interaction.user.id}>.`, - }); - } else if (subcommand === 'clear') { - await auditLogService.clearChannel(interaction.guild.id); - await interaction.editReply({ - content: `✅ 감사 채널 설정이 해제되었습니다.`, - }); - } else if (subcommand === 'status') { - const config = await auditLogService.getChannel(interaction.guild.id); - if (!config) { - await interaction.editReply({ - content: `설정된 감사 채널이 없습니다. 먼저 \`/audit-channel set\` 명령어로 채널을 설정해주세요.`, - }); - return; - } - - const disabled = config.disabledCategories.length > 0 - ? config.disabledCategories.map((c: string) => `\`${c}\``).join(', ') - : '없음 (모두 수신 중)'; - - const embed = new EmbedBuilder() - .setTitle('🔍 감사 채널 설정 상태') - .setColor(Colors.Blue) - .addFields( - { name: '지정된 채널', value: `<#${config.channelId}>`, inline: false }, - { name: '수신 차단된 카테고리 (Muted)', value: disabled, inline: false } - ) - .setTimestamp(); - - await interaction.editReply({ embeds: [embed] }); - } else if (subcommand === 'filter') { - const category = interaction.options.getString('category', true) as AuditCategory; - const enable = interaction.options.getBoolean('enable', true); - - const config = await auditLogService.getChannel(interaction.guild.id); - if (!config) { - await interaction.editReply({ - content: `설정된 감사 채널이 없습니다. 먼저 \`/audit-channel set\` 명령어로 채널을 설정해주세요.`, - }); - return; - } - - const newFilters = await auditLogService.setFilter(interaction.guild.id, category, enable); - - await interaction.editReply({ - content: `✅ **${category}** 카테고리의 감사 로그 수신이 **${enable ? '활성화(ON)' : '비활성화(OFF)'}** 되었습니다.`, - }); - } - }, -}; diff --git a/src/commands/auditPermissions.ts b/src/commands/auditPermissions.ts deleted file mode 100644 index ac903fb..0000000 --- a/src/commands/auditPermissions.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { - SlashCommandBuilder, - PermissionFlagsBits, - ChatInputCommandInteraction, - EmbedBuilder, - Colors, -} from 'discord.js'; -import { PermissionAuditService, AuditResult, AuditStatus } from '../services/PermissionAuditService'; -import { SupportedLocale, t } from '../i18n'; -import { auditLogService } from '../services/AuditLogService'; - -const STATUS_EMOJI: Record = { - SUCCESS: '✅', - WARNING: '⚠️', - FAIL: '❌', -}; - -function getOverallColor(results: AuditResult[]): number { - if (results.some((r) => r.status === 'FAIL')) return Colors.Red; - if (results.some((r) => r.status === 'WARNING')) return Colors.Yellow; - return Colors.Green; -} - -function buildResultLine(result: AuditResult, locale: SupportedLocale): string { - const emoji = STATUS_EMOJI[result.status]; - const featureName = - t(locale, `commands.permissionAudit.features.${result.featureKey}`) || result.featureKey; - - let line = `${emoji} **${featureName}**`; - - if (result.scope === 'channel' && result.channelId) { - line += ` (<#${result.channelId}>)`; - } - - if (result.missingPermissions.length > 0) { - line += `\n> \`${result.missingPermissions.join('`, `')}\``; - } - - if (result.scope === 'hierarchy' && result.hierarchyInfo) { - const { targetRoleName, botRolePosition, targetRolePosition } = result.hierarchyInfo; - if (result.status === 'FAIL') { - line += `\n> ${t(locale, 'commands.permissionAudit.hierarchyWarning', { - role: targetRoleName, - botPos: String(botRolePosition), - targetPos: String(targetRolePosition), - })}`; - } - } - - return line; -} - -export default { - data: new SlashCommandBuilder() - .setName('audit-permissions') - .setDescription('Check if the bot has all required permissions for its features.') - .setDescriptionLocalizations({ - ko: '봇이 원활하게 작동하기 위해 필요한 권한들을 진단합니다.', - }) - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), - - async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) { - if (!interaction.guild) return; - await interaction.deferReply({ ephemeral: true }); - - const results = await PermissionAuditService.auditGuild(interaction.guild); - - if (results.length === 0) { - await interaction.editReply({ content: t(locale, 'commands.permissionAudit.noResults') }); - return; - } - - // 결과를 ❌ FAIL → ⚠️ WARNING → ✅ SUCCESS 순으로 정렬 - const sorted = [...results].sort((a, b) => { - const order: Record = { FAIL: 0, WARNING: 1, SUCCESS: 2 }; - return order[a.status] - order[b.status]; - }); - - const lines = sorted.map((r) => buildResultLine(r, locale)); - - const failCount = results.filter((r) => r.status === 'FAIL').length; - const warnCount = results.filter((r) => r.status === 'WARNING').length; - - let summary: string; - if (failCount === 0 && warnCount === 0) { - summary = t(locale, 'commands.permissionAudit.summaryOk'); - } else { - summary = t(locale, 'commands.permissionAudit.summaryIssue', { - fail: String(failCount), - warn: String(warnCount), - }); - } - - const embed = new EmbedBuilder() - .setTitle(t(locale, 'commands.permissionAudit.title')) - .setDescription(lines.join('\n\n')) - .addFields({ name: t(locale, 'commands.permissionAudit.summaryLabel'), value: summary }) - .setColor(getOverallColor(results)) - .setTimestamp(); - - await interaction.editReply({ embeds: [embed] }); - - if (failCount > 0 || warnCount > 0) { - const issues = sorted.filter(r => r.status !== 'SUCCESS'); - const descLines = issues.map(r => { - let text = `- **${r.featureKey}** [${r.status}]`; - if (r.missingPermissions.length > 0) text += `\n 누락: \`${r.missingPermissions.join('`, `')}\``; - if (r.scope === 'channel') text += `\n 채널: <#${r.channelId}>`; - return text; - }); - - await auditLogService.log(interaction.guild, { - category: 'PERMISSION', - severity: failCount > 0 ? 'ERROR' : 'WARN', - title: '권한 감사에서 문제 감지', - description: `\`/audit-permissions\` 실행 중 권한 문제가 확인되었습니다.\n\n${descLines.join('\n')}` - }).catch(() => {}); - } - }, -}; diff --git a/src/commands/voice.ts b/src/commands/voice.ts new file mode 100644 index 0000000..d5d4523 --- /dev/null +++ b/src/commands/voice.ts @@ -0,0 +1,203 @@ +import { + SlashCommandBuilder, + PermissionFlagsBits, + ChatInputCommandInteraction, + ChannelType, + EmbedBuilder, + TextChannel +} from 'discord.js'; +import { prisma } from '../database'; +import { SupportedLocale, t } from '../i18n'; +import { logger } from '../utils/logger'; + +export default { + data: new SlashCommandBuilder() + .setName('voice') + .setDescription('Manage temporary voice channels.') + .setDescriptionLocalizations({ + ko: '임시 음성 채널 설정을 관리합니다.', + }) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + // --- Setup Subcommands --- + .addSubcommandGroup(group => + group + .setName('setup') + .setDescription('Configure the voice generator channel.') + .setDescriptionLocalizations({ ko: '음성 생성기 채널을 설정합니다.' }) + .addSubcommand(subcommand => + subcommand + .setName('set') + .setDescription('Set an existing voice channel as a Generator') + .setDescriptionLocalizations({ ko: '기존 음성 채널을 생성기로 설정합니다' }) + .addChannelOption(option => + option.setName('channel') + .setDescription('The voice channel to act as the Generator') + .setDescriptionLocalizations({ ko: '생성기로 사용할 음성 채널' }) + .setRequired(true) + .addChannelTypes(ChannelType.GuildVoice) + ) + .addChannelOption(option => + option.setName('category') + .setDescription('(Optional) The category where temp channels will be created') + .setDescriptionLocalizations({ ko: '(선택) 임시 채널이 생성될 카테고리' }) + .setRequired(false) + .addChannelTypes(ChannelType.GuildCategory) + ) + ) + .addSubcommand(subcommand => + subcommand + .setName('create') + .setDescription('Create a new voice channel and set it as a Generator') + .setDescriptionLocalizations({ ko: '새 음성 채널을 만들고 생성기로 설정합니다' }) + .addStringOption(option => + option.setName('name') + .setDescription('The name of the new generator voice channel') + .setDescriptionLocalizations({ ko: '새 생성기 음성 채널의 이름' }) + .setRequired(true) + ) + .addChannelOption(option => + option.setName('category') + .setDescription('(Optional) The category where the new channel will be created') + .setDescriptionLocalizations({ ko: '(선택) 새 채널이 생성될 카테고리' }) + .setRequired(false) + .addChannelTypes(ChannelType.GuildCategory) + ) + ) + ) + // --- Config Subcommands --- + .addSubcommandGroup(group => + group + .setName('config') + .setDescription('Manage default settings for temporary channels.') + .setDescriptionLocalizations({ ko: '임시 채널의 기본 설정을 관리합니다.' }) + .addSubcommand(subcommand => + subcommand + .setName('name') + .setDescription('Set the default naming template for new temp channels.') + .setDescriptionLocalizations({ ko: '임시 채널의 기본 이름 템플릿을 설정합니다.' }) + .addStringOption(option => + option.setName('template') + .setDescription('Template using {{username}} placeholder') + .setDescriptionLocalizations({ ko: '{{username}}을 포함한 이름 템플릿' }) + .setRequired(true) + ) + ) + .addSubcommand(subcommand => + subcommand + .setName('limit') + .setDescription('Set the default user limit for new temp channels.') + .setDescriptionLocalizations({ ko: '임시 채널의 기본 인원 제한을 설정합니다.' }) + .addIntegerOption(option => + option.setName('limit') + .setDescription('User limit (0-99, 0 = unlimited)') + .setDescriptionLocalizations({ ko: '인원 제한 (0-99, 0 = 무제한)' }) + .setRequired(true) + .setMinValue(0) + .setMaxValue(99) + ) + ) + .addSubcommand(subcommand => + subcommand + .setName('status') + .setDescription('View current guild voice settings.') + .setDescriptionLocalizations({ ko: '현재 서버의 음성 설정을 확인합니다.' }) + ) + ), + + async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) { + const group = interaction.options.getSubcommandGroup(); + const subcommand = interaction.options.getSubcommand(); + const guildId = interaction.guildId!; + + try { + // --- SETUP GROUP --- + if (group === 'setup') { + const category = interaction.options.getChannel('category'); + + if (subcommand === 'set') { + const channel = interaction.options.getChannel('channel', true); + await prisma.voiceGenerator.upsert({ + where: { channelId: channel.id }, + update: { categoryId: category?.id || null, guildId }, + create: { channelId: channel.id, guildId, categoryId: category?.id || null } + }); + return interaction.reply({ + content: t(locale, 'commands.voiceSetup.setSuccess', { channel: `${channel}` }), + ephemeral: true + }); + } + + if (subcommand === 'create') { + const name = interaction.options.getString('name', true); + const newChannel = await interaction.guild!.channels.create({ + name, + type: ChannelType.GuildVoice, + parent: category?.id || null, + }); + await prisma.voiceGenerator.create({ + data: { channelId: newChannel.id, guildId, categoryId: category?.id || null } + }); + return interaction.reply({ + content: t(locale, 'commands.voiceSetup.createSuccess', { channel: `${newChannel}` }), + ephemeral: true + }); + } + } + + // --- CONFIG GROUP --- + if (group === 'config') { + if (subcommand === 'name') { + const template = interaction.options.getString('template', true); + await prisma.voiceGuildConfig.upsert({ + where: { guildId }, + update: { defaultNameTemplate: template }, + create: { guildId, defaultNameTemplate: template } + }); + return interaction.reply({ + content: t(locale, 'commands.voiceConfig.setSuccess'), + ephemeral: true + }); + } + + if (subcommand === 'limit') { + const limit = interaction.options.getInteger('limit', true); + await prisma.voiceGuildConfig.upsert({ + where: { guildId }, + update: { defaultUserLimit: limit }, + create: { guildId, defaultUserLimit: limit } + }); + return interaction.reply({ + content: t(locale, 'commands.voiceConfig.setSuccess'), + ephemeral: true + }); + } + + if (subcommand === 'status') { + const config = await prisma.voiceGuildConfig.findUnique({ where: { guildId } }); + const embed = new EmbedBuilder() + .setTitle(t(locale, 'commands.voiceConfig.statusTitle')) + .setColor(0x5865F2) + .addFields( + { + name: t(locale, 'commands.voiceConfig.templateLabel'), + value: `\`${config?.defaultNameTemplate || t(locale, 'voice.defaultRoomName')}\``, + inline: true + }, + { + name: t(locale, 'commands.voiceConfig.limitLabel'), + value: t(locale, 'commands.voiceConfig.limitValue', { limit: String(config?.defaultUserLimit ?? 0) }), + inline: true + } + ); + return interaction.reply({ embeds: [embed], ephemeral: true }); + } + } + } catch (error) { + logger.error('Error in voice command', error); + return interaction.reply({ + content: t(locale, 'errors.E3003.userMessage'), + ephemeral: true + }); + } + }, +}; diff --git a/src/commands/voiceConfig.ts b/src/commands/voiceConfig.ts deleted file mode 100644 index ac36a57..0000000 --- a/src/commands/voiceConfig.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction, EmbedBuilder } from 'discord.js'; -import { prisma } from '../database'; -import { SupportedLocale } from '../i18n'; -import { t } from '../i18n'; -import { logger } from '../utils/logger'; - -export default { - data: new SlashCommandBuilder() - .setName('voice-config') - .setDescription('Manage guild-specific settings for temporary voice channels.') - .setDescriptionLocalizations({ - ko: '서버의 임시 음성 채널 설정을 관리합니다.', - }) - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - .addSubcommand(subcommand => - subcommand - .setName('set-name') - .setDescription('Set the default naming template for new temp channels.') - .setDescriptionLocalizations({ - ko: '임시 채널의 기본 이름 템플릿을 설정합니다.', - }) - .addStringOption(option => - option.setName('template') - .setDescription('Template using {{username}} placeholder') - .setDescriptionLocalizations({ - ko: '{{username}}을 포함한 이름 템플릿', - }) - .setRequired(true) - ) - ) - .addSubcommand(subcommand => - subcommand - .setName('set-limit') - .setDescription('Set the default user limit for new temp channels.') - .setDescriptionLocalizations({ - ko: '임시 채널의 기본 인원 제한을 설정합니다.', - }) - .addIntegerOption(option => - option.setName('limit') - .setDescription('User limit (0-99, 0 = unlimited)') - .setDescriptionLocalizations({ - ko: '인원 제한 (0-99, 0 = 무제한)', - }) - .setRequired(true) - .setMinValue(0) - .setMaxValue(99) - ) - ) - .addSubcommand(subcommand => - subcommand - .setName('status') - .setDescription('View current guild voice settings.') - .setDescriptionLocalizations({ - ko: '현재 서버의 음성 설정을 확인합니다.', - }) - ), - - async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) { - const subcommand = interaction.options.getSubcommand(); - const guildId = interaction.guildId!; - - try { - if (subcommand === 'set-name') { - const template = interaction.options.getString('template', true); - await prisma.voiceGuildConfig.upsert({ - where: { guildId }, - update: { defaultNameTemplate: template }, - create: { guildId, defaultNameTemplate: template } - }); - - return interaction.reply({ - content: t(locale, 'commands.voiceConfig.setSuccess'), - ephemeral: true - }); - } - - if (subcommand === 'set-limit') { - const limit = interaction.options.getInteger('limit', true); - await prisma.voiceGuildConfig.upsert({ - where: { guildId }, - update: { defaultUserLimit: limit }, - create: { guildId, defaultUserLimit: limit } - }); - - return interaction.reply({ - content: t(locale, 'commands.voiceConfig.setSuccess'), - ephemeral: true - }); - } - - if (subcommand === 'status') { - const config = await prisma.voiceGuildConfig.findUnique({ where: { guildId } }); - - const embed = new EmbedBuilder() - .setTitle(t(locale, 'commands.voiceConfig.statusTitle')) - .setColor(0x5865F2) - .addFields( - { - name: t(locale, 'commands.voiceConfig.templateLabel'), - value: `\`${config?.defaultNameTemplate || t(locale, 'voice.defaultRoomName')}\``, - inline: true - }, - { - name: t(locale, 'commands.voiceConfig.limitLabel'), - value: t(locale, 'commands.voiceConfig.limitValue', { limit: String(config?.defaultUserLimit ?? 0) }), - inline: true - } - ); - - return interaction.reply({ embeds: [embed], ephemeral: true }); - } - } catch (error) { - logger.error('Error in voice-config command', error); - return interaction.reply({ - content: t(locale, 'errors.E3003.userMessage'), - ephemeral: true - }); - } - }, -}; diff --git a/src/commands/voiceSetup.ts b/src/commands/voiceSetup.ts deleted file mode 100644 index ec3a9c0..0000000 --- a/src/commands/voiceSetup.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction, ChannelType } from 'discord.js'; -import { prisma } from '../database'; -import { SupportedLocale } from '../i18n'; -import { t } from '../i18n'; - -export default { - data: new SlashCommandBuilder() - .setName('voice-setup') - .setDescription('Setup a generator voice channel for temporary channels.') - .setDescriptionLocalizations({ - ko: '임시 음성 채널을 위한 생성기 채널을 설정합니다.', - }) - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - .addSubcommand(subcommand => - subcommand - .setName('set') - .setDescription('Set an existing voice channel as a Generator') - .setDescriptionLocalizations({ - ko: '기존 음성 채널을 생성기로 설정합니다', - }) - .addChannelOption(option => - option.setName('channel') - .setDescription('The voice channel to act as the Generator') - .setDescriptionLocalizations({ - ko: '생성기로 사용할 음성 채널', - }) - .setRequired(true) - .addChannelTypes(ChannelType.GuildVoice) - ) - .addChannelOption(option => - option.setName('category') - .setDescription('(Optional) The category where temp channels will be created') - .setDescriptionLocalizations({ - ko: '(선택) 임시 채널이 생성될 카테고리', - }) - .setRequired(false) - .addChannelTypes(ChannelType.GuildCategory) - ) - ) - .addSubcommand(subcommand => - subcommand - .setName('create') - .setDescription('Create a new voice channel and set it as a Generator') - .setDescriptionLocalizations({ - ko: '새 음성 채널을 만들고 생성기로 설정합니다', - }) - .addStringOption(option => - option.setName('name') - .setDescription('The name of the new generator voice channel') - .setDescriptionLocalizations({ - ko: '새 생성기 음성 채널의 이름', - }) - .setRequired(true) - ) - .addChannelOption(option => - option.setName('category') - .setDescription('(Optional) The category where the new channel will be created') - .setDescriptionLocalizations({ - ko: '(선택) 새 채널이 생성될 카테고리', - }) - .setRequired(false) - .addChannelTypes(ChannelType.GuildCategory) - ) - ), - - async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) { - const subcommand = interaction.options.getSubcommand(); - const category = interaction.options.getChannel('category'); - - if (subcommand === 'set') { - const channel = interaction.options.getChannel('channel', true); - - await prisma.voiceGenerator.upsert({ - where: { channelId: channel.id }, - update: { categoryId: category?.id || null, guildId: interaction.guildId! }, - create: { - channelId: channel.id, - guildId: interaction.guildId!, - categoryId: category?.id || null, - } - }); - - await interaction.reply({ - content: t(locale, 'commands.voiceSetup.setSuccess', { channel: `${channel}` }), - ephemeral: true - }); - } else if (subcommand === 'create') { - const name = interaction.options.getString('name', true); - const guild = interaction.guild!; - - const newChannel = await guild.channels.create({ - name: name, - type: ChannelType.GuildVoice, - parent: category?.id || null, - }); - - await prisma.voiceGenerator.create({ - data: { - channelId: newChannel.id, - guildId: guild.id, - categoryId: category?.id || null, - } - }); - - await interaction.reply({ - content: t(locale, 'commands.voiceSetup.createSuccess', { channel: `${newChannel}` }), - ephemeral: true - }); - } - }, -};