commit d343af3b14c87b33d40cf58cc89a7e271c4651b6 Author: MatteZ02 <47610069+MatteZ02@users.noreply.github.com> Date: Wed Feb 5 22:02:53 2020 +0200 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c5198c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dev/ +.env +.vscode/ +package-lock.json +src/struct/config/.env +src/struct/config/serviceAccount.json \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..c7f1f260 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "musix", + "version": "1.0.0", + "description": "V3 for Musix the discord music bot", + "main": "./src/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/MatteZ02/Musix-V3.git" + }, + "author": "Matte", + "license": "ISC", + "bugs": { + "url": "https://github.com/MatteZ02/Musix-V3/issues" + }, + "homepage": "https://github.com/MatteZ02/Musix-V3#readme", + "dependencies": { + "dblapi.js": "^2.3.1", + "discord.js": "github:discordjs/discord.js", + "dotenv": "^8.2.0", + "ffmpeg": "0.0.4", + "firebase": "^7.8.0", + "firebase-admin": "^8.9.2", + "fs": "0.0.1-security", + "he": "^1.2.0", + "ms": "^2.1.2", + "node-opus": "^0.3.3", + "prism-media": "github:hydrabolt/prism-media", + "request": "^2.88.0", + "simple-youtube-api": "^5.2.1", + "video-thumbnail-url": "^1.0.1", + "ytdl-core": "^1.0.7", + "ytdl-core-discord": "^1.1.0" + } +} diff --git a/src/commands/bug.js b/src/commands/bug.js new file mode 100644 index 00000000..d5d2d105 --- /dev/null +++ b/src/commands/bug.js @@ -0,0 +1,16 @@ +module.exports = { + name: 'bug', + alias: 'none', + usage: 'bug', + description: 'Report a bug', + onlyDev: false, + permission: 'none', + category: 'info', + async execute(msg, args, client, Discord, prefix) { + const embed = new Discord.MessageEmbed() + .setTitle(`Found a bug with ${client.user.username}?\nDM the core developer:`) + .setDescription(`Matte#0002\nOr join the support server: https://discord.gg/rvHuJtB`) + .setColor(client.config.embedColor); + msg.channel.send(embed); + }, +}; \ No newline at end of file diff --git a/src/commands/cmduses.js b/src/commands/cmduses.js new file mode 100644 index 00000000..f46dc4ae --- /dev/null +++ b/src/commands/cmduses.js @@ -0,0 +1,31 @@ +module.exports = { + name: 'cmduses', + alias: 'none', + usage: 'cmduses', + description: 'list all commands and how many times they\'ve been used', + onlyDev: true, + permission: 'dev', + category: 'info', + async execute(msg, args, client, Discord) { + const cmduses = []; + client.commands.forEach((value, key) => { + cmduses.push([key, value.uses]); + }); + cmduses.sort((a, b) => { + return b[1] - a[1]; + }); + const cmdnamelength = Math.max(...cmduses.map(x => x[0].length)) + 4; + const numberlength = Math.max(...cmduses.map(x => x[1].toString().length), 4); + const markdownrows = ['Command' + ' '.repeat(cmdnamelength - 'command'.length) + ' '.repeat(numberlength - 'uses'.length) + 'Uses']; + cmduses.forEach(x => { + if (x[1] > 0) markdownrows.push(x[0] + '.'.repeat(cmdnamelength - x[0].length) + ' '.repeat(numberlength - x[1].toString().length) + x[1].toString()); + }); + const embed = new Discord.MessageEmbed(); + embed + .setTitle('Musix Command Usage During Current Uptime') + .setDescription('```ml\n' + markdownrows.join('\n') + '\n```') + .setFooter('These statistics are from the current uptime.') + .setColor(client.config.embedColor); + msg.channel.send(embed); + }, +}; \ No newline at end of file diff --git a/src/commands/disconnect.js b/src/commands/disconnect.js new file mode 100644 index 00000000..e00b3142 --- /dev/null +++ b/src/commands/disconnect.js @@ -0,0 +1,16 @@ +module.exports = { + name: 'disconnect', + alias: 'dc', + usage: 'disconnect', + description: 'Disconnect the bot from a voice channel.', + onlyDev: false, + permission: 'MANAGE_CHANNELS', + category: 'util', + async execute(msg, args, client, Discord, prefix, command) { + const serverQueue = client.queue.get(msg.guild.id); + if (client.funcs.check(client, msg, command)) { + serverQueue.voiceChannel.leave(); + msg.channel.send('<:green_check_mark:674265384777416705> Left the voice channel!'); + } + } +}; \ No newline at end of file diff --git a/src/commands/eval.js b/src/commands/eval.js new file mode 100644 index 00000000..e24f9c92 --- /dev/null +++ b/src/commands/eval.js @@ -0,0 +1,29 @@ +module.exports = { + name: 'eval', + alias: 'e', + usage: 'eval ', + description: 'Evaluation command. DEV ONLY!', + onlyDev: true, + permission: 'dev', + category: 'util', + async execute(msg, args, client, Discord, prefix) { + const ytdl = require('ytdl-core'); + const serverQueue = client.queue.get(msg.guild.id); + let data; + if (serverQueue) { + data = await Promise.resolve(ytdl.getInfo(serverQueue.songs[0].url)); + } + const input = msg.content.slice(prefix.length + 4); + let output; + try { + output = await eval(input); + } catch (error) { + output = error.toString(); + } + const embed = new Discord.MessageEmbed() + .setTitle('Evaluation Command') + .setColor(client.config.embedColor) + .setDescription(`Input: \`\`\`js\n${input.replace(/; /g, ';').replace(/;/g, ';\n')}\n\`\`\`\nOutput: \`\`\`\n${output}\n\`\`\``); + return msg.channel.send(embed); + }, +}; diff --git a/src/commands/help.js b/src/commands/help.js new file mode 100644 index 00000000..7bef8f64 --- /dev/null +++ b/src/commands/help.js @@ -0,0 +1,36 @@ +module.exports = { + name: 'help', + alias: 'h', + usage: 'help ', + description: 'See the help for Musix.', + onlyDev: false, + permission: 'none', + category: 'info', + execute(msg, args, client, Discord, prefix, command) { + if (args[1]) { + if (!client.commands.has(args[1]) || (client.commands.has(args[1]) && client.commands.get(args[1]).omitFromHelp === true && msg.guild.id !== '489083836240494593')) return msg.channel.send('That command does not exist'); + const command = client.commands.get(args[1]); + const embed = new Discord.MessageEmbed() + .setTitle(`${client.global.db.guilds[msg.guild.id].prefix}${command.name} ${command.usage}`) + .setDescription(command.description) + .setFooter(`Command Alias: \`${command.alias}\``) + .setColor(client.config.embedColor) + msg.channel.send(embed); + } else { + const categories = []; + for (let i = 0; i < client.commands.size; i++) { + if (!categories.includes(client.commands.array()[i].category)) categories.push(client.commands.array()[i].category); + } + let commands = ''; + for (let i = 0; i < categories.length; i++) { + commands += `**ยป ${categories[i].toUpperCase()}**\n${client.commands.filter(x => x.category === categories[i] && !x.omitFromHelp).map(x => `\`${x.name}\``).join(', ')}\n`; + } + const embed = new Discord.MessageEmbed() + .setTitle(`${client.user.username} help:`) + .setDescription(commands) + .setFooter(`"${client.global.db.guilds[msg.guild.id].prefix}help " to see more information about a command.`) + .setColor(client.config.embedColor) + msg.channel.send(embed); + } + } +}; diff --git a/src/commands/invite.js b/src/commands/invite.js new file mode 100644 index 00000000..6d78cd3e --- /dev/null +++ b/src/commands/invite.js @@ -0,0 +1,16 @@ +module.exports = { + name: 'invite', + alias: 'i', + usage: 'invite', + description: 'Invite Musix.', + onlyDev: false, + permission: 'none', + category: 'info', + execute(msg, args, client, Discord, prefix) { + const embed = new Discord.MessageEmbed() + .setTitle(`Invite ${client.user.username} to your Discord server!`) + .setURL(client.config.invite) + .setColor(client.config.embedColor) + return msg.channel.send(embed); + } +}; \ No newline at end of file diff --git a/src/commands/join.js b/src/commands/join.js new file mode 100644 index 00000000..c500ae61 --- /dev/null +++ b/src/commands/join.js @@ -0,0 +1,24 @@ +module.exports = { + name: 'join', + alias: 'j', + usage: 'join', + description: 'Make Musix join the channel your channel', + onlyDev: false, + permission: 'none', + category: 'util', + async execute(msg, args, client, Discord, prefix) { + try { + const serverQueue = client.queue.get(msg.guild.id); + const voiceChannel = msg.member.voice.channel; + const connection = await voiceChannel.join(); + if (serverQueue) { + serverQueue.connection = connection; + } + msg.channel.send(`<:green_check_mark:674265384777416705> Joined ${voiceChannel.name}!`); + } catch (error) { + client.queue.delete(msg.guild.id); + client.channels.get(client.config.debug_channel).send("Error with connecting to voice channel: " + error); + return msg.channel.send(`<:redx:674263474704220182> An error occured: ${error}`); + } + } +}; \ No newline at end of file diff --git a/src/commands/loop.js b/src/commands/loop.js new file mode 100644 index 00000000..49497e29 --- /dev/null +++ b/src/commands/loop.js @@ -0,0 +1,21 @@ +module.exports = { + name: 'loop', + alias: 'none', + usage: 'loop', + description: 'loop the queue.', + onlyDev: false, + permission: 'MANAGE_MESSAGES', + category: 'music', + async execute(msg, args, client, Discord, prefix, command) { + const serverQueue = client.queue.get(msg.guild.id); + if (client.funcs.check(client, msg, command)) { + if (!serverQueue.looping) { + serverQueue.looping = true; + msg.channel.send('<:repeat1:674685561377914892> Looping the queue now!'); + } else { + serverQueue.looping = false; + msg.channel.send('<:repeat1:674685561377914892> No longer looping the queue!'); + } + } + } +}; diff --git a/src/commands/loopsong.js b/src/commands/loopsong.js new file mode 100644 index 00000000..79d4176d --- /dev/null +++ b/src/commands/loopsong.js @@ -0,0 +1,21 @@ +module.exports = { + name: 'loopsong', + alias: 'loops', + usage: 'loopsong', + description: 'loop the currently playing song.', + onlyDev: false, + permission: 'MANAGE_MESSAGES', + category: 'music', + async execute(msg, args, client, Discord, prefix, command) { + const serverQueue = client.queue.get(msg.guild.id); + if (client.funcs.check(client, msg, command)) { + if (!serverQueue.songLooping) { + serverQueue.songLooping = true; + msg.channel.send(`<:repeatsong:674685573419761716> Looping **${serverQueue.songs[0].title}** now!`); + } else { + serverQueue.songLooping = false; + msg.channel.send('<:repeatsong:674685573419761716> No longer looping the song!'); + } + } + } +}; diff --git a/src/commands/nowplaying.js b/src/commands/nowplaying.js new file mode 100644 index 00000000..a9e1560a --- /dev/null +++ b/src/commands/nowplaying.js @@ -0,0 +1,33 @@ +module.exports = { + name: 'nowplaying', + alias: 'np', + usage: 'nowplaying', + description: 'See the currently playing song position and length.', + onlyDev: false, + permission: 'none', + category: 'music', + async execute(msg, args, client, Discord, prefix) { + const getThumb = require('video-thumbnail-url'); + const ytdl = require('ytdl-core'); + const serverQueue = client.queue.get(msg.guild.id); + if (!serverQueue) return msg.channel.send('<:redx:674263474704220182> There is nothing playing.'); + if (!serverQueue.playing) return msg.channel.send('<:redx:674263474704220182> There is nothing playing.'); + let data = await Promise.resolve(ytdl.getInfo(serverQueue.songs[0].url)); + let songtime = (data.length_seconds * 1000).toFixed(0); + serverQueue.time = serverQueue.connection.dispatcher.streamTime; + let completed = (serverQueue.time.toFixed(0)); + let barlength = 30; + let completedpercent = ((completed / songtime) * barlength).toFixed(0); + let array = []; for (let i = 0; i < completedpercent - 1; i++) { array.push('โŽฏ'); } array.push('โญ—'); for (let i = 0; i < barlength - completedpercent - 1; i++) { array.push('โŽฏ'); } + const thumbnail = getThumb(serverQueue.songs[0].url); + const embed = new Discord.MessageEmbed() + .setTitle("__Now playing__") + .setDescription(`**Now playing:** ${serverQueue.songs[0].title}\n${array.join('')} | \`${client.funcs.msToTime(completed)} / ${client.funcs.msToTime(songtime)}\``) + .setFooter(`Queued by ${serverQueue.songs[0].author.tag}`) + .setURL(serverQueue.songs[0].url) + .setThumbnail(thumbnail._rejectionHandler0) + .setColor(client.config.embedColor) + return msg.channel.send(embed); + } +}; + diff --git a/src/commands/pause.js b/src/commands/pause.js new file mode 100644 index 00000000..d2853077 --- /dev/null +++ b/src/commands/pause.js @@ -0,0 +1,18 @@ +module.exports = { + name: 'pause', + alias: 'none', + usage: 'pause', + description: 'Pause the currently playing music.', + onlyDev: false, + permission: 'MANAGE_MESSAGES', + category: 'music', + execute(msg, args, client, Discord, prefix, command) { + const serverQueue = client.queue.get(msg.guild.id); + if (client.funcs.check(client, msg, command)) { + if (serverQueue.paused) return msg.channel.send('<:redx:674263474704220182> The music is already paused!'); + serverQueue.paused = true; + serverQueue.connection.dispatcher.pause(true); + return msg.channel.send('<:pause:674685548610322462> Paused the music!'); + } + } +}; diff --git a/src/commands/play.js b/src/commands/play.js new file mode 100644 index 00000000..049a6187 --- /dev/null +++ b/src/commands/play.js @@ -0,0 +1,58 @@ +const YouTube = require("simple-youtube-api"); + +module.exports = { + name: 'play', + alias: 'p', + usage: 'play ', + description: 'Play some music.', + onlyDev: false, + permission: 'none', + category: 'music', + async execute(msg, args, client, Discord, prefix) { + const youtube = new YouTube(client.config.api_key); + const searchString = args.slice(1).join(" "); + const url = args[1] ? args[1].replace(/<(.+)>/g, "$1") : ""; + const serverQueue = client.queue.get(msg.guild.id); + const voiceChannel = msg.member.voice.channel; + if (!serverQueue) { + if (!msg.member.voice.channel) return msg.channel.send('<:redx:674263474704220182> I\'m sorry but you need to be in a voice channel to play music!'); + } else { + if (voiceChannel !== serverQueue.voiceChannel) return msg.channel.send('<:redx:674263474704220182> I\'m sorry but you need to be in the same voice channel as Musix to play music!'); + } + if (!args[1]) return msg.channel.send('<:redx:674263474704220182> You need to use a link or search for a song!'); + const permissions = voiceChannel.permissionsFor(msg.client.user); + if (!permissions.has('CONNECT')) { + return msg.channel.send('<:redx:674263474704220182> I cannot connect to your voice channel, make sure I have the proper permissions!'); + } + if (!permissions.has('SPEAK')) { + return msg.channel.send('<:redx:674263474704220182> I cannot speak in your voice channel, make sure I have the proper permissions!'); + } + if (url.match(/^https?:\/\/(www.youtube.com|youtube.com)\/playlist(.*)$/)) { + const lmsg = await msg.channel.send(' Loading song(s)'); + const playlist = await youtube.getPlaylist(url); + const videos = await playlist.getVideos(); + for (const video of Object.values(videos)) { + const video2 = await youtube.getVideoByID(video.id) + .catch(err => { + console.error(err); + return lmsg.edit(`<:redx:674263474704220182> Error loading songs!\nNot all songs we're loaded! This may have been caused by the playlist containing privated/deleted videos!`); + }); + await client.funcs.handleVideo(video2, msg, voiceChannel, client, true); + } + return lmsg.edit(`<:green_check_mark:674265384777416705> Playlist: **${playlist.title}** has been added to the queue!`); + } else { + try { + var video = await youtube.getVideo(url); + } catch (error) { + try { + const videos = await youtube.searchVideos(searchString, 1); + var video = await youtube.getVideoByID(videos[0].id); + } catch (err) { + console.error(err); + return msg.channel.send('<:redx:674263474704220182> I could not obtain any search results!'); + } + } + return client.funcs.handleVideo(video, msg, voiceChannel, client, false); + } + } +}; diff --git a/src/commands/queue.js b/src/commands/queue.js new file mode 100644 index 00000000..34effbc5 --- /dev/null +++ b/src/commands/queue.js @@ -0,0 +1,40 @@ +module.exports = { + name: 'queue', + alias: 'q', + usage: 'queue ', + description: 'See the queue.', + onlyDev: false, + permission: 'none', + category: 'music', + async execute(msg, args, client, Discord, prefix) { + const serverQueue = client.queue.get(msg.guild.id); + if (!serverQueue) return msg.channel.send('<:redx:674263474704220182> There is nothing playing.'); + if (args[1]) { + if (isNaN(args[1])) return msg.channel.send('<:redx:674263474704220182> I\'m sorry, But you need to enter a valid __number__.'); + } + let page = parseInt(args[1]); + if (!page) page = 1; + let pagetext = `:page_facing_up: Page: ${page} :page_facing_up:` + if (page === 1) pagetext = ':arrow_down: Next in queue :arrow_down:' + let queuesongs = serverQueue.songs.slice((page - 1) * 20 + 1, page * 20 + 1); + let queuemessage = `${queuesongs.map(song => `**#** ${song.title}`).join('\n')}` + const hashs = queuemessage.split('**#**').length; + for (let i = 0; i < hashs; i++) { + queuemessage = queuemessage.replace('**#**', `**${i + 1}**`); + } + if (!serverQueue.looping) { + const embed = new Discord.MessageEmbed() + .setTitle("__Song queue__") + .setDescription(`**Now playing:** ${serverQueue.songs[0].title}\n${pagetext}\n${queuemessage}`) + .setColor(client.config.embedColor) + return msg.channel.send(embed); + } else { + const embed = new Discord.MessageEmbed() + .setTitle("__Song queue__") + .setDescription(`**Now playing:** ${serverQueue.songs[0].title}\n${pagetext}\n${queuemessage}`) + .setFooter('<:repeat1:674685561377914892> Currently looping the queue!') + .setColor(client.config.embedColor) + return msg.channel.send(embed); + } + } +}; diff --git a/src/commands/remove.js b/src/commands/remove.js new file mode 100644 index 00000000..1a77f162 --- /dev/null +++ b/src/commands/remove.js @@ -0,0 +1,21 @@ +module.exports = { + name: 'remove', + alias: 'rm', + usage: 'remove ', + description: 'Remove a song from the queue', + onlyDev: false, + permission: 'MANAGE_MESSAGES', + category: 'music', + execute(msg, args, client, Discord, prefix, command) { + const serverQueue = client.queue.get(msg.guild.id); + if (client.funcs.check(client, msg, command)) { + if (!args[1]) return msg.channel.send('<:redx:674263474704220182> Please provide a song position in queue for me to remove!'); + const pos = parseInt(args[1]); + if (isNaN(pos)) return msg.channel.send('<:redx:674263474704220182> You need to enter a number!'); + if (pos === 0) return msg.channel.send('<:redx:674263474704220182> You can not remove the currently playing song!'); + if (pos > serverQueue.songs.size) return msg.channel.send(`<:redx:674263474704220182> There is only ${serverQueue.songs.size} amount of songs in the queue!`); + msg.channel.send(`๐Ÿ—‘๏ธ removed \`${serverQueue.songs[pos].title}\` from the queue!`); + return serverQueue.songs.splice(pos, 1); + } + } +}; diff --git a/src/commands/resume.js b/src/commands/resume.js new file mode 100644 index 00000000..507f5151 --- /dev/null +++ b/src/commands/resume.js @@ -0,0 +1,18 @@ +module.exports = { + name: 'resume', + alias: 'none', + usage: 'resume', + description: 'Resume the paused music.', + onlyDev: false, + permission: 'MANAGE_MESSAGES', + category: 'music', + execute(msg, args, client, Discord, prefix, command) { + const serverQueue = client.queue.get(msg.guild.id); + if (client.funcs.check(client, msg, command)) { + if (!serverQueue.paused) return msg.channel.send('<:redx:674263474704220182> The music in not paused!'); + serverQueue.paused = false; + serverQueue.connection.dispatcher.resume(true); + return msg.channel.send('<:resume:674685585478254603> Resumed the music!'); + } + } +}; diff --git a/src/commands/search.js b/src/commands/search.js new file mode 100644 index 00000000..fb1e4aad --- /dev/null +++ b/src/commands/search.js @@ -0,0 +1,73 @@ +const YouTube = require("simple-youtube-api"); +const he = require('he'); + +module.exports = { + name: 'search', + alias: 'sr', + usage: 'search ', + description: 'Search the top 10 queryes and choose one.', + onlyDev: false, + permission: 'none', + category: 'music', + async execute(msg, args, client, Discord, prefix) { + const youtube = new YouTube(client.config.api_key); + const searchString = args.slice(1).join(" "); + const url = args[1] ? args[1].replace(/<(.+)>/g, "$1") : ""; + const serverQueue = client.queue.get(msg.guild.id); + const voiceChannel = msg.member.voice.channel; + if (!serverQueue) { + if (!msg.member.voice.channel) return msg.channel.send('<:redx:674263474704220182> I\'m sorry but you need to be in a voice channel to play music!'); + } else { + if (voiceChannel !== serverQueue.voiceChannel) return msg.channel.send('<:redx:674263474704220182> I\'m sorry but you need to be in the same voice channel as Musix to play music!'); + } + if (!args[1]) return msg.channel.send('<:redx:674263474704220182> You need to use a link or search for a song!'); + const permissions = voiceChannel.permissionsFor(msg.client.user); + if (!permissions.has('CONNECT')) { + return msg.channel.send('<:redx:674263474704220182> I cannot connect to your voice channel, make sure I have the proper permissions!'); + } + if (!permissions.has('SPEAK')) { + return msg.channel.send('<:redx:674263474704220182> I cannot speak in your voice channel, make sure I have the proper permissions!'); + } + if (url.match(/^https?:\/\/(www.youtube.com|youtube.com)\/playlist(.*)$/)) { + const lmsg = await msg.channel.send(' Loading song(s)'); + const playlist = await youtube.getPlaylist(url); + const videos = await playlist.getVideos(); + for (const video of Object.values(videos)) { + const video2 = await youtube.getVideoByID(video.id); + await client.funcs.handleVideo(video2, msg, voiceChannel, client, true); + } + return lmsg.edit(`<:green_check_mark:674265384777416705> Playlist: **${playlist.title}** has been added to the queue!`); + } else { + try { + var video = await youtube.getVideo(url); + } catch (error) { + try { + var videos = await youtube.searchVideos(searchString, 10); + let index = 0; + const embed = new Discord.MessageEmbed() + .setTitle("__Song Selection__") + .setDescription(`${videos.map(video2 => `**${++index}** ${he.decode(video2.title)} `).join('\n')}`) + .setFooter("Please provide a number ranging from 1-10 to select one of the search results.") + .setColor(client.config.embedColor) + msg.channel.send(embed); + try { + var response = await msg.channel.awaitMessages(message2 => message2.content > 0 && message2.content < 11 && message2.author === msg.author, { + maxMatches: 1, + time: 10000, + errors: ['time'] + }); + } catch (err) { + console.error(err); + return msg.channel.send('<:redx:674263474704220182> Cancelling video selection'); + } + const videoIndex = parseInt(response.first().content); + var video = await youtube.getVideoByID(videos[videoIndex - 1].id); + } catch (err) { + console.error(err); + return msg.channel.send('<:redx:674263474704220182> I could not obtain any search results!'); + } + } + return client.funcs.handleVideo(video, msg, voiceChannel, client, false); + } + } +}; \ No newline at end of file diff --git a/src/commands/seek.js b/src/commands/seek.js new file mode 100644 index 00000000..655d9961 --- /dev/null +++ b/src/commands/seek.js @@ -0,0 +1,23 @@ +module.exports = { + name: 'seek', + alias: 'none', + usage: 'seek ', + description: 'Seek to a specific point in the currently playing song.', + onlyDev: true, + permission: 'MANAGE_MESSAGES', + category: 'music', + async execute(msg, args, client, Discord, prefix, command) { + const ytdl = require('ytdl-core'); + const serverQueue = client.queue.get(msg.guild.id); + if (client.funcs.check(client, msg, command)) { + let data = await Promise.resolve(ytdl.getInfo(serverQueue.songs[0].url)); + if (!args[1]) return msg.channel.send(`<:redx:674263474704220182> Correct usage: \`${prefix}seek \``); + const pos = parseInt(args[1]); + if (isNaN(pos)) return msg.channel.send('<:redx:674263474704220182> I\'m sorry, But you need to enter a valid __number__.'); + if (pos < 0) return msg.channel.send('<:redx:674263474704220182> The seeking point needs to be a positive number!'); + if (pos > data.length_seconds) return msg.channel.send(`<:redx:674263474704220182> The lenght of this song is ${data.length_seconds} seconds! You can't seek further than that!`); + serverQueue.connection.dispatcher.end('seek'); + client.funcs.play(msg.guild, serverQueue.songs[0], client, msg, pos, false); + } + } +}; diff --git a/src/commands/settings.js b/src/commands/settings.js new file mode 100644 index 00000000..db7c6f04 --- /dev/null +++ b/src/commands/settings.js @@ -0,0 +1,43 @@ +module.exports = { + name: 'settings', + alias: 'pref', + usage: 'settings ', + description: 'Change the server settings for Musix.', + onlyDev: false, + permission: 'MANAGE_GUILD', + category: 'util', + async execute(msg, args, client, Discord, prefix, command) { + const embed = new Discord.MessageEmbed() + .setTitle('Guild settings for Musix') + .addField('prefix', 'Change the guild specific prefix. (string)', true) + .addField('volume', 'Change the default volume that the bot will start playing at. (number)', true) + .addField('permissions', 'Change whether to require permissions to use eg `skip, stop, pause, loop, etc...`', true) + .addField('setdj', 'Set a DJ role. This will allow chosen users to freely use all Musix commands. This will automatically set the `permissions` settings to true in order for the `DJ` role to have effect!', true) + .addField('announcesongs', 'Whether to announce songs that start playing or not.') + .setFooter(`how to use: ${prefix}settings `) + .setAuthor(client.user.username, client.user.displayAvatarURL) + .setColor(client.embedColor) + const permissions = msg.channel.permissionsFor(msg.author); + if (msg.author.id !== client.config.devId) { + if (!permissions.has(command.permission)) return msg.channel.send('<:redx:674263474704220182> You need the `MANAGE_SERVER` permission to change the settings!'); + } + if (args[1]) { + const optionName = args[1].toLowerCase(); + const option = client.settingCmd.get(optionName) || client.settingCmd.find(cmd => cmd.aliases && cmd.aliases.includes(optionName)); + if (!option) return msg.channel.send(embed); + try { + option.execute(msg, args, client, Discord, prefix); + } catch (error) { + msg.reply(`<:redx:674263474704220182> there was an error trying to execute that option! Please contact support with \`${prefix}bug\`!`); + const embed = new Discord.MessageEmbed() + .setTitle(`Musix ${error.toString()}`) + .setDescription(error.stack.replace(/at /g, '**at **')) + .setColor(client.config.embedColor); + client.fetchUser(client.config.devId).then(user => user.send(embed)).catch(console.error); + client.channels.get(client.config.debug_channel).send(embed); + } + } else { + return msg.channel.send(embed); + } + }, +}; diff --git a/src/commands/settings/announcesongs.js b/src/commands/settings/announcesongs.js new file mode 100644 index 00000000..a48cedf4 --- /dev/null +++ b/src/commands/settings/announcesongs.js @@ -0,0 +1,12 @@ +module.exports = { + name: 'announcesongs', + async execute(msg, args, client, Discord, prefix) { + if (client.global.db.guilds[msg.guild.id].startPlaying) { + client.global.db.guilds[msg.guild.id].startPlaying = false; + return msg.channel.send('<:green_check_mark:674265384777416705> announcesongs now set to `false`!'); + } else { + client.global.db.guilds[msg.guild.id].startPlaying = true; + return msg.channel.send('<:green_check_mark:674265384777416705> announcesongs now set to `true`!'); + } + } +}; \ No newline at end of file diff --git a/src/commands/settings/permissions.js b/src/commands/settings/permissions.js new file mode 100644 index 00000000..9e84b508 --- /dev/null +++ b/src/commands/settings/permissions.js @@ -0,0 +1,17 @@ +module.exports = { + name: 'permissions', + async execute(msg, args, client, Discord, prefix) { + if (!args[2]) return msg.channel.send(`๐Ÿ”’ Permission requirement: \`${client.global.db.guilds[msg.guild.id].permissions}\``); + if (args[2] === 'true') { + if (!client.global.db.guilds[msg.guild.id].permissions) { + client.global.db.guilds[msg.guild.id].permissions = true; + msg.channel.send(`<:green_check_mark:674265384777416705> Permissions requirement now set to: \`true\``); + } else return msg.channel.send('<:redx:674263474704220182> That value is already `true`!'); + } else if (args[2] === 'false') { + if (client.global.db.guilds[msg.guild.id].permissions) { + client.global.db.guilds[msg.guild.id].permissions = false; + msg.channel.send(`<:green_check_mark:674265384777416705> Permissions requirement now set to: \`false\``); + } else return msg.channel.send('<:redx:674263474704220182> That value is already `false`!'); + } else return msg.channel.send('<:redx:674263474704220182> Please define a boolean! (true/false)'); + } +}; \ No newline at end of file diff --git a/src/commands/settings/prefix.js b/src/commands/settings/prefix.js new file mode 100644 index 00000000..b62b9a57 --- /dev/null +++ b/src/commands/settings/prefix.js @@ -0,0 +1,8 @@ +module.exports = { + name: 'prefix', + async execute(msg, args, client, Discord, prefix) { + if (!args[2]) return msg.channel.send(`Current prefix: \`${client.global.db.guilds[msg.guild.id].prefix}\``); + client.global.db.guilds[msg.guild.id].prefix = args[2]; + msg.channel.send(`<:green_check_mark:674265384777416705> New prefix set to: \`${args[2]}\``); + } +}; \ No newline at end of file diff --git a/src/commands/settings/reset.js b/src/commands/settings/reset.js new file mode 100644 index 00000000..d9ee2eee --- /dev/null +++ b/src/commands/settings/reset.js @@ -0,0 +1,14 @@ +module.exports = { + name: 'reset', + async execute(msg, args, client, Discord, prefix) { + client.global.db.guilds[msg.guild.id] = { + prefix: client.config.prefix, + defaultVolume: 5, + permissions: false, + premium: false, + dj: false, + djrole: null + }; + msg.channel.send('<:green_check_mark:674265384777416705> Reset __all__ guild settings!'); + } +}; \ No newline at end of file diff --git a/src/commands/settings/setDj.js b/src/commands/settings/setDj.js new file mode 100644 index 00000000..9c0be4a2 --- /dev/null +++ b/src/commands/settings/setDj.js @@ -0,0 +1,28 @@ +module.exports = { + name: 'setdj', + async execute(msg, args, client, Discord, prefix) { + if (!client.global.db.guilds[msg.guild.id].dj) { + if (!client.global.db.guilds[msg.guild.id].permissions) { + client.global.db.guilds[msg.guild.id].permissions = true; + } + if (msg.guild.roles.find(x => x.name === "DJ")) { + client.global.db.guilds[msg.guild.id].djrole = msg.guild.roles.find(x => x.name === "DJ").id; + msg.channel.send('<:green_check_mark:674265384777416705> I found a `DJ` role from this guild! This role is now the DJ role.'); + client.global.db.guilds[msg.guild.id].dj = true; + } else { + const permissions = msg.channel.permissionsFor(msg.client.user); + if (!permissions.has('MANAGE_ROLES')) return msg.channel.send('<:redx:674263474704220182> I cannot create roles (Manage roles), make sure I have the proper permissions! I will need this permission to create a `DJ` role since i did not find one!'); + msg.guild.createRole({ + name: 'DJ', + }) + .then(role => client.global.db.guilds[msg.guild.id].djrole = role.id) + .catch(console.error) + client.global.db.guilds[msg.guild.id].dj = true; + msg.channel.send('<:green_check_mark:674265384777416705> I did not find a role `DJ` so i have created one for you!'); + } + } else { + client.global.db.guilds[msg.guild.id].dj = false; + msg.channel.send('<:green_check_mark:674265384777416705> `DJ` now set to `false`'); + } + } +}; \ No newline at end of file diff --git a/src/commands/settings/volume.js b/src/commands/settings/volume.js new file mode 100644 index 00000000..f635a271 --- /dev/null +++ b/src/commands/settings/volume.js @@ -0,0 +1,10 @@ +module.exports = { + name: 'volume', + async execute(msg, args, client, Discord, prefix) { + if (!args[2]) return msg.channel.send(`:speaker: Current default volume is: \`${client.global.db.guilds[msg.guild.id].defaultVolume}\``); + if (isNaN(args[2])) return msg.channel.send('<:redx:674263474704220182> I\'m sorry, But the default volume needs to be a valid __number__.'); + if (args[2].length > 2) return msg.channel.send('<:redx:674263474704220182> The default volume must be below `100` for quality and safety resons.'); + client.global.db.guilds[msg.guild.id].defaultVolume = args[2]; + msg.channel.send(`<:green_check_mark:674265384777416705> Default volume set to: \`${args[2]}\``); + } +}; \ No newline at end of file diff --git a/src/commands/shuffle.js b/src/commands/shuffle.js new file mode 100644 index 00000000..8960afec --- /dev/null +++ b/src/commands/shuffle.js @@ -0,0 +1,28 @@ +module.exports = { + name: 'shuffle', + alias: 'none', + usage: 'shuffle', + description: 'Shuffle the queue.', + onlyDev: false, + permission: 'MANAGE_MESSAGES', + category: 'music', + execute(msg, args, client, Discord, prefix, command) { + const serverQueue = client.queue.get(msg.guild.id); + if (client.funcs.check(client, msg, command)) { + client.funcs.shuffle(serverQueue.songs); + msg.channel.send('<:shuffle:674685595980791871> Queue suffled!'); + /*let currentIndex = serverQueue.songs.length, + temporaryValue, + randomIndex; + + while (0 !== currentIndex) { + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex -= 1; + + temporaryValue = serverQueue.songs[currentIndex]; + serverQueue.songs[currentIndex] = serverQueue.songs[randomIndex]; + serverQueue.songs[randomIndex] = temporaryValue;* + }*/ + } + } +}; \ No newline at end of file diff --git a/src/commands/skip.js b/src/commands/skip.js new file mode 100644 index 00000000..1487b5fb --- /dev/null +++ b/src/commands/skip.js @@ -0,0 +1,49 @@ +module.exports = { + name: 'skip', + alias: 's', + usage: 'skip', + description: 'Skip the currently playing song.', + onlyDev: false, + permission: 'MANAGE_MESSAGES', + category: 'music', + execute(msg, args, client, Discord, prefix, command) { + const serverQueue = client.queue.get(msg.guild.id); + const permissions = msg.channel.permissionsFor(msg.author); + if (!serverQueue || !serverQueue.playing) return msg.channel.send('<:redx:674263474704220182> There is nothing playing!'); + if (msg.author.id !== client.config.devId) { + if (msg.member.voice.channel !== serverQueue.voiceChannel) return msg.channel.send('<:redx:674263474704220182> I\'m sorry but you need to be in the same voice channel as Musix!'); + if (client.global.db.guilds[msg.guild.id].permissions === true) { + if (!msg.member.roles.has(client.global.db.guilds[msg.guild.id].djrole) && !permissions.has(command.permission)) { + return vote(serverQueue, msg, client); + } else { + return skipSong(serverQueue, msg); + } + } else { + return vote(serverQueue, msg, client); + } + } else { + return skipSong(serverQueue, msg); + } + } +}; +function skipSong(serverQueue, msg) { + msg.channel.send('<:skip:674685614221688832> Skipped the song!'); + serverQueue.connection.dispatcher.end('skipped'); +}; +function vote(serverQueue, msg) { + serverQueue.votesNeeded = Math.floor(msg.guild.voiceConnection.channel.members.size / 2); + serverQueue.votesNeeded.toFixed(); + if (msg.guild.voiceConnection.channel.members.size > 2) { + if (serverQueue.voters.includes(msg.member.id)) return msg.channel.send('<:redx:674263474704220182> You have already voted to skip!'); + serverQueue.votes++; + serverQueue.voters.push(msg.member.id); + if (serverQueue.votes >= serverQueue.votesNeeded) { + serverQueue.voters = []; + serverQueue.votes = 0; + serverQueue.votesNeeded = null; + return skipSong(serverQueue, msg); + } else return msg.channel.send(`<:redx:674263474704220182> Not enough votes! ${serverQueue.votes} / ${serverQueue.votesNeeded}!`); + } else { + return skipSong(serverQueue, msg); + } +}; diff --git a/src/commands/skipto.js b/src/commands/skipto.js new file mode 100644 index 00000000..d109d858 --- /dev/null +++ b/src/commands/skipto.js @@ -0,0 +1,25 @@ +module.exports = { + name: 'skipto', + alias: 'st', + usage: 'skipto ', + description: 'Skip to a point in the queue', + onlyDev: false, + permission: 'MANAGE_MESSAGES', + category: 'music', + async execute(msg, args, client, Discord, prefix, command) { + const serverQueue = client.queue.get(msg.guild.id); + if (client.funcs.check(client, msg, command)) { + if (!args[1]) return msg.channel.send(`<:redx:674263474704220182> correct usage: \`${command.usage}\``); + const point = parseInt(args[1] - 1); + if (isNaN(point)) return msg.channel.send('<:redx:674263474704220182> I\'m sorry, But you need to enter a valid __number__.'); + if (point > serverQueue.songs.size) return msg.channel.send('<:redx:674263474704220182> That song does not exist!'); + if (point < 1) return msg.channel.send('<:redx:674263474704220182> You can\'t skip to the song currently playing!'); + let i = 0; + while (i < point) { + i++; + serverQueue.songs.shift(); + } + serverQueue.connection.dispatcher.end('skipto'); + } + } +}; diff --git a/src/commands/status.js b/src/commands/status.js new file mode 100644 index 00000000..bee27b1b --- /dev/null +++ b/src/commands/status.js @@ -0,0 +1,28 @@ +module.exports = { + name: 'status', + alias: 'stats', + usage: 'status', + description: 'See the current status for Musix.', + onlyDev: false, + permission: 'none', + category: 'info', + execute(msg, args, client, Discord, prefix) { + const uptime = client.funcs.msToTime(client.uptime); + const ping = Math.floor(client.ping * 10) / 10; + msg.channel.send(' Pinging...').then(m => { + const latency = m.createdTimestamp - msg.createdTimestamp; + + const embed = new Discord.MessageEmbed() + .setTitle(`Status for ${client.user.username}`) + .addField(':signal_strength: Ping', client.ws.ping, true) + .addField('Latency', latency, true) + .addField(':stopwatch: Uptime', uptime, true) + .addField(`:play_pause: Currently playing music on`, `${client.voice.connections.size} guild(s)`, true) + .addField(`๐Ÿ’ฟ Operating system`, process.platform, true) + .setAuthor(client.user.username, client.user.displayAvatarURL) + .setColor(client.config.embedColor) + m.delete(); + return msg.channel.send(embed); + }); + } +}; diff --git a/src/commands/stop.js b/src/commands/stop.js new file mode 100644 index 00000000..a3350929 --- /dev/null +++ b/src/commands/stop.js @@ -0,0 +1,17 @@ +module.exports = { + name: 'stop', + description: 'Stop command.', + alias: 'none', + onlyDev: false, + permission: 'MANAGE_CHANNELS', + category: 'music', + execute(msg, args, client, Discord, prefix, command) { + const serverQueue = client.queue.get(msg.guild.id); + if (client.funcs.check(client, msg, command)) { + serverQueue.songs = []; + serverQueue.looping = false; + serverQueue.connection.dispatcher.end('Stopped'); + msg.channel.send('<:stop:674685626108477519> Stopped the music!') + } + } +}; diff --git a/src/commands/volume.js b/src/commands/volume.js new file mode 100644 index 00000000..3a760508 --- /dev/null +++ b/src/commands/volume.js @@ -0,0 +1,22 @@ +module.exports = { + name: 'volume', + description: 'Volume command.', + alias: 'none', + cooldown: 5, + onlyDev: false, + permission: 'MANAGE_MESSAGES', + category: 'music', + execute(msg, args, client, Discord, prefix, command) { + const serverQueue = client.queue.get(msg.guild.id); + if (!args[1] && serverQueue) return msg.channel.send(`:loud_sound: The current volume is: **${serverQueue.volume}**`); + const volume = parseFloat(args[1]); + if (client.funcs.check(client, msg, command)) { + if (isNaN(volume)) return msg.channel.send('<:redx:674263474704220182> I\'m sorry, But you need to enter a valid __number__.'); + if (volume > 100) return msg.channel.send('<:redx:674263474704220182> The max volume is `100`!'); + if (volume < 0) return msg.channel.send('<:redx:674263474704220182> The volume needs to be a positive number!'); + serverQueue.volume = volume; + serverQueue.connection.dispatcher.setVolume(volume / 5); + return msg.channel.send(`<:volumehigh:674685637626167307> I set the volume to: **${volume}**`); + } + } +}; diff --git a/src/events/dispatcher/finish.js b/src/events/dispatcher/finish.js new file mode 100644 index 00000000..ca48d6b2 --- /dev/null +++ b/src/events/dispatcher/finish.js @@ -0,0 +1,18 @@ +module.exports = async function (client, reason, guild) { + const serverQueue = client.queue.get(guild.id); + serverQueue.playing = false; + if (reason === "Stream is not generating quickly enough.") { + console.log("Song ended"); + } else if (reason === "seek") { + return; + } else { + console.log(reason); + } + if (!serverQueue.songLooping) { + if (serverQueue.looping) { + serverQueue.songs.push(serverQueue.songs[0]); + } + serverQueue.songs.shift(); + } + client.funcs.play(guild, serverQueue.songs[0], client, 0, true); +}; \ No newline at end of file diff --git a/src/events/guildCreate.js b/src/events/guildCreate.js new file mode 100644 index 00000000..c293187a --- /dev/null +++ b/src/events/guildCreate.js @@ -0,0 +1,21 @@ +module.exports = { + name: 'guildcreate', + async execute(client, guild) { + client.db.collection('guilds').doc(guild.id).set({ + prefix: client.config.prefix, + defaultVolume: client.config.defaultVolume, + permissions: client.config.permissions, + dj: client.config.dj, + djrole: client.config.djrole, + startPlaying: client.config.startPlaying + }); + client.global.db.guilds[guild.id] = { + prefix: client.config.prefix, + defaultVolume: client.config.defaultVolume, + permissions: client.config.permissions, + dj: client.config.dj, + djrole: client.config.djrole, + startPlaying: client.config.startPlaying + }; + } +} diff --git a/src/events/msg.js b/src/events/msg.js new file mode 100644 index 00000000..02f9292f --- /dev/null +++ b/src/events/msg.js @@ -0,0 +1,27 @@ +module.exports = { + name: 'message', + async execute(client, msg, Discord) { + if (msg.author.bot || !msg.guild) return; + let prefix = client.global.db.guilds[msg.guild.id].prefix; + if (client.config.devMode) prefix = "-"; + const args = msg.content.slice(prefix.length).split(' '); + if (msg.mentions.users.first()) { + if (msg.mentions.users.first().id === client.user.id) { + if (!args[1]) return; + if (args[1] === 'prefix') return msg.channel.send(`My prefix here is: \`${prefix}\`.`); + if (args[1] === 'help') { + const command = client.commands.get("help"); + return client.funcs.exe(msg, args, client, Discord, prefix, command); + } + } + } + if (client.config.devMode && msg.member.id !== client.config.devId) return msg.channel.send('<:redx:674263474704220182> Dev mode has been turned on! Commands are only available to developer(s)!'); + if (!msg.content.startsWith(prefix)) return; + if (!args[0]) return; + const commandName = args[0].toLowerCase(); + const command = client.commands.get(commandName) || client.commands.find(cmd => cmd.aliases && cmd.aliases.includes(commandName)) || client.commandAliases.get(commandName); + if (!command && msg.content !== `${prefix}`) return; + if (command.onlyDev && msg.author.id !== client.config.devId) return msg.channel.send('<:redx:674263474704220182> You are not allowed to do that!'); + client.funcs.exe(msg, args, client, Discord, prefix, command); + } +} diff --git a/src/events/ready.js b/src/events/ready.js new file mode 100644 index 00000000..20a94007 --- /dev/null +++ b/src/events/ready.js @@ -0,0 +1,53 @@ +const DBL = require("dblapi.js"); + +module.exports = { + name: 'ready', + async execute(client, Discord) { + const remoteMusixGuildsData = await client.funcs.dbget('guilds', null, client); + remoteMusixGuildsData.forEach(guildData => { + client.global.db.guilds[guildData.id] = guildData.d; + }); + if (client.devMode) { + client.guilds.forEach(guild => { + client.global.db.guilds[guild.id] = { + prefix: client.config.prefix, + defaultVolume: client.config.defaultVolume, + permissions: client.config.permissions, + dj: client.config.dj, + djrole: client.config.djrole, + startPlaying: client.config.startPlaying + }; + }); + } + console.log('- DB Set -'); + client.user.setActivity(`@${client.user.username} help | ๐ŸŽถ`, { type: 'LISTENING' }); + client.user.setStatus('dnd'); + const dbl = new DBL(client.config.DBLTOKEN, client); + if (client.config.dblApi && !client.config.devMode) { + dbl.on('error', error => { + console.log('Error with DBL: ' + error); + }) + dbl.postStats(client.guilds.size); + } + console.log('- Activated -'); + setInterval(async () => { + if (client.config.saveDB && !client.config.devMode) { + client.guilds.forEach(guild => { + client.db.collection('guilds').doc(guild.id).set({ + prefix: client.global.db.guilds[guild.id].prefix, + defaultVolume: client.global.db.guilds[guild.id].defaultVolume, + permissions: client.global.db.guilds[guild.id].permissions, + premium: client.global.db.guilds[guild.id].premium, + dj: client.global.db.guilds[guild.id].dj, + djrole: client.global.db.guilds[guild.id].djrole, + startPlaying: client.global.db.guilds[guild.id].startPlaying + }); + }); + } + if (client.config.dblApi && !client.config.devMode) dbl.postStats(client.guilds.size); + }, 1800000); + setInterval(() => { + client.funcs.ffmpeg(client, Discord); + }, 7200000); + } +} diff --git a/src/events/voiceStateUpdate.js b/src/events/voiceStateUpdate.js new file mode 100644 index 00000000..04400f6b --- /dev/null +++ b/src/events/voiceStateUpdate.js @@ -0,0 +1,13 @@ +module.exports = { + name: 'voiceStateUpdate', + async execute(client, newMember) { + const serverQueue = client.queue.get(newMember.guild.id); + if (!serverQueue) return; + if (newMember === client.user) { + if (newMember.voice.channel !== serverQueue.voiceChannel) { + serverQueue.voiceChannel = newMember.voice.channel; + console.log(`Changed serverQueue voiceChannel since Musix was moved to a different channel!`); + } + } + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..a28e79a9 --- /dev/null +++ b/src/index.js @@ -0,0 +1,4 @@ +const Discord = require('discord.js'); +const MusicClient = require('./Struct/Client'); +const client = new MusicClient({}); +require('dotenv/config'); \ No newline at end of file diff --git a/src/struct/client.js b/src/struct/client.js new file mode 100644 index 00000000..6c7b67c9 --- /dev/null +++ b/src/struct/client.js @@ -0,0 +1,70 @@ +const { Client, Collection } = require('discord.js'); +const Discord = require('discord.js'); +const admin = require('firebase-admin'); +const serviceAccount = require('./config/serviceAccount.json'); +const fs = require('fs'); +const path = require('path') +const events = '../events/'; + +module.exports = class extends Client { + constructor() { + super({ + disableEveryone: true, + disabledEvents: ['TYPING_START'] + }); + this.commands = new Collection(); + this.commandAliases = new Collection(); + this.settingCmd = new Collection(); + this.queue = new Map(); + this.funcs = {}; + this.dispatcher = {}; + this.config = require('./config/config.js'); + this.dispatcher.finish = require('../events/dispatcher/finish.js'); + + fs.readdirSync(path.join(__dirname, 'funcs')).forEach(filename => { + this.funcs[filename.slice(0, -3)] = require(`./funcs/${filename}`); + }); + + const commandFiles = fs.readdirSync(path.join(path.dirname(__dirname), 'commands')).filter(f => f.endsWith('.js')); + for (const file of commandFiles) { + const command = require(`../commands/${file}`); + command.uses = 0; + this.commands.set(command.name, command); + this.commandAliases.set(command.alias, command); + } + const settingFiles = fs.readdirSync(path.join(path.dirname(__dirname), 'commands/settings')).filter(f => f.endsWith('.js')); + for (const file of settingFiles) { + const option = require(`../commands/settings/${file}`); + this.settingCmd.set(option.name, option); + } + + admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), + }); + + this.db = admin.firestore(); + + this.global = { + db: { + guilds: {}, + }, + }; + + this.db.FieldValue = require('firebase-admin').firestore.FieldValue; + + this.on('ready', () => { + require(`${events}ready`).execute(this, Discord); + }); + this.on('message', (msg) => { + require(`${events}msg`).execute(this, msg, Discord); + }); + this.on('guildCreate', (guild) => { + require(`${events}msg`).execute(this, guild); + }); + this.on('voiceStateUpdate', (newMember) => { + require(`${events}voiceStateUpdate`).execute(this, newMember); + }); + + this.login(this.config.token).catch(err => console.log('Failed to login: ' + err)); + } +}; diff --git a/src/struct/config/config.js b/src/struct/config/config.js new file mode 100644 index 00000000..773853f1 --- /dev/null +++ b/src/struct/config/config.js @@ -0,0 +1,22 @@ +module.exports = { + //credentials + token: process.env.TOKEN, + api_key: process.env.API_KEY, + //channels + debug_channel: "634718645188034560", + devId: "360363051792203779", + //misc + embedColor: "#b50002", + invite: "https://discordapp.com/api/oauth2/authorize?client_id=607266889537945605&permissions=271600640&redirect_uri=https%3A%2F%2Fdiscordapp.com%2Foauth2%2Fauthorize%3Fclient_id%3D607266889537945605%26%3Bscope%3Dbot%26%3Bpermissions%3D0&scope=bot", + //Settings + devMode: true, + dblApi: false, + saveDB: false, + //db values + prefix: "-", + defaultVolume: 5, + permissions: false, + dj: false, + djrole: null, + startPlaying: true, +} diff --git a/src/struct/funcs/check.js b/src/struct/funcs/check.js new file mode 100644 index 00000000..0c235b51 --- /dev/null +++ b/src/struct/funcs/check.js @@ -0,0 +1,19 @@ +module.exports = function (client, msg, command) { + const serverQueue = client.queue.get(msg.guild.id); + const permissions = msg.channel.permissionsFor(msg.author); + if (!serverQueue || !serverQueue.playing) return msg.channel.send('<:redx:674263474704220182> There is nothing playing!'); + if (msg.author.id !== client.config.devId) { + if (msg.member.voice.channel !== serverQueue.voiceChannel) return msg.channel.send(`<:redx:674263474704220182> I'm sorry but you need to be in the same voice channel as Musix to use this command!`); + if (client.global.db.guilds[msg.guild.id].permissions === true) { + if (client.global.db.guilds[msg.guild.id].dj) { + if (!msg.member.roles.has(client.global.db.guilds[msg.guild.id].djrole)) { + msg.channel.send('<:redx:674263474704220182> You need the `DJ` role to use this command!'); + return false; + } else return true; + } else if (!permissions.has(command.permission)) { + msg.channel.send(`<:redx:674263474704220182> You need the \`${command.permission}\` permission to use this command!`); + return false; + } else return true; + } else return true; + } else return true; +}; diff --git a/src/struct/funcs/dbget.js b/src/struct/funcs/dbget.js new file mode 100644 index 00000000..77d76492 --- /dev/null +++ b/src/struct/funcs/dbget.js @@ -0,0 +1,22 @@ +module.exports = async function (collection, doc, client) { + if (doc) { + let d = await client.db.collection(collection).doc(doc).get().catch(err => { + console.log('Error getting document', err); + return 'error'; + }); + return d.data(); + } else { + let d = await client.db.collection(collection).get().catch(err => { + console.log('Error getting document', err); + return 'error'; + }); + let finalD = []; + d.forEach(doc => { + finalD.push({ + id: doc.id, + d: doc.data(), + }); + }); + return finalD; + } +}; \ No newline at end of file diff --git a/src/struct/funcs/exe.js b/src/struct/funcs/exe.js new file mode 100644 index 00000000..4b6287ca --- /dev/null +++ b/src/struct/funcs/exe.js @@ -0,0 +1,16 @@ +module.exports = function (msg, args, client, Discord, prefix, command) { + const permissions = msg.channel.permissionsFor(msg.client.user); + if (!permissions.has('EMBED_LINKS')) return msg.channel.send('<:redx:674263474704220182> I cannot send embeds (Embed links), make sure I have the proper permissions!'); + try { + command.uses++; + command.execute(msg, args, client, Discord, prefix, command); + } catch (error) { + msg.reply(`<:redx:674263474704220182> there was an error trying to execute that command! Please contact support with \`${prefix}bug\`!`); + const embed = new Discord.MessageEmbed() + .setTitle(`Musix ${error.toString()}`) + .setDescription(error.stack.replace(/at /g, '**at **')) + .setColor('#b50002'); + //client.fetchUser(client.config.devId).then(user => user.send(embed)).catch(console.error); + client.channels.get(client.config.debug_channel).send(embed); + } +}; diff --git a/src/struct/funcs/ffmpeg.js b/src/struct/funcs/ffmpeg.js new file mode 100644 index 00000000..d1a01e25 --- /dev/null +++ b/src/struct/funcs/ffmpeg.js @@ -0,0 +1,7 @@ +module.exports = async function (client) { + try { + await client.channels.get('570531724002328577').join() + } catch (error) { + client.channels.get(client.config.debug_channel).send("Error detected: " + error); + } +}; \ No newline at end of file diff --git a/src/struct/funcs/handleVideo.js b/src/struct/funcs/handleVideo.js new file mode 100644 index 00000000..0a0a4bc2 --- /dev/null +++ b/src/struct/funcs/handleVideo.js @@ -0,0 +1,44 @@ +module.exports = async function (video, msg, voiceChannel, client, playlist = false) { + const Discord = require('discord.js'); + const song = { + id: video.id, + title: Discord.Util.escapeMarkdown(video.title), + url: `https://www.youtube.com/watch?v=${video.id}`, + author: msg.author + } + const serverQueue = client.queue.get(msg.guild.id); + if (serverQueue) { + serverQueue.songs.push(song); + if (playlist) return; + return msg.channel.send(`<:green_check_mark:674265384777416705> **${song.title}** has been added to the queue!`); + } + + const construct = { + textChannel: msg.channel, + voiceChannel: voiceChannel, + connection: null, + songs: [], + volume: client.global.db.guilds[msg.guild.id].defaultVolume, + playing: false, + paused: false, + looping: false, + songLooping: false, + votes: 0, + voters: [], + votesNeeded: null, + time: 0, + }; + construct.songs.push(song); + client.queue.set(msg.guild.id, construct); + + try { + const connection = await voiceChannel.join(); + construct.connection = connection; + client.funcs.play(msg.guild, construct.songs[0], client, 0, true); + } catch (error) { + client.queue.delete(msg.guild.id); + client.channels.get(client.config.debug_channel).send("Error with connecting to voice channel: " + error); + return msg.channel.send(`<:redx:674263474704220182> An error occured: ${error}`); + } + return; +} diff --git a/src/struct/funcs/msToTime.js b/src/struct/funcs/msToTime.js new file mode 100644 index 00000000..87a16ab3 --- /dev/null +++ b/src/struct/funcs/msToTime.js @@ -0,0 +1,11 @@ +module.exports = function msToTime(duration) { + var seconds = Math.floor((duration / 1000) % 60), + minutes = Math.floor((duration / (1000 * 60)) % 60), + hours = Math.floor((duration / (1000 * 60 * 60)) % 24); + + hours = (hours < 10) ? "0" + hours : hours; + minutes = (minutes < 10) ? "0" + minutes : minutes; + seconds = (seconds < 10) ? "0" + seconds : seconds; + + return `${hours}:${minutes}:${seconds}`; +} \ No newline at end of file diff --git a/src/struct/funcs/play.js b/src/struct/funcs/play.js new file mode 100644 index 00000000..5b07c33e --- /dev/null +++ b/src/struct/funcs/play.js @@ -0,0 +1,35 @@ +module.exports = async function (guild, song, client, seek, play) { + const Discord = require('discord.js'); + const ytdl = require('ytdl-core'); + const getThumb = require('video-thumbnail-url'); + + const serverQueue = client.queue.get(guild.id); + if (!song) { + console.log('No song') + serverQueue.voiceChannel.leave(); + client.queue.delete(guild.id); + return; + } + const dispatcher = serverQueue.connection + .play(await ytdl(song.url, { filter: "audio", highWaterMark: /*512*/1 << 25, volume: false }), { seek: seek, bitrate: 1024, passes: 10, volume: 1 }) + .on("finish", reason => { + client.dispatcher.finish(client, reason, guild); + }); + dispatcher.on('start', () => { + dispatcher.player.streamingData.pausedTime = 0; + }); + dispatcher.on('error', error => console.error(error)); + dispatcher.setVolume(serverQueue.volume / 10); + if (client.global.db.guilds[guild.id].startPlaying || play) { + const data = await Promise.resolve(ytdl.getInfo(serverQueue.songs[0].url)); + const songtime = (data.length_seconds * 1000).toFixed(0); + const thumbnail = getThumb(serverQueue.songs[0].url); + const embed = new Discord.MessageEmbed() + .setTitle(` Start playing: **${song.title}**`) + .setDescription(`Song duration: \`${client.funcs.msToTime(songtime)}\``) + .setThumbnail(thumbnail._rejectionHandler0) + .setColor("#b50002") + serverQueue.textChannel.send(embed); + } + serverQueue.playing = true; +} diff --git a/src/struct/funcs/shuffle.js b/src/struct/funcs/shuffle.js new file mode 100644 index 00000000..23def67e --- /dev/null +++ b/src/struct/funcs/shuffle.js @@ -0,0 +1,11 @@ +module.exports = function (a) { + for (let i = a.length - 1; i > 1; i--) { + const j = Math.floor(Math.random() * (i + 1)); + if (i === 0 || j === 0) { + console.log(`J or I is 0. I: ${i} J: ${j}`); + } else { + [a[i], a[j]] = [a[j], a[i]]; + } + } + return a; +}; \ No newline at end of file diff --git a/src/struct/funcs/urlMatch.js b/src/struct/funcs/urlMatch.js new file mode 100644 index 00000000..125fbaed --- /dev/null +++ b/src/struct/funcs/urlMatch.js @@ -0,0 +1,16 @@ +module.exports = async function (client, msg, youtube, voiceChannel, url) { + if (url.match(/^https?:\/\/(www.youtube.com|youtube.com)\/playlist(.*)$/)) { + const lmsg = await msg.channel.send(' Loading song(s)'); + const playlist = await youtube.getPlaylist(url); + const videos = await playlist.getVideos(); + for (const video of Object.values(videos)) { + const video2 = await youtube.getVideoByID(video.id); + await client.funcs.handleVideo(video2, msg, voiceChannel, client, true); + } + lmsg.edit(`<:green_check_mark:674265384777416705> Playlist: **${playlist.title}** has been added to the queue!`); + return true; + } else { + console.log('return false') + return false; + } +}; \ No newline at end of file