♻️ Refactor from synchronous replies to asynchronous deferred replies with edits #64

This commit is contained in:
Manuel 2023-01-22 21:53:50 +01:00
parent 38d48ec708
commit 066d27b351
11 changed files with 101 additions and 85 deletions

View File

@ -4,7 +4,6 @@ import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction } from 'discord.js'; import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service'; import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { GenericCustomReply } from '../models/generic-try-handler';
@Command({ @Command({
name: 'disconnect', name: 'disconnect',
@ -17,20 +16,28 @@ export class DisconnectCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService, private readonly discordMessageService: DiscordMessageService,
) {} ) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars async handler(interaction: CommandInteraction): Promise<void> {
handler(interaction: CommandInteraction): GenericCustomReply { await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Disconnecting...',
}),
],
});
const disconnect = this.discordVoiceService.disconnect(); const disconnect = this.discordVoiceService.disconnect();
if (!disconnect.success) { if (!disconnect.success) {
return disconnect.reply; await interaction.editReply(disconnect.reply);
return;
} }
return { await interaction.editReply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Disconnected from your channel', title: 'Disconnected from your channel',
}), }),
], ],
}; });
} }
} }

View File

@ -3,7 +3,6 @@ import { TransformPipe } from '@discord-nestjs/common';
import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core'; import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction } from 'discord.js'; import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { GenericCustomReply } from '../models/generic-try-handler';
@Command({ @Command({
name: 'help', name: 'help',
@ -13,9 +12,8 @@ import { GenericCustomReply } from '../models/generic-try-handler';
export class HelpCommand implements DiscordCommand { export class HelpCommand implements DiscordCommand {
constructor(private readonly discordMessageService: DiscordMessageService) {} constructor(private readonly discordMessageService: DiscordMessageService) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars async handler(interaction: CommandInteraction): Promise<void> {
handler(commandInteraction: CommandInteraction): GenericCustomReply { await interaction.reply({
return {
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Jellyfin Discord Bot', title: 'Jellyfin Discord Bot',
@ -40,6 +38,6 @@ export class HelpCommand implements DiscordCommand {
}, },
}), }),
], ],
}; });
} }
} }

View File

@ -1,7 +1,7 @@
import { TransformPipe } from '@discord-nestjs/common'; import { TransformPipe } from '@discord-nestjs/common';
import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core'; import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction, InteractionReplyOptions } from 'discord.js'; import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { PlaybackService } from '../playback/playback.service'; import { PlaybackService } from '../playback/playback.service';
@ -16,26 +16,23 @@ export class SkipTrackCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService, private readonly discordMessageService: DiscordMessageService,
) {} ) {}
handler( async handler(interaction: CommandInteraction): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interactionCommand: CommandInteraction,
): InteractionReplyOptions | string {
if (!this.playbackService.nextTrack()) { if (!this.playbackService.nextTrack()) {
return { await interaction.reply({
embeds: [ embeds: [
this.discordMessageService.buildErrorMessage({ this.discordMessageService.buildErrorMessage({
title: 'There is no next track', title: 'There is no next track',
}), }),
], ],
}; });
} }
return { await interaction.reply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Skipped to the next track', title: 'Skipped to the next track',
}), }),
], ],
}; });
} }
} }

View File

@ -16,18 +16,15 @@ export class PausePlaybackCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService, private readonly discordMessageService: DiscordMessageService,
) {} ) {}
handler( async handler(interaction: CommandInteraction): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
commandInteraction: CommandInteraction,
): string | InteractionReplyOptions {
const shouldBePaused = this.discordVoiceService.togglePaused(); const shouldBePaused = this.discordVoiceService.togglePaused();
return { await interaction.reply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: shouldBePaused ? 'Paused' : 'Unpaused', title: shouldBePaused ? 'Paused' : 'Unpaused',
}), }),
], ],
}; });
} }
} }

View File

