diff options
Diffstat (limited to 'src/cmd/voice.js')
| -rw-r--r-- | src/cmd/voice.js | 356 |
1 files changed, 356 insertions, 0 deletions
diff --git a/src/cmd/voice.js b/src/cmd/voice.js new file mode 100644 index 0000000..7f12603 --- /dev/null +++ b/src/cmd/voice.js @@ -0,0 +1,356 @@ +const superagent = require('superagent'); +const wait = require('../Wait'); +const time = require('../Time'); +const hastebin = require('../Hastebin'); + +const querystring = require('querystring'); +const chunk = require('chunk-text'); + +async function resolveTracks(node, search) { + let result; + try { + result = await superagent.get(`http://${node.host}:${node.port}/loadtracks?identifier=${search}`) + .set('Authorization', node.password) + .set('Accept', 'application/json'); + } catch (err) { + throw err; + } + + if (!result) { + throw 'Unable play that video.'; + } + + return result.body; // array of tracks resolved from lavalink +} + +function getLavalinkPlayer(channel, bot) { + if (!channel || !channel.guild) { + return Promise.reject('Not a guild channel.'); + } + + let player = bot.voiceConnections.get(channel.guild.id); + if (player) { + return Promise.resolve(player); + } + + let options = {}; + if (channel.guild.region) { + options.region = channel.guild.region; + } + + return bot.joinVoiceChannel(channel.id, options); +} + +function getRandomColor() { + var letters = '0123456789ABCDEF'; + var color = ''; + for (var i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 16)]; + } + return parseInt(color); +} + +async function crawl(player, bot, message) { + if (bot.voices[message.channel.guild.id].queue.length === 0) { + bot.voices[message.channel.guild.id].crawling = false; + delete bot.voices[message.channel.guild.id].current; + bot.leaveVoiceChannel(player.channelId); + return; + }; + + bot.voices[message.channel.guild.id].crawling = true; + let next = bot.voices[message.channel.guild.id].queue.shift(); + bot.voices[message.channel.guild.id].current = next; + player.play(next.track); + + player.removeAllListeners('disconnect'); + player.once('disconnect', (err) => { + if (err) { + Logger.error(err); + } + bot.voices[message.channel.guild.id].crawling = false; + delete bot.voices[message.channel.guild.id].current; //Attempt to fix disconnect bug, implement `come' command to recrawl is still to do. Refer to previous commit for previous implementation + bot.leaveVoiceChannel(player.channelId); + }); + + player.removeAllListeners('error'); + player.once('error', (err) => { + Logger.error(`Player error: ${err}`); + // I do not know how it behaves after there's an error so I just put these in comment for now + // bot.voices[message.channel.guild.id].crawling = false; + // delete bot.voices[message.channel.guild.id].current; + // bot.leaveVoiceChannel(player.channelId); + + // Just in case, continue crawling + crawl(player, bot, message); + }); + + player.removeAllListeners('end'); + player.once('end', async (data) => { + // REPLACED reason is emitted when playing without stopping, I ignore these to prevent skip loops + if (data.reason && data.reason === 'REPLACED') return; + await wait(750); + crawl(player, bot, message); + }); +} + +module.exports.loadModule = function loadModule(bot) { + bot.handler.endpoint('^(?:add|meme|play|p|a)([;:-][0-9,-]+)? (.+)$', [], async (match, message) => { + let ones = []; + // The following messy loop parses playlist selection. + // A hacky limitation has been used here to prevent abuse. A new system HAS to be used. + if (match[1]) { + let param = match[1].substring(1); + let selectors = param.split(','); + selectors.forEach(s => { + let r = s.split('-'); + if (r.length === 2) { + let indS = parseInt(r[0]); + let indE = parseInt(r[1]); + if (indE <= indS) return; + if (indE - indS > 100) return; + for (let i = indS; i <= indE; i++) { + if (ones.indexOf(i) <= -1) { + ones.push(i); + } + } + } + else if (r.length === 1) { + let ind = parseInt(r[0]); + if (ones.indexOf(ind) <= -1) { + ones.push(ind); + } + } + }); + } + + if (!bot.voices[message.channel.guild.id]) bot.voices[message.channel.guild.id] = { queue: [] }; + + let m = match[2]; + + if (!m.match(/^<?https?:\/\//)) { + m = 'ytsearch:' + m; + } + else if (m.charAt(0) === '<' && m.charAt(m.length - 1) === '>') { + m = m.slice(1); + m = m.slice(0, -1); + } + + let t = await resolveTracks(bot._main, `${m}`); + if (t.loadType === 'TRACK_LOADED' || t.loadType === 'SEARCH_RESULT') { + let tr = t.tracks[0]; + if (tr.info.isStream) { + bot.createMessage(message.channel.id, `I do not yet support streams, I'm sorry :(`).catch(Logger.error); + return; + } + tr.adder = `${message.author.username}#${message.author.discriminator}`; + bot.voices[message.channel.guild.id].queue.push(tr); + let ign = !match[1] ? '' : ' Playlist selectors parameters have been ignored as it is a sole song.'; + bot.createMessage(message.channel.id, `\`${tr.info.title}\` has been added by \`${tr.adder}\`.${ign}`).catch(Logger.error); + } + else if (t.loadType === 'PLAYLIST_LOADED') { + let co = 0; + for (let i = 0; i < t.tracks.length; i++) { + if (ones.indexOf(i + 1) > -1 || ones.length === 0) { + let tr = t.tracks[i]; + tr.adder = `${message.author.username}#${message.author.discriminator}`; + bot.voices[message.channel.guild.id].queue.push(tr); + co++; + } + } + bot.createMessage(message.channel.id, `${co} songs have been added to the queue by \`${message.author.username}#${message.author.discriminator}\``).catch(Logger.error); + } + else if (t.loadType === 'NO_MATCHES' || t.loadType === 'LOAD_FAILED') { + bot.createMessage(message.channel.id, `No results, sorry.`).catch(Logger.error); + return; + } + + if (!message.member.voiceState.channelID) return; + let channel = message.channel.guild.channels.find(m => m.id === message.member.voiceState.channelID); + if (!bot.voices[message.channel.guild.id].crawling) { + let player = await getLavalinkPlayer(channel, bot); + player.switchChannel(message.member.voiceState.channelID, true); + bot.createMessage(message.channel.id, 'Joined voice channel.').catch(Logger.error); // TEMPORARY + crawl(player, bot, message); + } + }); + + bot.handler.endpoint('^skip$', [], async (match, message) => { + let player = bot.voiceConnections.get(message.channel.guild.id); + if (player) { + player.stop(); + bot.createMessage(message.channel.id, 'Current song skipped').catch(Logger.error); + } + else { + bot.createMessage(message.channel.id, 'The bot wasn\'t playing.').catch(Logger.error); + } + }); + + bot.handler.endpoint('^clear$', [], async (match, message) => { + bot.voices[message.channel.guild.id].queue = []; + bot.createMessage(message.channel.id, 'The current queue has been cleared!').catch(Logger.error); + }); + + bot.handler.endpoint('^(?:stop|quit|halt)!?$', [], async (match, message) => { + let player = bot.voiceConnections.get(message.channel.guild.id); + bot.voices[message.channel.guild.id].queue = []; + if (player) { + player.stop(); + bot.createMessage(message.channel.id, 'Successfully stopped!').catch(Logger.error); + } + else { + bot.createMessage(message.channel.id, 'The bot wasn\'t playing but the queue still has been cleared.').catch(Logger.error); + } + }); + + bot.handler.endpoint('^(?:come|resume)!?$', [], async (match, message) => { + if (!message.member.voiceState.channelID) return; + let channel = message.channel.guild.channels.find(m => m.id === message.member.voiceState.channelID); + let player = bot.voiceConnections.get(message.channel.guild.id); + if (player) { + player.switchChannel(message.member.voiceState.channelID, true); + if (player.paused) { + player.resume(); + bot.createMessage(message.channel.id, 'Resumed the song.').catch(Logger.error); + } + } + else { + if (!message.member.voiceState.channelID) return; + if (!bot.voices[message.channel.guild.id]) bot.voices[message.channel.guild.id] = { queue: [] }; + if (bot.voices[message.channel.guild.id].queue.length >= 1) { + let channel = message.channel.guild.channels.find(m => m.id === message.member.voiceState.channelID); + //if (!bot.voices[message.channel.guild.id].crawling) { + let player = await getLavalinkPlayer(channel, bot); + player.switchChannel(message.member.voiceState.channelID, true); + bot.createMessage(message.channel.id, 'Joined voice channel.').catch(Logger.error); // TEMPORARY + crawl(player, bot, message); + //} + } + else { + bot.createMessage(message.channel.id, 'The bot wasn\'t in any voice channel.').catch(Logger.error); + } + } + }); + + bot.handler.endpoint('^pause!?$', [], async (match, message) => { + let player = bot.voiceConnections.get(message.channel.guild.id); + if (player) { + player.pause(); + bot.createMessage(message.channel.id, 'Song paused.').catch(Logger.error); + } + else { + bot.createMessage(message.channel.id, 'The bot isn\'t in any voice channel.').catch(Logger.error); + } + }); + + bot.handler.endpoint('^skip-to!? ([0-9]+)$', [], async (match, message) => { + let player = bot.voiceConnections.get(message.channel.guild.id); + if (!bot.voices[message.channel.guild.id]) bot.voices[message.channel.guild.id] = { queue: [] }; + let arg = parseInt(match[1]); + if (arg <= 0) { + bot.createMessage(message.channel.id, `The argument should be greater than 0 (the queue is one-indexed), but 0 was provided.`).catch(Logger.error); + return; + } + if (arg > bot.voices[message.channel.guild.id].queue.length) { + bot.createMessage(message.channel.id, `Can't skip that far, the queue is currently only ${bot.voices[message.channel.guild.id].queue.length} titles long :')`).catch(Logger.error); + return; + } + bot.voices[message.channel.guild.id].queue = bot.voices[message.channel.guild.id].queue.splice(arg - 1); + if (player) { + player.stop(); + bot.createMessage(message.channel.id, `Current song and ${arg - 1} following where skipped.`).catch(Logger.error); + } + else { + bot.createMessage(message.channel.id, `The bot wasn\'t playing but the ${arg - 1} next songs were successfully removed.`).catch(Logger.error); + } + }); + + bot.handler.endpoint('^remove-to!? ([0-9]+)$', [], async (match, message) => { + if (!bot.voices[message.channel.guild.id]) bot.voices[message.channel.guild.id] = { queue: [] }; + let arg = parseInt(match[1]); + if (arg <= 0) { + bot.createMessage(message.channel.id, `The argument should be greater than 0 (the queue is one-indexed), but 0 was provided.`).catch(Logger.error); + return; + } + if (arg > bot.voices[message.channel.guild.id].queue.length) { + bot.createMessage(message.channel.id, `Can't remove that far, the queue is currently only ${bot.voices[message.channel.guild.id].queue.length} titles long :')`).catch(Logger.error); + return; + } + bot.voices[message.channel.guild.id].queue = bot.voices[message.channel.guild.id].queue.splice(arg); + bot.createMessage(message.channel.id, `${arg} next songs in the queue were removed.`).catch(Logger.error); + }); + + bot.handler.endpoint('^remove!? ([0-9]+)$', [], async (match, message) => { + if (!bot.voices[message.channel.guild.id]) bot.voices[message.channel.guild.id] = { queue: [] }; + let arg = parseInt(match[1]); + if (arg <= 0) { + bot.createMessage(message.channel.id, `The argument should be greater than 0 (the queue is one-indexed), but 0 was provided.`).catch(Logger.error); + return; + } + if (arg > bot.voices[message.channel.guild.id].queue.length) { + bot.createMessage(message.channel.id, `Can't remove that far, the queue is currently only ${bot.voices[message.channel.guild.id].queue.length} titles long :')`).catch(Logger.error); + return; + } + bot.voices[message.channel.guild.id].queue.splice(arg - 1, 1); + bot.createMessage(message.channel.id, `The ${arg}e song in the queue was removed.`).catch(Logger.error); + }); + + bot.handler.endpoint('^now\\??$', [], async (match, message) => { + let player = bot.voiceConnections.get(message.channel.guild.id); + if (player && bot.voices[message.channel.guild.id].current) { + bot.createMessage(message.channel.id, `Now playing \`${bot.voices[message.channel.guild.id].current.info.title}\` at position ${time.msToMinutes(player.state.position)} added by \`${bot.voices[message.channel.guild.id].current.adder}\`. Total duration: ${time.msToMinutes(bot.voices[message.channel.guild.id].current.info.length)}`).catch(Logger.error); + } + else { + bot.createMessage(message.channel.id, 'The bot is not playing anything ¯\\_(ツ)_/¯').catch(Logger.error); + } + }); + + bot.handler.endpoint('^(?:queue|playlist|q|list)\\??$', [], async (match, message) => { + if (!bot.voices[message.channel.guild.id].queue || bot.voices[message.channel.guild.id].queue.length === 0) { + bot.createMessage(message.channel.id, 'The queue is empty!').catch(Logger.error); + return; + } + let buff = ""; + let count = 1; + bot.voices[message.channel.guild.id].queue.forEach(t => { + buff += `${count}. ${t.info.title}, added by \`${t.adder}\` and lasts ${time.msToMinutes(t.info.length)}\n`; + count++; + }); + bot.createMessage(message.channel.id, buff).catch(Logger.error); + }); + + bot.handler.endpoint('^(?:lyrics|text)\\??(?: (.+))?$', [], async (match, message) => { + let s = match[1] ? match[1] : null; + if (!s && (bot.voices[message.channel.guild.id] && bot.voices[message.channel.guild.id].current)) s = bot.voices[message.channel.guild.id].current.info.title; + if (!s) { + bot.createMessage(message.channel.id, 'No request mentionned and no song currently playing ¯\\_(ツ)_/¯').catch(Logger.error); + return; + } + let result; + try { + result = await superagent.get(`https://lyrics.tsu.sh/v1/?q=${s}`); + } + catch (e) { + return; + } + let res = JSON.parse(result.text); + let chunks = chunk(res.content, 2048); + if (chunks.length <= 4) { + let col = getRandomColor(); + for (let i = 0; i < chunks.length; i++) { + let data = { + embed: { + description: chunks[i], + color: col, + } + } + if (i === 0) data.embed.title = res.song.full_title; + await bot.createMessage(message.channel.id, data).catch(Logger.error); + } + } + else { + let key = await hastebin(res.content); + bot.createMessage(message.channel.id, `https://hasteb.in/${key}.txt`).catch(Logger.error); + } + }); +}; |
