diff options
| -rw-r--r-- | server/package.json | 4 | ||||
| -rw-r--r-- | server/src/Config.ts | 3 | ||||
| -rw-r--r-- | server/src/client/BotClient.ts | 5 | ||||
| -rw-r--r-- | server/src/commands/voice/Play.ts | 52 | ||||
| -rw-r--r-- | server/src/utils/Play.ts | 138 | ||||
| -rw-r--r-- | server/src/utils/Utils.ts | 12 |
6 files changed, 211 insertions, 3 deletions
diff --git a/server/package.json b/server/package.json index e54e63d..1844ce0 100644 --- a/server/package.json +++ b/server/package.json @@ -17,9 +17,11 @@ "node-emoji": "^1.10.0", "node-superfetch": "^0.1.10", "rss-parser": "^3.9.0", + "soundcloud-downloader": "^0.1.3", "svg-captcha": "^1.4.0", "svg2img": "^0.7.2", - "winston": "^3.3.3" + "winston": "^3.3.3", + "ytdl-core-discord": "^1.2.1" }, "devDependencies": { "@types/express": "^4.17.7", diff --git a/server/src/Config.ts b/server/src/Config.ts index 81ed605..316b093 100644 --- a/server/src/Config.ts +++ b/server/src/Config.ts @@ -23,4 +23,5 @@ export const captchaSettings: object = { size: 6 }; export const verificationRole: string = "Un-verified"; -export const verificationChannel: string = "verification";
\ No newline at end of file +export const verificationChannel: string = "verification"; +export const youtubeApiKey: string = "";
\ No newline at end of file diff --git a/server/src/client/BotClient.ts b/server/src/client/BotClient.ts index 37c9396..cf8b778 100644 --- a/server/src/client/BotClient.ts +++ b/server/src/client/BotClient.ts @@ -14,7 +14,8 @@ declare module 'discord-akairo' { logger: Logger; settings: SettingsProvider; img, - wait + wait, + queue } } @@ -39,6 +40,8 @@ export default class BotClient extends AkairoClient { public wait = require("util").promisify(setTimeout); + public queue = new Map(); + public inhibitorHandler: InhibitorHandler = new InhibitorHandler(this, { directory: join(__dirname, '..', 'inhibitors') }); diff --git a/server/src/commands/voice/Play.ts b/server/src/commands/voice/Play.ts new file mode 100644 index 0000000..9ea335d --- /dev/null +++ b/server/src/commands/voice/Play.ts @@ -0,0 +1,52 @@ +import { Command } from 'discord-akairo'; +import { Message } from 'discord.js'; +import request from 'node-superfetch'; + +export default class PlayVoice extends Command { + public constructor() { + super('play', { + aliases: ['play'], + category: 'voice', + description: { + content: 'Gives you the bot\'s IP address.', + usage: '[song url/ name]', + examples: [ + '' + ] + }, + ratelimit: 3, + args: [ + { + id: 'song', + type: 'string' + } + ] + }); + } + + public async exec(msg: Message, { song }): Promise<Message> { + const { channel } = msg.member.voice; + + const serverQueue = this.client.queue.get(msg.guild.id); + if (!channel) return msg.reply('You need to join a voice channel first!'); //error + if (serverQueue && channel !== msg.guild.me.voice.channel) + return msg.reply(`You must be in the same channel as ${this.client.user}`); //error + + const permissions = channel.permissionsFor(msg.client.user); + if (!permissions.has('CONNECT')) + return msg.reply('Cannot connect to voice channel, missing permissions.'); + if (!permissions.has('SPEAK')) + return msg.reply('I cannto speak in this voice channel, make sure I have the proper permissions!'); + + const search = song; + const videoPattern = /^(https?:\/\/)?(www\.)?(m\.)?(youtube\.com|youtu\.?be)\/.+$/gi; + const playlistPattern = /^.*(list=)([^#\&\?]*).*/gi; + const scRegex = /^https?:\/\/(soundcloud\.com)\/(.*)$/; + const url = song; + const urlValid = videoPattern.test(song); + + if (!videoPattern.test(song) && playlistPattern.test(song)) { + return this.client.commandHandler.commandUtil. + } + } +}
\ No newline at end of file diff --git a/server/src/utils/Play.ts b/server/src/utils/Play.ts new file mode 100644 index 0000000..507cc0c --- /dev/null +++ b/server/src/utils/Play.ts @@ -0,0 +1,138 @@ +import { Message } from "discord.js"; +import { AkairoClient } from "discord-akairo"; +import ytdlDiscord from 'ytdl-core-discord'; +import * as scdl from 'soundcloud-downloader'; +import { StreamDispatcher } from "discord.js"; +import { MessageCollector } from "discord.js"; +import Util from '../utils/Utils'; + +module.exports = { + async play(song, client: AkairoClient, msg: Message) { + const { PRUNING, SOUNDCLOUD_CLIENT_ID } = require('../Config'); + const queue = client.queue.get(msg.guild.id); + + if (!song) { + queue.channel.leave(); + client.queue.delete(msg.guild.id); + return queue.channel.send('Music queue ended.'); // catch + } + + let stream = null; + + try { + if (song.url.includes('youtube.com')) { + stream = await ytdlDiscord(song.url, { highWaterMark: 1 << 25 }); + } else if (song.url.includes('soundcloud.com') && SOUNDCLOUD_CLIENT_ID) { + stream = await scdl.downloadFormat(song.url, scdl.FORMATS.OPUS, SOUNDCLOUD_CLIENT_ID); + } + } catch (error) { + if (queue) { + queue.songs.shift(); + module.exports.play(queue.songs[0], msg); + } + + // error + return msg.channel.send(`Error: ${error.message ? error.message : error}`); + } + + queue.connection.on('disconnect', () => client.queue.delete(msg.guild.id)); + + const type = song.url.includes('youtube.com') ? 'opus' : 'ogg/opus'; + const dispatcher: StreamDispatcher = queue.connection + .play(stream, { type: type }) + .on('finish', () => { + if (collector && !collector.ended) collector.stop(); + + if (queue.loop) { + let lastSong = queue.songs.shift(); + queue.songs.push(lastSong); + // export + } else { + queue.songs.shift(); + // export + } + }) + .on('error', (err) => { + // error + queue.songs.shift(); + module.exports.play(queue.songs[0], msg); + }); + dispatcher.setVolumeLogarithmic(queue.volume / 100); + + try { + var playingMessage = await queue.channel.send(`Started playing: **${song.title}** ${song.url}`); + await playingMessage.react("⏭"); + await playingMessage.react("⏯"); + await playingMessage.react("🔁"); + await playingMessage.react("⏹"); + } catch (error) { + // error + } + + const filter = (reaction, user) => user.id !== client.user.id; + var collector: MessageCollector = playingMessage.createReactionCollector(filter, { + time: song.duration > 0 ? song.duration * 1000 : 600000 + }); + + collector.on('collect', (reaction, user) => { + if (!queue) return; + const member = msg.guild.member(user); + + switch (reaction.emoji.name) { + case "⏭": + queue.playing = true; + reaction.users.remove(user).catch(console.error); + if (!Util.canModifyQueue(member)) return; + queue.connection.dispatcher.end(); + queue.textChannel.send(`${user} ⏩ skipped the song`).catch(console.error); + collector.stop(); + break; + + case "⏯": + reaction.users.remove(user).catch(console.error); + if (!Util.canModifyQueue(member)) return; + if (queue.playing) { + queue.playing = !queue.playing; + queue.connection.dispatcher.pause(true); + queue.textChannel.send(`${user} ⏸ paused the music.`).catch(console.error); + } else { + queue.playing = !queue.playing; + queue.connection.dispatcher.resume(); + queue.textChannel.send(`${user} ▶ resumed the music!`).catch(console.error); + } + break; + + case "🔁": + reaction.users.remove(user).catch(console.error); + if (!Util.canModifyQueue(member)) return; + queue.loop = !queue.loop; + queue.textChannel.send(`Loop is now ${queue.loop ? "**on**" : "**off**"}`).catch(console.error); + break; + + case "⏹": + reaction.users.remove(user).catch(console.error); + if (!Util.canModifyQueue(member)) return; + queue.songs = []; + queue.textChannel.send(`${user} ⏹ stopped the music!`).catch(console.error); + try { + queue.connection.dispatcher.end(); + } catch (error) { + console.error(error); + queue.connection.disconnect(); + } + collector.stop(); + break; + + default: + reaction.users.remove(user).catch(console.error); + break; + } + }); + + collector.on('end', () => { + playingMessage.reactions.removeAll(); // catch + if (PRUNING && playingMessage && !playingMessage.deleted) + playingMessage.delete({ timeout: 3000 }); // catch + }); + } +}
\ No newline at end of file diff --git a/server/src/utils/Utils.ts b/server/src/utils/Utils.ts index 454840e..e7972c2 100644 --- a/server/src/utils/Utils.ts +++ b/server/src/utils/Utils.ts @@ -2,4 +2,16 @@ export default class Util { static shorten(text: string, maxLen = 2000) { return text.length > maxLen ? `${text.substr(0, maxLen - 3)}...` : text; } + + static canModifyQueue(member) { + const { channel } = member.voice; + const botChannel = member.guild.me.voice.channel; + + if (channel !== botChannel) { + member.send('You need to join the voice channel first!'); // error + return false; + } + + return false; + } }
\ No newline at end of file |