@ -21,6 +21,7 @@ import { TrackRequestDto } from '../models/track-request.dto';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { RemoteImageResult } from '@jellyfin/sdk/lib/generated-client/models';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service'; import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service'; import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service';
import { import {
@ -28,7 +29,6 @@ import {
searchResultAsJellyfinAudio, searchResultAsJellyfinAudio,
} from '../models/jellyfinAudioItems'; } from '../models/jellyfinAudioItems';
import { PlaybackService } from '../playback/playback.service'; import { PlaybackService } from '../playback/playback.service';
import { RemoteImageResult } from '@jellyfin/sdk/lib/generated-client/models';
import { chooseSuitableRemoteImage } from '../utils/remoteImages/remoteImages'; import { chooseSuitableRemoteImage } from '../utils/remoteImages/remoteImages';
import { trimStringToFixedLength } from '../utils/stringUtils/stringUtils'; import { trimStringToFixedLength } from '../utils/stringUtils/stringUtils';
@ -52,9 +52,10 @@ export class PlayItemCommand
async handler( async handler(
@Payload() dto: TrackRequestDto, @Payload() dto: TrackRequestDto,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
executionContext: TransformedCommandExecutionContext<any>, executionContext: TransformedCommandExecutionContext<any>,
): Promise<InteractionReplyOptions | string> { ): Promise<InteractionReplyOptions | string> {
await executionContext.interaction.deferReply();
const items = await this.jellyfinSearchService.search(dto.search); const items = await this.jellyfinSearchService.search(dto.search);
const parsedItems = await Promise.all( const parsedItems = await Promise.all(
items.map( items.map(
@ -68,14 +69,15 @@ export class PlayItemCommand
); );
if (parsedItems.length === 0) { if (parsedItems.length === 0) {
return { await executionContext.interaction.followUp({
embeds: [ embeds: [
this.discordMessageService.buildErrorMessage({ this.discordMessageService.buildErrorMessage({
title: 'No results for your search query found', title: 'No results for your search query found',
description: `I was not able to find any matches for your query \`\`${dto.search}\`\`. Please check that I have access to the desired libraries and that your query is not misspelled`, description: `I was not able to find any matches for your query \`\`${dto.search}\`\`. Please check that I have access to the desired libraries and that your query is not misspelled`,
}), }),
], ],
}; });
return;
} }
const firstItems = parsedItems.slice(0, 10); const firstItems = parsedItems.slice(0, 10);
@ -107,7 +109,7 @@ export class PlayItemCommand
emoji: item.getEmoji(), emoji: item.getEmoji(),
})); }));
return { await executionContext.interaction.followUp({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Jellyfin Search Results', title: 'Jellyfin Search Results',
@ -126,7 +128,7 @@ export class PlayItemCommand
], ],
}, },
], ],
}; });
} }
@On(Events.InteractionCreate) @On(Events.InteractionCreate)
@ -144,6 +146,18 @@ export class PlayItemCommand
return; return;
} }
await interaction.deferUpdate();
await interaction.editReply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Applying your selection to the queue...',
description: `This may take a moment. Please wait`,
}),
],
components: [],
});
const guildMember = interaction.member as GuildMember; const guildMember = interaction.member as GuildMember;
const tryResult = const tryResult =
@ -156,7 +170,7 @@ export class PlayItemCommand
`Unable to process select result because the member was not in a voice channcel`, `Unable to process select result because the member was not in a voice channcel`,
); );
const replyOptions = tryResult.reply as InteractionReplyOptions; const replyOptions = tryResult.reply as InteractionReplyOptions;
await interaction.update({ await interaction.editReply({
embeds: replyOptions.embeds, embeds: replyOptions.embeds,
content: undefined, content: undefined,
components: [], components: [],
@ -183,7 +197,7 @@ export class PlayItemCommand
bitrate, bitrate,
remoteImagesOfCurrentAlbum, remoteImagesOfCurrentAlbum,
); );
interaction.update({ await interaction.editReply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: item.Name, title: item.Name,
@ -212,7 +226,7 @@ export class PlayItemCommand
remoteImages, remoteImages,
); );
}); });
interaction.update({ await interaction.editReply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: `Added ${album.TotalRecordCount} items from your album`, title: `Added ${album.TotalRecordCount} items from your album`,
@ -247,7 +261,7 @@ export class PlayItemCommand
} }
const bestPlaylistRemoteImage = const bestPlaylistRemoteImage =
chooseSuitableRemoteImage(addedRemoteImages); chooseSuitableRemoteImage(addedRemoteImages);
interaction.update({ await interaction.editReply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: `Added ${playlist.TotalRecordCount} items from your playlist`, title: `Added ${playlist.TotalRecordCount} items from your playlist`,
@ -267,7 +281,7 @@ export class PlayItemCommand
}); });
break; break;
default: default:
interaction.update({ await interaction.editReply({
embeds: [ embeds: [
this.discordMessageService.buildErrorMessage({ this.discordMessageService.buildErrorMessage({
title: 'Unable to process your selection', title: 'Unable to process your selection',

View File

@ -3,7 +3,6 @@ import { TransformPipe } from '@discord-nestjs/common';
import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core'; import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction } from 'discord.js'; import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { GenericCustomReply } from '../models/generic-try-handler';
import { PlaybackService } from '../playback/playback.service'; import { PlaybackService } from '../playback/playback.service';
import { Constants } from '../utils/constants'; import { Constants } from '../utils/constants';
import { chooseSuitableRemoteImageFromTrack } from '../utils/remoteImages/remoteImages'; import { chooseSuitableRemoteImageFromTrack } from '../utils/remoteImages/remoteImages';
@ -21,12 +20,11 @@ export class PlaylistCommand implements DiscordCommand {
private readonly playbackService: PlaybackService, private readonly playbackService: PlaybackService,
) {} ) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars async handler(interaction: CommandInteraction): Promise<void> {
handler(interaction: CommandInteraction): GenericCustomReply {
const playList = this.playbackService.getPlaylist(); const playList = this.playbackService.getPlaylist();
if (playList.tracks.length === 0) { if (playList.tracks.length === 0) {
return { await interaction.reply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Your Playlist', title: 'Your Playlist',
@ -34,7 +32,7 @@ export class PlaylistCommand implements DiscordCommand {
'You do not have any tracks in your playlist.\nUse the play command to add new tracks to your playlist', 'You do not have any tracks in your playlist.\nUse the play command to add new tracks to your playlist',
}), }),
], ],
}; });
} }
const tracklist = playList.tracks const tracklist = playList.tracks
@ -63,7 +61,7 @@ export class PlaylistCommand implements DiscordCommand {
const activeTrack = this.playbackService.getActiveTrack(); const activeTrack = this.playbackService.getActiveTrack();
const remoteImage = chooseSuitableRemoteImageFromTrack(activeTrack.track); const remoteImage = chooseSuitableRemoteImageFromTrack(activeTrack.track);
return { await interaction.reply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Your Playlist', title: 'Your Playlist',
@ -77,7 +75,7 @@ export class PlaylistCommand implements DiscordCommand {
}, },
}), }),
], ],
}; });
} }
private getListPoint(isCurrent: boolean, index: number) { private getListPoint(isCurrent: boolean, index: number) {

View File

@ -1,7 +1,7 @@
import { TransformPipe } from '@discord-nestjs/common'; import { TransformPipe } from '@discord-nestjs/common';
import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core'; import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction, InteractionReplyOptions } from 'discord.js'; import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { PlaybackService } from '../playback/playback.service'; import { PlaybackService } from '../playback/playback.service';
@ -16,26 +16,23 @@ export class PreviousTrackCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService, private readonly discordMessageService: DiscordMessageService,
) {} ) {}
handler( async handler(interaction: CommandInteraction): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
dcommandInteraction: CommandInteraction,
): InteractionReplyOptions | string {
if (!this.playbackService.previousTrack()) { if (!this.playbackService.previousTrack()) {
return { await interaction.reply({
embeds: [ embeds: [
this.discordMessageService.buildErrorMessage({ this.discordMessageService.buildErrorMessage({
title: 'There is no previous track', title: 'There is no previous track',
}), }),
], ],
}; });
} }
return { await interaction.reply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Went to previous track', title: 'Went to previous track',
}), }),
], ],
}; });
} }
} }

