234 lines
9.9 KiB
TypeScript
234 lines
9.9 KiB
TypeScript
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<AuditStatus, string> = {
|
|
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<AuditStatus, number> = { 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: '명령 실행 중 오류가 발생했습니다.' });
|
|
}
|
|
},
|
|
};
|
|
|
|
|