Kord/src/events/interactionCreate.ts

307 lines
14 KiB
TypeScript

import { Events, Interaction, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, ChannelType, VoiceChannel, PermissionFlagsBits, UserSelectMenuBuilder, GuildMember } from 'discord.js';
import { KordClient } from '../client/KordClient';
import { logger } from '../utils/logger';
import { prisma } from '../database';
import { BotError } from '../errors/BotError';
import { ErrorDefs, createBotError } from '../errors/ErrorCodes';
import { ErrorReporter, withErrorHandler } from '../errors/ErrorReporter';
import { t } from '../i18n';
import { getInteractionLocale } from '../i18n/localeHelper';
import { handleSetupWizardInteraction } from '../interactions/handlers/setupWizardHandler';
import { MusicService } from '../services/MusicService';
export default {
name: Events.InteractionCreate,
once: false,
async execute(interaction: Interaction, client: KordClient) {
if (interaction.isChatInputCommand()) {
const command = client.commands.get(interaction.commandName);
if (!command) return;
const locale = await getInteractionLocale(interaction);
await withErrorHandler(interaction, async () => {
await command.execute(interaction, locale);
}, locale);
}
else if (interaction.isButton() && interaction.customId.startsWith('music_')) {
const locale = await getInteractionLocale(interaction);
await withErrorHandler(interaction, async () => {
await MusicService.handleControlInteraction(interaction, locale);
}, locale);
}
else if (interaction.isButton() && interaction.customId.startsWith('fishing_')) {
const { FishingService } = require('../services/FishingService');
const locale = await getInteractionLocale(interaction);
await withErrorHandler(interaction, async () => {
await FishingService.handleButton(interaction, locale);
}, locale);
}
else if (interaction.isMessageComponent() && interaction.customId.startsWith('setup_')) {
const locale = await getInteractionLocale(interaction);
await withErrorHandler(interaction, async () => {
await handleSetupWizardInteraction(interaction, locale);
}, locale);
}
else if (interaction.isButton() && interaction.customId.startsWith('refine_')) {
const { handleRefinementInteraction } = require('../interactions/handlers/refinementHandler');
const locale = await getInteractionLocale(interaction);
await withErrorHandler(interaction, async () => {
await handleRefinementInteraction(interaction, locale);
}, locale);
}
else if (interaction.isStringSelectMenu()) {
const customId = interaction.customId;
if (customId.startsWith('vc_control_')) {
const locale = await getInteractionLocale(interaction);
await withErrorHandler(interaction, async () => {
const parts = customId.split('_');
const ownerId = parts[2];
const action = interaction.values[0];
if (interaction.user.id !== ownerId) {
throw createBotError(ErrorDefs.NOT_CHANNEL_OWNER);
}
const member = interaction.member as GuildMember;
const voiceChannel = member?.voice?.channel as VoiceChannel;
if (!voiceChannel || interaction.channelId !== voiceChannel.id) {
const tempDb = await prisma.tempVoiceChannel.findUnique({ where: { channelId: voiceChannel?.id || '' }});
if (!tempDb || tempDb.ownerId !== ownerId) {
throw createBotError(ErrorDefs.MUST_BE_IN_VOICE_CHANNEL);
}
}
if (action === 'rename') {
const modal = new ModalBuilder()
.setCustomId(`modal_vc_rename_${ownerId}`)
.setTitle(t(locale, 'modals.renameTitle'));
const nameInput = new TextInputBuilder()
.setCustomId('newName')
.setLabel(t(locale, 'modals.renameLabel'))
.setStyle(TextInputStyle.Short)
.setValue(voiceChannel.name)
.setRequired(true)
.setMaxLength(100);
modal.addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(nameInput));
await interaction.showModal(modal);
}
else if (action === 'limit') {
const modal = new ModalBuilder()
.setCustomId(`modal_vc_limit_${ownerId}`)
.setTitle(t(locale, 'modals.limitTitle'));
const limitInput = new TextInputBuilder()
.setCustomId('limit')
.setLabel(t(locale, 'modals.limitLabel'))
.setStyle(TextInputStyle.Short)
.setValue(voiceChannel.userLimit.toString())
.setRequired(true)
.setMaxLength(2);
modal.addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(limitInput));
await interaction.showModal(modal);
}
else if (action === 'lock') {
const everyonePerms = voiceChannel.permissionOverwrites.cache.get(voiceChannel.guild.id);
const isLocked = everyonePerms?.deny.has(PermissionFlagsBits.Connect);
if (isLocked) {
await voiceChannel.permissionOverwrites.edit(voiceChannel.guild.id, { Connect: null });
await interaction.reply({ content: t(locale, 'voice.responses.channelUnlocked'), ephemeral: true });
} else {
await voiceChannel.permissionOverwrites.edit(voiceChannel.guild.id, { Connect: false });
await interaction.reply({ content: t(locale, 'voice.responses.channelLocked'), ephemeral: true });
}
}
else if (action === 'kick') {
const select = new UserSelectMenuBuilder()
.setCustomId(`select_vc_kick_${ownerId}`)
.setPlaceholder(t(locale, 'selects.kickUser'))
.setMinValues(1)
.setMaxValues(1);
await interaction.reply({ components: [new ActionRowBuilder<UserSelectMenuBuilder>().addComponents(select)], ephemeral: true });
}
else if (action === 'ban') {
const select = new UserSelectMenuBuilder()
.setCustomId(`select_vc_ban_${ownerId}`)
.setPlaceholder(t(locale, 'selects.banUser'))
.setMinValues(1)
.setMaxValues(1);
await interaction.reply({ content: t(locale, 'voice.responses.banPrompt'), components: [new ActionRowBuilder<UserSelectMenuBuilder>().addComponents(select)], ephemeral: true });
}
else if (action === 'transfer') {
const select = new UserSelectMenuBuilder()
.setCustomId(`select_vc_transfer_${ownerId}`)
.setPlaceholder(t(locale, 'selects.transferOwner'))
.setMinValues(1)
.setMaxValues(1);
await interaction.reply({ content: t(locale, 'voice.responses.transferPrompt'), components: [new ActionRowBuilder<UserSelectMenuBuilder>().addComponents(select)], ephemeral: true });
}
}, locale);
}
}
else if (interaction.isButton() && interaction.customId.startsWith('autorole_')) {
const locale = await getInteractionLocale(interaction);
const { autoRoleService } = require('../services/AutoRoleService');
await withErrorHandler(interaction, async () => {
// 타임아웃 방지를 위해 즉시 승인
await interaction.deferUpdate();
const guild = interaction.guild!;
if (interaction.customId === 'autorole_retroactive') {
const config = await autoRoleService.getConfig(guild.id);
if (config?.userRoleIds && config.userRoleIds.length > 0) {
await autoRoleService.applyRetroactively(guild, config.userRoleIds, interaction.user.id);
const { generateAutoRoleDashboard } = require('../commands/autorole');
const dashboard = await generateAutoRoleDashboard(guild, locale);
await interaction.editReply({ content: t(locale, 'commands.autorole.retroactiveStarted'), ...dashboard });
}
}
// 나머지 버튼 처리
}, locale);
}
else if (interaction.isRoleSelectMenu() && interaction.customId.startsWith('autorole_select_')) {
const locale = await getInteractionLocale(interaction);
const { autoRoleService } = require('../services/AutoRoleService');
await withErrorHandler(interaction, async () => {
// 타임아웃 방지를 위해 즉시 승인
await interaction.deferUpdate();
const guild = interaction.guild!;
const isBot = interaction.customId.includes('bot');
const roleIds = interaction.values;
await autoRoleService.updateConfig(guild.id, {
[isBot ? 'botRoleIds' : 'userRoleIds']: roleIds
});
const { generateAutoRoleDashboard } = require('../commands/autorole');
const dashboard = await generateAutoRoleDashboard(guild, locale);
await interaction.editReply({
content: '',
...dashboard
});
}, locale);
}
else if (interaction.isModalSubmit()) {
const customId = interaction.customId;
if (customId.startsWith('modal_vc_')) {
const locale = await getInteractionLocale(interaction);
await withErrorHandler(interaction, async () => {
const parts = customId.split('_');
const action = parts[2];
const ownerId = parts[3];
if (interaction.user.id !== ownerId) {
throw createBotError(ErrorDefs.NOT_CHANNEL_OWNER);
}
const member = interaction.member as GuildMember;
const voiceChannel = member?.voice?.channel as VoiceChannel;
if (!voiceChannel) {
throw createBotError(ErrorDefs.MUST_BE_IN_VOICE_CHANNEL);
}
if (action === 'rename') {
const newName = interaction.fields.getTextInputValue('newName');
await voiceChannel.setName(newName);
await prisma.userVoiceProfile.upsert({
where: { userId_guildId: { userId: ownerId, guildId: interaction.guildId! } },
update: { customName: newName },
create: { userId: ownerId, guildId: interaction.guildId!, customName: newName }
});
await interaction.reply({ content: t(locale, 'voice.responses.channelRenamed', { name: newName }), ephemeral: true });
}
else if (action === 'limit') {
const limitStr = interaction.fields.getTextInputValue('limit');
const limit = parseInt(limitStr);
if (isNaN(limit) || limit < 0 || limit > 99) {
throw createBotError(ErrorDefs.INVALID_USER_LIMIT);
}
await voiceChannel.setUserLimit(limit);
await prisma.userVoiceProfile.upsert({
where: { userId_guildId: { userId: ownerId, guildId: interaction.guildId! } },
update: { userLimit: limit },
create: { userId: ownerId, guildId: interaction.guildId!, userLimit: limit }
});
const limitDisplay = limit === 0 ? t(locale, 'voice.responses.limitUnlimited') : String(limit);
await interaction.reply({ content: t(locale, 'voice.responses.limitSet', { limit: limitDisplay }), ephemeral: true });
}
}, locale);
}
}
else if (interaction.isUserSelectMenu()) {
const customId = interaction.customId;
if (customId.startsWith('select_vc_')) {
const locale = await getInteractionLocale(interaction);
await withErrorHandler(interaction, async () => {
const parts = customId.split('_');
const action = parts[2];
const ownerId = parts[3];
if (interaction.user.id !== ownerId) {
throw createBotError(ErrorDefs.NOT_CHANNEL_OWNER);
}
const member = interaction.member as GuildMember;
const voiceChannel = member?.voice?.channel as VoiceChannel;
if (!voiceChannel) {
throw createBotError(ErrorDefs.MUST_BE_IN_VOICE_CHANNEL);
}
const targetUserId = interaction.values[0];
if (targetUserId === ownerId) {
throw createBotError(ErrorDefs.SELF_TARGET_NOT_ALLOWED);
}
if (action === 'kick') {
const targetMember = await interaction.guild?.members.fetch(targetUserId);
if (targetMember && targetMember.voice.channelId === voiceChannel.id) {
await targetMember.voice.disconnect('Kicked by channel owner');
await interaction.reply({ content: t(locale, 'voice.responses.kicked', { user: `<@${targetUserId}>` }), ephemeral: true });
} else {
throw createBotError(ErrorDefs.USER_NOT_IN_CHANNEL);
}
}
else if (action === 'ban') {
await voiceChannel.permissionOverwrites.edit(targetUserId, {
ViewChannel: false,
Connect: false
});
const targetMember = await interaction.guild?.members.fetch(targetUserId);
if (targetMember && targetMember.voice.channelId === voiceChannel.id) {
await targetMember.voice.disconnect('Banned by channel owner');
}
await interaction.reply({ content: t(locale, 'voice.responses.banned', { user: `<@${targetUserId}>` }), ephemeral: true });
}
else if (action === 'transfer') {
const targetMember = await interaction.guild?.members.fetch(targetUserId);
if (targetMember && targetMember.voice.channelId === voiceChannel.id) {
await prisma.tempVoiceChannel.update({
where: { channelId: voiceChannel.id },
data: { ownerId: targetUserId }
});
const { VoiceService } = require('../services/VoiceService');
await VoiceService.applyOwnershipTransfer(voiceChannel, ownerId, targetUserId);
await interaction.reply({ content: t(locale, 'voice.responses.transferDone', { user: `<@${targetUserId}>` }), ephemeral: true });
} else {
throw createBotError(ErrorDefs.USER_NOT_IN_CHANNEL);
}
}
}, locale);
}
}
},
};