diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..2743837c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.env +package-lock.json +.vscode/ +node_modules/ +serviceAccount.json +package.json \ No newline at end of file diff --git a/README.md b/README.md index 93069e5c..39ea59e3 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ +<<<<<<< HEAD # Musix OSS +======= +# Musix-V2 + +THIS VERSION OF MUSIX IS NO LONGER SUPPORTED! + +## Discord music bot + +Second version of Musix discord music bot. + +Made with discord.js V11 + +NOTE! THIS BOT CANNOT BE USED AFTER OCTOBER 4TH 2020! This is due to new rules for bots by discord. + +## Installation + +npm install (idk how yarn works) + +Some modules are outdated to updating is recommended! + +## Usage + +You will need you own .env file and serviceAccount.json for database! +>>>>>>> musix-v2/master diff --git a/Struct/Client.js b/Struct/Client.js new file mode 100644 index 00000000..ec1f2633 --- /dev/null +++ b/Struct/Client.js @@ -0,0 +1,48 @@ +const { Client, Collection } = require('discord.js'); +const admin = require('firebase-admin'); +const serviceAccount = require('./serviceAccount.json'); + +module.exports = class extends Client { + constructor() { + super({ + disableEveryone: true, + disabledEvents: ['TYPING_START'] + }); + + this.commands = new Collection(); + + this.commandAliases = new Collection(); + + this.playlistCmd = new Collection(); + + this.settingCmd = new Collection(); + + this.events = new Collection(); + + this.queue = new Map(); + + this.funcs = {}; + + this.funcs.handleVideo = require('./funcs/handleVideo.js'); + this.funcs.play = require('./funcs/play.js'); + this.funcs.msToTime = require('./funcs/msToTime.js'); + this.funcs.dbget = require('./funcs/dbget.js'); + this.funcs.exe = require('./funcs/exe.js'); + this.funcs.ffmpeg = require('./funcs/ffmpeg.js'); + + admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), + }); + + this.db = admin.firestore(); + + this.global = { + db: { + guilds: {}, + playlists: {}, + }, + }; + + this.db.FieldValue = require('firebase-admin').firestore.FieldValue; + } +}; diff --git a/Struct/funcs/dbget.js b/Struct/funcs/dbget.js new file mode 100644 index 00000000..7aa3d641 --- /dev/null +++ b/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/Struct/funcs/exe.js b/Struct/funcs/exe.js new file mode 100644 index 00000000..b8849e60 --- /dev/null +++ b/Struct/funcs/exe.js @@ -0,0 +1,16 @@ +module.exports = function (message, args, client, Discord, prefix, command) { + const permissions = message.channel.permissionsFor(message.client.user); + if (!permissions.has('EMBED_LINKS')) return message.channel.send(':x: I cannot send embeds (Embed links), make sure I have the proper permissions!'); + try { + command.uses++; + command.execute(message, args, client, Discord, prefix); + } catch (error) { + message.reply(`:x: there was an error trying to execute that command! Please contact support with \`${prefix}bug\`!`); + const embed = new Discord.RichEmbed() + .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/Struct/funcs/ffmpeg.js b/Struct/funcs/ffmpeg.js new file mode 100644 index 00000000..d1a01e25 --- /dev/null +++ b/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/Struct/funcs/handleVideo.js b/Struct/funcs/handleVideo.js new file mode 100644 index 00000000..4f524bef --- /dev/null +++ b/Struct/funcs/handleVideo.js @@ -0,0 +1,41 @@ +module.exports = async function (video, message, voiceChannel, client, playlist = false) { + const Discord = require('discord.js'); + let song = { + id: video.id, + title: Discord.Util.escapeMarkdown(video.title), + url: `https://www.youtube.com/watch?v=${video.id}`, + author: message.author + } + const serverQueue = client.queue.get(message.guild.id); + if (!serverQueue) { + const construct = { + textChannel: message.channel, + voiceChannel: voiceChannel, + connection: null, + songs: [], + volume: client.global.db.guilds[message.guild.id].defaultVolume, + playing: false, + paused: false, + looping: false, + votes: 0, + voters: [], + votesNeeded: null + }; + construct.songs.push(song); + client.queue.set(message.guild.id, construct); + try { + var connection = await voiceChannel.join(); + construct.connection = connection; + client.funcs.play(message.guild, construct.songs[0], client, message, 0, true); + } catch (error) { + client.queue.delete(message.guild.id); + client.channels.get(client.config.debug_channel).send("Error with connecting to voice channel: " + error); + return message.channel.send(`:x: An error occured: ${error}`); + } + } else { + serverQueue.songs.push(song); + if (playlist) return undefined; + return message.channel.send(`:white_check_mark: **${song.title}** has been added to the queue!`); + } + return undefined; +} diff --git a/Struct/funcs/msToTime.js b/Struct/funcs/msToTime.js new file mode 100644 index 00000000..87a16ab3 --- /dev/null +++ b/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/Struct/funcs/play.js b/Struct/funcs/play.js new file mode 100644 index 00000000..b41ddc41 --- /dev/null +++ b/Struct/funcs/play.js @@ -0,0 +1,40 @@ +module.exports = async function (guild, song, client, message, seek, play) { + const Discord = require('discord.js'); + const ytdl = require('ytdl-core'); + 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 + .playStream(ytdl(song.url, { filter: "audio", highWaterMark: 1 << 25 }), { seek: seek, bitrate: 1024, passes: 10, volume: 1 }) + .on("end", reason => { + if (reason === "Stream is not generating quickly enough.") { + console.log("Song ended"); + } else if (reason === "seek") { + return; + } else { + console.log(reason); + } + serverQueue.playing = false; + if (serverQueue.looping) { + serverQueue.songs.push(serverQueue.songs[0]); + } + serverQueue.songs.shift(); + client.funcs.play(guild, serverQueue.songs[0], client, message); + }); + dispatcher.setVolume(serverQueue.volume / 10); + dispatcher.on("error", error => console.error(error)); + if (client.global.db.guilds[guild.id].startPlaying || play) { + let data = await Promise.resolve(ytdl.getInfo(serverQueue.songs[0].url)); + let songtime = (data.length_seconds * 1000).toFixed(0); + const embed = new Discord.RichEmbed() + .setTitle(`:musical_note: Start playing: **${song.title}**`) + .setDescription(`Song duration: \`${client.funcs.msToTime(songtime)}\``) + .setColor("#b50002") + serverQueue.textChannel.send(embed); + } + serverQueue.playing = true; +} diff --git a/commands/bug.js b/commands/bug.js new file mode 100644 index 00000000..f998f668 --- /dev/null +++ b/commands/bug.js @@ -0,0 +1,14 @@ +module.exports = { + name: 'bug', + description: 'Report a bug', + alias: 'bug', + cooldown: 5, + onlyDev: false, + async execute(message, args, client, Discord, prefix) { + const embed = new Discord.RichEmbed() + .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); + message.channel.send(embed); + }, +}; \ No newline at end of file diff --git a/commands/cmduses.js b/commands/cmduses.js new file mode 100644 index 00000000..0c10ab63 --- /dev/null +++ b/commands/cmduses.js @@ -0,0 +1,28 @@ +module.exports = { + name: 'cmduses', + usage: '', + description: 'Command usage statistics', + uses: 0, + 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.RichEmbed(); + 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/commands/eval.js b/commands/eval.js new file mode 100644 index 00000000..4e4bfd02 --- /dev/null +++ b/commands/eval.js @@ -0,0 +1,26 @@ +module.exports = { + name: 'eval', + description: 'Evaluation command', + alias: 'eval', + cooldown: 5, + onlyDev: true, + async execute(message, args, client, Discord, prefix) { + const ytdl = require('ytdl-core'); + const serverQueue = client.queue.get(message.guild.id); + if (serverQueue) { + let data = await Promise.resolve(ytdl.getInfo(serverQueue.songs[0].url)); + } + const input = message.content.slice(prefix.length + 4); + let output; + try { + output = await eval(input); + } catch (error) { + output = error.toString(); + } + const embed = new Discord.RichEmbed() + .setTitle('Evaluation Command') + .setColor(client.config.embedColor) + .setDescription(`Input: \`\`\`js\n${input.replace(/; /g, ';').replace(/;/g, ';\n')}\n\`\`\`\nOutput: \`\`\`\n${output}\n\`\`\``); + return message.channel.send(embed); + }, +}; diff --git a/commands/forcestop.js b/commands/forcestop.js new file mode 100644 index 00000000..8004686a --- /dev/null +++ b/commands/forcestop.js @@ -0,0 +1,11 @@ +module.exports = { + name: 'forcestop', + description: 'force stop command.', + alias: 'fs', + cooldown: 5, + onlyDev: true, + execute(message, args, client, Discord, prefix) { + client.queue.delete(message.guild.id); + message.channel.send('queue deleted') + } +}; diff --git a/commands/help.js b/commands/help.js new file mode 100644 index 00000000..0b3a38bf --- /dev/null +++ b/commands/help.js @@ -0,0 +1,29 @@ +module.exports = { + name: 'help', + description: 'Help command.', + alias: 'help', + cooldown: 5, + onlyDev: false, + execute(message, args, client, Discord, prefix) { + const embed = new Discord.RichEmbed() + .setTitle(`Commands for ${client.user.username}!`) + .addField(`${prefix}play | ${prefix}p`, 'Play a song.', true) + .addField(`${prefix}skip | ${prefix}s`, 'Skip a song.', true) + .addField(`${prefix}queue | ${prefix}q`, 'Display the queue.', true) + .addField(`${prefix}nowplaying | ${prefix}np`, 'Display what\'s currently playing.', true) + .addField(`${prefix}remove | ${prefix}rm`, 'Remove songs from the queue.', true) + .addField(`${prefix}volume`, 'Change or check the volume.', true) + .addField(`${prefix}pause`, 'Pause the music.', true) + .addField(`${prefix}resume`, 'Resume the music.', true) + .addField(`${prefix}loop`, 'Loop the queue.', true) + .addField(`${prefix}stop`, 'Stop the music, Clear the queue and leave the current voice channel.', true) + .addField(`${prefix}invite`, 'Invite Musix.', true) + .addField(`${prefix}status`, 'See different information for Musix.', true) + .addField(`${prefix}bug`, 'Report a bug.', true) + .addField(`${prefix}settings`, 'Change the guild specific settings.', true) + .addField(`${prefix}help`, 'Display the help.', true) + .setAuthor(message.member.username, message.member.displayAvatarURL) + .setColor(client.config.embedColor) + return message.channel.send(embed); + } +}; diff --git a/commands/invite.js b/commands/invite.js new file mode 100644 index 00000000..f33e3d24 --- /dev/null +++ b/commands/invite.js @@ -0,0 +1,14 @@ +module.exports = { + name: 'invite', + description: 'Invite command.', + alias: 'invite', + cooldown: 5, + onlyDev: false, + execute(message, args, client, Discord, prefix) { + const embed = new Discord.RichEmbed() + .setTitle(`Invite ${client.user.username} to your Discord server!`) + .setURL(client.config.invite) + .setColor(client.config.embedColor) + return message.channel.send(embed); + } +}; \ No newline at end of file diff --git a/commands/loop.js b/commands/loop.js new file mode 100644 index 00000000..59337ae8 --- /dev/null +++ b/commands/loop.js @@ -0,0 +1,28 @@ +module.exports = { + name: 'loop', + description: 'loop command.', + alias: 'loop', + cooldown: 10, + onlyDev: false, + async execute(message, args, client, Discord, prefix) { + const serverQueue = client.queue.get(message.guild.id); + const permissions = message.channel.permissionsFor(message.author); + const { voiceChannel } = message.member; + if (!serverQueue) return message.channel.send(':x: There is nothing playing.'); + if (message.author.id !== client.config.devId) { + if (voiceChannel !== serverQueue.voiceChannel) return message.channel.send(':x: I\'m sorry but you need to be in the same voice channel as Musix to loop the queue!'); + if (client.global.db.guilds[message.guild.id].permissions === true) { + if (client.global.db.guilds[message.guild.id].dj) { + if (!message.member.roles.has(client.global.db.guilds[message.guild.id].djrole)) return message.channel.send(':x: You need the `DJ` role to loop the queue!'); + } else if (!permissions.has('MANAGE_MESSAGES')) return message.channel.send(':x: You need the `MANAGE_MESSAGES` permission to loop the queue!'); + } + } + if (!serverQueue.looping) { + serverQueue.looping = true; + message.channel.send(':repeat: Looping the queue now!'); + } else { + serverQueue.looping = false; + message.channel.send(':repeat: No longer looping the queue!'); + } + } +}; diff --git a/commands/nowplaying.js b/commands/nowplaying.js new file mode 100644 index 00000000..b4bd1671 --- /dev/null +++ b/commands/nowplaying.js @@ -0,0 +1,27 @@ +module.exports = { + name: 'nowplaying', + description: 'Now playing command.', + alias: 'np', + cooldown: 5, + onlyDev: false, + async execute(message, args, client, Discord, prefix) { + const ytdl = require('ytdl-core'); + const serverQueue = client.queue.get(message.guild.id); + if (!serverQueue) return message.channel.send(':x: There is nothing playing.'); + if (!serverQueue.playing) return message.channel.send(':x: There is nothing playing.'); + let data = await Promise.resolve(ytdl.getInfo(serverQueue.songs[0].url)); + let songtime = (data.length_seconds * 1000).toFixed(0); + let completed = (serverQueue.connection.dispatcher.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 embed = new Discord.RichEmbed() + .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) + .setColor(client.config.embedColor) + return message.channel.send(embed); + } +}; + diff --git a/commands/pause.js b/commands/pause.js new file mode 100644 index 00000000..c8f37a9f --- /dev/null +++ b/commands/pause.js @@ -0,0 +1,26 @@ +module.exports = { + name: 'pause', + description: 'Pause command.', + alias: 'pause', + cooldown: 5, + onlyDev: false, + execute(message, args, client, Discord, prefix) { + const serverQueue = client.queue.get(message.guild.id); + const permissions = message.channel.permissionsFor(message.author); + const { voiceChannel } = message.member; + if (!serverQueue) return message.channel.send(':x: There is nothing playing.'); + if (serverQueue.playing && !serverQueue.paused) { + if (voiceChannel !== serverQueue.voiceChannel) return message.channel.send(':x: I\'m sorry but you need to be in the same voice channel as Musix to pause the music!'); + if (message.author.id !== client.config.devId) { + if (client.global.db.guilds[message.guild.id].permissions === true) { + if (client.global.db.guilds[message.guild.id].dj) { + if (!message.member.roles.has(client.global.db.guilds[message.guild.id].djrole)) return message.channel.send(':x: You need the `DJ` role to pause the music!'); + } else if (!permissions.has('MANAGE_MESSAGES')) return message.channel.send(':x: You need the `MANAGE_MESSAGES` permission to pause the music!'); + } + } + serverQueue.paused = true; + serverQueue.connection.dispatcher.pause(); + return message.channel.send('⏸ Paused the music!'); + } else return message.channel.send(':x: There is nothing playing.'); + } +}; diff --git a/commands/play.js b/commands/play.js new file mode 100644 index 00000000..244a9994 --- /dev/null +++ b/commands/play.js @@ -0,0 +1,85 @@ +const YouTube = require("simple-youtube-api"); +const he = require('he'); + +module.exports = { + name: 'play', + description: 'Play command.', + usage: '[song name]', + alias: 'p', + args: true, + cooldown: 3, + onlyDev: false, + async execute(message, 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(message.guild.id); + const voiceChannel = message.member.voiceChannel; + if (!serverQueue) { + if (!voiceChannel) return message.channel.send(':x: I\'m sorry but you need to be in a voice channel to play music!'); + } else { + if (voiceChannel !== serverQueue.voiceChannel) return message.channel.send(':x: I\'m sorry but you need to be in the same voice channel as Musix to play music!'); + } + if (!args[1]) return message.channel.send(':x: You need to use a link or search for a song!'); + const permissions = voiceChannel.permissionsFor(message.client.user); + if (!permissions.has('CONNECT')) { + return message.channel.send(':x: I cannot connect to your voice channel, make sure I have the proper permissions!'); + } + if (!permissions.has('SPEAK')) { + return message.channel.send(':x: 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 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, message, voiceChannel, client, true); + } + return message.channel.send(`:white_check_mark: Playlist: **${playlist.title}** has been added to the queue!`); + } else if (client.global.db.guilds[message.guild.id].songSelection) { + try { + var video = await youtube.getVideo(url); + } catch (error) { + try { + var videos = await youtube.searchVideos(searchString, 10); + let index = 0; + const embed = new Discord.RichEmbed() + .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) + message.channel.send(embed); + try { + var response = await message.channel.awaitMessages(message2 => message2.content > 0 && message2.content < 11 && message2.author === message.author, { + maxMatches: 1, + time: 10000, + errors: ['time'] + }); + } catch (err) { + console.error(err); + return message.channel.send(':x: Cancelling video selection'); + } + const videoIndex = parseInt(response.first().content); + var video = await youtube.getVideoByID(videos[videoIndex - 1].id); + } catch (err) { + console.error(err); + return message.channel.send(':x: I could not obtain any search results!'); + } + } + return client.funcs.handleVideo(video, message, voiceChannel, client, false); + } else { + try { + var video = await youtube.getVideo(url); + } catch (error) { + try { + var videos = await youtube.searchVideos(searchString, 1); + var video = await youtube.getVideoByID(videos[0].id); + } catch (err) { + console.error(err); + return message.channel.send(':x: I could not obtain any search results!'); + } + } + return client.funcs.handleVideo(video, message, voiceChannel, client, false); + } + } +}; diff --git a/commands/playlist.js b/commands/playlist.js new file mode 100644 index 00000000..e549ccf1 --- /dev/null +++ b/commands/playlist.js @@ -0,0 +1,50 @@ +const YouTube = require("simple-youtube-api"); +const he = require('he'); + +module.exports = { + name: 'playlist', + usage: '[option]', + description: 'Save and load queues', + alias: 'pl', + cooldown: 10, + onlyDev: false, + async execute(message, args, client, Discord, prefix) { + const embed = new Discord.RichEmbed() + .setTitle('Options for playlist!') + .addField('play', 'Play the guild specific queue.', true) + .addField('save', 'Save the currently playing queue. Note that this will overwrite the currently saved queue!', true) + .addField('add', 'Add songs to the playlist. Like song selection', true) + .addField('remove', 'Remove songs from the playlist.', true) + .addField('list', 'Display the playlist.', true) + .setFooter(`how to use: ${prefix}playlist