View File

@ -6,12 +6,7 @@ import {
InjectDiscordClient, InjectDiscordClient,
UsePipes, UsePipes,
} from '@discord-nestjs/core'; } from '@discord-nestjs/core';
import { import { Client, CommandInteraction, Status } from 'discord.js';
Client,
CommandInteraction,
InteractionReplyOptions,
Status,
} from 'discord.js';
import { formatDuration, intervalToDuration } from 'date-fns'; import { formatDuration, intervalToDuration } from 'date-fns';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
@ -33,10 +28,15 @@ export class StatusCommand implements DiscordCommand {
private readonly jellyfinService: JellyfinService, private readonly jellyfinService: JellyfinService,
) {} ) {}
async handler( async handler(interaction: CommandInteraction): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars await interaction.reply({
commandInteraction: CommandInteraction, embeds: [
): Promise<string | InteractionReplyOptions> { this.discordMessageService.buildMessage({
title: 'Retrieving status information...',
}),
],
});
const ping = this.client.ws.ping; const ping = this.client.ws.ping;
const status = Status[this.client.ws.status]; const status = Status[this.client.ws.status];
@ -49,7 +49,7 @@ export class StatusCommand implements DiscordCommand {
const jellyfinSystemApi = getSystemApi(this.jellyfinService.getApi()); const jellyfinSystemApi = getSystemApi(this.jellyfinService.getApi());
const jellyfinSystemInformation = await jellyfinSystemApi.getSystemInfo(); const jellyfinSystemInformation = await jellyfinSystemApi.getSystemInfo();
return { await interaction.editReply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Discord Bot Status', title: 'Discord Bot Status',
@ -90,6 +90,6 @@ export class StatusCommand implements DiscordCommand {
}, },
}), }),
], ],
}; });
} }
} }

