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 --- .addSubcommand(subcommand => subcommand .setName('channel') .setDescription('Manage audit log channel settings.') .setDescriptionLocalizations({ ko: '감사 채널 설정을 관리합니다.' }) .addStringOption(option => option.setName('action') .setDescription('Action to perform') .setRequired(true) .addChoices( { name: 'set (Set Channel)', value: 'set' }, { name: 'clear (Reset)', value: 'clear' }, { name: 'status (Check)', value: 'status' }, { name: 'filter (Toggle Categories)', value: 'filter' }, ) ) .addChannelOption(option => option.setName('channel') .setDescription('Channel to use (for set action)') .addChannelTypes(ChannelType.GuildText) ) .addStringOption(option => option.setName('category') .setDescription('Category to toggle (for filter action)') .addChoices( { name: 'SYSTEM', value: 'SYSTEM' }, { name: 'BOOT', value: 'BOOT' }, { name: 'VOICE', value: 'VOICE' }, { name: 'PERMISSION', value: 'PERMISSION' }, { name: 'INVITE', value: 'INVITE' }, { name: 'MIMIC', value: 'MIMIC' } ) ) .addBooleanOption(option => option.setName('enable') .setDescription('Enable or disable (for filter action)') ) ) // --- Bot Subcommand --- .addSubcommand(subcommand => subcommand .setName('bot') .setDescription('Diagnose and manage bot-specific status.') .setDescriptionLocalizations({ ko: '봇의 상태를 진단하고 관리합니다.' }) .addStringOption(option => option.setName('action') .setDescription('Action to perform') .setRequired(true) .addChoices( { name: 'permissions (Diagnose)', value: 'permissions' }, ) ) ), async execute(interaction: ChatInputCommandInteraction, locale: SupportedLocale) { const subcommand = interaction.options.getSubcommand(); const guild = interaction.guild!; try { // --- CHANNEL --- if (subcommand === 'channel') { const action = interaction.options.getString('action', true); if (action === 'set') { const channel = interaction.options.getChannel('channel') as TextChannel; if (!channel) return interaction.editReply({ content: '❌ `channel` 옵션을 선택해주세요.' }); 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 (action === 'clear') { await auditLogService.clearChannel(guild.id); return interaction.editReply({ content: `✅ 감사 채널 설정이 해제되었습니다.` }); } if (action === 'status') { const config = await auditLogService.getChannel(guild.id); if (!config) return interaction.editReply({ content: `설정된 감사 채널이 없습니다. 먼저 \`/audit channel action: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 (action === 'filter') { const category = interaction.options.getString('category') as AuditCategory; const enable = interaction.options.getBoolean('enable'); if (!category || enable === null) { return interaction.editReply({ content: '❌ `category` 및 `enable` 옵션을 모두 입력해주세요.' }); } const config = await auditLogService.getChannel(guild.id); if (!config) return interaction.editReply({ content: `설정된 감사 채널이 없습니다. 먼저 \`/audit channel action:set\` 명령어로 채널을 설정해주세요.` }); await auditLogService.setFilter(guild.id, category, enable); return interaction.editReply({ content: `✅ **${category}** 카테고리의 감사 로그 수신이 **${enable ? '활성화(ON)' : '비활성화(OFF)'}** 되었습니다.` }); } } // --- BOT --- if (subcommand === 'bot') { const action = interaction.options.getString('action', true); if (action === 'permissions') { 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 bot action:permissions\` 실행 중 권한 문제가 확인되었습니다.\n\n${descLines.join('\n')}` }).catch(() => {}); } return; } } } catch (error) { console.error('Error in audit command', error); return interaction.editReply({ content: '명령 실행 중 오류가 발생했습니다.' }); } }, };