├── .gitignore ├── events ├── guildDelete.js ├── guildCreate.js ├── ready.js └── message.js ├── util ├── Event.js ├── Command.js ├── MusicHandling.js └── Embeds.js ├── commands ├── ping.js ├── reboot.js ├── np.js ├── queue.js ├── reload.js ├── skip.js ├── stop.js ├── pause.js ├── join.js ├── leave.js ├── resume.js ├── update.js ├── loop.js ├── volume.js ├── settings.js ├── eval.js ├── play.js ├── search.js └── help.js ├── .eslintrc.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | /node_modules/ 3 | /data/ -------------------------------------------------------------------------------- /events/guildDelete.js: -------------------------------------------------------------------------------- 1 | const Event = require('../util/Event'); 2 | 3 | class GuildDelete extends Event { 4 | constructor(...args) { 5 | super(...args); 6 | } 7 | 8 | run(guild) { 9 | this.client.settings.delete(guild.id); 10 | } 11 | } 12 | 13 | module.exports = GuildDelete; -------------------------------------------------------------------------------- /events/guildCreate.js: -------------------------------------------------------------------------------- 1 | const Event = require('../util/Event'); 2 | 3 | class GuildCreate extends Event { 4 | constructor(...args) { 5 | super(...args); 6 | } 7 | 8 | run(guild) { 9 | this.client.setDefaultGuildSettings(guild.id); 10 | } 11 | } 12 | 13 | module.exports = GuildCreate; -------------------------------------------------------------------------------- /util/Event.js: -------------------------------------------------------------------------------- 1 | class Event { 2 | 3 | constructor(client, file, options = {}) { 4 | this.client = client; 5 | this.name = options.name || file.name; 6 | this.file = file; 7 | } 8 | 9 | async _run(...args) { 10 | try { 11 | await this.run(...args); 12 | } catch (err) { 13 | console.error(err); 14 | } 15 | } 16 | 17 | reload() { 18 | const path = `../events/${this.name}.js`; 19 | delete require.cache[path]; 20 | require(`../events/${this.name}.js`); 21 | } 22 | } 23 | 24 | module.exports = Event; -------------------------------------------------------------------------------- /commands/ping.js: -------------------------------------------------------------------------------- 1 | const Command = require('../util/Command.js'); 2 | 3 | class Ping extends Command { 4 | constructor(client) { 5 | super(client, { 6 | name: 'ping', 7 | description: 'This command will get the ping of the client', 8 | usage: 'ping', 9 | aliases: ['pong'], 10 | cooldown: 5, 11 | category: 'System' 12 | 13 | }); 14 | } 15 | 16 | async run(message) { 17 | const m = await message.channel.send('Pinging...'); 18 | m.edit(`Ping: ${m.createdTimestamp - message.createdTimestamp}ms`); 19 | } 20 | } 21 | 22 | module.exports = Ping; -------------------------------------------------------------------------------- /commands/reboot.js: -------------------------------------------------------------------------------- 1 | const Command = require('../util/Command.js'); 2 | const { promisify } = require('util'); 3 | const writeFile = promisify(require('fs').writeFile); 4 | 5 | class Reboot extends Command { 6 | constructor(client) { 7 | super(client, { 8 | name: 'reboot', 9 | description: 'Reboots the bot if running on pm2 or nodemon', 10 | usage: 'reboot', 11 | aliases: ['restart'], 12 | cooldown: 0, 13 | category: 'Owner' 14 | }); 15 | } 16 | 17 | async run(message) { 18 | const m = await message.channel.send('Rebooting the bot, please wait...'); 19 | await writeFile('./data/reboot.json', `{ "messageID": "${m.id}", "channelID": "${m.channel.id}" }`); 20 | process.exit(1); 21 | } 22 | } 23 | 24 | 25 | module.exports = Reboot; -------------------------------------------------------------------------------- /commands/np.js: -------------------------------------------------------------------------------- 1 | /* eslint linebreak-style: 0 */ 2 | const Command = require('../util/Command.js'); 3 | 4 | class NP extends Command { 5 | constructor(client) { 6 | super(client, { 7 | name: 'np', 8 | description: 'This command will display the current playing song.', 9 | usage: 'np', 10 | aliases: ['nowplaying', 'current'], 11 | cooldown: 5, 12 | category: 'Music' 13 | }); 14 | } 15 | 16 | async run(message) { 17 | const voiceChannel = message.member.voiceChannel; 18 | if (!voiceChannel) return this.client.embed('noVoiceChannel', message); 19 | if (!this.client.playlists.has(message.guild.id)) return this.client.embed('emptyQueue', message); 20 | return this.client.embed('nowPlaying', message); 21 | } 22 | } 23 | 24 | module.exports = NP; -------------------------------------------------------------------------------- /util/Command.js: -------------------------------------------------------------------------------- 1 | module.exports = class Command { 2 | constructor(client, options) { 3 | 4 | this.client = client; 5 | 6 | this.name = options.name; 7 | 8 | this.usage = options.usage || 'No usage provided'; 9 | 10 | this.description = options.description || 'No description provided'; 11 | 12 | this.extended = options.extended || 'No extended description provided'; 13 | 14 | this.aliases = options.aliases || 'No aliases for this certain command'; 15 | 16 | this.category = options.category || null; 17 | 18 | this.cooldown = options.cooldown || 3; 19 | 20 | } 21 | 22 | reload() { 23 | const path = `../commands/${this.name}.js`; 24 | delete require.cache[path]; 25 | const command = require(`../commands/${this.name}.js`); 26 | this.client.commands.set(command); 27 | this.client.aliases.set(command.aliases); 28 | } 29 | }; -------------------------------------------------------------------------------- /commands/queue.js: -------------------------------------------------------------------------------- 1 | const Command = require('../util/Command.js'); 2 | 3 | class Queue extends Command { 4 | constructor(client) { 5 | super(client, { 6 | name: 'queue', 7 | description: 'This command will display all songs in the queue.', 8 | extended: 'This command will display all of the songs in the queue, if the queue is too large, it will upload the queue to hastebin, where the user could see all of the songs', 9 | usage: 'queue', 10 | cooldown: 5, 11 | category: 'Music' 12 | }); 13 | } 14 | 15 | async run(message) { 16 | const voiceChannel = message.member.voiceChannel; 17 | if (!voiceChannel) return this.client.embed('noVoiceChannel', message); 18 | if (!this.client.playlists.has(message.guild.id)) return this.client.embed('emptyQueue', message); 19 | return this.client.embed('queueEmbed', message); 20 | } 21 | } 22 | 23 | 24 | module.exports = Queue; -------------------------------------------------------------------------------- /events/ready.js: -------------------------------------------------------------------------------- 1 | const Event = require('../util/Event.js'); 2 | const fs = require('fs'); 3 | 4 | class Ready extends Event { 5 | 6 | constructor(...args) { 7 | super(...args); 8 | } 9 | 10 | async run() { 11 | this.client.log(`${this.client.user.tag} is ready in ${this.client.guilds.size} guild and serving ${this.client.guilds.reduce((c, p) => c + p.memberCount, 0)} users!`); 12 | this.client.guilds.forEach(g => { 13 | if (!this.client.settings.has(g.id)) this.client.setDefaultGuildSettings(g.id); 14 | }); 15 | 16 | if (fs.existsSync('./data/reboot.json')) { 17 | const data = JSON.parse(fs.readFileSync('./data/reboot.json', 'utf8')); 18 | const channel = this.client.channels.get(data.channelID); 19 | const message = await channel.messages.fetch(data.messageID); 20 | message.edit('Successfully rebooted the bot!'); 21 | fs.unlinkSync('./data/reboot.json'); 22 | } 23 | } 24 | } 25 | 26 | module.exports = Ready; -------------------------------------------------------------------------------- /commands/reload.js: -------------------------------------------------------------------------------- 1 | const Command = require('../util/Command.js'); 2 | 3 | class Reload extends Command { 4 | constructor(client) { 5 | super(client, { 6 | name: 'reload', 7 | description: 'Reloads a command instead of restarting the bot', 8 | usage: 'r ', 9 | aliases: ['r'], 10 | cooldown: 0, 11 | category: 'Owner' 12 | }); 13 | } 14 | 15 | async run(message, args) { 16 | const command = this.client.commands.has(args[0]) ? this.client.commands.get(args[0]) : (this.client.aliases.has(args[0]) ? this.client.aliases.get(args[0]) : null); 17 | if (command) { 18 | try { 19 | await command.reload(); 20 | message.channel.send('Successfully reloaded command: ' + args[0]); 21 | } catch (e) { 22 | message.channel.send('Error : ' + e.stack); 23 | } 24 | } else { 25 | message.channel.send('Invalid file!'); 26 | } 27 | } 28 | } 29 | 30 | 31 | module.exports = Reload; -------------------------------------------------------------------------------- /commands/skip.js: -------------------------------------------------------------------------------- 1 | const Command = require('../util/Command.js'); 2 | 3 | class Skip extends Command { 4 | constructor(client) { 5 | super(client, { 6 | name: 'skip', 7 | description: 'This command will skip a current playing song.', 8 | usage: 'skip', 9 | aliases: ['next'], 10 | cooldown: 5, 11 | category: 'Music' 12 | }); 13 | } 14 | 15 | async run(message) { 16 | if (message.settings.djonly && !message.member.roles.some(c => c.name.toLowerCase() === message.settings.djrole.toLowerCase())) return message.client.embed('notDJ', message); 17 | const voiceChannel = message.member.voiceChannel; 18 | if (!voiceChannel) return this.client.embed('noVoiceChannel', message); 19 | if (!this.client.playlists.has(message.guild.id)) return this.client.embed('emptyQueue', message); 20 | const thisPlaylist = this.client.playlists.get(message.guild.id); 21 | thisPlaylist.loop = false; 22 | thisPlaylist.connection.dispatcher.end('skip'); 23 | return this.client.embed('skipped', message); 24 | } 25 | } 26 | 27 | module.exports = Skip; -------------------------------------------------------------------------------- /commands/stop.js: -------------------------------------------------------------------------------- 1 | /* eslint linebreak-style: 0 */ 2 | const Command = require('../util/Command.js'); 3 | 4 | class Stop extends Command { 5 | constructor(client) { 6 | super(client, { 7 | name: 'stop', 8 | description: 'This command will stop the current playing songs and clear the queue.', 9 | usage: 'stop', 10 | cooldown: 5, 11 | category: 'Music' 12 | }); 13 | } 14 | 15 | async run(message) { 16 | if (message.settings.djonly && !message.member.roles.some(c => c.name.toLowerCase() === message.settings.djrole.toLowerCase())) return message.client.embed('notDJ', message); 17 | const voiceChannel = message.member.voiceChannel; 18 | if (!voiceChannel) return this.client.embed('noVoiceChannel', message); 19 | if (!this.client.playlists.has(message.guild.id)) return this.client.embed('emptyQueue', message); 20 | const thisPlaylist = this.client.playlists.get(message.guild.id); 21 | thisPlaylist.songs = []; 22 | thisPlaylist.connection.dispatcher.end(); 23 | return this.client.embed('stopped', message); 24 | } 25 | } 26 | 27 | module.exports = Stop; -------------------------------------------------------------------------------- /commands/pause.js: -------------------------------------------------------------------------------- 1 | const Command = require('../util/Command'); 2 | 3 | class Pause extends Command { 4 | constructor(client) { 5 | super(client, { 6 | name: 'pause', 7 | description: 'This command will pause the current playing song.', 8 | usage: 'pause', 9 | cooldown: 5, 10 | category: 'Music' 11 | }); 12 | } 13 | 14 | async run(message) { 15 | if (message.settings.djonly && !message.member.roles.some(c => c.name.toLowerCase() === message.settings.djrole.toLowerCase())) return message.client.embed('notDJ', message); 16 | const voiceChannel = message.member.voiceChannel; 17 | if (!voiceChannel) return this.client.embed('noVoiceChannel', message); 18 | if (!this.client.playlists.has(message.guild.id)) return this.client.embed('emptyQueue', message); 19 | const thisPlaylist = this.client.playlists.get(message.guild.id); 20 | if (!thisPlaylist.playing) return this.client.embed('alreadyPaused', message); 21 | thisPlaylist.playing = false; 22 | thisPlaylist.connection.dispatcher.pause(); 23 | return this.client.embed('paused', message); 24 | } 25 | } 26 | 27 | module.exports = Pause; -------------------------------------------------------------------------------- /commands/join.js: -------------------------------------------------------------------------------- 1 | /* eslint linebreak-style: 0 */ 2 | const Command = require('../util/Command.js'); 3 | 4 | class Join extends Command { 5 | constructor(client) { 6 | super(client, { 7 | name: 'join', 8 | description: 'This command will force the bot to join your current call', 9 | usage: 'join', 10 | aliases: ['summon'], 11 | cooldown: 5, 12 | category: 'Music' 13 | 14 | }); 15 | } 16 | 17 | async run(message) { 18 | if (message.settings.djonly && !message.member.roles.some(c => c.name.toLowerCase() === message.settings.djrole.toLowerCase())) return message.client.embed('notDJ', message); 19 | const voiceChannel = message.member.voiceChannel; 20 | if (!voiceChannel) return this.client.embed('noVoiceChannel', message); 21 | const permissions = voiceChannel.permissionsFor(message.guild.me).toArray(); 22 | if (!permissions.includes('CONNECT')) return this.client.embed('noPerms-CONNECT', message); 23 | if (!permissions.includes('SPEAK')) return this.client.embed('noPerms-SPEAK', message); 24 | voiceChannel.join(); 25 | this.client.embed('summoned', message); 26 | } 27 | } 28 | 29 | module.exports = Join; -------------------------------------------------------------------------------- /commands/leave.js: -------------------------------------------------------------------------------- 1 | /* eslint linebreak-style: 0 */ 2 | const Command = require('../util/Command.js'); 3 | 4 | class Leave extends Command { 5 | constructor(client) { 6 | super(client, { 7 | name: 'leave', 8 | description: 'This command will force the bot to leave your current call', 9 | usage: 'leave', 10 | aliases: ['unsummon'], 11 | cooldown: 5, 12 | category: 'Music' 13 | }); 14 | } 15 | 16 | async run(message) { 17 | if (message.settings.djonly && !message.member.roles.some(c => c.name.toLowerCase() === message.settings.djrole.toLowerCase())) return message.client.embed('notDJ', message); 18 | const voiceChannel = message.member.voiceChannel; 19 | if (!voiceChannel) return this.client.embed('noVoiceChannel', message); 20 | const permissions = voiceChannel.permissionsFor(message.guild.me).toArray(); 21 | if (!permissions.includes('CONNECT')) return this.client.embed('noPerms-CONNECT', message); 22 | if (!permissions.includes('SPEAK')) return this.client.embed('noPerms-SPEAK', message); 23 | voiceChannel.leave(); 24 | this.client.embed('unsummoned', message); 25 | } 26 | } 27 | 28 | module.exports = Leave; -------------------------------------------------------------------------------- /commands/resume.js: -------------------------------------------------------------------------------- 1 | /* eslint linebreak-style: 0 */ 2 | const Command = require('../util/Command.js'); 3 | 4 | class Resume extends Command { 5 | constructor(client) { 6 | super(client, { 7 | name: 'resume', 8 | description: 'This command will resume the currently paused song.', 9 | usage: 'resume', 10 | cooldown: 5, 11 | category: 'Music' 12 | }); 13 | } 14 | 15 | async run(message) { 16 | if (message.settings.djonly && !message.member.roles.some(c => c.name.toLowerCase() === message.settings.djrole.toLowerCase())) return message.client.embed('notDJ', message); 17 | const voiceChannel = message.member.voiceChannel; 18 | if (!voiceChannel) return this.client.embed('noVoiceChannel', message); 19 | if (!this.client.playlists.has(message.guild.id)) return this.client.embed('emptyQueue', message); 20 | const thisPlaylist = this.client.playlists.get(message.guild.id); 21 | if (thisPlaylist.playing) return this.client.embed('alreadyResumed', message); 22 | thisPlaylist.playing = true; 23 | thisPlaylist.connection.dispatcher.resume(); 24 | return this.client.embed('resumed', message); 25 | } 26 | } 27 | 28 | module.exports = Resume; -------------------------------------------------------------------------------- /commands/update.js: -------------------------------------------------------------------------------- 1 | const Command = require('../util/Command.js'); 2 | const { promisify } = require('util'); 3 | const exec = promisify(require('child_process').exec); 4 | const path = require('path'); 5 | 6 | class Update extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'update', 10 | description: 'Updates the bots files by pulling from git, requires git cli to be installed', 11 | usage: 'update', 12 | aliases: ['pull', 'git', 'gitpull'], 13 | cooldown: 0, 14 | category: 'Owner' 15 | }); 16 | } 17 | 18 | async run(message) { 19 | const { stdout, stderr, err } = await exec(`git pull ${require('../package.json').repository.url.split('+')[1]}`, { cwd: path.join(__dirname, '../') }).catch(err => ({ err })); 20 | if (err) this.client.log(err); 21 | const out = []; 22 | if (stdout) out.push(stdout); 23 | if (stderr) out.push(stderr); 24 | await message.channel.send(out.join('---\n'), { code: true }); 25 | if (!stdout.toString().includes('Already up-to-date.') && (message.flags[0] === 'restart' || message.flags[0] === 'r')) { 26 | this.client.commands.get('reboot').run(message); 27 | } 28 | } 29 | } 30 | 31 | 32 | module.exports = Update; -------------------------------------------------------------------------------- /commands/loop.js: -------------------------------------------------------------------------------- 1 | /* eslint linebreak-style: 0 */ 2 | const Command = require('../util/Command.js'); 3 | 4 | class Loop extends Command { 5 | constructor(client) { 6 | super(client, { 7 | name: 'loop', 8 | description: 'This command will loop or unloop the current playing song.', 9 | usage: 'loop', 10 | aliases: ['unloop'], 11 | cooldown: 5, 12 | category: 'Music' 13 | }); 14 | } 15 | 16 | async run(message) { 17 | if (message.settings.djonly && !message.member.roles.some(c => c.name.toLowerCase() === message.settings.djrole.toLowerCase())) return message.client.embed('notDJ', message); 18 | const voiceChannel = message.member.voiceChannel; 19 | const thisPlaylist = this.client.playlists.get(message.guild.id); 20 | if (!voiceChannel) return this.client.embed('noVoiceChannel', message); 21 | if (!this.client.playlists.has(message.guild.id)) return this.client.embed('emptyQueue', message); 22 | if (thisPlaylist.loop) { 23 | thisPlaylist.loop = false; 24 | return this.client.embed('unloopedEmbed', message); 25 | } else { 26 | thisPlaylist.loop = true; 27 | return this.client.embed('loopedEmbed', message); 28 | } 29 | } 30 | } 31 | 32 | module.exports = Loop; -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2017 4 | }, 5 | "env": { 6 | "es6": true, 7 | "node": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "rules": { 11 | "no-console": "off", 12 | "indent": [ 13 | "error", 14 | 2, 15 | { 16 | "SwitchCase": 1 17 | } 18 | ], 19 | "linebreak-style": [ 20 | "error", 21 | "unix" 22 | ], 23 | "quotes": [ 24 | "error", 25 | "single" 26 | ], 27 | "semi": [ 28 | "error", 29 | "always" 30 | ], 31 | "keyword-spacing": [ 32 | "error", { 33 | "before": true, 34 | "after": true 35 | } 36 | ], 37 | "space-before-blocks": [ 38 | "error", { 39 | "functions":"always", 40 | "keywords": "always", 41 | "classes": "always" 42 | } 43 | ], 44 | "space-before-function-paren": [ 45 | "error", { 46 | "anonymous": "never", 47 | "named": "never", 48 | "asyncArrow": "always" 49 | } 50 | ], 51 | "prefer-const": [ 52 | "error", { 53 | "destructuring": "any", 54 | "ignoreReadBeforeAssign": false 55 | } 56 | ] 57 | } 58 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Music Bot - By Youssef 2 | 3 | This a music bot crafted in discord.js master, a discord api wrapper. So far, this plays music from youtube, and youtube only. 4 | 5 | # How to host: 6 | 7 | 1. Clone this repository. 8 | 2. Install all of the npm modules by running: `npm i`. 9 | 3. Install node-opus or opusscript, and ffmpeg, by also running `npm i ffmpeg-binaries node-opus`. 10 | 4. Create a file called config.json, make sure it looks like this: 11 | 12 | ```json 13 | { 14 | "token": "BOT TOKEN HERE", 15 | "ytapikey": "YOUTUBE API KEY", 16 | "ownerid": "YOUR ID", 17 | "defaultSettings": { 18 | "prefix": "-", 19 | "ratelimit": false, 20 | "djonly": false, 21 | "djrole": "dj", 22 | "maxqueuelength": 20 23 | } 24 | } 25 | ``` 26 | 5. Save it and run it using `node index.js` or `nodemon index.js` or `pm2 start index.js` 27 | 28 | This will be updated over time, so ensure to update your own version alongside mine to ensure that all of the files are up to date. If you want to make a pull request, go for it and ensure you include all of the information necessary and describe it as much as you could. "# Music-Bot" 29 | 30 | I've got to give some credit to York from An Idiot's Guide. This command handler was inspired by his from Misaki, an anime based bot which could be found [here](https://github.com/NotAWeebDev/Misaki). -------------------------------------------------------------------------------- /commands/volume.js: -------------------------------------------------------------------------------- 1 | /* eslint linebreak-style: 0 */ 2 | const Command = require('../util/Command.js'); 3 | 4 | class Volume extends Command { 5 | constructor(client) { 6 | super(client, { 7 | name: 'volume', 8 | description: 'This command will set the volume of the songs.', 9 | usage: 'volume [number]', 10 | cooldown: 5, 11 | category: 'Music' 12 | }); 13 | } 14 | 15 | async run(message, args) { 16 | if (message.settings.djonly && !message.member.roles.some(c => c.name.toLowerCase() === message.settings.djrole.toLowerCase())) return message.client.embed('notDJ', message); 17 | const voiceChannel = message.member.voiceChannel; 18 | if (!voiceChannel) return this.client.embed('noVoiceChannel', message); 19 | if (!this.client.playlists.has(message.guild.id)) return this.client.embed('emptyQueue', message); 20 | if (!args[0]) return this.client.embed('currentVolume', message); 21 | if (Number(args[0]) < 0 || Number(args[0]) > 100) return this.client.embed('errorVolume', message); 22 | message.guild.voiceConnection.volume = Number(args[0]) / 100; 23 | this.client.playlists.get(message.guild.id).volume = Number(args[0]); 24 | this.client.playlists.get(message.guild.id).connection.dispatcher.setVolumeLogarithmic(Number(args[0]) / 100); 25 | return this.client.embed('volumeSet', message, args); 26 | } 27 | } 28 | 29 | module.exports = Volume; -------------------------------------------------------------------------------- /commands/settings.js: -------------------------------------------------------------------------------- 1 | const Command = require('../util/Command.js'); 2 | const { MessageEmbed } = require('discord.js'); 3 | 4 | class Settings extends Command { 5 | constructor(client) { 6 | super(client, { 7 | name: 'settings', 8 | description: 'This command will force the bot to leave your current call', 9 | usage: 'set ', 10 | aliases: ['set', 'config'], 11 | cooldown: 3, 12 | category: 'Administrator' 13 | }); 14 | } 15 | 16 | run(message, [prop, ...value]) { 17 | if (!prop && !value.length) { 18 | const embed = new MessageEmbed() 19 | .setAuthor('Settings ⚙') 20 | .setThumbnail('https://invisible.io/wp-content/themes/invisible/assets/img/animation.png') 21 | .setColor(message.guild.me.roles.highest.color); 22 | for (const prop in message.settings) { 23 | const value = message.settings[prop]; 24 | embed.addField(prop, value, true); 25 | } 26 | return message.channel.send(embed); 27 | } 28 | if (prop && !value && message.settings[prop]) { 29 | return message.reply(`${prop}'s value: ${message.settings[prop]}`); 30 | } else if (prop && value && message.settings[prop]) { 31 | if (!message.settings[prop]) message.reply('Invalid value!'); 32 | value = value.join(' ').toLowerCase(); 33 | if (value === 'true' || value === 'false') value = value === 'true'; 34 | if (!isNaN(Number(value))) value === Number(value); 35 | this.client.setPropSettings(message.guild.id, prop, value); 36 | return message.channel.send(`Set ${prop}'s value to equal \`${value}\``); 37 | } else { 38 | return message.reply('Invalid value!'); 39 | } 40 | } 41 | } 42 | 43 | module.exports = Settings; -------------------------------------------------------------------------------- /commands/eval.js: -------------------------------------------------------------------------------- 1 | const Command = require('../util/Command.js'); 2 | const { MessageEmbed } = require('discord.js'); 3 | const { inspect } = require('util'); 4 | const { post } = require('snekfetch'); 5 | 6 | class Eval extends Command { 7 | constructor(client) { 8 | super(client, { 9 | name: 'eval', 10 | description: 'Evaluates arbitrary Javascript.', 11 | usage: 'eval ', 12 | aliases: ['ev'], 13 | cooldown: 0, 14 | category: 'Owner' 15 | }); 16 | } 17 | 18 | async run(message, args) { 19 | const code = args.join(' '); 20 | const token = this.client.token.split('').join('[^]{0,2}'); 21 | const rev = this.client.token.split('').reverse().join('[^]{0,2}'); 22 | const filter = new RegExp(`${token}|${rev}`, 'g'); 23 | try { 24 | let output = eval(code); 25 | if (message.flags.includes('s') || message.flags.includes('silent')) return; 26 | if (output instanceof Promise || (Boolean(output) && typeof output.then === 'function' && typeof output.catch === 'function')) output = await output; 27 | output = inspect(output, { depth: 0, maxArrayLength: null }); 28 | output = output.replace(filter, '[TOKEN]'); 29 | output = clean(output); 30 | if (output.length < 1000) { 31 | const embed = new MessageEmbed() 32 | .addField('Input 📥', `\`\`\`js\n${code}\`\`\``) 33 | .addField('Output 📤', `\`\`\`js\n${output}\`\`\``) 34 | .setColor(message.guild.member(this.client.user.id).roles.highest.color || 0x00AE86); 35 | message.channel.send(embed); 36 | } else { 37 | try { 38 | const { body } = await post('https://www.hastebin.com/documents').send(output); 39 | const embed = new MessageEmbed() 40 | .setTitle('Output was too long, uploaded to hastebin!') 41 | .setURL(`https://www.hastebin.com/${body.key}.js`) 42 | .setColor(message.guild.member(this.client.user.id).roles.highest.color || 0x00AE86); 43 | message.channel.send(embed); 44 | } catch (error) { 45 | message.channel.send(`I tried to upload the output to hastebin but encountered this error ${error.name}:${error.message}`); 46 | } 47 | } 48 | } catch (error) { 49 | message.channel.send(`The following error occured \`\`\`js\n${error.stack}\`\`\``); 50 | } 51 | } 52 | } 53 | 54 | 55 | function clean(text) { 56 | return text 57 | .replace(/`/g, '`' + String.fromCharCode(8203)) 58 | .replace(/@/g, '@' + String.fromCharCode(8203)); 59 | } 60 | 61 | module.exports = Eval; -------------------------------------------------------------------------------- /commands/play.js: -------------------------------------------------------------------------------- 1 | const Command = require('../util/Command'); 2 | const { MessageEmbed } = require('discord.js'); 3 | const { ytapikey } = require('../config.json'); 4 | const ytapi = require('simple-youtube-api'); 5 | const handleVideo = require('../util/MusicHandling'); 6 | const youtube = new ytapi(ytapikey); 7 | 8 | class Play extends Command { 9 | constructor(client) { 10 | super(client, { 11 | name: 'play', 12 | usage: '-play ', 13 | description: 'This command plays a song using the bot', 14 | cooldown: 1, 15 | extended: 'This command plays a song from youtube using the bot, but you must be in the voice call to use this', 16 | category: 'Music' 17 | }); 18 | } 19 | 20 | async run(message, args) { 21 | if (message.settings.djonly && !message.member.roles.some(c => c.name.toLowerCase() === message.settings.djrole.toLowerCase())) return message.client.embed('notDJ', message); 22 | if (!args.length) return this.client.embed('noArgs', message); 23 | const voiceChannel = message.member.voiceChannel; 24 | if (!voiceChannel) return this.client.embed('noVoiceChannel', message); 25 | const url = args[0] ? args[0].replace(/<(.+)>/g, '$1') : ''; 26 | const permissions = voiceChannel.permissionsFor(this.client.user).toArray(); 27 | if (!permissions.includes('CONNECT')) return this.client.embed('noPerms-CONNECT', message); 28 | if (!permissions.includes('SPEAK')) return this.client.embed('noPerms-SPEAK', message); 29 | if (url.match(/^https?:\/\/(www.youtube.com|youtube.com)\/playlist(.*)$/)) { 30 | const playlist = await youtube.getPlaylist(url); 31 | const videos = await playlist.getVideos(); 32 | for (const video of Object.values(videos)) { 33 | const video2 = await youtube.getVideoByID(video.id); // eslint-disable-line no-await-in-loop 34 | await handleVideo(video2, message, voiceChannel, true); // eslint-disable-line no-await-in-loop 35 | } 36 | const embed = new MessageEmbed() 37 | .setAuthor('Playlist') 38 | .setDescription(`✅ Playlist: **${playlist.title}** has been added to the queue!`) 39 | .setColor(message.guild.me.roles.highest.color || 0x00AE86); 40 | message.channel.send(embed); 41 | } else { 42 | let video; 43 | try { 44 | video = await youtube.getVideo(url); 45 | } catch (error) { 46 | const videos = await youtube.searchVideos(args.join(' '), 1); 47 | if (!videos.length) return this.client.embed('noSongsFound', message, args); 48 | video = await youtube.getVideoByID(videos[0].id); 49 | } 50 | return handleVideo(video, message, voiceChannel); 51 | } 52 | } 53 | } 54 | 55 | module.exports = Play; -------------------------------------------------------------------------------- /commands/search.js: -------------------------------------------------------------------------------- 1 | /* eslint linebreak-style: 0 */ 2 | const Command = require('../util/Command.js'); 3 | const { MessageEmbed } = require('discord.js'); 4 | const ytapi = require('simple-youtube-api'); 5 | const { ytapikey } = require('../config.json'); 6 | const youtube = new ytapi(ytapikey); 7 | const handleVideo = require('../util/MusicHandling'); 8 | 9 | class Search extends Command { 10 | constructor(client) { 11 | super(client, { 12 | name: 'search', 13 | description: 'This command will allow a user to choose from 10 songs.', 14 | extended: 'This command will allow a user to choose from a selection of songs with a search phrase, limitting to 10 results', 15 | usage: 'search ', 16 | cooldown: 2, 17 | category: 'Music' 18 | }); 19 | } 20 | 21 | async run(message, args) { 22 | if (message.settings.djonly && !message.member.roles.some(c => c.name.toLowerCase() === message.settings.djrole.toLowerCase())) return message.client.embed('notDJ', message); 23 | const voiceChannel = message.member.voiceChannel; 24 | const permissions = voiceChannel.permissionsFor(message.client.user); 25 | if (!voiceChannel) return this.client.embed('noVoiceChannel', message); 26 | if (!args[0]) return this.client.embed('noArgs', message); 27 | if (!permissions.has('CONNECT')) return this.client.embed('noPerms-CONNECT', message); 28 | if (!permissions.has('SPEAK')) return this.client.embed('noPerms-SPEAK', message); 29 | let video; 30 | try { 31 | const videos = await youtube.searchVideos(args.join(' '), 10); 32 | if (!videos.length) return this.client.embed('noSongsFound', message, args); 33 | let index = 0; 34 | const embed = new MessageEmbed() 35 | .setAuthor('Song Selection') 36 | .setDescription(`${videos.map(video2 => `**${++index} -** ${video2.title}`).join('\n')}`) 37 | .setFooter('Please provide a value to select one of the search results ranging from 1-10.') 38 | .setColor(message.guild.me.roles.highest.color || 0x00AE86); 39 | message.channel.send(embed); 40 | const response = await message.channel.awaitMessages(msg2 => (msg2.content > 0 && msg2.content < 11) && msg2.author.id === message.author.id, { 41 | max: 1, 42 | time: 10000, 43 | errors: ['time'] 44 | }); 45 | if (!response) return this.client.embed('invalidEntry', message); 46 | const videoIndex = parseInt(response.first().content); 47 | video = await youtube.getVideoByID(videos[videoIndex - 1].id); 48 | } catch (err) { 49 | return this.client.embed('noSearchResults', message); 50 | } 51 | return handleVideo(video, message, voiceChannel); 52 | } 53 | } 54 | 55 | module.exports = Search; 56 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js'); 2 | const fs = require('fs'); 3 | const Enmap = require('enmap'); 4 | const EnmapSQLite = require('enmap-sqlite'); 5 | const { token, defaultSettings } = require('./config.json'); 6 | 7 | const files = fs.readdirSync('./commands'); 8 | const events = fs.readdirSync('./events'); 9 | if (!files.length) throw Error('No command files found!'); 10 | if (!events.length) throw Error('No event files found!'); 11 | const jsfiles = files.filter(c => c.split('.').pop() === 'js'); 12 | const jsevents = events.filter(c => c.split('.').pop() === 'js'); 13 | 14 | class Client extends Discord.Client { 15 | constructor() { 16 | super(); 17 | 18 | this.commands = new Discord.Collection(); 19 | 20 | this.aliases = new Discord.Collection(); 21 | 22 | this.playlists = new Discord.Collection(); 23 | 24 | this.embed = require('./util/Embeds'); 25 | 26 | this.settings = new Enmap({ provider: new EnmapSQLite({ name: 'settings' }) }); 27 | 28 | 29 | } 30 | 31 | setGuildSettings(id, obj) { 32 | return this.settings.set(id, obj); 33 | } 34 | 35 | addGuildSettings(name, val) { 36 | this.guilds.forEach(c => { 37 | const id = c.id; 38 | const current = this.settings.get(id); 39 | current[name] = val; 40 | this.settings.set(id, current); 41 | }); 42 | } 43 | 44 | removeGuildSettings(id, name) { 45 | const current = this.settings.get(id); 46 | delete current[name]; 47 | this.settings.set(id, current); 48 | } 49 | 50 | setDefaultGuildSettings(id) { 51 | this.settings.set(id, defaultSettings); 52 | } 53 | 54 | setPropSettings(id, name, val) { 55 | const current = this.settings.get(id); 56 | current[name] = val; 57 | this.settings.set(id, current); 58 | } 59 | 60 | log(message) { 61 | console.log(`[${new Date().toLocaleString()}] > ${message}`); 62 | } 63 | } 64 | 65 | const client = new Client(); 66 | 67 | for (let i = 0; i < jsfiles.length; i++) { 68 | if (!jsfiles.length) throw Error('No javascript command files found!'); 69 | const file = require(`./commands/${jsfiles[i]}`); 70 | const command = new file(client); 71 | if (typeof command.run !== 'function') throw Error(`No run function found in ${jsfiles[i]}`); 72 | client.commands.set(command.name, command); 73 | client.log(`Command loaded: ${command.name} `); 74 | if (command && command.aliases && command.aliases.constructor.name === 'Array') { 75 | for (let i = 0; i < command.aliases.length; i++) { 76 | client.aliases.set(command.aliases[i], command); 77 | } 78 | } 79 | } 80 | 81 | for (let i = 0; i < jsevents.length; i++) { 82 | if (!jsevents.length) throw Error('No javascript event files found!'); 83 | const file = require(`./events/${jsevents[i]}`); 84 | const event = new file(client, file); 85 | client.log(`Event loaded: ${event.name}`); 86 | if (typeof event.run !== 'function') throw Error(`No run function found in ${jsevents[i]}`); 87 | client.on(jsevents[i].split('.')[0], (...args) => event.run(...args)); 88 | } 89 | 90 | client.login(token); 91 | 92 | process.on('unhandledRejection', err => client.log(err)); -------------------------------------------------------------------------------- /events/message.js: -------------------------------------------------------------------------------- 1 | const Event = require('../util/Event.js'); 2 | const { MessageEmbed } = require('discord.js'); 3 | const { Collection } = require('discord.js'); 4 | const moment = require('moment'); 5 | const { ownerid } = require('../config.json'); 6 | require('moment-duration-format'); 7 | 8 | class Message extends Event { 9 | constructor(...args) { 10 | super(...args); 11 | 12 | this.ratelimits = new Collection(); 13 | } 14 | 15 | async run(message) { 16 | if (message.channel.type === 'dm') return message.channel.send('Commands are disabled in DMs.'); 17 | message.settings = this.client.settings.get(message.guild.id); 18 | const prefixMention = new RegExp(`^<@!?${this.client.user.id}> `); 19 | const prefix = message.content.match(prefixMention) ? message.content.match(prefixMention)[0] : message.settings.prefix; 20 | if (!message.content.startsWith(prefix)) return; 21 | message.flags = []; 22 | const args = message.content.split(' '); 23 | let command; 24 | if (message.content.startsWith('<')) { 25 | args.shift(); 26 | command = args.shift().toLowerCase(); 27 | } else { 28 | command = args.shift().toLowerCase().substring(prefix.length); 29 | } 30 | while (args[0] && args[0].split('')[0] === '-') message.flags.push(args.shift().slice(1)); 31 | if (this.client.commands.has(command)) { 32 | const cmd = this.client.commands.get(command); 33 | if (message.settings.ratelimit) { 34 | const rateLimit = this.ratelimit(message, cmd); 35 | if (typeof rateLimit === 'string') { 36 | const embed = new MessageEmbed() 37 | .setAuthor('Ratelimit') 38 | .setDescription(`Please wait ${rateLimit.toPlural()} before running \`${command}\` again`) 39 | .setColor(message.guild.me.roles.highest.color) 40 | .setTimestamp(); 41 | return message.channel.send(embed); 42 | } 43 | } 44 | if (cmd.category === 'Owner' && message.author.id !== ownerid) return; 45 | if (cmd.category === 'Administrator' && message.member.permissions.has('ADMINISTRATOR')) return; 46 | 47 | this.client.log(`CMD RUN: ${message.author.tag} (${message.author.id}) used command: ${cmd.name}`); 48 | await cmd.run(message, args); 49 | } else if (this.client.aliases.has(command)) { 50 | const cmd = this.client.aliases.get(command); 51 | if (message.settings.ratelimit) { 52 | const rateLimit = this.ratelimit(message, cmd); 53 | if (typeof rateLimit === 'string') { 54 | const embed = new MessageEmbed() 55 | .setAuthor('Ratelimit') 56 | .setDescription(`Please wait ${rateLimit.toPlural()} before running \`${command}\` again`) 57 | .setColor(message.guild.me.roles.highest.color) 58 | .setTimestamp(); 59 | return message.channel.send(embed); 60 | } 61 | } 62 | this.client.log(`CMD RUN: ${message.author.tag} (${message.author.id}) used command: ${cmd.name}`); 63 | await cmd.run(message, args); 64 | } else return; 65 | } 66 | 67 | 68 | ratelimit(message, cmd) { 69 | if (message.member.permissions.has('ADMINISTRATOR') || message.author.id === ownerid) return false; 70 | 71 | const cooldown = cmd.cooldown * 1000; 72 | const ratelimits = this.ratelimits.get(message.author.id) || {}; 73 | if (!ratelimits[cmd.name]) ratelimits[cmd.name] = Date.now() - cooldown; 74 | const difference = Date.now() - ratelimits[cmd.name]; 75 | if (difference < cooldown) { 76 | return moment.duration(cooldown - difference).format('D [days], H [hours], m [minutes], s [seconds]', 1); 77 | } else { 78 | ratelimits[cmd.name] = Date.now(); 79 | this.ratelimits.set(message.author.id, ratelimits); 80 | return true; 81 | } 82 | } 83 | } 84 | 85 | module.exports = Message; 86 | 87 | String.prototype.toPlural = function() { 88 | return this.replace(/((?:\D|^)1 .+?)s/g, '$1'); 89 | }; -------------------------------------------------------------------------------- /util/MusicHandling.js: -------------------------------------------------------------------------------- 1 | const { MessageEmbed, Util } = require('discord.js'); 2 | const ytdl = require('ytdl-core'); 3 | 4 | const handleVideo = async (video, message, voiceChannel, playlist = false) => { 5 | const queue = message.client.playlists; 6 | const song = { 7 | id: video.id, 8 | title: Util.escapeMarkdown(video.title), 9 | url: `https://www.youtube.com/watch?v=${video.id}`, 10 | channel: video.channel.title, 11 | channelurl: `https://www.youtube.com/channel/${video.channel.id}`, 12 | durationh: video.duration.hours, 13 | durationm: video.duration.minutes, 14 | durations: video.duration.seconds, 15 | thumbnail: video.thumbnails.default.url, 16 | author: message.author.username, 17 | }; // create the object for each song 18 | if (!queue.has(message.guild.id)) { // check if there isn't a queue for the guild already 19 | const queueConstruct = { // create the object with information we require 20 | textChannel: message.channel, 21 | voiceChannel: voiceChannel, 22 | connection: null, 23 | songs: [], 24 | volume: 5, 25 | playing: true, 26 | loop: false 27 | }; 28 | queue.set(message.guild.id, queueConstruct); // set the object we just made 29 | queueConstruct.songs.push(song); // push the song object so we can use it later 30 | try { 31 | const connection = await voiceChannel.join(); // join the voice channel 32 | queueConstruct.connection = connection; // set the connection to be used globally 33 | play(message.guild, queueConstruct.songs[0]); // play the first song in the queue 34 | } catch (error) { // any errors, HANDLED 35 | queue.delete(message.guild.id); 36 | const embed = new MessageEmbed() 37 | .setAuthor('Error') 38 | .setDescription(`An error has occured: ${error}`) 39 | .setColor(message.guild.me.roles.highest.color || 0x00AE86); 40 | return message.channel.send(embed); 41 | } 42 | } else { 43 | if (queue.get(message.guild.id).songs.length >= message.settings.maxqueuelength) return message.client.embed('maxQueue', message); 44 | queue.get(message.guild.id).songs.push(song); // if the queue exists, it'll push the song object 45 | if (playlist) return; // if it's a playlist it wont do this so doesn't spam adding songs 46 | else { 47 | const embed = new MessageEmbed() 48 | .setAuthor('Song added!') 49 | .setDescription(`✅ **${song.title}** has been added to the queue!`) 50 | .setColor(message.guild.me.roles.highest.color || 0x00AE86); 51 | return message.channel.send(embed); 52 | } 53 | } 54 | return; 55 | }; 56 | 57 | function play(guild, song) { 58 | const queue = guild.client.playlists; 59 | const serverQueue = queue.get(guild.id); 60 | if (!song) { 61 | serverQueue.voiceChannel.leave(); // if there are no songs leave the channel 62 | queue.delete(guild.id); // and also remove the guild from the collection 63 | return; 64 | } 65 | const dispatcher = queue.get(guild.id).connection.play(ytdl(song.url, {quality:'lowest', filter:'audioonly'}, {passes: 3, volume: guild.voiceConnection.volume || 0.2})) // play the song 66 | .on('end', () => { // when the song ends 67 | if (!serverQueue.loop) { // if its not looped 68 | queue.get(guild.id).songs.shift(); // remove the first item from the queue, eg. first song 69 | setTimeout(() => { // wait 250ms before playing a song due to songs skipping 70 | play(guild, queue.get(guild.id).songs[0]); // play the song 71 | }, 250); 72 | } else { // if it is looped it doens't remove the first item 73 | setTimeout(() => { // wait 250ms before playing a song due to songs skipping 74 | play(guild, queue.get(guild.id).songs[0]); // play the song 75 | }, 250); 76 | } 77 | }); 78 | dispatcher.setVolumeLogarithmic(queue.get(guild.id).volume / 5); // set the volume of the dispatcher 79 | const songdurm = String(song.durationm).padStart(2, '0'); // format the time 80 | const songdurh = String(song.durationh).padStart(2, '0'); // same ^ 81 | const songdurs = String(song.durations).padStart(2, '0'); // same ^^ 82 | 83 | const embed = new MessageEmbed() // create a message embed with all of the information 84 | .setTitle(song.channel) 85 | .setURL(song.channelurl) 86 | .setThumbnail(song.thumbnail) 87 | .setDescription(`[${song.title}](${song.url})`) 88 | .addField('__Duration__',`${songdurh}:${songdurm}:${songdurs}`, true) 89 | .addField('__Requested by__', song.author, true) 90 | .setColor(guild.member(guild.client.user.id).roles.highest.color || 0x00AE86); 91 | if (!serverQueue.loop) return queue.get(guild.id).textChannel.send(embed); 92 | } 93 | 94 | module.exports = handleVideo; // export the function -------------------------------------------------------------------------------- /commands/help.js: -------------------------------------------------------------------------------- 1 | const Command = require('../util/Command.js'); 2 | const { MessageEmbed } = require('discord.js'); 3 | 4 | const EMOJIS = ['⏮', '◀', '⏹', '▶', '⏭', '🔢']; 5 | const perpage = 10; 6 | 7 | class Help extends Command { 8 | constructor(client) { 9 | super(client, { 10 | name: 'help', 11 | description: 'Get help on a command or a command category,', 12 | category: 'System', 13 | usage: 'help [page-num]', 14 | cooldown: 5, 15 | aliases: ['h', 'halp', 'commands'] 16 | }); 17 | } 18 | 19 | async run(message, [type, page=1]) { 20 | let num = 0; 21 | if (type) type = type.toLowerCase(); 22 | const helpembed = new MessageEmbed() 23 | .setTimestamp() 24 | .setColor(message.guild.me.roles.highest.color || 0x00AE86) 25 | .setFooter(`Requested by ${message.author.tag}`, message.author.displayAvatarURL()); 26 | 27 | const sorted = this.client.commands.filter(c => c.category !== 'Owner'); 28 | 29 | if (!type) { 30 | let output = ''; 31 | const pg = Number(page); 32 | for (const cmd of sorted.values()) { 33 | if (num < perpage * pg && num > perpage * pg - (perpage + 1)) { 34 | output += `\n\`${message.settings.prefix + cmd.name}\` | ${cmd.description.length > 80 ? `${cmd.description.slice(0, 80)}...` : cmd.description}`; 35 | } 36 | num++; 37 | } 38 | 39 | if (num) { 40 | helpembed.setTitle(`Page ${page}/${Math.ceil(num / perpage)}`) 41 | .addField('Commands', output); 42 | } 43 | } 44 | if (this.client.commands.has(type) || this.client.aliases.has(type)) { 45 | const cm = this.client.commands.get(type) || this.client.aliases.get((type)); 46 | helpembed.setTitle(cm.name) 47 | .addField('Command Description', cm.description) 48 | .addField('Extended Description', cm.extended) 49 | .addField('Command Usage', `\`${cm.usage}\``) 50 | .addField('Command Aliases', cm.aliases.constructor.name === 'String' ? cm.aliases : cm.aliases.join(', ')); 51 | } 52 | if (!helpembed.description && !helpembed.fields.length) return; 53 | const msg2 = await message.channel.send(helpembed); 54 | const totalpages = Math.ceil(num / perpage); 55 | if (!message.guild.me.hasPermission(['MANAGE_MESSAGES'])) { 56 | await message.channel.send('I don\'t have permission to remove reactions, please do this manually.'); 57 | } 58 | 59 | if (msg2.embeds[0].title && msg2.embeds[0].title.includes('Page') && totalpages > 1) { 60 | for (const emoji of EMOJIS) await msg2.react(emoji); 61 | } 62 | 63 | const select = msg2.createReactionCollector( 64 | (reaction, user) => EMOJIS.includes(reaction.emoji.name) && user.id === message.author.id, 65 | { time: 30000 } 66 | ); 67 | 68 | let on = false; 69 | select.on('collect', async (r) => { 70 | const currentpage = Number(msg2.embeds[0].title.split(' ')[1].split('/')[0]); 71 | switch (r.emoji.name) { 72 | case '▶': 73 | pages(message, msg2, currentpage + 1, sorted, r, 'forward'); 74 | break; 75 | case '◀': 76 | pages(message, msg2, currentpage - 1, sorted, r, 'backward'); 77 | break; 78 | case '⏮': 79 | pages(message, msg2, 1, sorted, r); 80 | break; 81 | case '⏭': 82 | pages(message, msg2, totalpages, sorted, r); 83 | break; 84 | case '⏹': 85 | select.stop(); 86 | r.message.reactions.removeAll(); 87 | break; 88 | case '🔢': { 89 | if (on) return; 90 | on = true; 91 | await r.message.channel.send(`Please enter a selection from 1 to ${totalpages}`); 92 | const whichpage = await message.channel.awaitMessages(m => !isNaN(m.content) && m.author.id === message.author.id && Number(m.content) <= totalpages && Number(m.content) > 0, { 93 | max: 1, 94 | time: 300000, 95 | errors: ['time'] 96 | }); 97 | const pagenumber = Number(whichpage.first().content); 98 | pages(message, msg2, pagenumber, sorted, r); 99 | on = false; 100 | break; 101 | } 102 | } 103 | }); 104 | 105 | select.on('end', (r, reason) => { 106 | if (reason === 'time') { 107 | msg2.reactions.removeAll(); 108 | } 109 | }); 110 | } 111 | } 112 | 113 | module.exports = Help; 114 | 115 | String.prototype.toProperCase = function(opt_lowerCaseTheRest) { 116 | return (opt_lowerCaseTheRest ? this.toLowerCase() : this) 117 | .replace(/(^|[\s\xA0])[^\s\xA0]/g, function(s) { 118 | return s.toUpperCase(); 119 | }); 120 | }; 121 | 122 | Array.prototype.unique = function() { 123 | return this.filter((x, i, a) => a.indexOf(x) == i); 124 | }; 125 | 126 | async function pages(message, helpmessage, pagenumber, sorted, reactions, direction) { 127 | let num = 0; 128 | let output = ''; 129 | const pg = Number(pagenumber); 130 | for (const cmd of sorted.values()) { 131 | if (num < perpage * pg && num > perpage * pg - (perpage + 1)) { 132 | output += `\n\`${message.settings.prefix + cmd.name}\` | ${cmd.description.length > 80 ? `${cmd.description.slice(0, 80)}...` : cmd.description}`; 133 | } 134 | num++; 135 | } 136 | reactions.users.remove(message.author); 137 | if (direction === 'forward' && pg > Math.ceil(num / perpage)) return; 138 | if (direction === 'backward' && pg === 0) return; 139 | const helpembed = new MessageEmbed() 140 | .setTitle(`Page ${pg}/${Math.ceil(num / perpage)}`) 141 | .addField('Commands', output) 142 | .setColor(message.guild.me.roles.highest.color || 5198940) 143 | .setFooter(`Requested by ${message.author.tag}`, message.author.displayAvatarURL()); 144 | await helpmessage.edit(helpembed); 145 | } -------------------------------------------------------------------------------- /util/Embeds.js: -------------------------------------------------------------------------------- 1 | const { MessageEmbed } = require('discord.js'); 2 | const { post } = require('snekfetch'); 3 | 4 | /** 5 | * @description Makes an embed with an error 6 | * @param {String} type The type of error 7 | * @param {DiscordMessage} message The message object 8 | * @param {Array} [args = []] The arguments of the message 9 | * @returns {MessageEmbed} Sends the embed to whichever channel the original message was sent it 10 | */ 11 | 12 | async function embeds(type, message, args) { 13 | const embed = new MessageEmbed() 14 | .setColor(message.guild.me.roles.highest.color || 0x00AE86); 15 | switch (type) { 16 | case 'noVoiceChannel': { 17 | embed.setAuthor('Error') 18 | .setDescription('You must be in a voice channel first!'); 19 | break; 20 | } 21 | case 'emptyQueue': { 22 | embed.setAuthor('Error') 23 | .setDescription('There is nothing playing!'); 24 | break; 25 | } 26 | case 'errorVolume': { 27 | embed.setAuthor('Error') 28 | .setDescription('Volume must be a value between 0 and 100.'); 29 | break; 30 | } 31 | case 'currentVolume': { 32 | embed.setAuthor('Volume') 33 | .setDescription(`Current volume is ${message.client.playlists.get(message.guild.id).connection.dispatcher.volumeLogarithmic * 100}%`); 34 | break; 35 | } 36 | case 'volumeSet': { 37 | embed.setAuthor('Volume') 38 | .setDescription(`Volume has been set to ${args[0]}%`); 39 | break; 40 | } 41 | case 'queueEmbed': { 42 | embed.setAuthor('Queue') 43 | .setDescription(message.client.playlists.get(message.guild.id).songs.map(song => `**-** ${song.title}`).join('\n')) 44 | .setFooter(`Now playing: ${message.client.playlists.get(message.guild.id).songs[0].title}`); 45 | break; 46 | } 47 | case 'loopedEmbed': { 48 | embed.setAuthor('Looped') 49 | .setDescription(`The song has been looped by ${message.member.displayName}`); 50 | break; 51 | } 52 | case 'unloopedEmbed': { 53 | embed.setAuthor('Unlooped') 54 | .setDescription(`The song has been unlooped by ${message.member.displayName}`); 55 | break; 56 | } 57 | case 'nowPlaying': { 58 | const playingFor = new Date () - message.client.playlists.get(message.guild.id).connection.dispatcher.startTime; 59 | embed.setAuthor('Now Playing') 60 | .setDescription(`**${message.client.playlists.get(message.guild.id).songs[0].title}**\nHas been playing for **${formattedUptime(playingFor)}**`); 61 | break; 62 | } 63 | case 'alreadyPaused': { 64 | embed.setAuthor('Error') 65 | .setDescription('The song is already paused!'); 66 | break; 67 | } 68 | case 'paused': { 69 | embed.setAuthor('Paused') 70 | .setDescription(`The song has been paused by ${message.member.displayName}.`); 71 | break; 72 | } 73 | case 'alreadyResumed': { 74 | embed.setAuthor('Error') 75 | .setDescription('The song isn\'t paused!'); 76 | break; 77 | } 78 | case 'resumed': { 79 | embed.setAuthor('Resumed') 80 | .setDescription(`The song has been resume by ${message.member.displayName}.`); 81 | break; 82 | } 83 | case 'stopped': { 84 | embed.setAuthor('Stopped') 85 | .setDescription(`The song has been stopped by ${message.member.displayName}.`); 86 | break; 87 | } 88 | case 'skipped': { 89 | embed.setAuthor('Skipped') 90 | .setDescription(`The song has been skipped by ${message.member.displayName}.`); 91 | break; 92 | } 93 | case 'noArgs': { 94 | embed.setAuthor('Error') 95 | .setDescription('Please provide me with some arguments!'); 96 | break; 97 | } 98 | case 'noPerms-CONNECT': { 99 | embed.setAuthor('Error') 100 | .setDescription('I cannot connect to your voice channel, make sure I have the proper permissions!'); 101 | break; 102 | } 103 | case 'noPerms-SPEAK': { 104 | embed.setAuthor('Error') 105 | .setDescription('I cannot speak in this voice channel, make sure I have the proper permissions!'); 106 | break; 107 | } 108 | case 'invalidEntry': { 109 | embed.setAuthor('Error') 110 | .setDescription('No or invalid value entered, cancelling video selection.'); 111 | break; 112 | } 113 | case 'noSearchResults': { 114 | embed.setAuthor('Error') 115 | .setDescription('I could not obtain any search results.'); 116 | break; 117 | } 118 | case 'inactiveCall': { 119 | embed.setAuthor('Error') 120 | .setDescription('No one is in the call, leaving due to inactivity.'); 121 | break; 122 | } 123 | case 'noSongsFound': { 124 | embed.setAuthor('Error') 125 | .setDescription(`No songs found with the search term: ${args.join(' ')}`); 126 | break; 127 | } 128 | case 'summoned': { 129 | embed.setAuthor('Summoned') 130 | .setDescription(`${message.client.user.tag} has been forced to join ${message.member.voiceChannel}`); 131 | break; 132 | } 133 | case 'unsummoned': { 134 | embed.setAuthor('Unsummoned') 135 | .setDescription(`${message.client.user.tag} has been forced to leave ${message.member.voiceChannel}`); 136 | break; 137 | } 138 | case 'notDJ': { 139 | embed.setAuthor('Error') 140 | .setDescription('You must be a DJ to use these commands, please ask an admin disable djonly if you wish to allow non-djs to use music commands'); 141 | break; 142 | } 143 | case 'maxQueue': { 144 | embed.setAuthor('Error') 145 | .setDescription(`You have reached the max queue length of ${message.settings.maxqueuelength}, please ask an admin to increase the limit`); 146 | break; 147 | } 148 | default: { 149 | embed.setAuthor('Error') 150 | .setDescription('Sorry, but an error has occured.'); 151 | } 152 | } 153 | try { 154 | await message.channel.send(embed); 155 | } catch (error) { 156 | const { body } = await post('https://www.hastebin.com/documents').send(message.client.playlists.get(message.guild.id).songs.map(song => `- ${song.title}`).join('\n')); 157 | message.channel.send(`Queue was too long, uploaded it to hastebin: https://www.hastebin.com/${body.key}.txt`); 158 | } 159 | } 160 | 161 | module.exports = embeds; 162 | 163 | function formattedUptime(ms) { 164 | let m, s; 165 | s = Math.floor(ms / 1000); 166 | m = Math.floor(s / 60); 167 | s %= 60; 168 | const h = Math.floor(m / 60); 169 | m %= 60; 170 | const hours = `${h === 0 ? '' : h} ${h === 1 ? 'hour,' : h === 0 ? '' : 'hours,'}`; 171 | const minutes = `${m === 0 ? '' : m} ${m === 1 ? 'minute and' : m === 0 ? '' : 'minutes and'}`; 172 | const seconds = `${s} ${s === 1 ? 'second' : 'seconds'}`; 173 | return `${hours} ${minutes} ${seconds}`.trim(); 174 | } --------------------------------------------------------------------------------