diff --git a/COMMANDS.md b/COMMANDS.md index 72cabde..0466d9c 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -2,14 +2,16 @@ ## Music Commands -## /volume +### `/volume` Adjust the volume of the music player. | Name | Description | Required | Type | Choices | |------|-------------|----------|------|---------| | level | The volume level to set (0-100). | false | Number | | -## /swap +--- + +### `/swap` Swap the position of two songs in the queue | Name | Description | Required | Type | Choices | @@ -17,90 +19,126 @@ Swap the position of two songs in the queue | first | The position of the first song | true | Number | | | second | The position of the second song | true | Number | | -## /stop +--- + +### `/stop` Stop the playback. -## /skipto +--- + +### `/skipto` Skip to the given song, removing others on the way | Name | Description | Required | Type | Choices | |------|-------------|----------|------|---------| | position | The position of the song to skip to | true | Number | | -## /skip +--- + +### `/skip` Skip to the next song -## /shuffle +--- + +### `/shuffle` Toggle shuffle mode for this queue. -## /seek +--- + +### `/seek` Seek to a specific timestamp in the current track. | Name | Description | Required | Type | Choices | |------|-------------|----------|------|---------| | timestamp | The timestamp to seek to (in seconds). | true | Number | | -## /resume +--- + +### `/resume` Resume the playback -## /restart +--- + +### `/replay` + +Replay the current song from the beginning -Restart the current song +--- -## /repeat status +### `/repeat status` Show the current repeat mode. -## /repeat off +--- + +### `/repeat off` Disable repeat mode. -## /repeat queue +--- + +### `/repeat queue` Repeat the entire queue. -## /repeat song +--- + +### `/repeat song` Repeat the current song. -## /repeat autoplay +--- + +### `/repeat autoplay` Automatically play related songs based on your queue. -## /remove +--- + +### `/remove` Remove a song from the queue | Name | Description | Required | Type | Choices | |------|-------------|----------|------|---------| | position | The position of the song to remove | true | Number | | -## /queue +--- + +### `/queue` Show the songs in the queue. | Name | Description | Required | Type | Choices | |------|-------------|----------|------|---------| | page | The page number of the queue | false | Number | | -## /play +--- + +### `/play` Play a song or playlist from url or name | Name | Description | Required | Type | Choices | |------|-------------|----------|------|---------| | query | The name or url of the song, you want to play. | true | String | | -## /pause +--- + +### `/pause` Pause the playback -## /now +--- + +### `/now` Show the current playing song -## /move +--- + +### `/move` Move a song in the queue | Name | Description | Required | Type | Choices | @@ -108,73 +146,101 @@ Move a song in the queue | from | The current position of the song | true | Number | | | to | The new position to move to | true | Number | | -## /lyrics +--- + +### `/lyrics` Get lyrics for a song. | Name | Description | Required | Type | Choices | |------|-------------|----------|------|---------| | query | The title of the song to get lyrics for. | false | String | | -## /jump +--- + +### `/jump` Jump to specific song on the queue without removing others | Name | Description | Required | Type | Choices | |------|-------------|----------|------|---------| | position | The position of the song to jump to | true | Number | | -## /filters clear +--- -Remove all applied filters. +### `/filters clear` -## /filters show +Remove all applied audio filters. -Show all applied filters. +--- -## /filters toggle +### `/filters status` -Enable or disable a specific filter. +Show the status of all audio filters. + +--- + +### `/filters toggle` + +Enable or disable a specific audio filter. | Name | Description | Required | Type | Choices | |------|-------------|----------|------|---------| | name | The name of the filter to toggle. | true | String | Bassboost, Chorus, Compressor, Dim, Earrape, Expander, Fadein, Flanger, Gate, Haas, Karaoke, Lofi, Mcompand, Mono, Nightcore, Normalizer, Phaser, Pulsator, Reverse, Softlimiter, Subboost, Surrounding, Treble, Vaporwave, Vibrato | -## /clear +--- + +### `/clear` Clear songs from the queue, history, or all. | Name | Description | Required | Type | Choices | |------|-------------|----------|------|---------| | type | Select the type of songs to clear. | true | String | Queue, History, All | -## /back +--- + +### `/back` Go back to the previous song +--- + ## Misc Commands -## /uptime +### `/uptime` Show how long the bot has been up -## /support +--- + +### `/support` Join the support server and get some help -## /ping +--- + +### `/ping` Ping? Pong! -## /invite +--- + +### `/invite` Invite the bot to your server -## /info +--- + +### `/info` Show info about the bot +--- + ## Dev Commands -## /eval +### `/eval` Execute a piece of javascript code | Name | Description | Required | Type | Choices | |------|-------------|----------|------|---------| | code | The code to execute | true | String | | + +--- diff --git a/src/commands/dev/eval.js b/src/commands/dev/eval.js index d217a92..04c3e25 100644 --- a/src/commands/dev/eval.js +++ b/src/commands/dev/eval.js @@ -18,13 +18,14 @@ export const data = { }; export async function execute(interaction) { + await interaction.deferReply({ ephemeral: true }); + const code = interaction.options.getString("code", true); // Security check to prevent execution of potentially harmful code const classified = ["env", "token", "config", "secret", "process"]; if (classified.some((item) => code.toLowerCase().includes(item))) { - return interaction.reply({ - ephemeral: true, + return interaction.editReply({ embeds: [ ErrorEmbed( "This operation is cancelled because it may include secrets." @@ -65,8 +66,7 @@ export async function execute(interaction) { color: error ? Colors.Red : Colors.Green, }; - return interaction.reply({ - ephemeral: true, + return interaction.editReply({ embeds: [headerEmbed, codeEmbed, resultEmbed], }); } diff --git a/src/commands/misc/invite.js b/src/commands/misc/invite.js index 65dc95e..dc2300c 100644 --- a/src/commands/misc/invite.js +++ b/src/commands/misc/invite.js @@ -8,6 +8,7 @@ export const data = { export function execute(interaction) { const inviteLink = `https://discord.com/api/oauth2/authorize?client_id=${interaction.client.user.id}&permissions=281424481600&scope=bot%20applications.commands`; + const button = new ButtonBuilder() .setLabel("Click to add the bot to your server.") .setStyle(ButtonStyle.Link) diff --git a/src/commands/music/back.js b/src/commands/music/back.js index 49dc52c..0abc3c7 100644 --- a/src/commands/music/back.js +++ b/src/commands/music/back.js @@ -18,9 +18,11 @@ export async function execute(interaction) { embeds: [ErrorEmbed("There is no previous song to go back to.")], }); + await interaction.deferReply(); + await history.previous(); - return await interaction.reply({ + return interaction.editReply({ embeds: [SuccessEmbed("Went back to the previous song.")], }); } diff --git a/src/commands/music/filters.js b/src/commands/music/filters.js index dc53bc1..10f103b 100644 --- a/src/commands/music/filters.js +++ b/src/commands/music/filters.js @@ -1,5 +1,6 @@ import { ApplicationCommandOptionType } from "discord.js"; import { BaseEmbed, ErrorEmbed, SuccessEmbed } from "../../modules/Embeds.js"; +import { titleCase } from "../../modules/utils.js"; const avlFilters = [ "Bassboost", "Chorus", @@ -34,17 +35,17 @@ export const data = { { type: ApplicationCommandOptionType.Subcommand, name: "clear", - description: "Remove all applied filters.", + description: "Remove all applied audio filters.", }, { type: ApplicationCommandOptionType.Subcommand, - name: "show", - description: "Show all applied filters.", + name: "status", + description: "Show the status of all audio filters.", }, { type: ApplicationCommandOptionType.Subcommand, name: "toggle", - description: "Enable or disable a specific filter.", + description: "Enable or disable a specific audio filter.", options: [ { type: ApplicationCommandOptionType.String, @@ -80,23 +81,35 @@ export async function execute(interaction, queue) { embeds: [SuccessEmbed("Cleared all applied filters.")], }); } + case "toggle": { const filterName = interaction.options.getString("name", true); - queue.filters.ffmpeg.toggle(filterName); - return interaction.reply({ - embeds: [SuccessEmbed(`Toggled the ${filterName} audio filter.`)], + await interaction.deferReply(); + const mode = await queue.filters.ffmpeg.toggle(filterName); + return interaction.editReply({ + embeds: [ + SuccessEmbed( + `${mode ? "Enabled" : "Disabled"} the ${filterName} audio filter.` + ), + ], }); } + default: { const enabledFilters = queue.filters.ffmpeg.getFiltersEnabled(); const disabledFilters = queue.filters.ffmpeg.getFiltersDisabled(); - const enFDes = enabledFilters.map((f) => `${f} --> ✅`).join("\n"); - const disFDes = disabledFilters.map((f) => `${f} --> ❌`).join("\n"); + const formatFilters = (thatFilters, status) => + thatFilters + .map((filter) => `${titleCase(filter)} --> ${status}`) + .join("\n"); + + const enabledFiltersDesc = formatFilters(enabledFilters, "✅"); + const disabledFiltersDesc = formatFilters(disabledFilters, "❌"); const embed = BaseEmbed() .setTitle("All Filters") - .setDescription(`${enFDes}\n\n${disFDes}`); + .setDescription(`${enabledFiltersDesc}\n\n${disabledFiltersDesc}`); return interaction.reply({ ephemeral: true, embeds: [embed] }); } diff --git a/src/commands/music/jump.js b/src/commands/music/jump.js index aade5ce..f68d7d5 100644 --- a/src/commands/music/jump.js +++ b/src/commands/music/jump.js @@ -24,17 +24,17 @@ export function execute(interaction, queue) { embeds: [ErrorEmbed("The queue is empty.")], }); - const position = interaction.options.getNumber("position", true) - 1; + const position = interaction.options.getNumber("position", true); - if (position >= queue.size) + if (position > queue.size) return interaction.reply({ ephemeral: true, embeds: [ErrorEmbed("The provided position is not valid.")], }); - queue.node.jump(position); + queue.node.jump(position - 1); return interaction.reply({ - embeds: [SuccessEmbed(`Jumped to song ${position + 1}.`)], + embeds: [SuccessEmbed(`Jumped to the ${position} song.`)], }); } diff --git a/src/commands/music/play.js b/src/commands/music/play.js index d084c22..2296deb 100644 --- a/src/commands/music/play.js +++ b/src/commands/music/play.js @@ -62,6 +62,7 @@ export async function execute(interaction) { const query = interaction.options.getString("query", true); const player = useMainPlayer(); + await interaction.deferReply(); const result = await player.search(query, { diff --git a/src/commands/music/restart.js b/src/commands/music/replay.js similarity index 75% rename from src/commands/music/restart.js rename to src/commands/music/replay.js index 9d154a3..d96d044 100644 --- a/src/commands/music/restart.js +++ b/src/commands/music/replay.js @@ -1,8 +1,8 @@ import { ErrorEmbed, SuccessEmbed } from "../../modules/Embeds.js"; export const data = { - name: "restart", - description: "Restart the current song", + name: "replay", + description: "Replay the current song from the beginning", category: "music", queueOnly: true, validateVC: true, @@ -19,6 +19,6 @@ export function execute(interaction, queue) { queue.node.seek(0); return interaction.reply({ - embeds: [SuccessEmbed("Restarted the current song.")], + embeds: [SuccessEmbed("Replaying the current song.")], }); } diff --git a/src/commands/music/seek.js b/src/commands/music/seek.js index e639cab..c07ff10 100644 --- a/src/commands/music/seek.js +++ b/src/commands/music/seek.js @@ -10,6 +10,7 @@ export const data = { description: "The timestamp to seek to (in seconds).", type: ApplicationCommandOptionType.Number, required: true, + min_value: 0, }, ], category: "music", @@ -17,7 +18,7 @@ export const data = { validateVC: true, }; -export function execute(interaction, queue) { +export async function execute(interaction, queue) { const timestamp = interaction.options.getNumber("timestamp", true) * 1000; if (!queue.currentTrack) { @@ -38,9 +39,11 @@ export function execute(interaction, queue) { }); } - queue.node.seek(timestamp); + await interaction.deferReply(); - return interaction.reply({ + await queue.node.seek(timestamp); + + return interaction.editReply({ embeds: [SuccessEmbed(`Seeked to ${timestamp / 1000} seconds.`)], }); } diff --git a/src/commands/music/skipto.js b/src/commands/music/skipto.js index cea0208..5b743fb 100644 --- a/src/commands/music/skipto.js +++ b/src/commands/music/skipto.js @@ -9,8 +9,8 @@ export const data = { type: ApplicationCommandOptionType.Number, name: "position", description: "The position of the song to skip to", - min_value: 1, required: true, + min_value: 1, }, ], category: "music", @@ -25,17 +25,17 @@ export function execute(interaction, queue) { embeds: [ErrorEmbed("The queue has no song to skip to.")], }); - const position = interaction.options.getNumber("position", true) - 1; + const position = interaction.options.getNumber("position", true); - if (position >= queue.size) + if (position > queue.size) interaction.reply({ ephemeral: true, embeds: [ErrorEmbed("The provided position is not valid.")], }); - queue.node.skipTo(position); + queue.node.skipTo(position - 1); return interaction.reply({ - embeds: [SuccessEmbed(`Skipped to song ${position + 1}.`)], + embeds: [SuccessEmbed(`Skipped to the ${position} song.`)], }); } diff --git a/src/commands/music/swap.js b/src/commands/music/swap.js index f7e441e..f3c2691 100644 --- a/src/commands/music/swap.js +++ b/src/commands/music/swap.js @@ -57,12 +57,15 @@ export function execute(interaction, queue) { }); } + const song1 = queue.tracks.at(first); + const song2 = queue.tracks.at(second); + queue.node.swap(first, second); return interaction.reply({ embeds: [ SuccessEmbed( - `Songs at positions ${first + 1} and ${second + 1} have been swapped.` + `Swapped the position of \`${song1.title}\` and \`${song2.title}\`.` ), ], }); diff --git a/src/events/player/connection.js b/src/events/player/connection.js index 935de16..7a8b6f9 100644 --- a/src/events/player/connection.js +++ b/src/events/player/connection.js @@ -5,7 +5,7 @@ export const data = { name: GuildQueueEvent.Connection, type: "player", }; -export async function execute(queue) { +export function execute(queue) { const textChannel = queue.metadata.channel; const embed = BaseEmbed() diff --git a/src/events/player/disconnect.js b/src/events/player/disconnect.js index 416e43f..5102050 100644 --- a/src/events/player/disconnect.js +++ b/src/events/player/disconnect.js @@ -5,7 +5,7 @@ export const data = { name: GuildQueueEvent.Disconnect, type: "player", }; -export async function execute(queue) { +export function execute(queue) { const embed = InfoEmbed("Looks like my job here is done, leaving now."); return queue.metadata.channel.send({ embeds: [embed] }); diff --git a/src/events/player/error.js b/src/events/player/error.js index 1404a13..1312690 100644 --- a/src/events/player/error.js +++ b/src/events/player/error.js @@ -6,7 +6,7 @@ export const data = { name: GuildQueueEvent.Error, type: "player", }; -export async function execute(queue, error) { +export function execute(queue, error) { console.error(error); const embed = BaseEmbed({ color: Colors.Red }) diff --git a/src/events/player/playerError.js b/src/events/player/playerError.js index e28686a..40a1027 100644 --- a/src/events/player/playerError.js +++ b/src/events/player/playerError.js @@ -6,7 +6,7 @@ export const data = { name: GuildQueueEvent.PlayerError, type: "player", }; -export async function execute(queue, error) { +export function execute(queue, error) { const embed = BaseEmbed({ color: Colors.Red }) .setTitle("An error occured while playing") .setDescription(`Reason:\n${codeBlock(error.message)}`); diff --git a/src/modules/utils.js b/src/modules/utils.js index 39c6557..aa99614 100644 --- a/src/modules/utils.js +++ b/src/modules/utils.js @@ -28,3 +28,17 @@ export function formatDuration(duration) { export function formatNumber(number) { return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } + +/** + * Converts a string to title case. + * @param {string} str - The input string to be converted to title case. + * @returns {string} The input string converted to title case. + */ +export function titleCase(str) { + if (!str) return ""; + + return str + .trim() + .toLowerCase() + .replace(/(?:^|\s|-|_)\S/g, (match) => match.toUpperCase()); +} diff --git a/src/scripts/cmd-doc.js b/src/scripts/cmd-doc.js index 4679ca6..4fcbf17 100644 --- a/src/scripts/cmd-doc.js +++ b/src/scripts/cmd-doc.js @@ -1,8 +1,9 @@ import fs from "fs"; import { Collection } from "discord.js"; import { loadCommands } from "../handlers/command.js"; +import { titleCase } from "../modules/utils.js"; -const optionTypeMap = { +const OPTION_TYPE_MAP = { 1: "Subcommand", 2: "Subcommand Group", 3: "String", @@ -16,14 +17,6 @@ const optionTypeMap = { 11: "Attachment", }; -const titleCase = (str) => { - if (!str) return ""; - return str - .trim() - .toLowerCase() - .replace(/\b\w/g, (char) => char.toUpperCase()); -}; - const formatOptions = (options) => { if (!options || options.length === 0) return ""; @@ -32,9 +25,9 @@ const formatOptions = (options) => { options.forEach((option) => { const choices = option.choices - ? option.choices.map((choice) => `${choice.name}`).join(", ") + ? option.choices.map((choice) => choice.name).join(", ") : ""; - optionsTable += `| ${option.name} | ${option.description || ""} | ${option.required || false} | ${optionTypeMap[option.type] || option.type} | ${choices} |\n`; + optionsTable += `| ${option.name} | ${option.description || ""} | ${option.required || false} | ${OPTION_TYPE_MAP[option.type] || option.type} | ${choices} |\n`; if (option.options) { optionsTable += formatOptions(option.options); @@ -45,38 +38,44 @@ const formatOptions = (options) => { }; const formatCommand = (command) => { - const hasSubCommand = command.data.options?.[0]?.type === 1; - let commandStr = hasSubCommand + const { data } = command; + const hasSubcommand = data.options?.[0]?.type === 1; + let commandStr = hasSubcommand ? "" - : `## /${command.data.name}\n${command.data.description}\n`; + : `### \`/${data.name}\`\n${data.description}\n`; - if (command.data.options) { - if (hasSubCommand) { - command.data.options.forEach((subCommand) => { - commandStr += `## /${command.data.name} ${subCommand.name}\n${subCommand.description}\n`; - commandStr += formatOptions(subCommand.options); + if (data.options) { + if (hasSubcommand) { + data.options.forEach((subcommand) => { + commandStr += `### \`/${data.name} ${subcommand.name}\`\n${subcommand.description}\n`; + commandStr += formatOptions(subcommand.options); + commandStr += "\n---\n"; }); } else { - commandStr += formatOptions(command.data.options); + commandStr += formatOptions(data.options); } } + if (!hasSubcommand) commandStr += "\n---\n"; + return commandStr; }; -const generateMarkdown = (commands) => { - let markdown = "# Slash Commands List\n\n"; - const categories = {}; +const generateContent = (commands) => { + const categories = new Map(); commands.forEach((command) => { - const category = `${titleCase(command.data.category)} Commands`; - categories[category] = categories[category] || []; - categories[category].push(command); + const { category } = command.data; + const categoryName = titleCase(category); + const categoryCommands = categories.get(categoryName) || []; + categoryCommands.push(command); + categories.set(categoryName, categoryCommands); }); - for (const category in categories) { - markdown += `## ${category}\n\n`; - categories[category].forEach((command) => { + let markdown = "# Slash Commands List\n\n"; + for (const [category, categoryCommands] of categories) { + markdown += `## ${category} Commands\n\n`; + categoryCommands.forEach((command) => { markdown += formatCommand(command); }); } @@ -84,10 +83,18 @@ const generateMarkdown = (commands) => { return markdown; }; -(async function main() { - const fakeClient = { commands: new Collection() }; - await loadCommands(fakeClient); - const markdown = generateMarkdown(fakeClient.commands); - fs.writeFileSync("COMMANDS.md", markdown); - console.log("COMMANDS.md has been generated."); -})(); +async function generateMarkdown() { + try { + const fakeClient = { commands: new Collection() }; + await loadCommands(fakeClient); + + const content = generateContent(fakeClient.commands); + + fs.writeFileSync("COMMANDS.md", content); + console.log("COMMANDS.md has been generated."); + } catch (error) { + console.error("An error occurred:", error); + } +} + +generateMarkdown(); diff --git a/src/scripts/register.js b/src/scripts/register.js index 08dbda0..926a5ec 100644 --- a/src/scripts/register.js +++ b/src/scripts/register.js @@ -2,6 +2,13 @@ import "dotenv/config"; import { Collection, REST, Routes } from "discord.js"; import { loadCommands } from "../handlers/command.js"; +// check env variables +const envVariables = ["DISCORD_TOKEN", "CLIENT_ID", "DEV_GUILD"]; +for (const variable of envVariables) { + if (!process.env[variable]) { + throw new Error(`[ENV]: '${variable}' is missing.`); + } +} // Initialize a fake client with an empty commands collection const fakeClient = { commands: new Collection() }; await loadCommands(fakeClient); @@ -47,7 +54,7 @@ const registerCommands = async () => { Routes.applicationCommands(process.env.CLIENT_ID), { body: otherCommands } ); - console.log(`Registered ${otherData.length} other commands.`); + console.log(`Registered ${otherData.length} other (/) commands.`); } catch (error) { console.error("Error registering commands:", error); }