Added supporting classes

- added class GuildHandler -> use this for commands that sould be run on a server
- added class Servant -> handles commands, used by GuildHandler
- added connected getter to DJ
- changed from checking for Connection to checking if connected
- added possibility to assign a VoiceChannel at connecting
- commands now contain the prefix instead of being a child element of it
- added passing additional arguments as third parameter of the command callback
- removed complex command parsing function, rewrote it with better pattern matching
- removed music commands from bot.js and moved them to GuildHandler class
- changed from cmd.createCommand to createGlobalCommand
pull/9/head
Trivernis 6 years ago
parent d153eed7f5
commit e16965c86f

111
bot.js

@ -3,6 +3,7 @@ const Discord = require("discord.js"),
logger = require('./lib/logging').getLogger(), logger = require('./lib/logging').getLogger(),
music = require('./lib/music'), music = require('./lib/music'),
cmd = require("./lib/cmd"), cmd = require("./lib/cmd"),
guilding = require('./lib/guilding'),
client = new Discord.Client(), client = new Discord.Client(),
args = require('args-parser')(process.argv), args = require('args-parser')(process.argv),
authToken = args.token, authToken = args.token,
@ -32,108 +33,15 @@ function savePlaylist(url, name) {
} }
function registerCommands() { function registerCommands() {
cmd.createCommand(prefix, 'play', (msg, argv) => { cmd.createGlobalCommand(prefix + 'ping', () => {
let vc = msg.member.voiceChannel;
let url = argv['url'];
if (!url) return 'No url given.';
if (!url.match(/http/g)) {
if (savedplaylists[url]) {
url = savedplaylists[url];
}
}
try {
return music.play(vc, url);
} catch(err) {
logger.error(err);
msg.reply(`${JSON.stringify(err)}`);
}
}, ['url'], "Adds the url to the YouTube video/playlist into the queue.");
cmd.createCommand(prefix, 'playnext', (msg, argv) => {
let vc = msg.member.voiceChannel;
let url = argv['url'];
if (!url) return 'No url given.';
if (!url.match(/http/g)) {
if (savedplaylists[url]) {
url = savedplaylists[url];
}
}
try {
return music.playnext(vc, url);
} catch(err) {
logger.error(err);
msg.reply(`${JSON.stringify(err)}`);
}
}, ['url'], "Plays the YouTube video after the currently playing song.");
cmd.createCommand(prefix, 'ping', () => {
return 'Pong!'; return 'Pong!';
}, [], "Try it yourself."); }, [], "Try it yourself.");
cmd.createCommand(prefix, 'join', (msg) => { cmd.createGlobalCommand(prefix + 'repeatafterme', (msg, argv, args) => {
if (msg.member.voiceChannel) { return args.join(' ');
music.connect(msg.member.voiceChannel); },[], "Repeats what you say");
}
else {
msg.reply("You are not connected to a voicechannel.");
}
}, [], "Joins the VC you are in.");
cmd.createCommand(prefix, 'stop', (msg) => {
let gid = msg.guild.id;
music.stop(gid);
}, [], "Stops playling music and leavs.");
cmd.createCommand(prefix, 'pause', (msg) => {
let gid = msg.guild.id;
music.pause(gid);
}, [], "Pauses playing.");
cmd.createCommand(prefix, 'resume', (msg) => {
let gid = msg.guild.id;
music.resume(gid);
}, [], "Resumes playing.");
cmd.createCommand(prefix, 'skip', (msg) => {
let gid = msg.guild.id;
music.skip(gid);
}, [], "Skips the current song.");
cmd.createCommand(prefix, 'clear', (msg) => {
let gid = msg.guild.id;
music.clearQueue(gid);
return "All songs have been deleted, commander :no_mouth: "
}, [],"Clears the playlist.");
cmd.createCommand(prefix, 'playlist', (msg) => { cmd.createGlobalCommand(prefix + 'save', (msg, argv) => {
let gid = msg.guild.id;
let songs = music.getQueue(gid);
logger.debug(`found ${songs.length} songs`);
let songlist = `**${songs.length} Songs in playlist**\n`;
for (let i = 0; i < songs.length; i++) {
if (i > 10) break;
songlist += songs[i] + '\n';
}
return songlist;
}, [], "Shows the next ten songs.");
cmd.createCommand(prefix, 'shuffle', (msg) => {
let gid = msg.guild.id;
music.shuffle(gid);
return "The queue has successfully been shuffled :slight_smile:"
}, [], "Shuffles the playlist.");
cmd.createCommand(prefix, 'current', (msg) => {
let gid = msg.guild.id;
let song = music.nowPlaying(gid);
return `Playing: ${song.title}\n ${song.url}`;
}, [], "Shows the currently playing song.");
cmd.createCommand(prefix, 'repeatafterme', (msg, argv) => {
return argv['word'];
}, ['word'], "Repeats a single word you say.");
cmd.createCommand(prefix, 'save', (msg, argv) => {
savePlaylist(argv['url'], argv['name']); savePlaylist(argv['url'], argv['name']);
return `Saved song/playlist as ${argv['name']}` return `Saved song/playlist as ${argv['name']}`
}, ['url', 'name'], "Saves the YouTube song/playlist with a specific name"); }, ['url', 'name'], "Saves the YouTube song/playlist with a specific name");
@ -153,10 +61,11 @@ client.on('message', msg => {
return; return;
} }
logger.verbose(`<${msg.author.username}>: ${msg.content}`); logger.verbose(`<${msg.author.username}>: ${msg.content}`);
if (!msg.guild) {
let reply = cmd.parseMessage(msg); let reply = cmd.parseMessage(msg);
if (reply) { if (reply) msg.channel.send(reply);
msg.reply(reply); } else {
return; guilding.getHandler(msg.guild, prefix).handleMessage(msg);
} }
} catch (err) { } catch (err) {
logger.error(err.stack); logger.error(err.stack);

@ -2,10 +2,61 @@
/* Variable Definition */ /* Variable Definition */
let logger = require('winston'), let logger = require('winston'),
commands = {}; globCommands = {};
/* Function Definition */ /* Function Definition */
/**
* TODO: Configure commander with functions:
* - parsing commands from messages
* - add/remove commands
* - prefix settings
* @type {Commander}
*/
exports.Servant = class {
constructor(prefix) {
this.commands = {};
this.createCommand(((prefix || '~')+'help') || "~help", () => {
let helpstr = "```markdown\n";
helpstr += "Commands\n---\n";
Object.entries(globCommands).concat(Object.entries(this.commands)).forEach(([key, value]) => {
let cmdhelp = `${key} [${value.args.join('] [')}]`.padEnd(32, ' ');
cmdhelp += value.description || '';
helpstr += `\n${cmdhelp}\n`;
});
helpstr += "```";
return helpstr;
}, [], "Shows this help.");
}
createCommand(command, call, args, description) {
this.commands[command] = {
'args': args,
'description': description,
'callback': call
};
}
parseCommand(msg) {
let globResult = parseGlobalCommand(msg);
logger.debug(`Global command result is ${globResult}`);
let content = msg.content;
let command = (content.match(/^.\w+/) || [])[0];
if (!command || !this.commands[command]) return globResult;
let cmd = this.commands[command];
let argvars = content.match(/(?<= )\S+/g) || [];
let kwargs = {};
let nLength = Math.min(cmd.args.length, argvars.length);
for (let i = 0; i < nLength; i++) {
kwargs[cmd.args[i]] = argvars[i];
}
let argv = argvars.slice(nLength);
logger.debug(`Executing callback for command: ${command}, kwargs: ${kwargs}, argv: ${argv}`);
return cmd.callback(msg, kwargs, argv) || globResult;
}
};
/** /**
* Getting the logger * Getting the logger
* @param {Object} newLogger * @param {Object} newLogger
@ -14,80 +65,54 @@ exports.setLogger = function(newLogger) {
logger = newLogger; logger = newLogger;
}; };
exports.createCommand = function(prefix, command, call, argv, description) { exports.createGlobalCommand = function(command, call, args, description) {
try { globCommands[command] = {
logger.debug(`Creating command ${command} with prefix ${prefix} and arguments ${argv}`); 'args': args || [],
if (!commands[prefix]) commands[prefix] = {}; // create Object commands prefix 'description': description,
commands[prefix][command] = { // assign the command 'callback': call
args: argv || [],
callback: call,
description: description
}; };
logger.debug(`Created command ${prefix}${command}`); logger.debug(`Created command: ${command}, args: ${args}`);
} catch (err) {
logger.error(JSON.stringify(err));
}
}; };
/**
* Parses the message by calling the assigned function for the command with arguments
* @param msg
* @returns {string}
*/
exports.parseMessage = function(msg) { exports.parseMessage = function(msg) {
logger.debug(`Recieved message ${msg.content} from ${msg.author.username}`); return parseGlobalCommand(msg);
let content = msg.content;
let matches = content.match(/^./g); // match with first symbol
logger.debug(matches);
if (matches) {
logger.debug(matches);
logger.debug(`Found prefix ${matches[0]} in message`);
let prefix = matches[0];
let prefixData = commands[prefix];
matches = content.replace(prefix, '').match(/^\w+/g); // match with the second word
if (matches && prefixData) {
logger.debug(`found command ${matches[0]} in message`);
let command = matches[0];
let commandFunction = prefixData[command];
let args = content
.replace(prefix, '')
.replace(command, '')
.replace(/^\s+/g, '')
.split(' ');
if (commandFunction) {
let argv = {};
if (commandFunction.args) {
for (let i = 0; i < commandFunction.args.length; i++) {
let arg = commandFunction.args[i];
argv[arg] = args[i];
}
} }
if (commandFunction.callback) {
logger.debug(`Found callback and args ${JSON.stringify(argv)} in message`);
return commandFunction.callback(msg, argv); // call the command function and return the result
}
}
}
}
};
/** /**
* Initializes the module by creating a help command * Initializes the module by creating a help command
*/ */
exports.init = function(prefix) { exports.init = function(prefix) {
logger.verbose("Created help command"); logger.verbose("Created help command");
this.createCommand(prefix || '~', "help", () => { this.createGlobalCommand((prefix+'help') || "~help", () => {
let helpstr = "```markdown\n"; let helpstr = "```markdown\n";
helpstr += "Commands\n---\n"; helpstr += "Commands\n---\n";
Object.keys(commands).forEach((key) => { Object.entries(globCommands).forEach(([key, value]) => {
Object.keys(commands[key]).forEach((cmd) => { let cmdhelp = `${key} [${value.args.join('] [')}]`.padEnd(32, ' ');
helpstr += "\n" + key + cmd + " " + JSON.stringify(commands[key][cmd].args).replace(/"|\[\]/g, ''); cmdhelp += value.description || '';
if (commands[key][cmd].description) { helpstr += `\n${cmdhelp}\n`;
helpstr += '\t' + commands[key][cmd].description + '\n';
}
});
}); });
helpstr += "```"; helpstr += "```";
return helpstr; return helpstr;
}, [], "Shows this help."); }, [], "Shows this help.");
}; };
/**
* Parses the message by calling the assigned function for the command with arguments
* @param msg
*/
function parseGlobalCommand(msg) {
let content = msg.content;
let command = (content.match(/^.\w+/) || [])[0];
if (!command || !globCommands[command]) return false;
let cmd = globCommands[command];
let argvars = content.match(/(?<= )\S+/g) || [];
let kwargs = {};
let nLength = Math.min(cmd.args.length, argvars.length);
for (let i = 0; i < nLength; i++) {
kwargs[cmd.args[i]] = argvars[i];
}
let argv = argvars.slice(nLength);
logger.debug(`Executing callback for command: ${command}, kwargs: ${JSON.stringify(kwargs)}, argv: ${argv}`);
return cmd.callback(msg, kwargs, argv);
}

@ -0,0 +1,162 @@
const cmd = require('./cmd'),
music = require('./music'),
handlers = {};
let logger = require('winston');
exports.setLogger = function(newLogger) {
logger = newLogger;
};
exports.GuildHandler = class{
constructor(guild, prefix) {
this.guild = guild;
this.dj = null;
this.servant = null;
this.mention = false;
this.prefix = prefix || '~';
this.data = {
savedplaylists: {}
};
this.registerMusicCommands();
}
registerMusicCommands(cmdPrefix) {
let prefix = cmdPrefix || this.prefix;
this.dj = new music.DJ();
// play command
this.createCommand(prefix + 'play', (msg, argv) => {
let vc = msg.member.voiceChannel;
let url = argv['url'];
if (!vc)
return 'You are not connected to a VoiceChannel';
if (!url)
return 'No url given.';
if (!url.match(/http/g)) {
if (this.data.savedplaylists[url]) {
url = this.data.savedplaylists[url];
} else {
return 'Not a valid url.';
}
}
try {
if (!this.dj.connected) {
this.dj.connect(vc).then(() => {
this.dj.playYouTube(url);
});
}else {
return this.dj.playYouTube(url);
}
} catch(err) {
logger.error(err);
return `${JSON.stringify(err)}`;
}
}, ['url'], "Adds the url to the YouTube video/playlist into the queue.");
// playnext command
this.createCommand(prefix + 'playnext', (msg, argv) => {
let vc = msg.member.voiceChannel;
if (!this.dj.connected) this.dj.voiceChannel = vc;
let url = argv['url'];
if (!url) return 'No url given.';
if (!url.match(/http/g)) {
if (this.data.savedplaylists[url]) {
url = this.data.savedplaylists[url];
} else {
return 'Not a valid url';
}
}
try {
return this.dj.playYouTube(url, true);
} catch(err) {
logger.error(err);
return `${JSON.stringify(err)}`;
}
}, ['url'], "Adds the url to the YouTube video as next song to the queue.");
// join command
this.createCommand(prefix + 'join', (msg) => {
if (msg.member.voiceChannel) {
this.dj.connect(msg.member.voiceChannel);
}
else {
msg.reply("You are not connected to a voicechannel.");
}
}, [], "Joins the VC you are in.");
// stop command
this.createCommand(prefix + 'stop', () => {
this.dj.stop();
return "Stopping now";
}, [], "Stops playing music and leaves.");
// pause command
this.createCommand(prefix + 'pause', () => {
this.dj.pause();
return "Pausing playing";
}, [], "Pauses playing.");
// resume command
this.createCommand(prefix + 'resume', () => {
this.dj.resume();
return "Resuming playing";
}, [], "Resumes playing.");
// skip command
this.createCommand(prefix + 'skip', () => {
this.dj.skip();
return "Skipping Song";
}, [], "Skips the current song.");
// clear command
this.createCommand(prefix + 'clear', () => {
this.dj.clear();
return "DJ-Queue cleared";
}, [],"Clears the playlist.");
// playlist command
this.createCommand(prefix + 'playlist', () => {
let songs = this.dj.playlist;
logger.debug(`found ${songs.length} songs`);
let songlist = `**${songs.length} Songs in playlist**\n`;
for (let i = 0; i < songs.length; i++) {
if (i > 10) break;
songlist += songs[i] + '\n';
}
return songlist;
}, [], "Shows the next ten songs.");
// np command
this.createCommand(prefix + 'np', () => {
let song = this.dj.song;
return `Playing: ${song.title}\n ${song.url}`;
}, [], "Shows the currently playing song.");
// shuffle command
this.createCommand(prefix + 'shuffle', () => {
this.dj.shuffle();
return "Randomized the order of the queue."
}, [], "Shuffles the playlist.");
}
createCommand(command, call, args, description) {
if (!this.servant) this.servant = new cmd.Servant(this.prefix);
this.servant.createCommand(command, call, args, description);
}
handleMessage(msg) {
if (!this.servant) this.servant = new cmd.Servant(this.prefix);
let answer = this.servant.parseCommand(msg);
if (!answer) return;
if (this.mention) {
msg.reply(answer);
} else {
msg.channel.send(answer);
}
}
};
exports.getHandler = function(guild, prefix) {
if (!handlers[guild.id]) handlers[guild.id] = new this.GuildHandler(guild, prefix);
return handlers[guild.id];
};

@ -10,7 +10,7 @@ let connections = {};
/* Function Definition */ /* Function Definition */
class DJ { exports.DJ = class{
/* /*
TODO: Disconnect when no user is left in the channel after a (not constant) amout of time. TODO: Disconnect when no user is left in the channel after a (not constant) amout of time.
Could be accomplished by checking after every song, if the VoiceChannel (saved in class) still contains Could be accomplished by checking after every song, if the VoiceChannel (saved in class) still contains
@ -31,26 +31,35 @@ class DJ {
* Connects to the given voice channel. Disconnects from the previous one if it exists. * Connects to the given voice channel. Disconnects from the previous one if it exists.
* When the bot was moved and connect is executed again, it connects to the initial VoiceChannel because the * When the bot was moved and connect is executed again, it connects to the initial VoiceChannel because the
* VoiceChannel is saved as object variable. * VoiceChannel is saved as object variable.
* @returns {Promise<T | never>}
*/ */
connect() { connect(voiceChannel) {
if (this.conn) { this.voiceChannel = voiceChannel || this.voiceChannel;
if (this.connected) {
this.stop(); this.stop();
} }
logger.verbose(`Connecting to voiceChannel ${this.voiceChannel.name}`); logger.verbose(`Connecting to voiceChannel ${this.voiceChannel.name}`);
return this.voiceChannel.join().then(connection => { return this.voiceChannel.join().then(connection => {
logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`); logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`);
this.conn = connection; this.conn = connection;
this.checkListeners();
}); });
} }
get connected() {
return (
this.conn !== null &&
this.conn !== undefined &&
this.conn.status !== 4 // status 4 means disconnected.
);
}
/** /**
* Plays a file for the given filename. * Plays a file for the given filename.
* TODO: Implement queue * TODO: Implement queue
* @param filename * @param filename
*/ */
playFile(filename) { playFile(filename) {
if (this.conn !== null) { if (this.connected) {
this.disp = this.conn.playFile(filename); this.disp = this.conn.playFile(filename);
this.playing = true; this.playing = true;
} else { } else {
@ -61,14 +70,34 @@ class DJ {
} }
} }
/**
* Checks if there are still members listening and sets an exit timeout (5 min) before checking again
* and exiting when noone is listening. Once this function is executed, it calls itself every 10 seconds.
* TODO: Make this work
*/
checkListeners() {
if (this.connected && this.conn.channel.members.size === 0) {
logger.verbose(`Set exit timout for ${this.voiceChannel.name}`);
setTimeout(() => {
if (this.voiceChannel && this.voiceChannel.members.size === 0)
logger.verbose(`Exiting ${this.voiceChannel.name}`);
this.stop();
}, 300000);
} else if (this.connected)
setTimeout(() => this.checkListeners(), 10000);
}
/** /**
* Plays the url of the current song if there is no song playing or puts it in the queue. * Plays the url of the current song if there is no song playing or puts it in the queue.
* If the url is a playlist (regex match), the videos of the playlist are fetched and put * If the url is a playlist (regex match), the videos of the playlist are fetched and put
* in the queue. For each song the title is saved in the queue too. * in the queue. For each song the title is saved in the queue too.
* @param url * @param url
* @param playnext
*/ */
playYouTube(url, playnext) { playYouTube(url, playnext) {
if (!this.conn) this.connect().then(this.playYouTube(url)); if (!this.connected) {
this.connect().then(this.playYouTube(url));
}
let plist = url.match(/(?<=\?list=)[\w\-]+/g); let plist = url.match(/(?<=\?list=)[\w\-]+/g);
if (plist) { if (plist) {
logger.debug(`Adding playlist ${plist} to queue`); logger.debug(`Adding playlist ${plist} to queue`);
@ -180,14 +209,23 @@ class DJ {
stop() { stop() {
this.queue = []; this.queue = [];
logger.verbose("Stopping music..."); logger.verbose("Stopping music...");
if (this.disp !== null) { try {
this.disp.end(); // FIXME: Triggers the dispatcher end event that calls stop again if (this.disp) {
this.disp.end();
logger.debug("Ended dispatcher"); logger.debug("Ended dispatcher");
} }
if (this.conn !== null) { if (this.conn) {
this.conn.channel.leave();
this.conn.disconnect(); this.conn.disconnect();
logger.debug("Ended connection"); logger.debug("Ended connection");
} }
if (this.voiceChannel) {
this.voiceChannel.leave();
logger.debug("Left VoiceChannel");
}
} catch(error) {
logger.verbose(JSON.stringify(error));
}
} }
/** /**
@ -250,7 +288,7 @@ exports.setLogger = function (newLogger) {
*/ */
exports.connect = function(voiceChannel) { exports.connect = function(voiceChannel) {
let gid = voiceChannel.guild.id; let gid = voiceChannel.guild.id;
let voiceDJ = new DJ(voiceChannel); let voiceDJ = new this.DJ(voiceChannel);
djs[gid] = voiceDJ; djs[gid] = voiceDJ;
return voiceDJ.connect(); return voiceDJ.connect();
}; };

Loading…
Cancel
Save