refactor: Consolidate voice-related commands into `voice.ts` and audit-related commands into `audit.ts`.
This commit is contained in:
parent
0ccbbf9d31
commit
031a8b3146
|
|
@ -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<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 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<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 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 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -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)'}** 되었습니다.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -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<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-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<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: 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(() => {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Loading…
Reference in New Issue