diff --git a/settings.json b/settings.json index e98b3dd..ba0e94f 100644 --- a/settings.json +++ b/settings.json @@ -1 +1 @@ -{"requestChannels":{"777923126981558282":{"channelId":"1219764003421556816","messageId":"1296249229009752138"}}} \ No newline at end of file +{"requestChannels":{"777923126981558282":{"channelId":"1334001448832335945","messageId":"1334034171164753973"}}} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index a9f3c13..347520f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ +// Import discord.js import { - type APIEmbed, ActionRowBuilder, ButtonBuilder, ButtonStyle, @@ -8,36 +8,59 @@ import { EmbedBuilder, GatewayIntentBits, type Message, - SlashCommandBuilder, Interaction, -} from "npm:discord.js"; +} from "discord.js"; -import { Connectors, Track } from "shoukaku"; +// Import shoukaku +import { Connectors } from "shoukaku"; +// Import kazagumo import { Kazagumo, type KazagumoPlayer, type KazagumoTrack, type Payload, -} from "npm:kazagumo"; +} from "kazagumo"; -import KazagumoPlugin from "npm:kazagumo-spotify"; -import prettyMilliseconds from "npm:pretty-ms"; +// Import spotify and deezer plugins +import KazagumoPlugin from "kazagumo-spotify"; +import Deezer from "kazagumo-deezer"; +// Import pretty-ms for better time formatting +import prettyMilliseconds from "pretty-ms"; + +// Import dotenv +import * as dotenv from "dotenv"; + +// Enable writing and reading files +import { readFileSync, writeFileSync } from "fs"; + +//Import process +import process from "process"; + +// Set GatewayIntents const { Guilds, GuildVoiceStates, GuildMessages, MessageContent } = GatewayIntentBits; -import * as dotenv from "npm:dotenv"; -import { readFileSync, writeFileSync } from "node:fs"; - -import process from "node:process"; -import Deezer from "npm:kazagumo-deezer"; +// Setup dotenv dotenv.config(); +// Check if spotify is setup if (!process.env.SPOTIFY_ID || !process.env.SPOTIFY_SECRET) { throw "Spotify Credentials missing"; } +// Define settings interface +interface requestChannels { + [guildId: string]: { + channelId: string; + messageId: string; + }; +} +interface settings { + requestChannels: requestChannels; +} +// Initialize and load settings from file let settings: settings; try { @@ -46,6 +69,7 @@ try { settings = { requestChannels: {} }; // Init if no data found } +// Setup lavalink config const Nodes = [ { name: "lavalink", @@ -55,19 +79,26 @@ const Nodes = [ }, ]; +// Setup discord client const client = new Client({ intents: [Guilds, GuildVoiceStates, GuildMessages, MessageContent], }); +// Setup kazagumo const kazagumo = new Kazagumo( { + // Set youtube as fallback search engine defaultSearchEngine: "youtube", + + // Setup support for sharding i guess? Only here cause the shoukaku example says its important send: (guildId: string, payload: Payload) => { const guild = client.guilds.cache.get(guildId); if (guild) guild.shard.send(payload); }, + + // Setup plugins plugins: [ - new Deezer({playlistLimit:20}), + new Deezer(), new KazagumoPlugin({ clientId: process.env.SPOTIFY_ID, clientSecret: process.env.SPOTIFY_SECRET, @@ -82,32 +113,102 @@ const kazagumo = new Kazagumo( Nodes ); -interface requestChannels { - [guildId: string]: { - channelId: string; - messageId: string; - }; -} -interface settings { - requestChannels: requestChannels; -} + +// Setup empty embed +const emptyEmbed = new EmbedBuilder() + .setAuthor({ + name: "Moe", + iconURL: "https://cdn.m3.fyi/MoeLogo.gif", + }) + .setTitle("Nothing is being played right now") + .setDescription("Enter message to search") + .addFields( + { + name: "Author", + value: "None", + inline: true, + }, + { + name: "Duration", + value: "NaN:NaN", + inline: true, + } + ) + .setColor("#ff0047"); +// Setup buttons +const play = new ButtonBuilder() + .setCustomId("play") + .setLabel("Play/Pause") + .setStyle(ButtonStyle.Secondary) + .setEmoji("889943073793122355"); + +const stop = new ButtonBuilder() + .setCustomId("stop") + .setLabel("Stop Playing") + .setStyle(ButtonStyle.Secondary) + .setEmoji("889943074258694184"); + +const skip = new ButtonBuilder() + .setCustomId("skip") + .setLabel("Skip Song") + .setStyle(ButtonStyle.Secondary) + .setEmoji("889943074233516072"); + +const loop = new ButtonBuilder() + .setCustomId("loop") + .setLabel("Loop Song") + .setStyle(ButtonStyle.Secondary) + .setEmoji("889943073667289099"); + +const shuffle = new ButtonBuilder() + .setCustomId("shuffle") + .setLabel("Shuffle Queue") + .setStyle(ButtonStyle.Secondary) + .setEmoji("890325437962678352"); + +const seek = new ButtonBuilder() + .setCustomId("seek") + .setLabel("Seek Forward") + .setStyle(ButtonStyle.Secondary) + .setEmoji("890325511878889504"); + +const previous = new ButtonBuilder() + .setCustomId("previous") + .setLabel("Previous Song") + .setStyle(ButtonStyle.Secondary) + .setEmoji("890325512071831562"); + +// Add buttons to action row +const row = new ActionRowBuilder().addComponents( + play, + stop, + skip, + loop +); +const row2 = new ActionRowBuilder().addComponents( + shuffle, + seek, + previous +); client.on("ready", () => { - // console.log(client) console.log(`${client.user!.tag}·Ready!`); }); kazagumo.shoukaku.on("ready", (name: string) => console.log(`Lavalink ${name}: Ready!`) ); + kazagumo.shoukaku.on("error", (name: string, error: Error) => console.error(`Lavalink ${name}: Error Caught,`, error) ); + kazagumo.shoukaku.on("close", (name: string, code: unknown, reason: unknown) => console.warn( `Lavalink ${name}: Closed, Code ${code}, Reason ${reason || "No reason"}` ) ); + kazagumo.shoukaku.on("debug", (name: string, info: string) => console.debug(`Lavalink ${name}: Debug,`, info) ); @@ -118,156 +219,119 @@ kazagumo.shoukaku.on("disconnect", (name: string, count: number) => { console.warn(`Lavalink ${name}: Disconnected`); }); + kazagumo.on("playerStart", (player: KazagumoPlayer, track: KazagumoTrack) => { + // Exit if player is not initialized yet if (!player.textId) return; const channel = client.channels.cache.get(player.textId); if (!channel) return; if (channel.type === ChannelType.GuildText) { - channel - .send({ - content: `Now playing **${track.title}** by **${track.author}**`, - }) + // TODO:Also update player embed + channel.send({ + content: `Now playing **${track.title}** by **${track.author}**`, + }) .then((x: any) => { - //TODO: Use embed message instead - player.data.set("message", x); setTimeout(() => x.delete(), 3000); }); } }); +// Called when track ends kazagumo.on("playerEnd", (player: KazagumoPlayer) => { - //TODO: Use embed message instead + // Replace old embed with embed of next track + let currentTrack = player.queue[0] + + let track_author: string; + if (currentTrack.author) { + track_author = currentTrack.author; + } else { + track_author = ""; + } + let track_duration: string; + if (currentTrack.length) { + track_duration = prettyMilliseconds(currentTrack.length); + } else { + track_duration = "NaN:NaN"; + } + let track_thumbnail: string; + if (currentTrack.thumbnail) { + track_thumbnail = currentTrack.thumbnail.toString(); + } else { + track_thumbnail = "NaN:NaN"; + } const embed = new EmbedBuilder() .setAuthor({ - name: "Moe", + name: "Now playing", iconURL: "https://cdn.m3.fyi/MoeLogo.gif", }) - .setTitle("Nothing is being played right now") - .setDescription("Enter message to search") + .setTitle( + player.queue.length > 0 + ? `Playing ${player.queue.length} from ${player.data.get("playlist")}` + : `Playing ${currentTrack.title}` + ) + .setDescription(`[${currentTrack.title}](${currentTrack.uri})`) .addFields( { name: "Author", - value: "None", + value: track_author, inline: true, }, { name: "Duration", - value: "NaN:NaN", + value: track_duration, inline: true, } ) + .setThumbnail(track_thumbnail) .setColor("#ff0047"); - - player.data.get("message")?.edit({ embed: [embed] }); + player.data.get("message")?.edit({ embeds: [embed] }); }); +// Called when disconnecting kazagumo.on("playerEmpty", (player: KazagumoPlayer) => { + + // Return if player is not initialized if (!player.textId) return; const channel = client.channels.cache.get(player.textId); if (!channel) return; if (channel.type === ChannelType.GuildText) { - //TODO: Use embed message instead - channel - .send({ content: "Destroyed player due to inactivity." }) + + // Replace old embed with empty one + // also needed here as playerEnd doesn't get called if only one song is in the queue + player.data.get("message")?.edit({ embeds: [emptyEmbed] }); + + // Send message before disconnecting + channel.send({ content: "Destroyed player due to inactivity." }) .then((x: any) => { - player.data.set("message", x); setTimeout(() => x.delete(), 3000); }); } player.destroy(); }); +// TODO: Better command handling + +// Called when a new message is send client.on("messageCreate", async (msg: Message) => { - console.log(msg.content); - if (msg.author.bot) return; - if (!msg.guild) return; - if (!msg.member) return; - if (msg.channel.type !== ChannelType.GuildText) return; + if (msg.author.bot) return; // Ignore bot messages + if (!msg.guild) return; // Ignore dms + if (!msg.member) return; // Ignore Interactions + if (msg.channel.type !== ChannelType.GuildText) return; // Only listen in text channels + + // If message contains command prefix if (msg.content.startsWith(".")) { + // Split prefix for easier comparison let cmd = msg.content.split(".")[1]; switch (cmd) { + + // Initialize channel on server case "init": if (msg.guildId && msg.channelId) { if (!settings.requestChannels[msg.guildId]) { await msg.delete(); - const play = new ButtonBuilder() - .setCustomId("play") - .setLabel("Play/Pause") - .setStyle(ButtonStyle.Secondary) - .setEmoji("889943073793122355"); - - const stop = new ButtonBuilder() - .setCustomId("stop") - .setLabel("Stop Playing") - .setStyle(ButtonStyle.Secondary) - .setEmoji("889943074258694184"); - - const skip = new ButtonBuilder() - .setCustomId("skip") - .setLabel("Skip Song") - .setStyle(ButtonStyle.Secondary) - .setEmoji("889943074233516072"); - - const loop = new ButtonBuilder() - .setCustomId("loop") - .setLabel("Loop Song") - .setStyle(ButtonStyle.Secondary) - .setEmoji("889943073667289099"); - - const shuffle = new ButtonBuilder() - .setCustomId("shuffle") - .setLabel("Shuffle Queue") - .setStyle(ButtonStyle.Secondary) - .setEmoji("890325437962678352"); - - const seek = new ButtonBuilder() - .setCustomId("seek") - .setLabel("Seek Forward") - .setStyle(ButtonStyle.Secondary) - .setEmoji("890325511878889504"); - - const previous = new ButtonBuilder() - .setCustomId("previous") - .setLabel("Previous Song") - .setStyle(ButtonStyle.Secondary) - .setEmoji("890325512071831562"); - - const row = new ActionRowBuilder().addComponents( - play, - stop, - skip, - loop - ); - const row2 = new ActionRowBuilder().addComponents( - shuffle, - seek, - previous - ); - - const embed = new EmbedBuilder() - .setAuthor({ - name: "Moe", - iconURL: "https://cdn.m3.fyi/MoeLogo.gif", - }) - .setTitle("Nothing is being played right now") - .setDescription("Enter message to search") - .addFields( - { - name: "Author", - value: "None", - inline: true, - }, - { - name: "Duration", - value: "NaN:NaN", - inline: true, - } - ) - .setColor("#ff0047"); - const response = await msg.channel.send({ - embeds: [embed], + embeds: [emptyEmbed], components: [row, row2], }); @@ -294,29 +358,26 @@ client.on("messageCreate", async (msg: Message) => { } return; } - if (msg.channel.name === "moe-song-requests") { + // Return if guild has not setup bot yet + if (!Object.keys(settings.requestChannels).includes(msg.guild.id)) { + return + } + // If the msg channel is the request channel, use message as search query + if (msg.channel.id === settings.requestChannels[msg.guild.id].channelId) { let controls; - if (!settings.requestChannels[msg.guild.id]) { - let answer = await msg.reply("Use .init first"); - console.log(answer); - setTimeout(() => msg.delete(), 3000); - setTimeout(() => answer.delete(), 3000); - return; - } else { - let channelId = settings.requestChannels[msg.guild.id].channelId; - let messageId = settings.requestChannels[msg.guild.id].messageId; - let channel = msg.guild.channels.cache.get(channelId); - if (channel && channel.type === ChannelType.GuildText) { - controls = await channel.messages.fetch(messageId); - } - // let controls = msg.channel.guild - // get(requestChannels[msg.guild.id]); - // client.channels.fetch(requestChannels[msg.guild.id]) + let channelId = settings.requestChannels[msg.guild.id].channelId; + let messageId = settings.requestChannels[msg.guild.id].messageId; + let requestChannel = msg.guild.channels.cache.get(channelId); + if (requestChannel && requestChannel.type === ChannelType.GuildText) { + controls = await requestChannel.messages.fetch(messageId); } + // let controls = msg.channel.guild + // get(requestChannels[msg.guild.id]); + // client.channels.fetch(requestChannels[msg.guild.id]) const query = msg.content; - const { channel } = msg.member.voice; - if (!channel) { + let voiceChannel = msg.member.voice.channel; + if (!voiceChannel) { let answer = await msg.reply( "You need to be in a voice channel to use this command!" ); @@ -328,19 +389,15 @@ client.on("messageCreate", async (msg: Message) => { const player = await kazagumo.createPlayer({ guildId: msg.guild.id, textId: msg.channel.id, - voiceId: channel.id, + voiceId: voiceChannel.id, volume: 40, }); player.data.set("message", controls); - console.log("Player created, Searching with deezer"); - let result = await kazagumo.search(query, { requester: msg.author, engine: "deezer"}) - console.log(result.tracks); + let result = await kazagumo.search(query, { requester: msg.author, engine: "deezer" }) if (result.tracks.length === 0) { - console.log("Searching with youtube"); - result = await kazagumo.search(query, { requester: msg.author}) + result = await kazagumo.search(query, { requester: msg.author }) if (result.tracks.length === 0) { - console.log("no yt result found") let answer = await msg.reply("No results found!"); setTimeout(() => answer.delete(), 3000); return; @@ -351,60 +408,6 @@ client.on("messageCreate", async (msg: Message) => { for (const track of result.tracks) player.queue.add(track); else player.queue.add(result.tracks[0]); - const play = new ButtonBuilder() - .setCustomId("play") - .setLabel("Play/Pause") - .setStyle(ButtonStyle.Secondary) - .setEmoji("889943073793122355"); - - const stop = new ButtonBuilder() - .setCustomId("stop") - .setLabel("Stop Playing") - .setStyle(ButtonStyle.Secondary) - .setEmoji("889943074258694184"); - - const skip = new ButtonBuilder() - .setCustomId("skip") - .setLabel("Skip Song") - .setStyle(ButtonStyle.Secondary) - .setEmoji("889943074233516072"); - - const loop = new ButtonBuilder() - .setCustomId("loop") - .setLabel("Loop Song") - .setStyle(ButtonStyle.Secondary) - .setEmoji("889943073667289099"); - - const shuffle = new ButtonBuilder() - .setCustomId("shuffle") - .setLabel("Shuffle Queue") - .setStyle(ButtonStyle.Secondary) - .setEmoji("890325437962678352"); - - const seek = new ButtonBuilder() - .setCustomId("seek") - .setLabel("Seek Forward") - .setStyle(ButtonStyle.Secondary) - .setEmoji("890325511878889504"); - - const previous = new ButtonBuilder() - .setCustomId("previous") - .setLabel("Previous Song") - .setStyle(ButtonStyle.Secondary) - .setEmoji("890325512071831562"); - - const row = new ActionRowBuilder().addComponents( - play, - stop, - skip, - loop - ); - const row2 = new ActionRowBuilder().addComponents( - shuffle, - seek, - previous - ); - let track_author: string; if (result.tracks[0].author) { track_author = result.tracks[0].author; @@ -448,11 +451,11 @@ client.on("messageCreate", async (msg: Message) => { ) .setThumbnail(track_thumbnail) .setColor("#ff0047"); - + if (result.type === "PLAYLIST") player.data.set("playlist", result.playlistName); if (!player.playing && !player.paused) player.play(); if (controls) { - const response = controls.edit({ + controls.edit({ embeds: [embed], components: [row, row2], }); @@ -464,7 +467,6 @@ client.on("messageCreate", async (msg: Message) => { }); client.on("interactionCreate", async (interaction: Interaction) => { - console.log(interaction.id); if (interaction.isButton()) { const guild = client.guilds.cache.get(interaction.guildId!); const member = guild?.members.cache.get(interaction.member!.user.id); @@ -477,69 +479,83 @@ client.on("interactionCreate", async (interaction: Interaction) => { // volume: 40 // }) switch (interaction.customId) { + // Toggle Play/Pause case "play": player.pause(!player.paused); await interaction.reply({ - ephemeral: true, + flags: "Ephemeral", content: "Toggled Pause", }); break; + + // Stop playing and disconnect case "stop": - // player.disconnect(); + + player.data.get("message")?.edit({ embeds: [emptyEmbed] }); player.destroy(); await interaction.reply({ - ephemeral: true, + flags: "Ephemeral", content: "Stopped Playing", }); break; + + // Skip currently playing song case "skip": player.skip(); await interaction.reply({ - ephemeral: true, + flags: "Ephemeral", content: "Skipped Song", }); break; + + // Skip back to previously playing song + case "previous": + let track = player.getPrevious(true); + await interaction.reply({ + flags: "Ephemeral", + content: "Queued previous song", + }); + player.play(track); + break; + + // Enable/Disable Looping case "loop": if (player.loop != "none") { player.setLoop("none"); await interaction.reply({ - ephemeral: true, + flags: "Ephemeral", content: "Disabled Looping", }); } else { player.setLoop("track"); await interaction.reply({ - ephemeral: true, + flags: "Ephemeral", content: "Started Looping", }); } break; + + // Shuffle current queue case "shuffle": player.queue.shuffle(); await interaction.reply({ - ephemeral: true, + flags: "Ephemeral", content: "Shuffled Queue", }); break; + + // TODO: Skip forward 5 seconds case "seek": // let position = player.position / 1000; // position = position | 0; // console.log(position); // player.seek(position + 5); await interaction.reply({ - ephemeral: true, + flags: "Ephemeral", content: "Does not work :pensive:", // content: "Skipped 5s forward", }); break; - case "previous": - let track = player.getPrevious(); - await interaction.reply({ - ephemeral: true, - content: "Queued previous song", - }); - player.play(track[0]); - break; default: break;