Skip to content
This repository has been archived by the owner on Sep 3, 2024. It is now read-only.

Commit

Permalink
feat: Add new /history command to show previously played tracks
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusbegby committed Dec 11, 2023
1 parent 902f890 commit bf461c2
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 2 deletions.
7 changes: 5 additions & 2 deletions src/classes/interactions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import config from 'config';
import { GuildQueue, PlayerTimestamp, Track } from 'discord-player';
import { GuildQueue, GuildQueueHistory, PlayerTimestamp, Track } from 'discord-player';
import {
ApplicationCommandOptionChoiceData,
AutocompleteInteraction,
Expand Down Expand Up @@ -114,7 +114,10 @@ abstract class BaseInteraction {
return thumbnailUrl;
}

protected getFooterDisplayPageInfo(interaction: ChatInputCommandInteraction, queue: GuildQueue): EmbedFooterData {
protected getFooterDisplayPageInfo(
interaction: ChatInputCommandInteraction,
queue: GuildQueue | GuildQueueHistory
): EmbedFooterData {
if (!queue.tracks.data.length) {
return { text: 'Page 1 of 1 (0 tracks)' };
}
Expand Down
172 changes: 172 additions & 0 deletions src/interactions/commands/player/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { GuildQueue, GuildQueueHistory, Track, useHistory, useQueue } from 'discord-player';
import { ChatInputCommandInteraction, EmbedBuilder, EmbedFooterData, SlashCommandBuilder } from 'discord.js';
import { Logger } from 'pino';
import { BaseSlashCommandInteraction } from '../../../classes/interactions';
import { BaseSlashCommandParams, BaseSlashCommandReturnType } from '../../../types/interactionTypes';
import { checkHistoryExists } from '../../../utils/validation/queueValidator';
import { checkInVoiceChannel, checkSameVoiceChannel } from '../../../utils/validation/voiceChannelValidator';

class HistoryCommand extends BaseSlashCommandInteraction {
constructor() {
const data = new SlashCommandBuilder()
.setName('history')
.setDescription('Show history of tracks that have been played.')
.addNumberOption((option) =>
option.setName('page').setDescription('Page number to display for the history').setMinValue(1)
);
super(data);
}

async execute(params: BaseSlashCommandParams): BaseSlashCommandReturnType {
const { executionId, interaction } = params;
const logger = this.getLogger(this.name, executionId, interaction);

const history: GuildQueueHistory = useHistory(interaction.guild!.id)!;
const queue: GuildQueue = useQueue(interaction.guild!.id)!;

await this.runValidators({ interaction, history, executionId }, [
checkInVoiceChannel,
checkSameVoiceChannel,
checkHistoryExists
]);

const pageIndex: number = this.getPageIndex(interaction);
const totalPages: number = this.getTotalPages(history);

if (pageIndex > totalPages - 1) {
return await this.handleInvalidPage(logger, interaction, pageIndex, totalPages);
}

const historyTracksListString: string = this.getHistoryTracksListString(history, pageIndex);

const currentTrack: Track = history.currentTrack!;
if (currentTrack) {
return await this.handleCurrentTrack(
logger,
interaction,
queue,
history,
currentTrack,
historyTracksListString
);
}

return await this.handleNoCurrentTrack(logger, interaction, queue, history, historyTracksListString);
}

private async handleCurrentTrack(
logger: Logger,
interaction: ChatInputCommandInteraction,
queue: GuildQueue,
history: GuildQueueHistory,
currentTrack: Track,
historyTracksListString: string
) {
logger.debug('History exists with current track, gathering information.');

logger.debug('Responding with info embed.');
await interaction.editReply({
embeds: [
new EmbedBuilder()
.setAuthor(this.getEmbedQueueAuthor(interaction, queue))
.setDescription(
`**${this.embedOptions.icons.audioPlaying} Now playing**\n` +
`${this.getFormattedTrackUrl(currentTrack)}\n` +
`**Requested by:** ${this.getDisplayTrackRequestedBy(currentTrack)}\n` +
`${this.getDisplayQueueProgressBar(queue)}\n\n` +
`${this.getDisplayRepeatMode(queue.repeatMode)}` +
`**${this.embedOptions.icons.queue} Tracks in history**\n` +
historyTracksListString
)
.setThumbnail(this.getTrackThumbnailUrl(currentTrack))
.setFooter(this.getDisplayFullFooterInfo(interaction, history))
.setColor(this.embedOptions.colors.info)
]
});
return Promise.resolve();
}

private async handleNoCurrentTrack(
logger: Logger,
interaction: ChatInputCommandInteraction,
queue: GuildQueue,
history: GuildQueueHistory,
historyTracksListString: string
) {
logger.debug('History exists but there is no current track.');

logger.debug('Responding with info embed.');
return await interaction.editReply({
embeds: [
new EmbedBuilder()
.setAuthor(this.getEmbedQueueAuthor(interaction, queue))
.setDescription(
`${this.getDisplayRepeatMode(queue.repeatMode)}` +
`**${this.embedOptions.icons.queue} Tracks in history**\n` +
historyTracksListString
)
.setFooter(this.getDisplayFullFooterInfo(interaction, history))
.setColor(this.embedOptions.colors.info)
]
});
}

private async handleInvalidPage(
logger: Logger,
interaction: ChatInputCommandInteraction,
pageIndex: number,
totalPages: number
) {
logger.debug('Specified page was higher than total pages.');

logger.debug('Responding with warning embed.');
await interaction.editReply({
embeds: [
new EmbedBuilder()
.setDescription(
`**${this.embedOptions.icons.warning} Oops!**\n` +
`Page **\`${pageIndex + 1}\`** is not a valid page number.\n\n` +
`There are only a total of **\`${totalPages}\`** pages in the history.`
)
.setColor(this.embedOptions.colors.warning)
]
});
return Promise.resolve();
}

private getPageIndex(interaction: ChatInputCommandInteraction): number {
return (interaction.options.getNumber('page') || 1) - 1;
}

private getTotalPages(history: GuildQueueHistory): number {
return Math.ceil(history.tracks.data.length / 10) || 1;
}

private getHistoryTracksListString(history: GuildQueueHistory, pageIndex: number): string {
if (!history || history.tracks.data.length === 0) {
return 'The history is empty, add some tracks with **`/play`**!';
}

return history.tracks.data
.slice(pageIndex * 10, pageIndex * 10 + 10)
.map((track, index) => {
return `**${pageIndex * 10 + index + 1}.** ${this.getDisplayTrackDurationAndUrl(track)}`;
})
.join('\n');
}

private getDisplayFullFooterInfo(
interaction: ChatInputCommandInteraction,
history: GuildQueueHistory
): EmbedFooterData {
const pagination = this.getFooterDisplayPageInfo(interaction, history);

const fullFooterData = {
text: `${pagination.text}`
};

return fullFooterData;
}
}

export default new HistoryCommand();
1 change: 1 addition & 0 deletions src/types/utilTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type Validator = (args: ValidatorParams) => Promise<void>;
export type ValidatorParams = {
interaction: ChatInputCommandInteraction | MessageComponentInteraction;
queue?: GuildQueue;
history?: GuildQueueHistory;
executionId: string;
};

Expand Down
37 changes: 37 additions & 0 deletions src/utils/validation/queueValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,43 @@ export const checkQueueExists = async ({ interaction, queue, executionId }: Vali
return;
};

export const checkHistoryExists = async ({ interaction, history, executionId }: ValidatorParams) => {
const logger: Logger = loggerModule.child({
module: 'utilValidation',
name: 'historyDoesNotExist',
executionId: executionId,
shardId: interaction.guild?.shardId,
guildId: interaction.guild?.id
});

const interactionIdentifier =
interaction.type === InteractionType.ApplicationCommand ? interaction.commandName : interaction.customId;

if (!history) {
await interaction.editReply({
embeds: [
new EmbedBuilder()
.setDescription(
`**${embedOptions.icons.warning} Oops!**\nThere are no tracks in the history and nothing currently playing. First add some tracks with **\`/play\`**!`
)
.setColor(embedOptions.colors.warning)
.setFooter({
text:
interaction.member instanceof GuildMember
? interaction.member.nickname || interaction.user.username
: interaction.user.username,
iconURL: interaction.user.avatarURL() || embedOptions.info.fallbackIconUrl
})
]
});

logger.debug(`User tried to use command '${interactionIdentifier}' but there was no history.`);
throw new InteractionValidationError('History does not exist.');
}

return;
};

export const checkQueueCurrentTrack = async ({ interaction, queue, executionId }: ValidatorParams) => {
const logger: Logger = loggerModule.child({
module: 'utilValidation',
Expand Down

0 comments on commit bf461c2

Please # to comment.