diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3b1ac6..6db5b5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,11 +8,9 @@ permissions: on: push: - branches: ["main"] pull_request: - branches: ["main"] -# Si hay un action en progreso cancela las actions antiguas si están en uso +# Si hay un action en progreso, cancela las actions antiguas si están en progreso concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c06bca8..a6a74c6 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,9 +13,7 @@ name: "CodeQL" on: push: - branches: [ "main" ] pull_request: - branches: [ "main" ] schedule: - cron: '0 7 * * 1' # Se ejecuta todos los lunes a las 9:00 de la mañana diff --git a/src/commands/playlist.js b/src/commands/playlist.js deleted file mode 100644 index d5690d1..0000000 --- a/src/commands/playlist.js +++ /dev/null @@ -1,291 +0,0 @@ -const { useMainPlayer } = require("discord-player"); -const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); -const { MongoClient } = require("mongodb"); - -const MONGO_URI = process.env.MONGODB_URI; - -const mongo = new MongoClient(MONGO_URI); -const db = mongo.db("ritmosbot"); -const coleccion = db.collection("playlists"); - -const { crearPlaylist, eliminarPlaylist, playPlaylist, playCheckPlaylist, mostrarPlaylists, addCancionPlaylist, eliminarCancionPlaylist } = require("../utils/playlistController.js"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("playlist") - .setDescription("Gestiona playlists con opciones para crear, editar y eliminar") - .addSubcommand(sub => - sub.setName("create") - .setDescription("Crea una playlist") - .addStringOption(option => - option.setName("name") - .setDescription("Nombre de la playlist") - .setRequired(true), - ), - ) - .addSubcommand(sub => - sub.setName("play") - .setDescription("Reproduce una playlist") - .addStringOption(option => - option.setName("name") - .setDescription("Nombre de la playlist") - .setRequired(true) - .setAutocomplete(true), - ), - ) - .addSubcommand(sub => - sub.setName("remove") - .setDescription("Elimina una playlist") - .addStringOption(option => - option.setName("name") - .setDescription("Nombre de la playlist") - .setRequired(true) - .setAutocomplete(true), - ), - ) - .addSubcommand(sub => - sub.setName("list") - .setDescription("Muestra todas las playlists"), - ) - .addSubcommand(sub => - sub.setName("add") - .setDescription("Añade una canción a una playlist") - .addStringOption(option => - option.setName("playlist") - .setDescription("Nombre de la playlist") - .setRequired(true) - .setAutocomplete(true), - ) - .addStringOption(option => - option.setName("url") - .setDescription("Url de la cancion") - .setRequired(true), - ), - ) - .addSubcommand(sub => - sub.setName("delete") - .setDescription("Elimina una canción de una playlist") - .addStringOption(option => - option.setName("playlist") - .setDescription("Nombre de la playlist") - .setRequired(true) - .setAutocomplete(true), - ) - .addStringOption(option => - option.setName("name") - .setDescription("nombre de la cancion") - .setRequired(true) - .setAutocomplete(true), - ), - ), - - async autocomplete(interaction) { - await mongo.connect(); - try { - const guildId = interaction.guildId; - const playlists = await coleccion.find({ serverId: guildId }).toArray(); - const focusedValue = interaction.options.getFocused() || ""; - const { options } = interaction; - - const obtenerPlaylistNombres = () => { - const playlistList = []; - playlists.forEach((doc) => { - Object.keys(doc).forEach((playlistName) => { - if (playlistName !== "serverId" && playlistName !== "_id") { - playlistList.push(playlistName); - } - }); - }); - return playlistList; - }; - - switch (options.getSubcommand()) { - case "remove": - case "add": { - let filteredPlaylist = obtenerPlaylistNombres().filter(playlistName => - String(playlistName).toLowerCase().startsWith(focusedValue.toLowerCase()), - ); - - var playlistsRespuesta = filteredPlaylist.map(playlist => ({ name: playlist, value: playlist })); - - if (playlistsRespuesta.length === 0 && focusedValue !== "") { - playlistsRespuesta.push({ name: "No hay playlists que empiecen por " + focusedValue.toLowerCase(), value: "none" }); - } else if (filteredPlaylist.length === 0) { - playlistsRespuesta.push({ name: "No existen playlists en este servidor", value: "none" }); - } - - await interaction.respond(playlistsRespuesta); - } - break; - case "delete": { - switch (options.getFocused(true).name) { - case "playlist": { - let filteredPlaylist = []; - let playlistList = obtenerPlaylistNombres().filter(name => { - const songs = playlists[0]?.[name]; - return Object.keys(songs).length > 0; - }); - - // Filtramos las playlists que empiezan con el texto que el usuario está escribiendo - if (focusedValue.length > 0) { - filteredPlaylist = playlistList.filter(playlistName => - String(playlistName).toLowerCase().startsWith(focusedValue.toLowerCase()), - ); - playlistsRespuesta = filteredPlaylist; - } else { - playlistsRespuesta = playlistList; - } - - var playlistsRespuesta1 = playlistsRespuesta.map(playlist => ({ name: playlist, value: playlist })); - - if (playlistsRespuesta1.length === 0) { - if (focusedValue.length > 0) { - playlistsRespuesta1.push({ name: "No hay playlists que empiecen por " + focusedValue.toLowerCase(), value: "none" }); - } else { - playlistsRespuesta1.push({ name: "No existen playlists en este servidor", value: "none" }); - } - } - - await interaction.respond(playlistsRespuesta1); - } - break; - case "name": { - const focusedPlaylistName = options.getString("playlist"); - - if (playlists) { - const songs = playlists[0]?.[focusedPlaylistName]; - - if (songs && Object.keys(songs).length > 0) { - const songOptions = Object.entries(songs).map(([title, url]) => ({ - name: `${title}`, - value: url, - })); - await interaction.respond(songOptions); - } else { - await interaction.respond([{ name: "No hay canciones en esta categoría", value: "none" }]); - } - } else { - await interaction.respond([ - { name: "No existe la playlist selecionada", value: "none" }, - ]); - } - } - } - } - break; - case "play": { - const obtenerPlaylists = obtenerPlaylistNombres().filter(name => { - const songs = playlists[0]?.[name]; - return Object.keys(songs).length > 0; - }); - - let filteredPlaylists = obtenerPlaylists.map(name => ({ - name, value: name, - })); - - if (filteredPlaylists.length === 0) { - filteredPlaylists.push({ - name: "No hay playlists que empiecen por " + focusedValue.toLowerCase(), - value: "none", - }); - } - - await interaction.respond(filteredPlaylists); - } - break; - } - } catch (error) { - console.error(error); - } finally { - await mongo.close(); - } - }, - - - run: async ({ interaction }) => { - const { options, guildId } = interaction; - const embed = new EmbedBuilder(); - const player = useMainPlayer(); - - switch (options.getSubcommand()) { - case "create": - try { - let arrayCrear = await crearPlaylist(guildId, options.getString("name")); - embed.setColor(arrayCrear["color"]) - .setDescription(arrayCrear["mensaje"]); - await interaction.reply({ embeds: [embed] }); - } catch (error) { - console.log(error); - } - break; - case "list": - try { - let arrayLista = await mostrarPlaylists(guildId); - embed.setColor(arrayLista["color"]) - .setTitle("🎶 Lista de Playlists 🎶") - .setDescription(arrayLista["mensaje"]); - await interaction.reply({ embeds: [embed] }); - } catch (error) { - console.log("Error al mostrar las playlists:" + error); - } - break; - case "add": - try { - const playlistName = options.getString("playlist"); - const url = options.getString("url"); - const result = await player.search(url, { - requestedBy: interaction.user, - }); - const track = result.tracks[0]; - - var tituloCancion; - if (track && track.title) { - tituloCancion = track.title; - } else { - tituloCancion = "Título no encontrado"; - } - - let arrayAdd = await addCancionPlaylist(guildId, url, playlistName, tituloCancion); - embed.setColor(arrayAdd["color"]) - .setDescription(arrayAdd["mensaje"]); - await interaction.reply({ embeds: [embed] }); - } catch (error) { - console.log(error); - } - break; - case "play": - try { - let arrayPlayCheck = await playCheckPlaylist(guildId, options.getString("name")); - embed.setColor(arrayPlayCheck["color"]) - .setDescription(arrayPlayCheck["mensaje"]); - await interaction.reply({ embeds: [embed] }); - if (arrayPlayCheck["color"] === "Green") { - playPlaylist(guildId, options.getString("name"), interaction); - } - } catch (error) { - console.log(error); - } - break; - case "remove": - try { - let arrayRemove = await eliminarPlaylist(guildId, options.getString("name")); - embed.setColor(arrayRemove["color"]) - .setDescription(arrayRemove["mensaje"]); - await interaction.reply({ embeds: [embed] }); - } catch (error) { - console.log(error); - } - break; - case "delete": - try { - let arrayDelete = await eliminarCancionPlaylist(guildId, options.getString("playlist"), options.getString("name")); - embed.setColor(arrayDelete["color"]) - .setDescription(arrayDelete["mensaje"]); - await interaction.reply({ embeds: [embed] }); - } catch (error) { - console.log(error); - } - break; - } - }, -}; diff --git a/src/commands/playlist.ts b/src/commands/playlist.ts new file mode 100644 index 0000000..ba5cd28 --- /dev/null +++ b/src/commands/playlist.ts @@ -0,0 +1,371 @@ +import { useMainPlayer } from "discord-player"; +import { + ApplicationCommandOptionChoiceData, + AutocompleteInteraction, + ChatInputCommandInteraction, + ColorResolvable, + EmbedBuilder, + SlashCommandBuilder, +} from "discord.js"; +import { MongoClient } from "mongodb"; + +const MONGO_URI = process.env.MONGODB_URI; +if (!MONGO_URI) { + throw new Error("La variable de entorno MONGODB_URI no está definida"); +} + +const mongo = new MongoClient(MONGO_URI); +const db = mongo.db("ritmosbot"); +const coleccion = db.collection("playlists"); + +import { + crearPlaylist, + eliminarPlaylist, + playPlaylist, + playCheckPlaylist, + mostrarPlaylists, + addCancionPlaylist, + eliminarCancionPlaylist, +} from "../utils/playlistController.js"; + +module.exports = { + data: new SlashCommandBuilder() + .setName("playlist") + .setDescription("Gestiona playlists con opciones para crear, editar y eliminar") + .addSubcommand((sub) => + sub + .setName("create") + .setDescription("Crea una playlist") + .addStringOption((option) => + option.setName("name").setDescription("Nombre de la playlist").setRequired(true), + ), + ) + .addSubcommand((sub) => + sub + .setName("play") + .setDescription("Reproduce una playlist") + .addStringOption((option) => + option + .setName("name") + .setDescription("Nombre de la playlist") + .setRequired(true) + .setAutocomplete(true), + ), + ) + .addSubcommand((sub) => + sub + .setName("remove") + .setDescription("Elimina una playlist") + .addStringOption((option) => + option + .setName("name") + .setDescription("Nombre de la playlist") + .setRequired(true) + .setAutocomplete(true), + ), + ) + .addSubcommand((sub) => sub.setName("list").setDescription("Muestra todas las playlists")) + .addSubcommand((sub) => + sub + .setName("add") + .setDescription("Añade una canción a una playlist") + .addStringOption((option) => + option + .setName("playlist") + .setDescription("Nombre de la playlist") + .setRequired(true) + .setAutocomplete(true), + ) + .addStringOption((option) => + option.setName("url").setDescription("Url de la cancion").setRequired(true), + ), + ) + .addSubcommand((sub) => + sub + .setName("delete") + .setDescription("Elimina una canción de una playlist") + .addStringOption((option) => + option + .setName("playlist") + .setDescription("Nombre de la playlist") + .setRequired(true) + .setAutocomplete(true), + ) + .addStringOption((option) => + option + .setName("name") + .setDescription("nombre de la cancion") + .setRequired(true) + .setAutocomplete(true), + ), + ), + + async autocomplete({ interaction }: { interaction: ChatInputCommandInteraction | AutocompleteInteraction }) { + await mongo.connect(); + try { + const { options } = interaction; + const guildId = interaction.guildId; + const playlists = await coleccion.find({ serverId: guildId }).toArray(); + + let focusedValue = ""; + if (interaction.isAutocomplete()) { + focusedValue = interaction.options.getFocused(); + } + + const obtenerPlaylistNombres = () => { + const playlistList: string[] = []; + playlists.forEach((doc) => { + Object.keys(doc).forEach((playlistName) => { + if (playlistName !== "serverId" && playlistName !== "_id") { + playlistList.push(playlistName); + } + }); + }); + return playlistList; + }; + + if (interaction.isAutocomplete()) { + switch (options.getSubcommand()) { + case "remove": + case "add": + { + let filteredPlaylist = obtenerPlaylistNombres().filter((playlistName) => + String(playlistName).toLowerCase().startsWith(focusedValue.toLowerCase()), + ); + + let playlistsRespuesta = filteredPlaylist.map((playlist) => ({ + name: playlist, + value: playlist, + })); + + if (playlistsRespuesta.length === 0 && focusedValue !== "") { + playlistsRespuesta.push({ + name: "No hay playlists que empiecen por " + focusedValue.toLowerCase(), + value: "none", + }); + } else if (filteredPlaylist.length === 0) { + playlistsRespuesta.push({ + name: "No existen playlists en este servidor", + value: "none", + }); + } + + await interaction.respond(playlistsRespuesta); + } + break; + case "delete": + { + switch (interaction.options.getFocused(true).name) { + case "playlist": + { + let filteredPlaylist = []; + let playlistList = obtenerPlaylistNombres().filter((name) => { + const songs = playlists[0]?.[name]; + return Object.keys(songs).length > 0; + }); + + // Filtramos las playlists que empiezan con el texto que el usuario está escribiendo + if (focusedValue.length > 0) { + filteredPlaylist = playlistList.filter((playlistName) => + String(playlistName) + .toLowerCase() + .startsWith(focusedValue.toLowerCase()), + ); + var playlistsRespuesta = filteredPlaylist; + } else { + var playlistsRespuesta = playlistList; + } + + const playlistsRespuesta1: ApplicationCommandOptionChoiceData[] = + playlistsRespuesta.map((playlist) => ({ + name: playlist, + value: playlist, + })); + if (playlistsRespuesta1.length === 0) { + if (focusedValue.length > 0) { + playlistsRespuesta1.push({ + name: + "No hay playlists que empiecen por " + + focusedValue.toLowerCase(), + value: "__no_match__", + }); + } else { + playlistsRespuesta1.push({ + name: "No existen playlists en este servidor", + value: "__empty__", + }); + } + } + + await interaction.respond(playlistsRespuesta1); + } + break; + case "name": { + const focusedPlaylistName = options.getString("playlist"); + + if (playlists && focusedPlaylistName) { + const songs = playlists[0]?.[focusedPlaylistName]; + + if (songs && Object.keys(songs).length > 0) { + const songOptions = Object.entries(songs).map(([title, url]) => ({ + name: `${title}`, + value: String(url), + })); + await interaction.respond(songOptions); + } else { + await interaction.respond([ + { name: "No hay canciones en esta categoría", value: "none" }, + ]); + } + } else { + if (interaction.isAutocomplete()) { + await interaction.respond([ + { name: "No existe la playlist selecionada", value: "none" }, + ]); + } + } + } + } + } + break; + case "play": + { + const obtenerPlaylists = obtenerPlaylistNombres().filter((name) => { + const songs = playlists[0]?.[name]; + return Object.keys(songs).length > 0; + }); + + let filteredPlaylists = obtenerPlaylists.map((name) => ({ + name, + value: name, + })); + + if (filteredPlaylists.length === 0) { + filteredPlaylists.push({ + name: "No hay playlists que empiecen por " + focusedValue.toLowerCase(), + value: "none", + }); + } + await interaction.respond(filteredPlaylists); + } + break; + } + } + } catch (error) { + console.error(error); + } finally { + await mongo.close(); + } + }, + + run: async ({ interaction }: { interaction: ChatInputCommandInteraction }) => { + const { options, guildId } = interaction; + const embed = new EmbedBuilder(); + const player = useMainPlayer(); + + switch (options.getSubcommand()) { + case "create": + try { + let arrayCrear = await crearPlaylist(guildId, options.getString("name")); + if (arrayCrear) { + embed + .setColor(arrayCrear["color"].toUpperCase() as ColorResolvable) + .setDescription(arrayCrear["mensaje"]); + await interaction.reply({ embeds: [embed] }); + } + } catch (error) { + console.log(error); + } + break; + case "list": + try { + let arrayLista = await mostrarPlaylists(guildId); + embed + .setColor(arrayLista["color"].toUpperCase() as ColorResolvable) + .setTitle("🎶 Lista de Playlists 🎶") + .setDescription(arrayLista["mensaje"]); + await interaction.reply({ embeds: [embed] }); + } catch (error) { + console.log("Error al mostrar las playlists:" + error); + } + break; + case "add": + try { + const playlistName = options.getString("playlist"); + const url = options.getString("url"); + + if (!playlistName || !url) { + await interaction.reply("❌| Faltan el nombre de la playlist o la url"); + return; + } + + const result = await player.search(url, { + requestedBy: interaction.user, + }); + const track = result.tracks[0]; + + var tituloCancion; + if (track && track.title) { + tituloCancion = track.title; + } else { + tituloCancion = "Título no encontrado"; + } + + let arrayAdd = (await addCancionPlaylist(guildId, url, playlistName, tituloCancion)) ?? { + color: "RED", + mensaje: "Error inesperado.", + }; + embed + .setColor(arrayAdd["color"].toUpperCase() as ColorResolvable) + .setDescription(arrayAdd["mensaje"]); + await interaction.reply({ embeds: [embed] }); + } catch (error) { + console.log(error); + } + break; + case "play": + try { + let arrayPlayCheck = (await playCheckPlaylist(guildId, options.getString("name"))) ?? { + color: "RED", + mensaje: "Error inesperado.", + }; + embed + .setColor(arrayPlayCheck["color"].toUpperCase() as ColorResolvable) + .setDescription(arrayPlayCheck["mensaje"]); + await interaction.reply({ embeds: [embed] }); + if (arrayPlayCheck["color"] === "Green") { + playPlaylist(guildId, options.getString("name"), interaction); + } + } catch (error) { + console.log(error); + } + break; + case "remove": + try { + let arrayRemove = await eliminarPlaylist(guildId, options.getString("name")); + embed + .setColor(arrayRemove["color"].toUpperCase() as ColorResolvable) + .setDescription(arrayRemove["mensaje"]); + await interaction.reply({ embeds: [embed] }); + } catch (error) { + console.log(error); + } + break; + case "delete": + try { + let arrayDelete = (await eliminarCancionPlaylist( + guildId, + options.getString("playlist"), + options.getString("name"), + )) ?? { color: "RED", mensaje: "Error inesperado." }; + embed + .setColor(arrayDelete["color"].toUpperCase() as ColorResolvable) + .setDescription(arrayDelete["mensaje"]); + await interaction.reply({ embeds: [embed] }); + } catch (error) { + console.log(error); + } + break; + } + }, +}; diff --git a/src/commands/queue.js b/src/commands/queue.js deleted file mode 100644 index 4b83ede..0000000 --- a/src/commands/queue.js +++ /dev/null @@ -1,58 +0,0 @@ -const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); -const { useMainPlayer } = require("discord-player"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("queue") - .setDescription("Muestra la cola de canciones actual"), - - run: async ({ interaction }) => { - const { member } = interaction; - const voiceChannel = member.voice.channel; - const embed = new EmbedBuilder(); - - if (!voiceChannel) { - embed.setColor("Red").setDescription("¡Debes estar en el canal de voz para usar este comando!"); - return interaction.reply({ embeds: [embed] }); - } else { - const player = useMainPlayer(); - const queue = player.nodes.get(interaction.guild.id); - await interaction.deferReply(); - - if (!queue || !queue.currentTrack) { - embed.setColor("Red").setDescription("No hay ninguna canción en la cola o reproduciendose"); - return await interaction.followUp({ embeds: [embed] }); - } else { - try { - const cancionesLimite = 20; - const tracksArray = queue.tracks.data; - var totalCanciones = queue.tracks.size; - var description = ""; - - if (totalCanciones >= 1) { - description = tracksArray.slice(0, cancionesLimite).map((song, id) => - `🎶 **${id + 1}.** ${song.title} - \`${song.duration}\``).join("\n"); - totalCanciones = totalCanciones + 1; - } else { - description = "No hay canciones en la cola"; - } - - const currentTrack = queue.currentTrack; - - return await interaction.followUp({ - embeds: [embed - .setColor("Blue") - .setDescription(`💿 **Está reproduciéndose 💿**\n${currentTrack.title} - ${currentTrack.duration}\n\n **Cola** \n` + description) - .setThumbnail(currentTrack.thumbnail) - .setFooter({ text: `Total de canciones: ${totalCanciones}` }), - ], - }); - } catch (error) { - console.log("Error al mostrar la cola de canciones:", error); - embed.setColor("Red").setDescription("Error al intentar mostrar la cola de canciones"); - await interaction.followUp({ embeds: [embed] }); - } - } - } - }, -}; diff --git a/src/commands/queue.ts b/src/commands/queue.ts new file mode 100644 index 0000000..735db3c --- /dev/null +++ b/src/commands/queue.ts @@ -0,0 +1,65 @@ +import { ChatInputCommandInteraction, EmbedBuilder, GuildMember, SlashCommandBuilder } from "discord.js"; +import { useMainPlayer } from "discord-player"; + +module.exports = { + data: new SlashCommandBuilder().setName("queue").setDescription("Muestra la cola de canciones actual"), + + run: async ({ interaction }: { interaction: ChatInputCommandInteraction }) => { + const { member } = interaction; + const embed = new EmbedBuilder(); + + if (member instanceof GuildMember) { + const voiceChannel = member.voice.channel; + + if (!voiceChannel) { + embed.setColor("Red").setDescription("¡Debes estar en el canal de voz para usar este comando!"); + return interaction.reply({ embeds: [embed] }); + } else { + const player = useMainPlayer(); + const queue = player.nodes.get(interaction.guild!.id); + await interaction.deferReply(); + + if (!queue || !queue.currentTrack) { + embed.setColor("Red").setDescription("No hay ninguna canción en la cola o reproduciendose"); + return await interaction.followUp({ embeds: [embed] }); + } else { + try { + const cancionesLimite = 20; + const tracksArray = queue.tracks.data; + var totalCanciones = queue.tracks.size; + var description = ""; + + if (totalCanciones >= 1) { + description = tracksArray + .slice(0, cancionesLimite) + .map((song, id) => `🎶 **${id + 1}.** ${song.title} - \`${song.duration}\``) + .join("\n"); + totalCanciones = totalCanciones + 1; + } else { + description = "No hay canciones en la cola"; + } + + const currentTrack = queue.currentTrack; + + return await interaction.followUp({ + embeds: [ + embed + .setColor("Blue") + .setDescription( + `💿 **Está reproduciéndose 💿**\n${currentTrack.title} - ${currentTrack.duration}\n\n **Cola** \n` + + description, + ) + .setThumbnail(currentTrack.thumbnail) + .setFooter({ text: `Total de canciones: ${totalCanciones}` }), + ], + }); + } catch (error) { + console.log("Error al mostrar la cola de canciones:", error); + embed.setColor("Red").setDescription("Error al intentar mostrar la cola de canciones"); + await interaction.followUp({ embeds: [embed] }); + } + } + } + } + }, +}; diff --git a/src/commands/shuffle.js b/src/commands/shuffle.js deleted file mode 100644 index 3858a7e..0000000 --- a/src/commands/shuffle.js +++ /dev/null @@ -1,34 +0,0 @@ -const { EmbedBuilder, SlashCommandBuilder } = require("discord.js"); -const { useMainPlayer } = require("discord-player"); - -module.exports = { - data: new SlashCommandBuilder() - .setName("shuffle") - .setDescription("Mezcla las canciones de la cola actual"), - - run: async ({ interaction }) => { - const { member } = interaction; - const voiceChannel = member.voice.channel; - const embed = new EmbedBuilder(); - - if (!voiceChannel) { - embed.setColor("Red").setDescription("¡Debes estar en el canal de voz para usar este comando!"); - return interaction.reply({ embeds: [embed] }); - } else { - const player = useMainPlayer(); - const queue = player.nodes.get(interaction.guild.id); - - if (!queue) { - embed.setColor("Red").setDescription("No hay ninguna canción en la cola"); - return await interaction.reply({ embeds: [embed] }); - } else if (!queue.tracks.size) { - embed.setColor("Red").setDescription("No hay más canciones en la cola"); - return await interaction.reply({ embeds: [embed] }); - } else { - queue.tracks.shuffle(); - embed.setColor("Blue").setDescription("¡La cola ha sido mezclada!"); - return await interaction.reply({ embeds: [embed] }); - } - } - }, -}; diff --git a/src/commands/shuffle.ts b/src/commands/shuffle.ts new file mode 100644 index 0000000..a14b076 --- /dev/null +++ b/src/commands/shuffle.ts @@ -0,0 +1,35 @@ +import { EmbedBuilder, SlashCommandBuilder, ChatInputCommandInteraction, GuildMember } from "discord.js"; +import { useMainPlayer } from "discord-player"; + +module.exports = { + data: new SlashCommandBuilder().setName("shuffle").setDescription("Mezcla las canciones de la cola actual"), + + run: async ({ interaction }: { interaction: ChatInputCommandInteraction }) => { + const { member } = interaction; + + if (member instanceof GuildMember) { + const voiceChannel = member.voice.channel; + const embed = new EmbedBuilder(); + + if (!voiceChannel) { + embed.setColor("Red").setDescription("¡Debes estar en el canal de voz para usar este comando!"); + return interaction.reply({ embeds: [embed] }); + } else { + const player = useMainPlayer(); + const queue = player.nodes.get(interaction.guild!.id); + + if (!queue) { + embed.setColor("Red").setDescription("No hay ninguna canción en la cola"); + return await interaction.reply({ embeds: [embed] }); + } else if (!queue.tracks.size) { + embed.setColor("Red").setDescription("No hay más canciones en la cola"); + return await interaction.reply({ embeds: [embed] }); + } else { + queue.tracks.shuffle(); + embed.setColor("Blue").setDescription("¡La cola ha sido mezclada!"); + return await interaction.reply({ embeds: [embed] }); + } + } + } + }, +}; diff --git a/tests/shuffle.test.js b/tests/shuffle.test.js index 772ec95..d87fe04 100644 --- a/tests/shuffle.test.js +++ b/tests/shuffle.test.js @@ -1,9 +1,11 @@ +// Mockeamos discord-player jest.mock("discord-player", () => ({ useMainPlayer: jest.fn(), })); const shuffleCommand = require("../src/commands/shuffle"); const { useMainPlayer } = require("discord-player"); +const { GuildMember, User } = require("discord.js"); const RED = 15548997; const BLUE = 3447003; @@ -13,18 +15,36 @@ const SHUFFLE_TEST = { GUILD_ID: "test-guild-id", }; -const createInteraction = (voiceChannel = null) => ({ - guild: { id: SHUFFLE_TEST.GUILD_ID }, - options: { - getString: jest.fn(), - }, - reply: jest.fn(), - member: { - voice: { - channel: voiceChannel, - }, - }, -}); +// Creamos un mock de GuildMember que pase el instanceof +class FakeGuildMember extends GuildMember { + constructor() { + super(null, null); + this._voiceChannel = null; + } + + get voice() { + return { channel: this._voiceChannel }; + } + + setVoiceChannel(channel) { + this._voiceChannel = channel; + } +} + +const createInteraction = (voiceChannel = null) => { + const member = new FakeGuildMember(); + member.setVoiceChannel(voiceChannel); + + const user = new User(null, { id: "user-id", username: "TestUser" }); + + return { + guild: { id: SHUFFLE_TEST.GUILD_ID }, + member, + user, + reply: jest.fn(), + channel: { id: "text-channel-id" }, + }; +}; const VOICE_CHANNEL = { id: SHUFFLE_TEST.VOICE_CHANNEL_ID, @@ -33,36 +53,44 @@ const VOICE_CHANNEL = { describe("/shuffle command", () => { let playerMock; let queueMock; - let tracksMock; beforeEach(() => { jest.clearAllMocks(); - tracksMock = { - shuffle: jest.fn(), - size: 0, + // Aquí creamos un mock de tracks con shuffle y size + const createTracksMock = (size) => { + return { + size, + shuffle: jest.fn(), + }; }; + queueMock = { - tracks: tracksMock, + tracks: createTracksMock(1), }; + playerMock = { - nodes: new Map(), + nodes: new Map([[SHUFFLE_TEST.GUILD_ID, queueMock]]), }; - playerMock.nodes.set(SHUFFLE_TEST.GUILD_ID, queueMock); + useMainPlayer.mockReturnValue(playerMock); }); test("Intenta hacer el shuffle y responde con un mensaje de error", async () => { + playerMock.nodes.clear(); + const interaction = createInteraction(VOICE_CHANNEL); await shuffleCommand.run({ interaction }); expect(interaction.reply).toHaveBeenCalledWith({ - embeds: [{ - data: { - color: RED, - description: "No hay más canciones en la cola", + embeds: [ + { + data: { + color: RED, + description: "No hay ninguna canción en la cola", + }, }, - }], + ], }); }); @@ -72,57 +100,70 @@ describe("/shuffle command", () => { await shuffleCommand.run({ interaction }); expect(interaction.reply).toHaveBeenCalledWith({ - embeds: [{ - data: { - color: RED, - description: "No hay ninguna canción en la cola", + embeds: [ + { + data: { + color: RED, + description: "No hay ninguna canción en la cola", + }, }, - }], + ], }); }); - test("Hace el shuffle con una canción", async () => { - tracksMock.size = 1; + queueMock.tracks.size = 1; + const interaction = createInteraction(VOICE_CHANNEL); await shuffleCommand.run({ interaction }); + expect(queueMock.tracks.shuffle).toHaveBeenCalled(); + expect(interaction.reply).toHaveBeenCalledWith({ - embeds: [{ - data: { - color: BLUE, - description: "¡La cola ha sido mezclada!", + embeds: [ + { + data: { + color: BLUE, + description: "¡La cola ha sido mezclada!", + }, }, - }], + ], }); }); test("Hace el shuffle con varias canciones", async () => { - tracksMock.size = 4; + queueMock.tracks.size = 5; + const interaction = createInteraction(VOICE_CHANNEL); await shuffleCommand.run({ interaction }); + expect(queueMock.tracks.shuffle).toHaveBeenCalled(); + expect(interaction.reply).toHaveBeenCalledWith({ - embeds: [{ - data: { - color: BLUE, - description: "¡La cola ha sido mezclada!", + embeds: [ + { + data: { + color: BLUE, + description: "¡La cola ha sido mezclada!", + }, }, - }], + ], }); }); - test("Muestra el embed de error al no estar el usuario en el canal de voz", async () => { - const interaction = createInteraction(); + test("Error si el usuario no está en el canal de voz", async () => { + const interaction = createInteraction(null); await shuffleCommand.run({ interaction }); expect(interaction.reply).toHaveBeenCalledWith({ - embeds: [{ - data: { - color: RED, - description: "¡Debes estar en el canal de voz para usar este comando!", + embeds: [ + { + data: { + color: RED, + description: "¡Debes estar en el canal de voz para usar este comando!", + }, }, - }], + ], }); }); });