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

113
bot.js

@ -3,6 +3,7 @@ const Discord = require("discord.js"),
logger = require('./lib/logging').getLogger(),
music = require('./lib/music'),
cmd = require("./lib/cmd"),
guilding = require('./lib/guilding'),
client = new Discord.Client(),
args = require('args-parser')(process.argv),
authToken = args.token,
@ -32,108 +33,15 @@ function savePlaylist(url, name) {
}
function registerCommands() {
cmd.createCommand(prefix, 'play', (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.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', () => {
cmd.createGlobalCommand(prefix + 'ping', () => {
return 'Pong!';
}, [], "Try it yourself.");
cmd.createCommand(prefix, 'join', (msg) => {
if (msg.member.voiceChannel) {
music.connect(msg.member.voiceChannel);
}
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.createGlobalCommand(prefix + 'repeatafterme', (msg, argv, args) => {
return args.join(' ');
},[], "Repeats what you say");
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) => {
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) => {
cmd.createGlobalCommand(prefix + 'save', (msg, argv) => {
savePlaylist(argv['url'], argv['name']);
return `Saved song/playlist as ${argv['name']}`
}, ['url', 'name'], "Saves the YouTube song/playlist with a specific name");
@ -153,10 +61,11 @@ client.on('message', msg => {
return;
}
logger.verbose(`<${msg.author.username}>: ${msg.content}`);
let reply = cmd.parseMessage(msg);
if (reply) {
msg.reply(reply);
return;
if (!msg.guild) {
let reply = cmd.parseMessage(msg);
if (reply) msg.channel.send(reply);
} else {
guilding.getHandler(msg.guild, prefix).handleMessage(msg);
}
} catch (err) {
logger.error(err.stack);

@ -2,10 +2,61 @@
/* Variable Definition */
let logger = require('winston'),
commands = {};
globCommands = {};
/* 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
* @param {Object} newLogger
@ -14,80 +65,54 @@ exports.setLogger = function(newLogger) {
logger = newLogger;
};
exports.createCommand = function(prefix, command, call, argv, description) {
try {
logger.debug(`Creating command ${command} with prefix ${prefix} and arguments ${argv}`);
if (!commands[prefix]) commands[prefix] = {}; // create Object commands prefix
commands[prefix][command] = { // assign the command
args: argv || [],
callback: call,
description: description
};
logger.debug(`Created command ${prefix}${command}`);
} catch (err) {
logger.error(JSON.stringify(err));
}
exports.createGlobalCommand = function(command, call, args, description) {
globCommands[command] = {
'args': args || [],
'description': description,
'callback': call
};
logger.debug(`Created command: ${command}, args: ${args}`);
};
/**
* Parses the message by calling the assigned function for the command with arguments
* @param msg
* @returns {string}
*/
exports.parseMessage = function(msg) {
logger.debug(`Recieved message ${msg.content} from ${msg.author.username}`);
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
}
}
}
}
};
return parseGlobalCommand(msg);
}
/**
* Initializes the module by creating a help command
*/
exports.init = function(prefix) {
logger.verbose("Created help command");
this.createCommand(prefix || '~', "help", () => {
this.createGlobalCommand((prefix+'help') || "~help", () => {
let helpstr = "```markdown\n";
helpstr += "Commands\n---\n";
Object.keys(commands).forEach((key) => {
Object.keys(commands[key]).forEach((cmd) => {
helpstr += "\n" + key + cmd + " " + JSON.stringify(commands[key][cmd].args).replace(/"|\[\]/g, '');
if (commands[key][cmd].description) {
helpstr += '\t' + commands[key][cmd].description + '\n';
}
});
Object.entries(globCommands).forEach(([key, value]) => {
let cmdhelp = `${key} [${value.args.join('] [')}]`.padEnd(32, ' ');
cmdhelp += value.description || '';
helpstr += `\n${cmdhelp}\n`;
});
helpstr += "```";
return helpstr;
}, [], "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 */
class DJ {
exports.DJ = class{
/*
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
@ -31,26 +31,35 @@ class DJ {
* 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
* VoiceChannel is saved as object variable.
* @returns {Promise<T | never>}
*/
connect() {
if (this.conn) {
connect(voiceChannel) {
this.voiceChannel = voiceChannel || this.voiceChannel;
if (this.connected) {
this.stop();
}
logger.verbose(`Connecting to voiceChannel ${this.voiceChannel.name}`);
return this.voiceChannel.join().then(connection => {
logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`);
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.
* TODO: Implement queue
* @param filename
*/
playFile(filename) {
if (this.conn !== null) {
if (this.connected) {
this.disp = this.conn.playFile(filename);
this.playing = true;
} 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.
* 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.
* @param url
* @param 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);
if (plist) {
logger.debug(`Adding playlist ${plist} to queue`);
@ -180,13 +209,22 @@ class DJ {
stop() {
this.queue = [];
logger.verbose("Stopping music...");
if (this.disp !== null) {
this.disp.end(); // FIXME: Triggers the dispatcher end event that calls stop again
logger.debug("Ended dispatcher");
}
if (this.conn !== null) {
this.conn.disconnect();
logger.debug("Ended connection");
try {
if (this.disp) {
this.disp.end();
logger.debug("Ended dispatcher");
}
if (this.conn) {
this.conn.channel.leave();
this.conn.disconnect();
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) {
let gid = voiceChannel.guild.id;
let voiceDJ = new DJ(voiceChannel);
let voiceDJ = new this.DJ(voiceChannel);
djs[gid] = voiceDJ;
return voiceDJ.connect();
};

Loading…
Cancel
Save