From bf280752cfdcf59ad172db24ea14b5254ead86ff Mon Sep 17 00:00:00 2001 From: kazukazu123123 <50506519+kazukazu123123@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:03:42 +0900 Subject: [PATCH] feat: prevent timeout in command response (#984) Co-authored-by: Mogyuchi <mogyuchi@mogyuchi.jp> --- src/commands/join.ts | 15 +++++++++++---- src/commands/leave.ts | 21 ++++++++++++++++----- src/commands/read.ts | 7 +++++-- src/commands/rejoin.ts | 7 +++++-- src/commands/reset.ts | 5 +++-- src/commands/userSettings.ts | 9 +++++++-- src/connectionCtx.ts | 17 +++++++++++------ src/guildCtx.ts | 8 ++++++-- src/utils.ts | 14 +++++++++++++- 9 files changed, 77 insertions(+), 26 deletions(-) diff --git a/src/commands/join.ts b/src/commands/join.ts index e482895c..e01c097b 100644 --- a/src/commands/join.ts +++ b/src/commands/join.ts @@ -3,7 +3,7 @@ import { guildCtxManager } from '../index.js' import { type InteractionReplyOptions } from 'discord.js' import { checkCanJoin, checkUserAlreadyJoined } from '../components/preCheck.js' import { newGuildTextBasedChannelId, newVoiceBasedChannelId } from '../id.js' -import { getErrorReply } from '../utils.js' +import { deferredReplyOrEdit, getErrorReply } from '../utils.js' export class JoinCommand extends Command { public constructor( @@ -48,10 +48,17 @@ export class JoinCommand extends Command { checkCanJoin(voiceChannel) const guildCtx = guildCtxManager.get(interaction.member.guild) + const readChannelId = newGuildTextBasedChannelId(interaction.channel) + const voiceChannelId = newVoiceBasedChannelId(voiceChannel) + + guildCtx.checkAlreadyJoined(voiceChannelId) + guildCtx.connectionManager.checkAlreadyUsedChannel(readChannelId) + + await interaction.deferReply() const worker = await guildCtx.join({ - voiceChannelId: newVoiceBasedChannelId(voiceChannel), - readChannelId: newGuildTextBasedChannelId(interaction.channel), + voiceChannelId, + readChannelId, }) interactionReplyOptions = { @@ -73,7 +80,7 @@ export class JoinCommand extends Command { interactionReplyOptions = getErrorReply(error) console.error(error) } finally { - void interaction.reply(interactionReplyOptions) + void deferredReplyOrEdit(interaction, interactionReplyOptions) } } } diff --git a/src/commands/leave.ts b/src/commands/leave.ts index 5aaeb836..9f9f20e4 100644 --- a/src/commands/leave.ts +++ b/src/commands/leave.ts @@ -4,7 +4,11 @@ import { checkUserAlreadyJoined } from '../components/preCheck.js' import type { InteractionReplyOptions } from 'discord.js' import { LeaveCause } from '../connectionCtx.js' import { newGuildTextBasedChannelId, newVoiceBasedChannelId } from '../id.js' -import { getErrorReply } from '../utils.js' +import { deferredReplyOrEdit, getErrorReply } from '../utils.js' +import { + HandleInteractionError, + HandleInteractionErrorType, +} from '../errors/index.js' export class LeaveCommand extends Command { public constructor( @@ -48,11 +52,18 @@ export class LeaveCommand extends Command { checkUserAlreadyJoined(voiceChannel) const ctx = guildCtxManager.get(interaction.member.guild) - const textChannelId = ctx.connectionManager.channelMap.get( + const readChannelId = ctx.connectionManager.channelMap.get( newVoiceBasedChannelId(voiceChannel), ) + if (readChannelId === undefined) + throw new HandleInteractionError( + HandleInteractionErrorType.userNotWithBot, + ) + + await interaction.deferReply() + const cause = - newGuildTextBasedChannelId(interaction.channel) === textChannelId + newGuildTextBasedChannelId(interaction.channel) === readChannelId ? undefined : LeaveCause.command const workerId = await ctx.leave({ @@ -67,7 +78,7 @@ export class LeaveCommand extends Command { title: 'ボイスチャンネルから退出しました。', description: [ `担当BOT: <@${workerId}>`, - `テキストチャンネル: <#${textChannelId}>`, + `テキストチャンネル: <#${readChannelId}>`, `ボイスチャンネル: ${voiceChannel}`, 'またのご利用をお待ちしております。', ].join('\n'), @@ -78,7 +89,7 @@ export class LeaveCommand extends Command { interactionReplyOptions = getErrorReply(error) console.error(error) } finally { - void interaction.reply(interactionReplyOptions) + void deferredReplyOrEdit(interaction, interactionReplyOptions) } } } diff --git a/src/commands/read.ts b/src/commands/read.ts index cf5b6f36..23b253c1 100644 --- a/src/commands/read.ts +++ b/src/commands/read.ts @@ -13,7 +13,7 @@ import { } from '../errors/index.js' import { checkUserAlreadyJoined } from '../components/preCheck.js' import { newVoiceBasedChannelId } from '../id.js' -import { getErrorReply } from '../utils.js' +import { deferredReplyOrEdit, getErrorReply } from '../utils.js' export class ReadCommand extends Command { public constructor( @@ -118,6 +118,7 @@ export class ReadCommand extends Command { try { checkUserAlreadyJoined(voiceChannel) + const text = interaction.options.getString('text', true) const connectionCtx = guildCtxManager @@ -130,6 +131,8 @@ export class ReadCommand extends Command { HandleInteractionErrorType.userNotWithBot, ) + await interaction.deferReply() + const convertedMessage = convertContent( text, [], @@ -152,7 +155,7 @@ export class ReadCommand extends Command { interactionReplyOptions = getErrorReply(error) console.error(error) } finally { - void interaction.reply(interactionReplyOptions) + void deferredReplyOrEdit(interaction, interactionReplyOptions) } } } diff --git a/src/commands/rejoin.ts b/src/commands/rejoin.ts index f89f67b1..a6a97b87 100644 --- a/src/commands/rejoin.ts +++ b/src/commands/rejoin.ts @@ -9,7 +9,7 @@ import { import { checkCanJoin, checkUserAlreadyJoined } from '../components/preCheck.js' import { LeaveCause } from '../connectionCtx.js' import { newGuildTextBasedChannelId, newVoiceBasedChannelId } from '../id.js' -import { getErrorReply } from '../utils.js' +import { deferredReplyOrEdit, getErrorReply } from '../utils.js' export class RejoinCommand extends Command { public constructor( @@ -75,6 +75,9 @@ export class RejoinCommand extends Command { existingJoinConfig.guildId, existingJoinConfig.channelId ?? '', ) + + await interaction.deferReply() + const cause = interaction.channel.id === guildCtx.connectionManager.channelMap.get(voiceChannelId) @@ -104,7 +107,7 @@ export class RejoinCommand extends Command { interactionReplyOptions = getErrorReply(error) console.error(error) } finally { - void interaction.reply(interactionReplyOptions) + void deferredReplyOrEdit(interaction, interactionReplyOptions) } } } diff --git a/src/commands/reset.ts b/src/commands/reset.ts index 10f5b070..edd47bd0 100644 --- a/src/commands/reset.ts +++ b/src/commands/reset.ts @@ -1,7 +1,7 @@ import { Command, type ChatInputCommand } from '@sapphire/framework' import { guildCtxManager } from '../index.js' import { PermissionFlagsBits, type InteractionReplyOptions } from 'discord.js' -import { getErrorReply } from '../utils.js' +import { deferredReplyOrEdit, getErrorReply } from '../utils.js' export class ResetCommand extends Command { public constructor( @@ -30,6 +30,7 @@ export class ResetCommand extends Command { interaction: ChatInputCommand.Interaction, ) { if (!interaction.inCachedGuild()) return + await interaction.deferReply() let interactionReplyOptions: InteractionReplyOptions = { embeds: [ @@ -55,7 +56,7 @@ export class ResetCommand extends Command { interactionReplyOptions = getErrorReply(error) console.error(error) } finally { - void interaction.reply(interactionReplyOptions) + void deferredReplyOrEdit(interaction, interactionReplyOptions) } } } diff --git a/src/commands/userSettings.ts b/src/commands/userSettings.ts index 0ebdaba8..4ad9bb3c 100644 --- a/src/commands/userSettings.ts +++ b/src/commands/userSettings.ts @@ -1,5 +1,6 @@ import { Subcommand } from '@sapphire/plugin-subcommands' import { + deferredReplyOrEdit, getErrorReply, userSettingToString, userSettingToDiff, @@ -124,6 +125,8 @@ export class UserSettingsCommand extends Subcommand { interaction: Subcommand.ChatInputCommandInteraction, ) { if (!interaction.inCachedGuild()) return + await interaction.deferReply({ ephemeral: true }) + const user = interaction.options.getUser('user') let interactionReplyOptions: InteractionReplyOptions = { @@ -160,7 +163,7 @@ export class UserSettingsCommand extends Subcommand { interactionReplyOptions = getErrorReply(error) console.error(error) } finally { - void interaction.reply(interactionReplyOptions) + void deferredReplyOrEdit(interaction, interactionReplyOptions) } } @@ -168,6 +171,8 @@ export class UserSettingsCommand extends Subcommand { interaction: Subcommand.ChatInputCommandInteraction, ) { if (!interaction.inCachedGuild()) return + await interaction.deferReply({ ephemeral: true }) + const allowedVoiceList = [ 'show', 'haruka', @@ -280,7 +285,7 @@ export class UserSettingsCommand extends Subcommand { interactionReplyOptions = getErrorReply(error) console.error(error) } finally { - void interaction.reply(interactionReplyOptions) + void deferredReplyOrEdit(interaction, interactionReplyOptions) } } } diff --git a/src/connectionCtx.ts b/src/connectionCtx.ts index 4d82fdd0..54917883 100644 --- a/src/connectionCtx.ts +++ b/src/connectionCtx.ts @@ -174,6 +174,15 @@ export class ConnectionCtxManager extends Map< return this.get(this.channelMap.get(voiceChannelId)) } + checkAlreadyUsedChannel(readChannelId: GuildTextBasedChannelId) { + const existingJoinConfig = this.get(readChannelId)?.connection.joinConfig + if (existingJoinConfig !== undefined) + throw new AlreadyUsedChannelError( + existingJoinConfig.guildId, + existingJoinConfig.channelId ?? '', + ) + } + connectionJoin({ voiceChannelId, guildId, @@ -190,12 +199,7 @@ export class ConnectionCtxManager extends Map< skipUser?: Set<UserId> }) { if (this.channelMap.has(voiceChannelId)) throw new AlreadyJoinedError() - const existingJoinConfig = this.get(readChannelId)?.connection.joinConfig - if (existingJoinConfig !== undefined) - throw new AlreadyUsedChannelError( - existingJoinConfig.guildId, - existingJoinConfig.channelId ?? '', - ) + this.checkAlreadyUsedChannel(readChannelId) this.channelMap.set(voiceChannelId, readChannelId) const connection = joinVoiceChannel({ channelId: voiceChannelId, @@ -219,6 +223,7 @@ export class ConnectionCtxManager extends Map< this.set(readChannelId, connectionContext) return connectionContext } + async connectionLeave({ voiceChannelId, cause = undefined, diff --git a/src/guildCtx.ts b/src/guildCtx.ts index e344be9b..e59ea1d3 100644 --- a/src/guildCtx.ts +++ b/src/guildCtx.ts @@ -33,6 +33,11 @@ export class GuildContext { this.connectionManager = new ConnectionCtxManager() } + checkAlreadyJoined(voiceChannelId: VoiceBasedChannelId) { + if (this.connectionManager.channelMap.has(voiceChannelId)) + throw new AlreadyJoinedError() + } + async join({ voiceChannelId, readChannelId, @@ -42,8 +47,7 @@ export class GuildContext { readChannelId: GuildTextBasedChannelId skipUser?: Set<UserId> }) { - if (this.connectionManager.channelMap.has(voiceChannelId)) - throw new AlreadyJoinedError() + this.checkAlreadyJoined(voiceChannelId) const vcArray = (await this.guild.channels.fetch()) .map((v) => { diff --git a/src/utils.ts b/src/utils.ts index 32d1deb3..4b2b023d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,7 +2,10 @@ import debug from 'debug' const debug__Queue = debug('utils.js:Queue') import { EventEmitter } from 'events' import { SpeakerList, type userSetting } from '@prisma/client' -import type { InteractionReplyOptions } from 'discord.js' +import type { + ChatInputCommandInteraction, + InteractionReplyOptions, +} from 'discord.js' import { PowError } from './errors/PowError.js' export function getProperty<T, K extends keyof T>(property: K) { @@ -118,3 +121,12 @@ export const getErrorReply = (error: unknown): InteractionReplyOptions => { throw error } } + +export const deferredReplyOrEdit = ( + interaction: ChatInputCommandInteraction, + replyOptions: InteractionReplyOptions, +) => { + return interaction.deferred + ? interaction.editReply(replyOptions) + : interaction.reply(replyOptions) +}