From f23fd4cf2dd86e837714fea41b288cfd0d1ff586 Mon Sep 17 00:00:00 2001 From: Jared Date: Tue, 12 Aug 2025 20:43:05 -0500 Subject: [PATCH 1/6] getting main random command finished --- src/commands/chat-commands/random-song.ts | 45 ++++++++++++- src/events/interactionCreate.ts | 80 +++++++++++++++++++++++ src/helpers/open-ai.ts | 22 +++++-- src/helpers/youtube.ts | 60 ++++++++++++++++- 4 files changed, 200 insertions(+), 7 deletions(-) diff --git a/src/commands/chat-commands/random-song.ts b/src/commands/chat-commands/random-song.ts index f46f3b8..c954961 100644 --- a/src/commands/chat-commands/random-song.ts +++ b/src/commands/chat-commands/random-song.ts @@ -1,4 +1,11 @@ -import { SlashCommandBuilder, ChatInputCommandInteraction } from "discord.js"; +import { + SlashCommandBuilder, + ChatInputCommandInteraction, + ButtonBuilder, + ButtonStyle, + ActionRowBuilder, + MessageActionRowComponentBuilder, +} from "discord.js"; import type { SlashCommand } from "../../@types/commands.js"; import { logger } from "../../helpers/logger.js"; import { getSongByGenre } from "../../helpers/open-ai.js"; @@ -19,6 +26,35 @@ async function execute( const playResult = await playSongFromYoutube(interaction, pickedSong); await interaction.editReply(playResult); + + const play = new ButtonBuilder() + .setCustomId("play") + .setLabel("Play") + .setStyle(ButtonStyle.Primary) + .setDisabled(true); + + const pause = new ButtonBuilder() + .setCustomId("pause") + .setLabel("Pause") + .setStyle(ButtonStyle.Secondary) + .setDisabled(false); + + const remove = new ButtonBuilder() + .setCustomId("remove") + .setLabel("Remove") + .setStyle(ButtonStyle.Danger) + .setDisabled(false); + + const row = + new ActionRowBuilder().addComponents( + remove, + pause, + play, + ); + await interaction.editReply({ + content: playResult, + components: [row], + }); } catch (err) { logger.error(`Error in execute function for command [RANDOM]: ${err}`); await interaction.editReply({ @@ -48,6 +84,13 @@ export const command: SlashCommand = { { name: "Pop", value: "Pop" }, { name: "Indie Pop", value: "Indie Pop" }, { name: "Early 2000s Country", value: "Early 2000s Country" }, + { name: "Metal", value: "Metal" }, + { name: "Synthwave", value: "Synthwave" }, + { name: "Reggaeton", value: "Reggaeton" }, + { name: "Alternative Rock", value: "Alternative Rock" }, + { name: "Funk", value: "Funk" }, + { name: "Lo-fi Hip Hop", value: "Lo-fi Hip Hop" }, + { name: "Classic Soul", value: "Classic Soul" }, ), ), execute, diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 1e21459..3e6e6ac 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,6 +1,11 @@ import { Events, MessageFlags, Interaction, CacheType } from "discord.js"; import { logger } from "../helpers/logger.js"; import { commandOnCooldown } from "../helpers/commands.js"; +import { + pausePlayback, + resumePlayback, + removePlayback, +} from "../helpers/youtube.js"; const interactionCreateEvent = { name: Events.InteractionCreate, @@ -28,6 +33,81 @@ const interactionCreateEvent = { } } if (command.kind === "chat") await command.execute(interaction); + } else if (interaction.isButton()) { + if (!interaction.guildId) return; + if (interaction.customId === "pause") { + const msg = pausePlayback(interaction.guildId); + await interaction.update({ + content: msg, + components: [ + { + type: 1, + components: [ + { + type: 2, + custom_id: "remove", + label: "Remove", + style: 4, + disabled: false, + }, + { + type: 2, + custom_id: "pause", + label: "Pause", + style: 2, + disabled: true, + }, + { + type: 2, + custom_id: "play", + label: "Play", + style: 1, + disabled: false, + }, + ], + }, + ], + }); + } else if (interaction.customId === "play") { + const msg = resumePlayback(interaction.guildId); + await interaction.update({ + content: msg, + components: [ + { + type: 1, + components: [ + { + type: 2, + custom_id: "remove", + label: "Remove", + style: 4, + disabled: false, + }, + { + type: 2, + custom_id: "pause", + label: "Pause", + style: 2, + disabled: false, + }, + { + type: 2, + custom_id: "play", + label: "Play", + style: 1, + disabled: true, + }, + ], + }, + ], + }); + } else if (interaction.customId === "remove") { + const msg = removePlayback(interaction.guildId); + await interaction.update({ + content: msg, + components: [], + }); + } } } catch (err) { logger.error(`Error executing command for Amplify: ${err}`); diff --git a/src/helpers/open-ai.ts b/src/helpers/open-ai.ts index d9dbfa3..15dbd7c 100644 --- a/src/helpers/open-ai.ts +++ b/src/helpers/open-ai.ts @@ -2,6 +2,7 @@ import { OpenAI } from "openai"; import { SongPick } from "../@types/open-ai.js"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); +const exclusions: SongPick[] = []; export async function getSongByGenre(genre: string): Promise { const schema = { @@ -20,7 +21,7 @@ export async function getSongByGenre(genre: string): Promise { additionalProperties: false, }, minItems: 1, - maxItems: 3, + maxItems: 15, }, }, required: ["picks"], @@ -32,12 +33,25 @@ export async function getSongByGenre(genre: string): Promise { messages: [ { role: "system", - content: - "You recommend well-known, real songs. Only return songs that exist. Prefer tracks with clear artist/title matches on YouTube.", + content: `You are a meticulous music selector. + Rules: + - Return exactly 15 unique songs under "picks". + - Prefer releases from the last 5 years UNLESS the genre name itself encodes a period (e.g., "80s synthpop", "1994 hip-hop"). + - Avoid any song whose {title, artist} appears in the provided exclusions. + - Mix well-known and lesser-known picks; avoid repeating the same artist >1 time unless the genre is extremely narrow. + - Validate years realistically for the artist/title. + - No commentary outside JSON. + `, }, { role: "user", - content: `Give me 3 amazing songs from the genre ${genre} as JSON under 'picks'. Try to stay in songs that were released in the last 5 years unless the genre specifically has a year attached to it.`, + content: `Select songs for genre: "${genre}. Avoid any in the exlcusions provided next.`, + }, + { + role: "user", + content: `Exclusions (JSON): ${JSON.stringify({ + exclusions, + })}`, }, ], response_format: { diff --git a/src/helpers/youtube.ts b/src/helpers/youtube.ts index 806970d..090f539 100644 --- a/src/helpers/youtube.ts +++ b/src/helpers/youtube.ts @@ -11,19 +11,74 @@ import { AudioPlayerStatus, entersState, VoiceConnectionStatus, + VoiceConnection, demuxProbe, + AudioPlayer, } from "@discordjs/voice"; import { SongPick } from "../@types/open-ai.js"; import { logger } from "./logger.js"; import ytdl from "@distube/ytdl-core"; import yts from "yt-search"; +interface PlaybackSession { + player: AudioPlayer; + guildId: string; + connection: VoiceConnection; +} + +const sessions = new Map(); + async function findYoutubeUrl(query: string): Promise { const res = await yts(query + " official audio"); const v = res.videos.find((v) => v.seconds > 30); return v?.url ?? null; } +export function pausePlayback(guildId: string): string { + const s = sessions.get(guildId); + if (!s) return "Nothing is currently playing"; + if (s.player.state.status !== AudioPlayerStatus.Playing) + return "Not currently playing."; + const ok = s.player.pause(true); + return ok ? "Paused." : "Failed to pause."; +} + +export function resumePlayback(guildId: string): string { + const s = sessions.get(guildId); + if (!s) return "Nothing is queued."; + if (s.player.state.status !== AudioPlayerStatus.Paused) return "Not paused."; + const ok = s.player.unpause(); + return ok ? "Resumed." : "Failed to resume."; +} + +export function removePlayback(guildId: string) { + const s = sessions.get(guildId); + if (!s) return "Nothing to remove."; + try { + s.player.stop(true); + } catch (err) { + logger.error(`Error in playback actions: ${err}.`); + } + try { + s.connection.destroy(); + } catch (err) { + logger.error(`Error in playback actions: ${err}.`); + } + sessions.delete(guildId); + return "Playback removed and disconnected."; +} + +function endSession(guildId: string) { + const s = sessions.get(guildId); + if (!s) return; + try { + s.connection.destroy(); + } catch (err) { + logger.error(`Error in playback actions: ${err}.`); + } + sessions.delete(guildId); +} + export async function playSongFromYoutube( i: ChatInputCommandInteraction, song: SongPick, @@ -83,6 +138,7 @@ export async function playSongFromYoutube( }); const player = createAudioPlayer(); + sessions.set(i.guildId!, { player, guildId: i.guildId!, connection }); connection.subscribe(player); player.play(resource); @@ -92,12 +148,12 @@ export async function playSongFromYoutube( player.on(AudioPlayerStatus.Idle, () => { logger.info(`Finished playing: ${song.title} by ${song.artist}`); - connection.destroy(); + endSession(i.guildId!); }); player.on("error", (error) => { logger.error(`Audio player error: ${error.message}`); - connection.destroy(); + endSession(i.guildId!); }); await entersState(player, AudioPlayerStatus.Playing, 10_000); From 1d52f121272b779265d500854e19d3326a2202b2 Mon Sep 17 00:00:00 2001 From: Jared Date: Tue, 12 Aug 2025 21:49:26 -0500 Subject: [PATCH 2/6] working on vibe commands --- src/commands/chat-commands/random-song.ts | 14 +++--- src/commands/chat-commands/vibe.test.ts | 11 +++++ src/commands/chat-commands/vibe.ts | 49 +++++++++++++++++++ src/events/interactionCreate.ts | 6 +-- .../{youtube.test.ts => playback.test.ts} | 0 src/helpers/{youtube.ts => playback.ts} | 0 6 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 src/commands/chat-commands/vibe.test.ts create mode 100644 src/commands/chat-commands/vibe.ts rename src/helpers/{youtube.test.ts => playback.test.ts} (100%) rename src/helpers/{youtube.ts => playback.ts} (100%) diff --git a/src/commands/chat-commands/random-song.ts b/src/commands/chat-commands/random-song.ts index c954961..f62f4f0 100644 --- a/src/commands/chat-commands/random-song.ts +++ b/src/commands/chat-commands/random-song.ts @@ -10,15 +10,15 @@ import type { SlashCommand } from "../../@types/commands.js"; import { logger } from "../../helpers/logger.js"; import { getSongByGenre } from "../../helpers/open-ai.js"; import { SongPick } from "../../@types/open-ai.js"; -import { playSongFromYoutube } from "../../helpers/youtube.js"; +import { playSongFromYoutube } from "../../helpers/playback.js"; async function execute( - interaction: ChatInputCommandInteraction, + interaction: ChatInputCommandInteraction ): Promise { try { await interaction.deferReply(); const songs: SongPick[] = await getSongByGenre( - interaction.options.getString("genre")!, + interaction.options.getString("genre")! ); const randomIndex = Math.floor(Math.random() * songs.length); @@ -49,7 +49,7 @@ async function execute( new ActionRowBuilder().addComponents( remove, pause, - play, + play ); await interaction.editReply({ content: playResult, @@ -69,7 +69,7 @@ export const command: SlashCommand = { data: new SlashCommandBuilder() .setName("random") .setDescription( - "Play's a song from a certain genre chosen by Amplify (at random)!", + "Play's a song from a certain genre chosen by Amplify (at random)!" ) .addStringOption((option) => option @@ -90,8 +90,8 @@ export const command: SlashCommand = { { name: "Alternative Rock", value: "Alternative Rock" }, { name: "Funk", value: "Funk" }, { name: "Lo-fi Hip Hop", value: "Lo-fi Hip Hop" }, - { name: "Classic Soul", value: "Classic Soul" }, - ), + { name: "Classic Soul", value: "Classic Soul" } + ) ), execute, }; diff --git a/src/commands/chat-commands/vibe.test.ts b/src/commands/chat-commands/vibe.test.ts new file mode 100644 index 0000000..db82259 --- /dev/null +++ b/src/commands/chat-commands/vibe.test.ts @@ -0,0 +1,11 @@ +import { describe, test, expect } from "@jest/globals"; + +describe("src/index.ts", () => { + test("sayHello function should return a greeting with the given name", () => { + // Arrange + const name = "Hello World"; + + // Assert + expect(name).toBe("Hello World"); + }); +}); \ No newline at end of file diff --git a/src/commands/chat-commands/vibe.ts b/src/commands/chat-commands/vibe.ts new file mode 100644 index 0000000..bfe6aa2 --- /dev/null +++ b/src/commands/chat-commands/vibe.ts @@ -0,0 +1,49 @@ +import { + SlashCommandBuilder, + ChatInputCommandInteraction, + ButtonBuilder, + ButtonStyle, + ActionRowBuilder, + MessageActionRowComponentBuilder, +} from "discord.js"; +import type { SlashCommand } from "../../@types/commands.js"; +import { logger } from "../../helpers/logger.js"; +import { getPlaylistsByVibe } from "../../helpers/open-ai.js"; +import { SongPick } from "../../@types/open-ai.js"; +import { playSongFromYoutube } from "../../helpers/playback.js"; + +async function execute( + interaction: ChatInputCommandInteraction +): Promise { + try { + await interaction.deferReply(); + const songs: SongPick[] = await getPlaylistsByVibe( + interaction.options.getString("vibe")! + ); + } catch (err) { + logger.error(`Error in execute function for command [VIBE]: ${err}`); + await interaction.editReply({ + content: "An unexpected error occured, try again later.", + }); + } +} + +export const command: SlashCommand = { + cooldown: 20, + kind: "chat", + data: new SlashCommandBuilder() + .setName("vibe") + .setDescription( + "Generates a curated playlist to listen to based on the desired vibe." + ) + .addStringOption((option) => + option + .setName("vibe") + .setDescription( + "Give a brief description of the vibe you are going for." + ) + .setRequired(true) + .setMaxLength(200) + ), + execute, +}; diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 3e6e6ac..2f1b1d7 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -5,7 +5,7 @@ import { pausePlayback, resumePlayback, removePlayback, -} from "../helpers/youtube.js"; +} from "../helpers/playback.js"; const interactionCreateEvent = { name: Events.InteractionCreate, @@ -13,11 +13,11 @@ const interactionCreateEvent = { try { if (interaction.isChatInputCommand()) { const command = interaction.client.commands.get( - interaction.commandName, + interaction.commandName ); if (!command) { logger.error( - `No command matching ${interaction.commandName} was found.`, + `No command matching ${interaction.commandName} was found.` ); return; } diff --git a/src/helpers/youtube.test.ts b/src/helpers/playback.test.ts similarity index 100% rename from src/helpers/youtube.test.ts rename to src/helpers/playback.test.ts diff --git a/src/helpers/youtube.ts b/src/helpers/playback.ts similarity index 100% rename from src/helpers/youtube.ts rename to src/helpers/playback.ts From b52026093c5e40eaa38702c4251930d9713db6ed Mon Sep 17 00:00:00 2001 From: Jared Gross Date: Wed, 13 Aug 2025 07:53:47 -0500 Subject: [PATCH 3/6] fixing linting rules --- src/commands/chat-commands/vibe.ts | 24 ++++++++++-------------- tsconfig.tsbuildinfo | 2 +- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/commands/chat-commands/vibe.ts b/src/commands/chat-commands/vibe.ts index bfe6aa2..2fe5f19 100644 --- a/src/commands/chat-commands/vibe.ts +++ b/src/commands/chat-commands/vibe.ts @@ -1,24 +1,20 @@ -import { - SlashCommandBuilder, - ChatInputCommandInteraction, - ButtonBuilder, - ButtonStyle, - ActionRowBuilder, - MessageActionRowComponentBuilder, -} from "discord.js"; +import { SlashCommandBuilder, ChatInputCommandInteraction } from "discord.js"; import type { SlashCommand } from "../../@types/commands.js"; import { logger } from "../../helpers/logger.js"; import { getPlaylistsByVibe } from "../../helpers/open-ai.js"; import { SongPick } from "../../@types/open-ai.js"; -import { playSongFromYoutube } from "../../helpers/playback.js"; +// import { playSongFromYoutube } from "../../helpers/playback.js"; async function execute( - interaction: ChatInputCommandInteraction + interaction: ChatInputCommandInteraction, ): Promise { try { await interaction.deferReply(); const songs: SongPick[] = await getPlaylistsByVibe( - interaction.options.getString("vibe")! + interaction.options.getString("vibe")!, + ); + logger.info( + `The songs we picked are ${songs.map((song) => song.title).join(", ")}`, ); } catch (err) { logger.error(`Error in execute function for command [VIBE]: ${err}`); @@ -34,16 +30,16 @@ export const command: SlashCommand = { data: new SlashCommandBuilder() .setName("vibe") .setDescription( - "Generates a curated playlist to listen to based on the desired vibe." + "Generates a curated playlist to listen to based on the desired vibe.", ) .addStringOption((option) => option .setName("vibe") .setDescription( - "Give a brief description of the vibe you are going for." + "Give a brief description of the vibe you are going for.", ) .setRequired(true) - .setMaxLength(200) + .setMaxLength(200), ), execute, }; diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index acbff54..c7765e5 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/index.ts","./src/@types/commands.ts","./src/@types/discord-augment.d.ts","./src/commands/chat-commands/ping.ts","./src/events/interactioncreate.ts","./src/events/ready.ts","./src/helpers/commands.ts","./src/helpers/events.ts","./src/helpers/logger.ts","./src/scripts/deploy-commands.ts"],"version":"5.9.2"} \ No newline at end of file +{"root":["./src/index.ts","./src/@types/commands.ts","./src/@types/discord-augment.d.ts","./src/@types/open-ai.ts","./src/commands/chat-commands/ping.ts","./src/commands/chat-commands/random-song.ts","./src/commands/chat-commands/vibe.ts","./src/events/interactioncreate.ts","./src/events/ready.ts","./src/helpers/commands.ts","./src/helpers/events.ts","./src/helpers/logger.ts","./src/helpers/music-players.ts","./src/helpers/open-ai.ts","./src/helpers/playback.ts","./src/scripts/deploy-commands.ts"],"version":"5.9.2"} \ No newline at end of file From 8f3259309d2aa35e6e535d5bf4ea5736ece136b3 Mon Sep 17 00:00:00 2001 From: Jared Gross Date: Wed, 13 Aug 2025 08:01:02 -0500 Subject: [PATCH 4/6] Update src/helpers/open-ai.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/helpers/open-ai.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/open-ai.ts b/src/helpers/open-ai.ts index 15dbd7c..d00595e 100644 --- a/src/helpers/open-ai.ts +++ b/src/helpers/open-ai.ts @@ -45,7 +45,7 @@ export async function getSongByGenre(genre: string): Promise { }, { role: "user", - content: `Select songs for genre: "${genre}. Avoid any in the exlcusions provided next.`, + content: `Select songs for genre: "${genre}. Avoid any in the exclusions provided next.`, }, { role: "user", From 332af2e13323c35f0ecdb075775fb87674fc5436 Mon Sep 17 00:00:00 2001 From: Jared Gross Date: Wed, 13 Aug 2025 08:02:40 -0500 Subject: [PATCH 5/6] minor bug fixes --- src/commands/chat-commands/random-song.ts | 13 ++++++------- src/helpers/open-ai.ts | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/commands/chat-commands/random-song.ts b/src/commands/chat-commands/random-song.ts index f62f4f0..b2682e1 100644 --- a/src/commands/chat-commands/random-song.ts +++ b/src/commands/chat-commands/random-song.ts @@ -13,19 +13,18 @@ import { SongPick } from "../../@types/open-ai.js"; import { playSongFromYoutube } from "../../helpers/playback.js"; async function execute( - interaction: ChatInputCommandInteraction + interaction: ChatInputCommandInteraction, ): Promise { try { await interaction.deferReply(); const songs: SongPick[] = await getSongByGenre( - interaction.options.getString("genre")! + interaction.options.getString("genre")!, ); const randomIndex = Math.floor(Math.random() * songs.length); const pickedSong = songs[randomIndex]!; const playResult = await playSongFromYoutube(interaction, pickedSong); - await interaction.editReply(playResult); const play = new ButtonBuilder() .setCustomId("play") @@ -49,7 +48,7 @@ async function execute( new ActionRowBuilder().addComponents( remove, pause, - play + play, ); await interaction.editReply({ content: playResult, @@ -69,7 +68,7 @@ export const command: SlashCommand = { data: new SlashCommandBuilder() .setName("random") .setDescription( - "Play's a song from a certain genre chosen by Amplify (at random)!" + "Play's a song from a certain genre chosen by Amplify (at random)!", ) .addStringOption((option) => option @@ -90,8 +89,8 @@ export const command: SlashCommand = { { name: "Alternative Rock", value: "Alternative Rock" }, { name: "Funk", value: "Funk" }, { name: "Lo-fi Hip Hop", value: "Lo-fi Hip Hop" }, - { name: "Classic Soul", value: "Classic Soul" } - ) + { name: "Classic Soul", value: "Classic Soul" }, + ), ), execute, }; diff --git a/src/helpers/open-ai.ts b/src/helpers/open-ai.ts index d00595e..2954489 100644 --- a/src/helpers/open-ai.ts +++ b/src/helpers/open-ai.ts @@ -45,7 +45,7 @@ export async function getSongByGenre(genre: string): Promise { }, { role: "user", - content: `Select songs for genre: "${genre}. Avoid any in the exclusions provided next.`, + content: `Select songs for genre: "${genre}". Avoid any in the exclusions provided next.`, }, { role: "user", From 0b524e233a0132f7746209f411901843a29677d1 Mon Sep 17 00:00:00 2001 From: Jared Gross Date: Wed, 13 Aug 2025 08:50:00 -0500 Subject: [PATCH 6/6] major refactors for better readability and maintainability --- src/@types/commands.ts | 7 +- src/@types/discord-augment.d.ts | 4 +- src/commands/chat-commands/ping.ts | 1 - src/commands/chat-commands/random-song.ts | 1 - src/commands/chat-commands/vibe.ts | 1 - src/events/interactionCreate.ts | 120 ++-------------------- src/helpers/buttons.test.ts | 11 ++ src/helpers/buttons.ts | 101 ++++++++++++++++++ src/helpers/commands.ts | 37 ++++++- src/helpers/playback.ts | 19 +++- src/index.ts | 2 + src/scripts/deploy-commands.ts | 6 +- 12 files changed, 177 insertions(+), 133 deletions(-) create mode 100644 src/helpers/buttons.test.ts create mode 100644 src/helpers/buttons.ts diff --git a/src/@types/commands.ts b/src/@types/commands.ts index ea1821c..c692ae5 100644 --- a/src/@types/commands.ts +++ b/src/@types/commands.ts @@ -7,15 +7,13 @@ import type { } from "discord.js"; type Handler = (interaction: I) => Promise; -type Kind = "chat" | "button" | "select" | "modal"; type SlashCommandData = | SlashCommandBuilder | SlashCommandOptionsOnlyBuilder | SlashCommandSubcommandsOnlyBuilder; -export type Command = { - kind: K; +export type Command = { data: D; execute: Handler; cooldown: C; // seconds @@ -24,10 +22,7 @@ export type Command = { export type SlashCommand = Command< ChatInputCommandInteraction, SlashCommandData, - "chat", number >; -export type DiscordCommand = SlashCommand; - export type CommandCooldowns = Collection>; diff --git a/src/@types/discord-augment.d.ts b/src/@types/discord-augment.d.ts index e072da9..bc21876 100644 --- a/src/@types/discord-augment.d.ts +++ b/src/@types/discord-augment.d.ts @@ -1,10 +1,10 @@ import "discord.js"; import type { Collection } from "discord.js"; -import type { DiscordCommand, CommandCooldowns } from "./commands.ts"; +import type { CommandCooldowns, SlashCommand } from "./commands.ts"; declare module "discord.js" { interface Client { - commands: Collection; + commands: Collection; cooldowns: CommandCooldowns; } } diff --git a/src/commands/chat-commands/ping.ts b/src/commands/chat-commands/ping.ts index a7f0a69..05d9e2f 100644 --- a/src/commands/chat-commands/ping.ts +++ b/src/commands/chat-commands/ping.ts @@ -9,7 +9,6 @@ async function execute( export const command: SlashCommand = { cooldown: 10, - kind: "chat", data: new SlashCommandBuilder() .setName("ping") .setDescription("Replies with Pong!"), diff --git a/src/commands/chat-commands/random-song.ts b/src/commands/chat-commands/random-song.ts index b2682e1..c176593 100644 --- a/src/commands/chat-commands/random-song.ts +++ b/src/commands/chat-commands/random-song.ts @@ -64,7 +64,6 @@ async function execute( export const command: SlashCommand = { cooldown: 15, - kind: "chat", data: new SlashCommandBuilder() .setName("random") .setDescription( diff --git a/src/commands/chat-commands/vibe.ts b/src/commands/chat-commands/vibe.ts index 2fe5f19..bed7203 100644 --- a/src/commands/chat-commands/vibe.ts +++ b/src/commands/chat-commands/vibe.ts @@ -26,7 +26,6 @@ async function execute( export const command: SlashCommand = { cooldown: 20, - kind: "chat", data: new SlashCommandBuilder() .setName("vibe") .setDescription( diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 2f1b1d7..caeefed 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,128 +1,24 @@ import { Events, MessageFlags, Interaction, CacheType } from "discord.js"; import { logger } from "../helpers/logger.js"; -import { commandOnCooldown } from "../helpers/commands.js"; -import { - pausePlayback, - resumePlayback, - removePlayback, -} from "../helpers/playback.js"; +import { handleCommandInteraction } from "../helpers/commands.js"; +import { handleButtonInteraction } from "../helpers/buttons.js"; const interactionCreateEvent = { name: Events.InteractionCreate, async execute(interaction: Interaction) { try { if (interaction.isChatInputCommand()) { - const command = interaction.client.commands.get( - interaction.commandName - ); - if (!command) { - logger.error( - `No command matching ${interaction.commandName} was found.` - ); - return; - } - const cooldown = commandOnCooldown(command, interaction); - if (cooldown !== -1) { - if (!interaction.isRepliable()) return; - else { - await interaction.reply({ - content: `This command is on cooldown for you, wait ${cooldown} seconds`, - flags: MessageFlags.Ephemeral, - }); - return; - } - } - if (command.kind === "chat") await command.execute(interaction); + await handleCommandInteraction(interaction); } else if (interaction.isButton()) { - if (!interaction.guildId) return; - if (interaction.customId === "pause") { - const msg = pausePlayback(interaction.guildId); - await interaction.update({ - content: msg, - components: [ - { - type: 1, - components: [ - { - type: 2, - custom_id: "remove", - label: "Remove", - style: 4, - disabled: false, - }, - { - type: 2, - custom_id: "pause", - label: "Pause", - style: 2, - disabled: true, - }, - { - type: 2, - custom_id: "play", - label: "Play", - style: 1, - disabled: false, - }, - ], - }, - ], - }); - } else if (interaction.customId === "play") { - const msg = resumePlayback(interaction.guildId); - await interaction.update({ - content: msg, - components: [ - { - type: 1, - components: [ - { - type: 2, - custom_id: "remove", - label: "Remove", - style: 4, - disabled: false, - }, - { - type: 2, - custom_id: "pause", - label: "Pause", - style: 2, - disabled: false, - }, - { - type: 2, - custom_id: "play", - label: "Play", - style: 1, - disabled: true, - }, - ], - }, - ], - }); - } else if (interaction.customId === "remove") { - const msg = removePlayback(interaction.guildId); - await interaction.update({ - content: msg, - components: [], - }); - } + await handleButtonInteraction(interaction); } } catch (err) { logger.error(`Error executing command for Amplify: ${err}`); if (!interaction.isRepliable()) return; - if (interaction.replied || interaction.deferred) { - await interaction.reply({ - content: "There was an error while executing this command!", - flags: MessageFlags.Ephemeral, - }); - } else { - await interaction.reply({ - content: "There was an error while executing this command!", - flags: MessageFlags.Ephemeral, - }); - } + await interaction.reply({ + content: "There was an error while executing this command!", + flags: MessageFlags.Ephemeral, + }); } }, }; diff --git a/src/helpers/buttons.test.ts b/src/helpers/buttons.test.ts new file mode 100644 index 0000000..bfda949 --- /dev/null +++ b/src/helpers/buttons.test.ts @@ -0,0 +1,11 @@ +import { describe, test, expect } from "@jest/globals"; + +describe("src/index.ts", () => { + test("sayHello function should return a greeting with the given name", () => { + // Arrange + const name = "Hello World"; + + // Assert + expect(name).toBe("Hello World"); + }); +}); diff --git a/src/helpers/buttons.ts b/src/helpers/buttons.ts new file mode 100644 index 0000000..01c3754 --- /dev/null +++ b/src/helpers/buttons.ts @@ -0,0 +1,101 @@ +import { ButtonInteraction, CacheType } from "discord.js"; +import { + pausePlayback, + resumePlayback, + removePlayback, +} from "../helpers/playback.js"; + +export async function handleButtonInteraction( + interaction: ButtonInteraction, +) { + if (!interaction.guildId) return; + switch (interaction.customId) { + case "play": + await handlePlayButton(interaction); + break; + case "remove": + await handleRemoveButton(interaction); + break; + case "pause": + await handlePauseButton(interaction); + break; + } +} + +async function handlePlayButton(interaction: ButtonInteraction) { + const msg = resumePlayback(interaction.guildId!); + await interaction.update({ + content: msg, + components: [ + { + type: 1, + components: [ + { + type: 2, + custom_id: "remove", + label: "Remove", + style: 4, + disabled: false, + }, + { + type: 2, + custom_id: "pause", + label: "Pause", + style: 2, + disabled: false, + }, + { + type: 2, + custom_id: "play", + label: "Play", + style: 1, + disabled: true, + }, + ], + }, + ], + }); +} + +async function handleRemoveButton(interaction: ButtonInteraction) { + const msg = removePlayback(interaction.guildId!); + await interaction.update({ + content: msg, + components: [], + }); +} + +async function handlePauseButton(interaction: ButtonInteraction) { + const msg = pausePlayback(interaction.guildId!); + await interaction.update({ + content: msg, + components: [ + { + type: 1, + components: [ + { + type: 2, + custom_id: "remove", + label: "Remove", + style: 4, + disabled: false, + }, + { + type: 2, + custom_id: "pause", + label: "Pause", + style: 2, + disabled: true, + }, + { + type: 2, + custom_id: "play", + label: "Play", + style: 1, + disabled: false, + }, + ], + }, + ], + }); +} diff --git a/src/helpers/commands.ts b/src/helpers/commands.ts index c8afee2..db0c8e4 100644 --- a/src/helpers/commands.ts +++ b/src/helpers/commands.ts @@ -1,10 +1,17 @@ -import { Client, Collection, Interaction, CacheType } from "discord.js"; +import { + Client, + Collection, + Interaction, + CacheType, + ChatInputCommandInteraction, + MessageFlags, +} from "discord.js"; import { readdir } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { logger } from "./logger.js"; import type { Dirent } from "node:fs"; -import { DiscordCommand } from "../@types/commands.js"; +import { SlashCommand } from "../@types/commands.js"; export async function attachCommandHandlers(client: Client): Promise { const __filename: string = fileURLToPath(import.meta.url); @@ -37,7 +44,7 @@ export async function attachCommandHandlers(client: Client): Promise { const fileUrl: string = pathToFileURL(filePath).href; const mod = await import(fileUrl); - const command: DiscordCommand = mod.command; + const command: SlashCommand = mod.command; if (command && "data" in command && "execute" in command) { client.commands.set(command.data.name, command); } else { @@ -51,7 +58,7 @@ export async function attachCommandHandlers(client: Client): Promise { } export function commandOnCooldown( - command: DiscordCommand, + command: SlashCommand, interaction: Interaction, ): number { const cooldowns = interaction.client.cooldowns; @@ -73,3 +80,25 @@ export function commandOnCooldown( setTimeout(() => timestamps?.delete(interaction.user.id), cooldownAmount); return -1; } + +export async function handleCommandInteraction( + interaction: ChatInputCommandInteraction, +) { + const command = interaction.client.commands.get(interaction.commandName); + if (!command) { + logger.error(`No command matching ${interaction.commandName} was found.`); + return; + } + const cooldown = commandOnCooldown(command, interaction); + if (cooldown !== -1) { + if (!interaction.isRepliable()) return; + else { + await interaction.reply({ + content: `This command is on cooldown for you, wait ${cooldown} seconds`, + flags: MessageFlags.Ephemeral, + }); + return; + } + } + await command.execute(interaction); +} diff --git a/src/helpers/playback.ts b/src/helpers/playback.ts index 090f539..98bbb35 100644 --- a/src/helpers/playback.ts +++ b/src/helpers/playback.ts @@ -24,6 +24,10 @@ interface PlaybackSession { player: AudioPlayer; guildId: string; connection: VoiceConnection; + songInfo: { + title: string; + artist: string; + }; } const sessions = new Map(); @@ -40,7 +44,9 @@ export function pausePlayback(guildId: string): string { if (s.player.state.status !== AudioPlayerStatus.Playing) return "Not currently playing."; const ok = s.player.pause(true); - return ok ? "Paused." : "Failed to pause."; + return ok + ? `Paused - ${s.songInfo.title} by ${s.songInfo.artist}` + : "Failed to pause."; } export function resumePlayback(guildId: string): string { @@ -48,7 +54,9 @@ export function resumePlayback(guildId: string): string { if (!s) return "Nothing is queued."; if (s.player.state.status !== AudioPlayerStatus.Paused) return "Not paused."; const ok = s.player.unpause(); - return ok ? "Resumed." : "Failed to resume."; + return ok + ? `Playing - ${s.songInfo.title} by ${s.songInfo.artist}` + : "Failed to resume."; } export function removePlayback(guildId: string) { @@ -138,7 +146,12 @@ export async function playSongFromYoutube( }); const player = createAudioPlayer(); - sessions.set(i.guildId!, { player, guildId: i.guildId!, connection }); + sessions.set(i.guildId!, { + player, + guildId: i.guildId!, + connection, + songInfo: { title: song.title, artist: song.artist }, + }); connection.subscribe(player); player.play(resource); diff --git a/src/index.ts b/src/index.ts index b0b5f92..729e07d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,8 @@ try { dotenv.config({ path: ".env" }); +logger.info("Successfully loaded .envs into the process!"); + if (!process.env.DISCORD_TOKEN) { logger.error( "DISCORD_TOKEN environment variable not set. This is required to run Amplify successfully", diff --git a/src/scripts/deploy-commands.ts b/src/scripts/deploy-commands.ts index d085835..1cedc5b 100644 --- a/src/scripts/deploy-commands.ts +++ b/src/scripts/deploy-commands.ts @@ -8,7 +8,7 @@ import { dirname, join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { logger } from "../helpers/logger.js"; import type { Dirent } from "node:fs"; -import { DiscordCommand } from "../@types/commands.js"; +import { SlashCommand } from "../@types/commands.js"; import dotenv from "dotenv"; dotenv.config({ path: ".env" }); @@ -51,9 +51,9 @@ for (const folder of commandFolders) { const fileUrl: string = pathToFileURL(filePath).href; const mod = await import(fileUrl); - const command: DiscordCommand = mod.command; + const command: SlashCommand = mod.command; if (command && "data" in command && "execute" in command) { - if (command.kind === "chat") slashCommands.push(command.data.toJSON()); + slashCommands.push(command.data.toJSON()); } else { logger.warn( `[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`,