// Load up the discord.js library const Discord = require("discord.js"); // Load up the shell command library var exec = require('child_process').exec; // Load up the queue library const Queue = require('./Queue.js'); // Initialize the bot. const client = new Discord.Client(); // this allows us to define a voice connection with global scope var connection; var dispatcher; // playlist-related globals var playlist = new Queue(); var played = [] var cursong; var repeatall = false; var repeatone = false; // Array of music classes users can call (artist, track, etc) const musicTypes = ['track', 'title', 'song', 'artist', 'album']; const musicTypesString = "track, title, song, artist, album"; // Here we load the config.json file that contains our token and our prefix values. const config = require("./config.json"); // Define a function to execute a command function execute(command, callback) { exec(command, function(error, stdout, stderr) { callback(stdout); }); }; // Define a new function to search + replace a char in a string String.prototype.replaceAll = function(remove, replace) { var target = this; return target.split(remove).join(replace); }; // This plays the next song in the queue, and logs that in the channel where it was requested. function play() { var nextSong = cursong; // if we aren't repeating cursong, dequeue if (!repeatone && cursong) { played.push(cursong); nextSong = playlist.dequeue(); } // if we set repeat, but had no songs played yet // we should dequeue else if (!nextSong) { nextSong = playlist.dequeue(); } dispatcher = connection.play(nextSong[0]); console.log(`Playing ${nextSong[2]}.`); nextSong[1].channel.send(`Playing ${nextSong[2]}.`); dispatcher.setVolume(0.2); dispatcher.setBitrate(96); cursong = nextSong; var endHandler = function endHandler(reason) { if (!(playlist.isEmpty())) { play(); } else { if (repeatall) { playlist.makeFilled(played); played = []; play(); console.log("Repeat all encountered."); } else { console.log("Playlist exhausted, music playback stopped."); } } } // what to do if it ends dispatcher.on("finish", endHandler); //dispatcher.on("close", endHandler); } // riven stuff // Load up the request library var Request = require("request"); var unrolledStats = new Map(); var rolledStats = new Map(); var cacheTime = 0; function updateRivens() { Request.get("http://n9e5v4d8.ssl.hwcdn.net/repos/weeklyRivensPC.json", (error, response, body) => { if (error) return console.dir(error); var rivenArr = JSON.parse(body); for (var i = 0; i < rivenArr.length; i++) { var info = Object.assign({}, rivenArr[i]); delete info.itemType; delete info.compatibility; delete info.rerolled; // veiled rivens if (!(rivenArr[i].compatibility)) { // set value in map to info unrolledStats.set(rivenArr[i].itemType.toUpperCase(), info); } else // weapon-specific, so check if rolled or unrolled { if (rivenArr[i].rerolled === true) { rolledStats.set(rivenArr[i].compatibility, info); } else { unrolledStats.set(rivenArr[i].compatibility, info); } } } }); } updateRivens(); cacheTime = new Date().getTime() / 1000; client.on("ready", () => { // This event will run if the bot starts, and logs in, successfully. console.log(`Bot has started, with ${client.users.cache.size} users, in ${client.channels.cache.size} channels of ${client.guilds.cache.size} guilds.`); console.log(`users: ${Array.from(client.users.cache.values()).map(each => each.username)}`); // Example of changing the bot's playing game to something useful. `client.user` is what the // docs refer to as the "ClientUser". client.user.setActivity("Taking Over Chicago"); }); client.on("guildCreate", guild => { // This event triggers when the bot joins a guild. console.log(`New guild joined: ${guild.name} (id: ${guild.id}). This guild has ${guild.memberCount} members!`); client.user.setActivity(`Serving ${client.guilds.cache.size} servers`); }); client.on("guildDelete", guild => { // this event triggers when the bot is removed from a guild. console.log(`I have been removed from: ${guild.name} (id: ${guild.id})`); client.user.setActivity("Taking Over Chicago"); }); client.on('message', async msg => { // Ignores bot msgs if (msg.author.bot) return; // ignores if message isn't prefixed if (msg.content.indexOf(config.prefix) !== 0) return; // Here we separate our "command" name, and our "arguments" for the command. // e.g. if we have the message "+say Is this the real life?" , we'll get the following: // command = say // args = ["Is", "this", "the", "real", "life?"] const args = msg.content.slice(config.prefix.length).trim().split(/ +/g); const command = args.shift().toLowerCase(); switch (command) { // respond w/ bot latency case 'ping': var m = await msg.channel.send('pong'); m.edit(`Pong! Latency is ${m.createdTimestamp - msg.createdTimestamp}ms. API Latency is ${Math.round(client.ws.ping)}ms`); break; // join the specified void channel case 'join': var channelName = args.join(' '); var channel = msg.guild.channels.cache.find(each => each.name === channelName && each.type === "voice"); if (channel) { channel.join().then(conn => { connection = conn; }).catch(console.error); } else { msg.reply(`Sorry ${msg.author.username}, that channel doesn't appear to exist.`); } break; // leave the specified voice channel case 'leave': var channelName = args.join(' '); var channel = msg.guild.channels.cache.find(each => each.name === channelName && each.type === "voice"); if (channel) { channel.leave(); } else { msg.reply(`Sorry ${msg.author.username}, that channel doesn't appear to exist.`); } break; // return the price of a riven for the specified weapon case 'price': // parse args var type = args[0]; args.splice(0, 1); var query = args.join(' '); query = query.toUpperCase(); // check cache freshness delta = (new Date().getTime() / 1000) - cacheTime; if (delta > 3600) updateRivens(); if (type == "rolled") { var result = rolledStats.get(query); if (!(result)) { return msg.channel.send("Sorry, I couldn't find that weapon. Please check your message and try again."); } result = JSON.stringify(result, undefined, 2); return msg.channel.send(result); } else if (type == "unrolled") { var result = unrolledStats.get(query); if (!(result)) { return msg.channel.send("Sorry, I couldn't find that weapon. Please check your message and try again."); } result = JSON.stringify(result, undefined, 2); return msg.channel.send(result); } else { return msg.channel.send("Sorry, please enter a command in the form: price unrolled/rolled [weapon_name]"); } break; // searches the DB for songs matching the user query, and responds w/ that info in chat case 'search': if (!(config.whitelist.includes(msg.author.tag))) { return msg.channel.send("Sorry, you're not allowed to run this command. Please contact the server owner to acquire that permission.") } var type = args[0]; if (!(musicTypes.includes(type))) { return msg.channel.send("Sorry, that is not a valid command. Please enter something from: " + musicTypesString); } if (type == 'song' || type == 'track') type = 'title'; // account for poor beets API args.splice(0, 1); var query = args.join(' '); // creates a single string of all args (the query) var path; // this will hold the filepaths from our query exec(`beet ls ${type}:${query} | wc -l`, function (error, stdout, stderr) { if (error) { return msg.channel.send(`Sorry, I encountered an issue looking for that: ${error}`); } else if (stdout === '\n' || stdout === '' || stdout === undefined) { return msg.channel.send('There were no results that matched your search. Please give the type and name of your query (e.g. song songname, album albumname...)'); } else { var result = 'Results:\n'; result += stdout.trim(); msg.channel.send(result); } }); break; // add the songs returned by the user query to the playlist, start if nothing playing case 'addmusic': if (!(config.whitelist.includes(msg.author.tag))) { return msg.channel.send("Sorry, you're not allowed to run this command. Please contact the server owner to acquire that permission.") } if (!connection) { return msg.channel.send("Please add me to a voice channel before adding music.") } var type = args[0]; if (!(musicTypes.includes(type))) { return msg.channel.send("Sorry, that is not a valid command. Please enter something from: " + musicTypesString); } if (type == 'song' || type == 'track') type = 'title'; // account for poor beets API args.splice(0, 1); query = args.join(' '); // creates a single string of all args (the query) var path; // this will hold the filepaths from our query exec(`beet ls -p ${type}:${query} | wc -l`, function (error, stdout, stderr) { if (error) { return msg.channel.send(`Sorry, I encountered an issue looking for that: ${error}`); } else if (stdout === '\n' || stdout === '' || stdout === undefined) { return msg.channel.send(`There were no results that matched your search. Please give the type and name of your query (e.g. song songname, album albumname...)`); } else { exec(`beet ls -p ${type}:${query}`, function (error, stdout, stderr) { if (error) { return msg.channel.send(`Sorry, I encountered an issue looking for that: ${error}`); } else { path = stdout.trim(); path = path.split("\n"); // now an array of paths (with spaces) // for each song, get the path and readable info, send to queue for (var i = 0; i < path.length; i++) { let filepathRaw = path[i]; path[i] = path[i].replaceAll(" ", "\\ "); path[i] = path[i].replaceAll("'", "\\'"); path[i] = path[i].replaceAll("&", "\\\46"); path[i] = path[i].replaceAll("(", "\\("); path[i] = path[i].replaceAll(")", "\\)"); let filepath = path[i]; // path[i] descoped in callback exec(`beet ls ${path[i]}`, function (error, stdouts, stderr) { if (error) { return msg.channel.send(`Sorry, I encountered an issue looking for song ${i}: ${error}`); } else { stdouts = stdouts.trim(); playlist.enqueue([filepathRaw, msg, stdouts]); // check if music is playing, if not start it if ((!dispatcher || dispatcher.ended || dispatcher.destroyed || dispatcher.writableFinished || dispatcher.writableEnded) && !(playlist.isEmpty())) { play(); } } }); } } }); } let amt = stdout.trim(); msg.channel.send(`${amt} songs added!`); }); break; // stops playback case 'stop': playlist.reset(); repeatone = false; repeatall = false; played = []; dispatcher.end(); console.log("Playback stopped, playlist cleared."); msg.channel.send("Playback stopped, playlist cleared."); break; // returns the next song in the playlist case 'next': if (playlist.isEmpty()) { msg.channel.send("The playlist is empty."); } else { var next = playlist.peek(); msg.channel.send(`Next song is: ${next[2]}.`); } break; // returns the last song played before the current one case 'previous': if (played.length <= 1) { msg.channel.send("No previous song."); } else { let temp = played.slice(-1).pop(); msg.channel.send(`Previous song was: ${temp[2]}`); } break; // returns the playlist case 'playlist': if (playlist.isEmpty()) { msg.channel.send("The playlist is empty."); } else { var list = playlist.read(); var retstr = "" for (var i = 0; i < list.length; i++) { retstr += `Song #${i + 1} is: ${list[i][2]}.\n`; } msg.channel.send(retstr); } break; // pauses the playback case 'pause': dispatcher.pause(true); msg.channel.send("Playback paused."); break; // resumes the playback case 'resume': dispatcher.resume(); msg.channel.send("Playback resumed."); break; // sets repeat behaviour case 'repeat': var param = args[0]; if (param === 'one' && cursong) { repeatone = true; // causes play function to repeat current cursong repeatall = false; msg.channel.send(`Repeating ${cursong[2]}.`); } else if (param === 'all') // track playlist, and repeat whole thing once empty { repeatone = false; repeatall = true; msg.channel.send("Repeating playlist."); } else if (param === 'off') // resets repeat variables { repeatone = false; repeatall = false; msg.channel.send("Repeat off."); } else { msg.channel.send("There was nothing to repeat, or an invalid option was given. Valid options are one, all, and off."); } break; // skips to the next song case 'skip': if (playlist.isEmpty()) { msg.channel.send("Sorry, the playlist is empty."); } else { function resolveEnd() { return new Promise((success, fail) => { dispatcher.end(); dispatcher.on("finish", () => { success('Track skipped!'); }); dispatcher.on("error", () => { fail('Couldn\'t skip :('); }); }); } resolveEnd(); } break; // goes back 1 song (plays last song, adds cursong back to the queue) case 'back': if (played.length == 0) { msg.channel.send("Sorry, there is no song to skip back to."); } else { function resolveEnd() { return new Promise((success, fail) => { playlist.insert(cursong, 0); // put cursong back on the front let tempsong = played[played.length - 1]; // captures the song to go back to played = played.splice(played.length - 1, 1); // removes the last song from played playlist.insert(tempsong, 0); // put old song on the front dispatcher.end(); // stop playing wrong song dispatcher.on("finish", () => { success('Track reversed!'); }); dispatcher.on("error", () => { fail('Couldn\'t skip :('); }); }); } resolveEnd(); } break; // respond w/ error if command not recognized default: msg.channel.send("Sorry, that command isn't recognized."); } }); client.login(config.token);