View File

@ -4,7 +4,6 @@ import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction } from 'discord.js'; import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service'; import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { GenericCustomReply } from '../models/generic-try-handler';
import { PlaybackService } from '../playback/playback.service'; import { PlaybackService } from '../playback/playback.service';
@Command({ @Command({
@ -19,22 +18,28 @@ export class StopPlaybackCommand implements DiscordCommand {
private readonly discordVoiceService: DiscordVoiceService, private readonly discordVoiceService: DiscordVoiceService,
) {} ) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
handler(CommandInteraction: CommandInteraction): GenericCustomReply { async handler(interaction: CommandInteraction): Promise<void> {
const hasActiveTrack = this.playbackService.hasActiveTrack() const hasActiveTrack = this.playbackService.hasActiveTrack();
const title = hasActiveTrack ? 'Playback stopped successfully' : 'Playback failed to stop' const title = hasActiveTrack
const description = hasActiveTrack ? 'In addition, your playlist has been cleared' : 'There is no active track in the queue' ? 'Playback stopped successfully'
: 'Playback failed to stop';
const description = hasActiveTrack
? 'In addition, your playlist has been cleared'
: 'There is no active track in the queue';
if (hasActiveTrack) { if (hasActiveTrack) {
this.playbackService.clear(); this.playbackService.clear();
this.discordVoiceService.stop(false); this.discordVoiceService.stop(false);
} }
return { await interaction.reply({
embeds: [ embeds: [
this.discordMessageService[hasActiveTrack ? 'buildMessage' : 'buildErrorMessage']({ this.discordMessageService[
hasActiveTrack ? 'buildMessage' : 'buildErrorMessage'
]({
title: title, title: title,
description: description, description: description,
}), }),
], ],
}; });
} }
} }

View File

@ -5,7 +5,6 @@ import { Logger } from '@nestjs/common';
import { CommandInteraction, GuildMember } from 'discord.js'; import { CommandInteraction, GuildMember } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service'; import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { GenericCustomReply } from '../models/generic-try-handler';
@Command({ @Command({
name: 'summon', name: 'summon',
@ -20,7 +19,15 @@ export class SummonCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService, private readonly discordMessageService: DiscordMessageService,
) {} ) {}
handler(interaction: CommandInteraction): GenericCustomReply { async handler(interaction: CommandInteraction): Promise<void> {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Joining your voice channel...',
}),
],
});
const guildMember = interaction.member as GuildMember; const guildMember = interaction.member as GuildMember;
const tryResult = const tryResult =
@ -29,10 +36,11 @@ export class SummonCommand implements DiscordCommand {
); );
if (!tryResult.success) { if (!tryResult.success) {
return tryResult.reply; interaction.editReply(tryResult.reply);
return;
} }
return { await interaction.editReply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Joined your voicehannel', title: 'Joined your voicehannel',
@ -40,6 +48,6 @@ export class SummonCommand implements DiscordCommand {
"I'm ready to play media. Use ``Cast to device`` in Jellyfin or the ``/play`` command to get started.", "I'm ready to play media. Use ``Cast to device`` in Jellyfin or the ``/play`` command to get started.",
}), }),
], ],
}; });
} }
} }

View File

@ -1,11 +1,6 @@
import { InteractionReplyOptions } from 'discord.js'; import { InteractionEditReplyOptions, MessagePayload } from 'discord.js';
export interface GenericTryHandler { export interface GenericTryHandler {
success: boolean; success: boolean;
reply: GenericCustomReply; reply: string | MessagePayload | InteractionEditReplyOptions;
} }
export type GenericCustomReply =
| string
| InteractionReplyOptions
| Promise<string | InteractionReplyOptions>;