♻️ 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 { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { GenericCustomReply } from '../models/generic-try-handler';
@Command({
name: 'disconnect',
@ -17,20 +16,28 @@ export class DisconnectCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService,
) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
handler(interaction: CommandInteraction): GenericCustomReply {
async handler(interaction: CommandInteraction): Promise<void> {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Disconnecting...',
}),
],
});
const disconnect = this.discordVoiceService.disconnect();
if (!disconnect.success) {
return disconnect.reply;
await interaction.editReply(disconnect.reply);
return;
}
return {
await interaction.editReply({
embeds: [
this.discordMessageService.buildMessage({
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 { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { GenericCustomReply } from '../models/generic-try-handler';
@Command({
name: 'help',
@ -13,9 +12,8 @@ import { GenericCustomReply } from '../models/generic-try-handler';
export class HelpCommand implements DiscordCommand {
constructor(private readonly discordMessageService: DiscordMessageService) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
handler(commandInteraction: CommandInteraction): GenericCustomReply {
return {
async handler(interaction: CommandInteraction): Promise<void> {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
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 { 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 { PlaybackService } from '../playback/playback.service';
@ -16,26 +16,23 @@ export class SkipTrackCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService,
) {}
handler(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interactionCommand: CommandInteraction,
): InteractionReplyOptions | string {
async handler(interaction: CommandInteraction): Promise<void> {
if (!this.playbackService.nextTrack()) {
return {
await interaction.reply({
embeds: [
this.discordMessageService.buildErrorMessage({
title: 'There is no next track',
}),
],
};
});
}
return {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Skipped to the next track',
}),
],
};
});
}
}

View File

@ -16,18 +16,15 @@ export class PausePlaybackCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService,
) {}
handler(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
commandInteraction: CommandInteraction,
): string | InteractionReplyOptions {
async handler(interaction: CommandInteraction): Promise<void> {
const shouldBePaused = this.discordVoiceService.togglePaused();
return {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
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 { RemoteImageResult } from '@jellyfin/sdk/lib/generated-client/models';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service';
import {
@ -28,7 +29,6 @@ import {
searchResultAsJellyfinAudio,
} from '../models/jellyfinAudioItems';
import { PlaybackService } from '../playback/playback.service';
import { RemoteImageResult } from '@jellyfin/sdk/lib/generated-client/models';
import { chooseSuitableRemoteImage } from '../utils/remoteImages/remoteImages';
import { trimStringToFixedLength } from '../utils/stringUtils/stringUtils';
@ -52,9 +52,10 @@ export class PlayItemCommand
async handler(
@Payload() dto: TrackRequestDto,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
executionContext: TransformedCommandExecutionContext<any>,
): Promise<InteractionReplyOptions | string> {
await executionContext.interaction.deferReply();
const items = await this.jellyfinSearchService.search(dto.search);
const parsedItems = await Promise.all(
items.map(
@ -68,14 +69,15 @@ export class PlayItemCommand
);
if (parsedItems.length === 0) {
return {
await executionContext.interaction.followUp({
embeds: [
this.discordMessageService.buildErrorMessage({
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`,
}),
],
};
});
return;
}
const firstItems = parsedItems.slice(0, 10);
@ -107,7 +109,7 @@ export class PlayItemCommand
emoji: item.getEmoji(),
}));
return {
await executionContext.interaction.followUp({
embeds: [
this.discordMessageService.buildMessage({
title: 'Jellyfin Search Results',
@ -126,7 +128,7 @@ export class PlayItemCommand
],
},
],
};
});
}
@On(Events.InteractionCreate)
@ -144,6 +146,18 @@ export class PlayItemCommand
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 tryResult =
@ -156,7 +170,7 @@ export class PlayItemCommand
`Unable to process select result because the member was not in a voice channcel`,
);
const replyOptions = tryResult.reply as InteractionReplyOptions;
await interaction.update({
await interaction.editReply({
embeds: replyOptions.embeds,
content: undefined,
components: [],
@ -183,7 +197,7 @@ export class PlayItemCommand
bitrate,
remoteImagesOfCurrentAlbum,
);
interaction.update({
await interaction.editReply({
embeds: [
this.discordMessageService.buildMessage({
title: item.Name,
@ -212,7 +226,7 @@ export class PlayItemCommand
remoteImages,
);
});
interaction.update({
await interaction.editReply({
embeds: [
this.discordMessageService.buildMessage({
title: `Added ${album.TotalRecordCount} items from your album`,
@ -247,7 +261,7 @@ export class PlayItemCommand
}
const bestPlaylistRemoteImage =
chooseSuitableRemoteImage(addedRemoteImages);
interaction.update({
await interaction.editReply({
embeds: [
this.discordMessageService.buildMessage({
title: `Added ${playlist.TotalRecordCount} items from your playlist`,
@ -267,7 +281,7 @@ export class PlayItemCommand
});
break;
default:
interaction.update({
await interaction.editReply({
embeds: [
this.discordMessageService.buildErrorMessage({
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 { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { GenericCustomReply } from '../models/generic-try-handler';
import { PlaybackService } from '../playback/playback.service';
import { Constants } from '../utils/constants';
import { chooseSuitableRemoteImageFromTrack } from '../utils/remoteImages/remoteImages';
@ -21,12 +20,11 @@ export class PlaylistCommand implements DiscordCommand {
private readonly playbackService: PlaybackService,
) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
handler(interaction: CommandInteraction): GenericCustomReply {
async handler(interaction: CommandInteraction): Promise<void> {
const playList = this.playbackService.getPlaylist();
if (playList.tracks.length === 0) {
return {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
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',
}),
],
};
});
}
const tracklist = playList.tracks
@ -63,7 +61,7 @@ export class PlaylistCommand implements DiscordCommand {
const activeTrack = this.playbackService.getActiveTrack();
const remoteImage = chooseSuitableRemoteImageFromTrack(activeTrack.track);
return {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Your Playlist',
@ -77,7 +75,7 @@ export class PlaylistCommand implements DiscordCommand {
},
}),
],
};
});
}
private getListPoint(isCurrent: boolean, index: number) {

View File

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

View File

@ -6,12 +6,7 @@ import {
InjectDiscordClient,
UsePipes,
} from '@discord-nestjs/core';
import {
Client,
CommandInteraction,
InteractionReplyOptions,
Status,
} from 'discord.js';
import { Client, CommandInteraction, Status } from 'discord.js';
import { formatDuration, intervalToDuration } from 'date-fns';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
@ -33,10 +28,15 @@ export class StatusCommand implements DiscordCommand {
private readonly jellyfinService: JellyfinService,
) {}
async handler(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
commandInteraction: CommandInteraction,
): Promise<string | InteractionReplyOptions> {
async handler(interaction: CommandInteraction): Promise<void> {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Retrieving status information...',
}),
],
});
const ping = this.client.ws.ping;
const status = Status[this.client.ws.status];
@ -49,7 +49,7 @@ export class StatusCommand implements DiscordCommand {
const jellyfinSystemApi = getSystemApi(this.jellyfinService.getApi());
const jellyfinSystemInformation = await jellyfinSystemApi.getSystemInfo();
return {
await interaction.editReply({
embeds: [
this.discordMessageService.buildMessage({
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 { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { GenericCustomReply } from '../models/generic-try-handler';
import { PlaybackService } from '../playback/playback.service';
@Command({
@ -19,22 +18,28 @@ export class StopPlaybackCommand implements DiscordCommand {
private readonly discordVoiceService: DiscordVoiceService,
) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
handler(CommandInteraction: CommandInteraction): GenericCustomReply {
const hasActiveTrack = this.playbackService.hasActiveTrack()
const title = hasActiveTrack ? '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'
async handler(interaction: CommandInteraction): Promise<void> {
const hasActiveTrack = this.playbackService.hasActiveTrack();
const title = hasActiveTrack
? '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) {
this.playbackService.clear();
this.discordVoiceService.stop(false);
}
return {
await interaction.reply({
embeds: [
this.discordMessageService[hasActiveTrack ? 'buildMessage' : 'buildErrorMessage']({
this.discordMessageService[
hasActiveTrack ? 'buildMessage' : 'buildErrorMessage'
]({
title: title,
description: description,
}),
],
};
});
}
}

View File

@ -5,7 +5,6 @@ import { Logger } from '@nestjs/common';
import { CommandInteraction, GuildMember } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { GenericCustomReply } from '../models/generic-try-handler';
@Command({
name: 'summon',
@ -20,7 +19,15 @@ export class SummonCommand implements DiscordCommand {
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 tryResult =
@ -29,10 +36,11 @@ export class SummonCommand implements DiscordCommand {
);
if (!tryResult.success) {
return tryResult.reply;
interaction.editReply(tryResult.reply);
return;
}
return {
await interaction.editReply({
embeds: [
this.discordMessageService.buildMessage({
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.",
}),
],
};
});
}
}

View File

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