Restructuring and new Commands

- Added Server Utility Commands
- Removed lib/cmd
- Renamed lib/music
- renamed lib/weblib
- removed command parsing function from GuildHandler
- renamed DJ to MusicPlayer
- updated graphql schema and interface to new names
pull/51/head
Trivernis 6 years ago
parent 556c3a5e7f
commit 4d7018d510

134
bot.js

@ -2,12 +2,10 @@ const Discord = require("discord.js"),
fs = require('fs-extra'),
logger = require('./lib/logging').getLogger(),
msgLib = require('./lib/MessageLib'),
cmd = require("./lib/cmd"),
guilding = require('./lib/guilding'),
utils = require('./lib/utils'),
config = require('./config.json'),
args = require('args-parser')(process.argv),
waterfall = require('promise-waterfall'),
sqliteAsync = require('./lib/sqliteAsync'),
authToken = args.token || config.api.botToken,
prefix = args.prefix || config.prefix || '~',
@ -15,21 +13,26 @@ const Discord = require("discord.js"),
let weblib = null;
/**
* The Bot class handles the initialization and Mangagement of the Discord bot and
* is the main class.
*/
class Bot {
constructor() {
this.client = new Discord.Client({autoReconnect: true});
this.mention = false;
this.rotator = null;
this.maindb = null;
this.presences = [];
this.messageHandler = new msgLib.MessageHandler(this.client, logger);
this.guildHandlers = [];
this.userRates = {};
this.guildHandlers = {};
logger.verbose('Verifying config');
let configVerifyer = new utils.ConfigVerifyer(config, [
"api.botToken", "api.youTubeApiKey"
"api.botToken", "api.youTubeApiKey",
"commandSettings.maxSequenceParallel",
"commandSettings.maxSequenceSerial"
]);
if (!configVerifyer.verifyConfig(logger))
if (!args.i) {
@ -38,7 +41,6 @@ class Bot {
process.exit(1);
});
}
cmd.setLogger(logger);
guilding.setLogger(logger);
}
@ -64,7 +66,7 @@ class Bot {
});
await this.initializeDatabase();
if (config.webservice && config.webservice.enabled)
if (config.webinterface && config.webinterface.enabled)
await this.initializeWebserver();
logger.verbose('Registering commands');
await this.messageHandler
@ -74,12 +76,18 @@ class Bot {
await this.messageHandler
.registerCommandModule(require('./lib/commands/InfoCommands').module, {client: this.client, messageHandler: this.messageHandler});
await this.messageHandler
.registerCommandModule(require('./lib/commands/MusicCommands').module, {getGuildHandler: (g) => {
return this.getGuildHandler(g, prefix);
}, logger: logger})
//this.registerCommands();
this.registerCallbacks();
cmd.init(prefix);
.registerCommandModule(require('./lib/commands/MusicCommands').module, {
getGuildHandler: async (g) => await this.getGuildHandler(g),
logger: logger
});
await this.messageHandler
.registerCommandModule(require('./lib/commands/ServerUtilityCommands').module, {
getGuildHandler: async (g) => await this.getGuildHandler(g),
logger: logger,
messageHandler: this.messageHandler,
config: config
});
this.registerEvents();
}
/**
@ -120,10 +128,10 @@ class Bot {
*/
async initializeWebserver() {
logger.verbose('Importing weblib');
weblib = require('./lib/weblib');
weblib = require('./lib/WebLib');
weblib.setLogger(logger);
logger.verbose('Creating WebServer');
this.webServer = new weblib.WebServer(config.webservice.port || 8080);
this.webServer = new weblib.WebServer(config.webinterface.port || 8080);
logger.debug('Setting Reference Objects to webserver');
await this.webServer.setReferenceObjects({
@ -131,7 +139,7 @@ class Bot {
presences: this.presences,
maindb: this.maindb,
prefix: prefix,
getGuildHandler: (guild) => this.getGuildHandler(guild, prefix),
getGuildHandler: async (g) => await this.getGuildHandler(g),
guildHandlers: this.guildHandlers
});
}
@ -172,14 +180,6 @@ class Bot {
}
}
/**
* registeres global commands
*/
registerCommands() {
cmd.registerUtilityCommands(prefix, this);
cmd.registerInfoCommands(prefix, this);
}
/**
* changes the presence of the bot by using one stored in the presences array
*/
@ -198,7 +198,7 @@ class Bot {
/**
* Registeres callbacks for client events message and ready
*/
registerCallbacks() {
registerEvents() {
this.client.on('error', (err) => {
logger.error(err.message);
logger.debug(err.stack);
@ -217,93 +217,27 @@ class Bot {
});
});
/*
this.client.on('message', async (msg) => {
try {
if (msg.author === this.client.user) {
logger.verbose(`ME: ${msg.content}`);
return;
}
if (this.checkRate(msg.author.tag)) {
logger.verbose(`<${msg.author.tag}>: ${msg.content}`);
if (!msg.guild) {
let reply = cmd.parseMessage(msg);
await this.answerMessage(msg, reply);
} else {
let gh = await this.getGuildHandler(msg.guild, prefix);
await gh.handleMessage(msg);
}
if (((Date.now() - this.userRates[msg.author.tag].last)/1000) > (config.rateLimitTime || 10))
this.userRates[msg.author.tag].count = 0;
else
this.userRates[msg.author.tag].count++;
this.userRates[msg.author.tag].last = Date.now();
this.userRates[msg.author.tag].reached = false;
} else if (!this.userRates[msg.author.tag].reached) {
logger.verbose(`${msg.author.tag} reached it's rate limit.`);
this.userRates[msg.author.tag].reached = true;
}
} catch (err) {
logger.error(err.message);
logger.debug(err.stack);
}
});*/
this.client.on('voiceStateUpdate', async (oldMember, newMember) => {
let gh = await this.getGuildHandler(newMember.guild, prefix);
let gh = await this.getGuildHandler(newMember.guild);
if (newMember.user === this.client.user) {
if (newMember.voiceChannel)
gh.dj.updateChannel(newMember.voiceChannel);
gh.musicPlayer.updateChannel(newMember.voiceChannel);
} else {
if (oldMember.voiceChannel === gh.dj.voiceChannel || newMember.voiceChannel === gh.dj.voiceChannel)
gh.dj.checkListeners();
if (oldMember.voiceChannel === gh.musicPlayer.voiceChannel || newMember.voiceChannel === gh.musicPlayer.voiceChannel)
gh.musicPlayer.checkListeners();
}
});
}
/**
* Returns true if the user has not reached it's rate limit.
* @param usertag
* @returns {boolean}
*/
checkRate(usertag) {
if (!this.userRates[usertag])
this.userRates[usertag] = {last: Date.now(), count: 0};
return ((Date.now() - this.userRates[usertag].last)/1000) > (config.rateLimitTime || 10) ||
this.userRates[usertag].count < (config.rateLimitCount || 5);
}
/**
* Sends the answer recieved from the commands callback.
* Handles the sending differently depending on the type of the callback return
* @param msg
* @param answer
*/
async answerMessage(msg, answer) {
if (answer instanceof Discord.RichEmbed) {
(this.mention) ? msg.reply('', answer) : msg.channel.send('', answer);
} else if (answer instanceof Promise) {
let resolvedAnswer = await answer;
await this.answerMessage(msg, resolvedAnswer);
} else if (answer instanceof Array) {
await waterfall(answer.map((x) => async () => await this.answerMessage(msg, x))); // execute each after another
} else if ({}.toString.call(answer) === '[object Function]') {
await this.answerMessage(msg, answer());
} else if (answer) {
(this.mention) ? msg.reply(answer) : msg.channel.send(answer);
}
}
/**
* Returns the guild handler by id, creates one if it doesn't exist and returns it then
* @param guild
* @param prefix
* @param guild {Guild}
* @returns {*}
*/
async getGuildHandler(guild, prefix) {
async getGuildHandler(guild) {
if (!this.guildHandlers[guild.id]) {
let newGuildHandler = new guilding.GuildHandler(guild, prefix);
let newGuildHandler = new guilding.GuildHandler(guild);
await newGuildHandler.initDatabase();
this.guildHandlers[guild.id] = newGuildHandler;
}
@ -314,7 +248,7 @@ class Bot {
// Executing the main function
if (typeof require !== 'undefined' && require.main === module) {
logger.info("Starting up... "); // log the current date so that the logfile is better to read.
logger.info("Starting up... ");
logger.debug('Calling constructor...');
let discordBot = new Bot();
logger.debug('Initializing services...');

@ -1,6 +1,7 @@
const Discord = require('discord.js'),
yaml = require('js-yaml'),
fsx = require('fs-extra'),
config = require('../config.json'),
utils = require('./utils');
const scopes = {
@ -101,7 +102,7 @@ class CommandHandler {
* Handles the command and responds to the message.
* @param commandMessage {String}
* @param message {Discord.Message}
* @returns {Boolean | Promise<String|Discord.RichEmbed>}
* @returns {Boolean | String | Promise<String|Discord.RichEmbed>}
*/
handleCommand(commandMessage, message) {
let commandName = commandMessage.match(/^\S+/);
@ -115,12 +116,14 @@ class CommandHandler {
.replace(/\s+$/, ''); // trailing whitespace
let args = argsString.match(/\S+/g);
let command = this.commands[commandName];
if (command) {
if (command && this._checkPermission(message, command.permission)) {
let kwargs = {};
if (args)
for (let i = 0; i < Math.min(command.args.length, args.length); i++)
kwargs[command.args[i]] = args[i];
return command.answer(message, kwargs, argsString);
} else if (command) {
return "You don't have permission for this command";
} else {
return false;
}
@ -131,7 +134,6 @@ class CommandHandler {
/**
* Registers the command so that the handler can use it.
* @param name {String}
* @param command {Command}
*/
registerCommand(command) {
@ -139,6 +141,26 @@ class CommandHandler {
this.commands[command.name] = command;
return this;
}
/**
* Checks if the author of the message has the given permission
* @param msg {Discord.Message}
* @param rolePerm {String} Permission String
* @returns {boolean}
* @private
*/
_checkPermission(msg, rolePerm) {
if (!rolePerm || ['all', 'any', 'everyone'].includes(rolePerm))
return true;
if (config.owners.includes(msg.author.tag))
return true;
else if (msg.member && rolePerm && rolePerm !== 'owner' && msg.member.roles
.some(role => (role.name.toLowerCase() === rolePerm.toLowerCase() ||
role.name.toLowerCase() === 'botcommander')))
return true;
return false;
}
}
/**

@ -3,6 +3,8 @@ const cmdLib = require('./CommandLib'),
Discord = require('discord.js'),
promiseWaterfall = require('promise-waterfall');
/* eslint no-useless-escape: 0 */
class MessageHandler {
/**
@ -20,6 +22,7 @@ class MessageHandler {
cmdLib.CommandScopes.User);
this.guildCmdHandler = new cmdLib.CommandHandler(config.prefix,
cmdLib.CommandScopes.Guild);
this.userRates = {};
this._registerEvents();
}
@ -51,6 +54,15 @@ class MessageHandler {
await cmdModule.register(this.getHandler(cmdModule.scope));
}
/**
* Parses a string to a command sequence Array.
* Workaround to not reveal the private parseSyntax function.
* @param synStr {String}
*/
parseSyntaxString(synStr) {
return this._parseSyntax({content: synStr});
}
/**
* Registering event handlers.
* @private
@ -59,10 +71,13 @@ class MessageHandler {
this.logger.debug('Registering message event...');
this.discordClient.on('message', async (msg) => {
this.logger.debug(`<${msg.guild? msg.channel.name+'@'+msg.guild.name : 'PRIVATE'}> ${msg.author.tag}: ${msg.content}`);
if (msg.author !== this.discordClient.user) {
if (msg.author !== this.discordClient.user
&& this._checkPrefixStart(msg.content)
&& !this._checkRateReached(msg.author)) {
let sequence = this._parseSyntax(msg);
this.logger.debug(`Syntax parsing returned: ${JSON.stringify(sequence)}`);
await this._executeCommandSequence(sequence, msg);
await this.executeCommandSequence(sequence, msg);
this.logger.debug('Executed command sequence');
}
});
@ -82,8 +97,8 @@ class MessageHandler {
for (let string of strings)
content = content.replace(string, string // escape all special chars
.replace(';', '\\;')
.replace('&', '\\&'));
.replace(/;/g, '\\;')
.replace(/&/g, '\\&'));
let independentCommands = content // independent command sequende with ;
.split(/(?<!\\);/g)
.map(x => x.replace(/^ +/, ''));
@ -98,7 +113,7 @@ class MessageHandler {
/**
* Executes a sequence of commands
*/
async _executeCommandSequence(cmdSequence, message) {
async executeCommandSequence(cmdSequence, message) {
this.logger.debug(`Executing command sequence: ${JSON.stringify(cmdSequence)} ...`);
let scopeCmdHandler = this.getScopeHandler(message);
await Promise.all(cmdSequence.map((sq) => promiseWaterfall(sq.map((cmd) => async () => {
@ -145,6 +160,40 @@ class MessageHandler {
else
message.channel.send(answer);
}
/**
* Checks if the messageString beginns with a command prefix.
* @param msgString {String}
* @private
*/
_checkPrefixStart(msgString) {
let p1 = this.globalCmdHandler.prefix;
let p2 = this.guildCmdHandler.prefix;
let p3 = this.userCmdHandler.prefix;
return (
new RegExp(`^\\s*?${p1}`).test(msgString) ||
new RegExp(`^\\s*?${p2}`).test(msgString) ||
new RegExp(`^\\s*?${p3}`).test(msgString));
}
/**
* Checks if the user has reached the command rate limit and updates it.
* @param user {Discord.User}
* @returns {boolean}
* @private
*/
_checkRateReached(user) {
if (!this.userRates[user.tag])
this.userRates[user.tag] = {last: 0, count: 0};
let userEntry = this.userRates[user.tag];
let reached = ((Date.now() - userEntry.last)/1000) < (config.rateLimitTime || 10)
&& userEntry.count > (config.rateLimitCount || 5);
if (((Date.now() - userEntry.last)/1000) > (config.rateLimitTime || 10))
this.userRates[user.tag].count = 0;
this.userRates[user.tag].last = Date.now();
this.userRates[user.tag].count++;
return reached;
}
}
Object.assign(exports, {

@ -1,20 +1,22 @@
const ytdl = require("ytdl-core"),
ypi = require('youtube-playlist-info'),
yttl = require('get-youtube-title'),
args = require('args-parser')(process.argv),
config = require('../config.json'),
utils = require('./utils.js'),
ytapiKey = args.ytapi || config.api.youTubeApiKey;
/* Variable Definition */
let logger = require('winston');
ytapiKey = config.api.youTubeApiKey;
/* Function Definition */
let logger = require('winston');
exports.setLogger = function (newLogger) {
logger = newLogger;
};
exports.DJ = class {
/**
* The Music Player class is used to handle music playing tasks on Discord Servers (Guilds).
* @type {MusicPlayer}
*/
class MusicPlayer {
constructor(voiceChannel) {
this.conn = null;
this.disp = null;
@ -51,7 +53,7 @@ exports.DJ = class {
/**
* Defining setter for listenOnRepeat to include the current song into the repeating loop.
* @param value
* @param value {Boolean}
*/
set listenOnRepeat(value) {
this.repeat = value;
@ -74,7 +76,7 @@ exports.DJ = class {
/**
* Updates the channel e.g. when the bot is moved to another channel.
* @param voiceChannel
* @param voiceChannel {Discord.VoiceChannel}
*/
updateChannel(voiceChannel) {
if (voiceChannel) {
@ -86,7 +88,7 @@ exports.DJ = class {
/**
* Plays a file for the given filename.
* TODO: Implement queue
* @param filename
* @param filename {String}
* @todo
*/
playFile(filename) {
@ -125,8 +127,8 @@ exports.DJ = class {
* 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, 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
* @param url {String}
* @param playnext {Boolean}
*/
async playYouTube(url, playnext) {
let plist = utils.YouTube.getPlaylistIdFromUrl(url);
@ -195,7 +197,7 @@ exports.DJ = class {
/**
* Gets the name of the YouTube Video at url
* @param url
* @param url {String}
* @returns {Promise<>}
*/
getVideoName(url) {
@ -213,7 +215,7 @@ exports.DJ = class {
/**
* Sets the volume of the dispatcher to the given value
* @param percentage
* @param percentage {Number}
*/
setVolume(percentage) {
logger.verbose(`Setting volume to ${percentage}`);
@ -323,4 +325,8 @@ exports.DJ = class {
clear() {
this.queue = [];
}
};
}
Object.assign(exports, {
MusicPlayer: MusicPlayer
});

@ -41,7 +41,7 @@ exports.WebServer = class {
this.app.use(require('cors')());
this.app.use(session({
store: new SQLiteStore({dir: './data', db: 'sessions.db'}),
secret: config.webservice.sessionSecret,
secret: config.webinterface.sessionSecret,
resave: false,
saveUninitialized: true,
cookie: {secure: 'auto'},
@ -89,7 +89,7 @@ exports.WebServer = class {
this.app.use('/graphql', graphqlHTTP({
schema: this.schema,
rootValue: this.root,
graphiql: config.webservice.graphiql || false
graphiql: config.webinterface.graphiql || false
}));
}
@ -98,14 +98,14 @@ exports.WebServer = class {
*/
start() {
this.configureExpress();
if (config.webservice.https && config.webservice.https.enabled) {
if (config.webinterface.https && config.webinterface.https.enabled) {
let sslKey = null;
let sslCert = null;
if (config.webservice.https.keyFile)
sslKey = fs.readFileSync(config.webservice.https.keyFile, 'utf-8');
if (config.webservice.https.certFile)
sslCert = fs.readFileSync(config.webservice.https.certFile, 'utf-8');
if (config.webinterface.https.keyFile)
sslKey = fs.readFileSync(config.webinterface.https.keyFile, 'utf-8');
if (config.webinterface.https.certFile)
sslCert = fs.readFileSync(config.webinterface.https.certFile, 'utf-8');
if (sslKey && sslCert) {
logger.verbose('Creating https server.');
this.server = require('https').createServer({key: sslKey, cert: sslCert}, this.app);
@ -200,8 +200,8 @@ exports.WebServer = class {
return dcGuilds.filter((x) => {
let gh = objects.guildHandlers[x.id];
if (gh)
if (gh.dj)
return gh.dj.playing;
if (gh.musicPlayer)
return gh.musicPlayer.playing;
else
return false;
else
@ -272,12 +272,12 @@ function generateUUID(input) {
}
/**
* Used for graphql attribute access to the lib/music/DJ
* Used for graphql attribute access to the lib/music/MusicPlayer
*/
class DJ {
constructor(musicDj) {
this.dj = musicDj;
this.quality = musicDj.quality;
class MusicPlayer {
constructor(musicPlayer) {
this.musicPlayer = musicPlayer;
this.quality = musicPlayer.quality;
}
queue(args) {
@ -297,35 +297,35 @@ class DJ {
}
get playing() {
return this.dj.playing;
return this.musicPlayer.playing;
}
get connected() {
return this.dj.connected;
return this.musicPlayer.connected;
}
get paused() {
return this.dj.disp? this.dj.disp.paused : false;
return this.musicPlayer.disp? this.dj.disp.paused : false;
}
get queueCount() {
return this.dj.queue.length;
return this.musicPlayer.queue.length;
}
get songStartTime() {
return this.dj.disp.player.streamingData.startTime;
return this.musicPlayer.disp.player.streamingData.startTime;
}
get volume() {
return this.dj.volume;
return this.musicPlayer.volume;
}
get repeat() {
return this.dj.repeat;
return this.musicPlayer.repeat;
}
get currentSong() {
let x = this.dj.current;
let x = this.musicPlayer.current;
return {
id: generateID(['Media', x.url]),
name: x.title,
@ -335,7 +335,7 @@ class DJ {
}
get voiceChannel() {
return this.dj.voiceChannel.name;
return this.musicPlayer.voiceChannel.name;
}
}
@ -358,7 +358,7 @@ class Guild {
this.ready = guildHandler.ready;
this.prSaved = null;
this.guildHandler = guildHandler;
this.dj = this.guildHandler.dj ? new DJ(this.guildHandler.dj) : null;
this.musicPlayer = this.guildHandler.musicPlayer ? new MusicPlayer(this.guildHandler.musicPlayer) : null;
}
async querySaved() {

@ -26,7 +26,7 @@ type GuildMember {
roles(first: Int = 10, offset: Int = 0, id: String): [Role]
highestRole: Role
}
type DJ {
type MusicPlayer {
queue(first: Int = 10, offset: Int = 0, id: String): [MediaEntry]
queueCount: Int!
songStartTime: String
@ -44,7 +44,7 @@ type Guild {
discordId: String
name: String
owner: GuildMember
dj: DJ
musicPlayer: MusicPlayer
members(first: Int = 10, offset: Int = 0, id: String): [GuildMember]
memberCount: Int!
roles(first: Int = 10, offset: Int = 0, id: String): [Role]

@ -1,540 +0,0 @@
/* Module definition */
/* Variable Definition */
const Discord = require('discord.js'),
args = require('args-parser')(process.argv),
config = require('../config.json'),
gcmdTempl = require('../commands/globalcommands'),
scmdTempl = require('../commands/servercommands'),
utils = require('./utils');
let logger = require('winston'),
globCommands = {};
/**
* @type {Servant}
*/
class Servant {
constructor(prefix) {
this.commands = {};
this.prefix = prefix;
// show all commands (except the owner commands if the user is not an owner)
this.createCommand(gcmdTempl.utils.help, (msg, kwargs) => {
if (kwargs.command) {
let cmd = kwargs.command;
let allCommands = {...globCommands, ...this.commands};
if (cmd.charAt(0) !== prefix)
cmd = this.prefix + cmd;
if (allCommands[cmd])
return new Discord.RichEmbed()
.setTitle(`Help for ${cmd}`)
.addField('Usage', `\`${cmd} [${allCommands[cmd].args.join('] [')}]\``.replace('[]', ''))
.addField('Description', allCommands[cmd].description)
.addField('Permission Role', allCommands[cmd].role || 'all');
else
return 'Command not found :(';
} else {
let allCommands = {...globCommands, ...this.commands};
return createHelpEmbed(allCommands, msg, prefix);
}
});
// show all roles that are used by commands
this.createCommand(scmdTempl.utils.roles, () => {
let roles = [];
Object.values(globCommands).concat(Object.values(this.commands)).sort().forEach((value) => {
roles.push(value.role || 'all');
});
return `**Roles**\n${[...new Set(roles)].join('\n')}`;
});
}
/**
* Creates a command entry in the private commands dict
* @param template
* @param call
*/
createCommand(template, call) {
if (!template.name) {
logger.debug(`Name of command template is null or undef. Failed creating command.`);
return;
}
this.commands[this.prefix + template.name] = {
'args': template.args || [],
'description': template.description,
'callback': call,
'role': template.permission,
'category': template.category || 'Other'
};
logger.debug(`Created server command: ${this.prefix + template.name}, args: ${template.args}`);
}
/**
* Removes a command
* @param command
* @deprecated Why would you want to remove a command?
*/
removeCommand(command) {
delete this.commands[command];
}
/**
* Processes the command
* @param msg
* @param globResult
* @param content
* @param returnFunction Boolean if the return value should be a function.
* @param fallback
* @returns {*}
*/
processCommand(msg, globResult, content, returnFunction, fallback) {
let command = (content.match(/^.\w+/) || [])[0];
if (!command || !this.commands[command])
if (fallback && !globResult) {
command = fallback;
content = `${fallback} ${content}`;
} else {
return globResult;
}
let cmd = this.commands[command];
if (!checkPermission(msg, cmd.role))
return 'No Permission';
logger.debug(`Permission <${cmd.role || 'all'}> granted for command ${command} for user <${msg.author.tag}>`);
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}`);
try {
let locResult = returnFunction ? () => cmd.callback(msg, kwargs, argv) : cmd.callback(msg, kwargs, argv);
return locResult || globResult;
} catch (err) {
logger.error(err.message);
return `The command \`${command}\` has thrown an error.`;
}
}
/**
* Parses the message and executes the command callback for the found command entry in the commands dict
* @param msg
* @returns {*}
*/
parseCommand(msg) {
let globResult = parseGlobalCommand(msg);
logger.debug(`Global command result is ${globResult}`);
let content = msg.content;
let commands = content.split(/(?<!\\);/).map(x => x.replace(/^ +/, ''));
if (commands.length === 1) {
return this.processCommand(msg, globResult, content);
} else if (commands.length < (config.maxCmdSequenceLength || 5)) {
let answers = [];
let previousCommand = (commands[0].match(/^.\w+/) || [])[0];
for (let i = 0; i < commands.length; i++) {
answers.push(this.processCommand(msg, globResult[i], commands[i],
true, previousCommand)); // return function to avoid "race conditions"
let commandMatch = (commands[i].match(/^.\w+/) || [])[0];
previousCommand = this.commands[commandMatch] ? commandMatch : previousCommand;
}
return answers;
} else {
return 'This command sequence is too long!';
}
}
}
/**
* Getting the logger
* @param {Object} newLogger
*/
function setModuleLogger(newLogger) {
logger = newLogger;
}
/**
* Creates a global command that can be executed in every channel.
* @param prefix
* @param template
* @param call
*/
function createGlobalCommand(prefix, template, call) {
if (!template.name) {
logger.debug(`Name of command template is null or undef. Failed to create command.`);
return;
}
globCommands[prefix + template.name] = {
'args': template.args || [],
'description': template.description,
'callback': call,
'role': template.permission,
'name': template.name,
'category': template.category || 'Other'
};
logger.debug(`Created global command: ${prefix + template.name}, args: ${template.args}`);
}
/**
* Parses a message for a global command
* @param msg
* @returns {boolean|*}
*/
exports.parseMessage = function (msg) {
return parseGlobalCommand(msg);
};
/**
* Initializes the module by creating a help command
*/
function initModule(prefix) {
logger.verbose("Creating help command...");
createGlobalCommand((prefix || config.prefix), gcmdTempl.utils.help, (msg, kwargs) => {
if (kwargs.command) {
let cmd = kwargs.command;
if (cmd.charAt(0) !== prefix)
cmd = prefix + cmd;
if (globCommands[cmd])
return new Discord.RichEmbed()
.setTitle(`Help for ${cmd}`)
.addField('Usage', `\`${cmd} [${globCommands[cmd].args.join('] [')}]\``.replace('[]', ''))
.addField('Description', globCommands[cmd].description)
.addField('Permission Role', globCommands[cmd].role || 'all');
} else {
return createHelpEmbed(globCommands, msg, prefix);
}
});
}
/**
* Processes commands for command series.
* @param cmd
* @param msg
* @param content
* @param returnFunction
* @returns {function(): *}
*/
function processCommand(cmd, msg, content, returnFunction) {
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: ${cmd.name}, kwargs: ${JSON.stringify(kwargs)}, argv: ${argv}`);
return returnFunction ? () => cmd.callback(msg, kwargs, argv) : cmd.callback(msg, kwargs, argv);
}
/**
* Parses the message by calling the assigned function for the command with arguments
* @param msg
*/
function parseGlobalCommand(msg) {
let content = msg.content;
let commands = content.split(/(?<!\\);/).map(x => x.replace(/^ +/, ''));
if (commands.length === 1) {
let command = (content.match(/^.\w+/) || [])[0];
if (!command || !globCommands[command])
return false;
let cmd = globCommands[command];
if (!checkPermission(msg, cmd.role))
return false;
logger.debug(`Permission <${cmd.role}> granted for command ${command} for user <${msg.author.tag}>`);
return processCommand(cmd, msg, content);
} else if (commands.length < (config.maxCmdSequenceLength || 5)) {
let answers = [];
let previousCommand = '';
for (let commandPart of commands) {
let command = (commandPart.match(/^.\w+/) || [])[0] || previousCommand;
previousCommand = globCommands[command] ? command : previousCommand;
if (!commandPart || !globCommands[command]) {
commandPart = `${previousCommand} ${commandPart}`;
command = previousCommand;
}
if (command && globCommands[command]) {
let cmd = globCommands[command];
if (checkPermission(msg, cmd.role)) {
logger.debug(`Permission <${cmd.role}> granted for command ${command} for user <${msg.author.tag}>`);
answers.push(processCommand(cmd, msg, commandPart,
true)); // return an function to avoid "race conditions"
} else {
answers.push(false);
}
} else {
answers.push(false);
}
}
return answers;
} else {
return 'This command sequence is too long!';
}
}
/**
* Creates a rich embed that contains help for all commands in the commands object
* @param commands {Object}
* @param msg {module:discord.js.Message}
* @param prefix {String}
* @returns {module:discord.js.RichEmbed}
*/
function createHelpEmbed(commands, msg, prefix) {
let helpEmbed = new Discord.RichEmbed()
.setTitle('Commands')
.setDescription('Create a sequence of commands with `;` (semicolon).')
.setTimestamp();
let categories = [];
let catCommands = {};
Object.entries(commands).sort().forEach(([key, value]) => {
if (value.role !== 'owner' || checkPermission(msg, 'owner'))
if (!categories.includes(value.category)) {
categories.push(value.category);
catCommands[value.category] = `\`${key}\` \t`;
} else {
catCommands[value.category] += `\`${key}\` \t`;
}
});
for (let cat of categories)
helpEmbed.addField(cat, catCommands[cat]);
helpEmbed.setFooter(prefix + 'help [command] for more info to each command');
return helpEmbed;
}
/**
* @param msg
* @param rolePerm {String}
* @returns {boolean}
*/
function checkPermission(msg, rolePerm) {
if (!rolePerm || ['all', 'any', 'everyone'].includes(rolePerm))
return true;
if (msg.author.tag === args.owner || config.owners.includes(msg.author.tag))
return true;
else if (msg.member && rolePerm && rolePerm !== 'owner' && msg.member.roles
.some(role => (role.name.toLowerCase() === rolePerm.toLowerCase() || role.name.toLowerCase() === 'botcommander')))
return true;
return false;
}
/**
* Registers the bot's utility commands
* @param prefix
* @param bot - the instance of the bot that called
*/
function registerUtilityCommands(prefix, bot) {
// responde with the commands args
createGlobalCommand(prefix, gcmdTempl.utils.say, (msg, argv, args) => {
return args.join(' ');
});
// adds a presence that will be saved in the presence file and added to the rotation
createGlobalCommand(prefix, gcmdTempl.utils.addpresence, async (msg, argv, args) => {
let p = args.join(' ');
this.presences.push(p);
await bot.maindb.run('INSERT INTO presences (text) VALUES (?)', [p]);
return `Added Presence \`${p}\``;
});
// shuts down the bot after destroying the client
createGlobalCommand(prefix, gcmdTempl.utils.shutdown, async (msg) => {
try {
await msg.reply('Shutting down...');
logger.debug('Destroying client...');
} catch (err) {
logger.error(err.message);
logger.debug(err.stack);
}
try {
await bot.client.destroy();
logger.debug('Exiting server...');
} catch (err) {
logger.error(err.message);
logger.debug(err.stack);
}
try {
await bot.webServer.stop();
logger.debug(`Exiting Process...`);
process.exit(0);
} catch (err) {
logger.error(err.message);
logger.debug(err.stack);
}
});
// forces a presence rotation
createGlobalCommand(prefix, gcmdTempl.utils.rotate, () => {
try {
bot.client.clearInterval(this.rotator);
bot.rotatePresence();
bot.rotator = this.client.setInterval(() => this.rotatePresence(), config.presence_duration);
} catch (error) {
logger.warn(error.message);
}
});
createGlobalCommand(prefix, gcmdTempl.utils.createUser, (msg, argv) => {
return new Promise((resolve, reject) => {
if (msg.guild) {
resolve("It's not save here! Try again via PM.");
} else if (argv.username && argv.scope) {
logger.debug(`Creating user entry ${argv.username}, scope: ${argv.scope}`);
bot.webServer.createUser(argv.username, argv.password, argv.scope, false).then((token) => {
resolve(`Created entry
username: ${argv.username},
scope: ${argv.scope},
token: ${token}
`);
}).catch((err) => reject(err.message));
}
});
});
createGlobalCommand(prefix, gcmdTempl.utils.bugreport, () => {
return new Discord.RichEmbed()
.setTitle('Where to report a bug?')
.setDescription(gcmdTempl.utils.bugreport.response.bug_report);
});
}
/**
* Registers the bot's info commands
* @param prefix {String}
* @param bot {Object}
*/
function registerInfoCommands(prefix, bot) {
// ping command that returns the ping attribute of the client
createGlobalCommand(prefix, gcmdTempl.info.ping, () => {
return `Current average ping: \`${bot.client.ping} ms\``;
});
// returns the time the bot is running
createGlobalCommand(prefix, gcmdTempl.info.uptime, () => {
let uptime = utils.getSplitDuration(bot.client.uptime);
return new Discord.RichEmbed().setDescription(`
**${uptime.days}** days
**${uptime.hours}** hours
**${uptime.minutes}** minutes
**${uptime.seconds}** seconds
**${uptime.milliseconds}** milliseconds
`).setTitle('Uptime');
});
// returns the number of guilds, the bot has joined
createGlobalCommand(prefix, gcmdTempl.info.guilds, () => {
return `Number of guilds: \`${bot.client.guilds.size}\``;
});
// returns information about the bot
createGlobalCommand(prefix, gcmdTempl.info.about, () => {
return new Discord.RichEmbed()
.setTitle('About')
.setDescription(gcmdTempl.info.about.response.about_creator)
.addField('Icon', gcmdTempl.info.about.response.about_icon);
});
}
/**
* Registers all commands that use the anilist api.
* @param prefix {String}
*/
function registerAnilistApiCommands(prefix) {
const anilistApi = require('./api/AnilistApi');
// returns the anime found for the name
createGlobalCommand(prefix, gcmdTempl.api.AniList.animeSearch, async (msg, kwargs, args) => {
try {
let animeData = await anilistApi.searchAnimeByName(args.join(' '));
if (animeData) {
let response = new Discord.RichEmbed()
.setTitle(animeData.title.romaji)
.setDescription(animeData.description.replace(/<\/?.*?>/g, ''))
.setThumbnail(animeData.coverImage.large)
.setURL(animeData.siteUrl)
.setColor(animeData.coverImage.color)
.addField('Genres', animeData.genres.join(', '))
.setFooter('Provided by anilist.co')
.setTimestamp();
if (animeData.studios.studioList.length > 0)
response.addField(animeData.studios.studioList.length === 1 ? 'Studio' : 'Studios', animeData.studios.studioList.map(x => `[${x.name}](${x.siteUrl})`));
response.addField('Scoring', `**Average Score:** ${animeData.averageScore}
**Favourites:** ${animeData.favourites}`);
if (animeData.episodes)
response.addField('Episodes', animeData.episodes);
response.addField('Season', animeData.season);
if (animeData.startDate.day)
response.addField('Start Date', `
${animeData.startDate.day}.${animeData.startDate.month}.${animeData.startDate.year}`);
if (animeData.nextAiringEpisode)
response.addField('Next Episode', `**Episode** ${animeData.nextAiringEpisode.episode}
**Airing at:** ${new Date(animeData.nextAiringEpisode.airingAt * 1000).toUTCString()}`);
if (animeData.endDate.day)
response.addField('End Date', `
${animeData.endDate.day}.${animeData.endDate.month}.${animeData.endDate.year}`);
return response;
} else {
return gcmdTempl.api.AniList.animeSearch.response.not_found;
}
} catch (err) {
if (err.message) {
logger.warn(err.message);
logger.debug(err.stack);
} else {
logger.debug(JSON.stringify(err));
}
return gcmdTempl.api.AniList.animeSearch.response.not_found;
}
});
createGlobalCommand(prefix, gcmdTempl.api.AniList.mangaSearch, async (msg, kwargs, args) => {
try {
let mangaData = await anilistApi.searchMangaByName(args.join(' '));
if (mangaData) {
let response = new Discord.RichEmbed()
.setTitle(mangaData.title.romaji)
.setThumbnail(mangaData.coverImage.large)
.setDescription(mangaData.description.replace(/<\/?.*?>/g, ''))
.setURL(mangaData.siteUrl)
.setFooter('Provided by anilist.co')
.setTimestamp();
if (mangaData.endDate.day)
response.addField('End Date', `
${mangaData.endDate.day}.${mangaData.endDate.month}.${mangaData.endDate.year}`);
return response;
} else {
return gcmdTempl.api.AniList.mangaSearch.response.not_found;
}
} catch (err) {
if (err.message) {
logger.warn(err.message);
logger.debug(err.stack);
} else {
logger.debug(JSON.stringify(err));
}
return gcmdTempl.api.AniList.mangaSearch.response.not_found;
}
});
}
// -- exports -- //
Object.assign(exports, {
init: initModule,
Servant: Servant,
registerAnilistApiCommands: registerAnilistApiCommands,
registerInfoCommands: registerInfoCommands,
registerUtilityCommands: registerUtilityCommands,
setLogger: setModuleLogger,
createGlobalCommand: createGlobalCommand
});

@ -78,8 +78,8 @@ class AniListCommandModule extends cmdLib.CommandModule {
);
// registering commands
commandHandler.registerCommand(animeSearch);
commandHandler.registerCommand(mangaSearch);
commandHandler.registerCommand(animeSearch)
.registerCommand(mangaSearch);
}
}

@ -39,5 +39,6 @@ help:
Shows help for bot ocmmands.
permission: all
category: Info
embed_color: 0xffffff
args:
- command

@ -21,9 +21,10 @@ class InfoCommandModule extends cmdLib.CommandModule {
this._messageHandler = opts.messageHandler;
}
_createHelpEmbed(commands, msg, prefix) {
_createHelpEmbed(commands, msg, prefix, embedColor = 0xfff) {
let helpEmbed = new cmdLib.ExtendedRichEmbed('Commands')
.setDescription('Create a sequence of commands with `;` and `&&`.');
.setDescription('Create a sequence of commands with `;` and `&&`.')
.setColor(embedColor);
let categories = [];
let catCommands = {};
Object.entries(commands).sort().forEach(([key, value]) => {
@ -89,10 +90,10 @@ class InfoCommandModule extends cmdLib.CommandModule {
if (k.command) {
k.command = k.command.replace(globH.prefix, '');
let commandInstance = globH.commands[k.command] || scopeH.commands[k.command];
return commandInstance.help;
return commandInstance.help.setColor(this.template.help.embed_color);
} else {
let commandObj = {...globH.commands, ...scopeH.commands};
return this._createHelpEmbed(commandObj, m, globH.prefix);
return this._createHelpEmbed(commandObj, m, globH.prefix, this.template.help.embed_color);
}
})
);

@ -53,7 +53,7 @@ stop:
name: stop
description: >
Stops the media playback and leaves the VoiceChannel.
permission: dj
permission: musicPlayer
category: Music
response:
success: >
@ -89,7 +89,7 @@ skip:
name: skip
description: >
Skips the currently playing song.
permission: dj
permission: musicPlayer
category: Music
response:
success: >
@ -101,7 +101,7 @@ clear:
name: clear
description: >
Clears the media queue.
permission: dj
permission: musicPlayer
category: Music
response:
success: >
@ -150,7 +150,7 @@ save_media:
name: savemedia
description: >
Saves the YouTube URL with a specific name.
permission: dj
permission: musicPlayer
category: Music
args:
- url
@ -160,7 +160,7 @@ delete_media:
name: deletemedia
description: >
Deletes a saved YouTube URL from saved media.
permission: dj
permission: musicPlayer
category: Music
usage: deletemedia [name]
response:

@ -30,11 +30,11 @@ class MusicCommandModule extends cmdLib.CommandModule {
* @private
*/
async _connectAndPlay(gh, vc, url, next) {
if (!gh.dj.connected) {
await gh.dj.connect(vc);
await gh.dj.playYouTube(url, next);
if (!gh.musicPlayer.connected) {
await gh.musicPlayer.connect(vc);
await gh.musicPlayer.playYouTube(url, next);
} else {
await gh.dj.playYouTube(url, next);
await gh.musicPlayer.playYouTube(url, next);
}
}
@ -50,7 +50,7 @@ class MusicCommandModule extends cmdLib.CommandModule {
*/
async _playFunction(m, k, s, t, n) {
let gh = await this._getGuildHandler(m.guild);
let vc = gh.dj.voiceChannel || m.member.voiceChannel;
let vc = gh.musicPlayer.voiceChannel || m.member.voiceChannel;
let url = k['url'];
if (!vc)
return t.response.no_voicechannel;
@ -94,7 +94,7 @@ class MusicCommandModule extends cmdLib.CommandModule {
new cmdLib.Answer(async (m) => {
let gh = await this._getGuildHandler(m.guild);
if (m.member.voiceChannel)
await gh.dj.connect(m.member.voiceChannel);
await gh.musicPlayer.connect(m.member.voiceChannel);
else
return this.template.join.response.no_voicechannel;
})
@ -104,8 +104,8 @@ class MusicCommandModule extends cmdLib.CommandModule {
this.template.stop,
new cmdLib.Answer(async (m) => {
let gh = await this._getGuildHandler(m.guild);
if (gh.dj.connected) {
gh.dj.stop();
if (gh.musicPlayer.connected) {
gh.musicPlayer.stop();
return this.template.stop.success;
} else {
return this.template.stop.not_playing;
@ -117,8 +117,8 @@ class MusicCommandModule extends cmdLib.CommandModule {
this.template.pause,
new cmdLib.Answer(async (m) => {
let gh = await this._getGuildHandler(m.guild);
if (gh.dj.playing) {
gh.dj.pause();
if (gh.musicPlayer.playing) {
gh.musicPlayer.pause();
return this.template.pause.response.success;
} else {
return this.template.pause.response.not_playing;
@ -130,8 +130,8 @@ class MusicCommandModule extends cmdLib.CommandModule {
this.template.resume,
new cmdLib.Answer(async (m) => {
let gh = await this._getGuildHandler(m.guild);
if (gh.dj.playing) {
gh.dj.resume();
if (gh.musicPlayer.playing) {
gh.musicPlayer.resume();
return this.template.resume.response.success;
} else {
return this.template.resume.response.not_playing;
@ -143,8 +143,8 @@ class MusicCommandModule extends cmdLib.CommandModule {
this.template.skip,
new cmdLib.Answer(async (m) => {
let gh = await this._getGuildHandler(m.guild);
if (gh.dj.playing) {
gh.dj.skip();
if (gh.musicPlayer.playing) {
gh.musicPlayer.skip();
return this.template.skip.response.success;
} else {
return this.template.skip.response.not_playing;
@ -156,7 +156,7 @@ class MusicCommandModule extends cmdLib.CommandModule {
this.template.clear,
new cmdLib.Answer(async (m) => {
let gh = await this._getGuildHandler(m.guild);
gh.dj.clear();
gh.musicPlayer.clear();
return this.template.clear.response.success;
})
);
@ -165,14 +165,14 @@ class MusicCommandModule extends cmdLib.CommandModule {
this.template.media_queue,
new cmdLib.Answer(async (m) => {
let gh = await this._getGuildHandler(m.guild);
this._logger.debug(`Found ${gh.dj.queue.length} songs.`);
this._logger.debug(`Found ${gh.musicPlayer.queue.length} songs.`);
let description = '';
for (let i = 0; i < Math.min(gh.dj.queue.length, 9); i++) {
let entry = gh.dj.queue[i];
for (let i = 0; i < Math.min(gh.musicPlayer.queue.length, 9); i++) {
let entry = gh.musicPlayer.queue[i];
description += `[${entry.title}](${entry.url})\n`;
}
return new cmdLib.ExtendedRichEmbed(`${gh.dj.queue.length} songs in queue`)
return new cmdLib.ExtendedRichEmbed(`${gh.musicPlayer.queue.length} songs in queue`)
.setDescription(description);
})
);
@ -181,7 +181,7 @@ class MusicCommandModule extends cmdLib.CommandModule {
this.template.media_current,
new cmdLib.Answer(async (m) => {
let gh = await this._getGuildHandler(m.guild);
let song = gh.dj.song;
let song = gh.musicPlayer.song;
if (song)
return new cmdLib.ExtendedRichEmbed('Now playing:')
.setDescription(`[${song.title}](${song.url})`)
@ -196,7 +196,7 @@ class MusicCommandModule extends cmdLib.CommandModule {
this.template.shuffle,
new cmdLib.Answer(async (m) => {
let gh = await this._getGuildHandler(m.guild);
gh.dj.shuffle();
gh.musicPlayer.shuffle();
return this.template.shuffle.response.success;
})
);
@ -205,8 +205,8 @@ class MusicCommandModule extends cmdLib.CommandModule {
this.template.toggle_repeat,
new cmdLib.Answer(async (m) => {
let gh = await this._getGuildHandler(m.guild);
gh.dj.repeat = !gh.dj.repeat;
return gh.dj.repeat?
gh.musicPlayer.repeat = !gh.musicPlayer.repeat;
return gh.musicPlayer.repeat?
this.template.toggle_repeat.response.repeat_true :
this.template.toggle_repeat.response.repeat_false;
})

@ -20,9 +20,9 @@ class UtilityCommandModule extends cmdLib.CommandModule {
constructor(opts) {
super(cmdLib.CommandScopes.User);
this.templateFile = location + '/UtilityCommandsTemplate.yaml';
this.bot = opts.bot;
this.logger = opts.logger;
this.config = opts.config;
this._bot = opts.bot;
this._logger = opts.logger;
this._config = opts.config;
}
async register(commandHandler) {
@ -31,8 +31,8 @@ class UtilityCommandModule extends cmdLib.CommandModule {
let addPresence = new cmdLib.Command(
this.template.add_presence,
new cmdLib.Answer(async (m, k, s) => {
this.bot.presences.push(s);
await this.bot.maindb.run('INSERT INTO presences (text) VALUES (?)', [s]);
this._bot.presences.push(s);
await this._bot.maindb.run('INSERT INTO presences (text) VALUES (?)', [s]);
return `Added Presence \`${s}\``;
})
);
@ -41,12 +41,12 @@ class UtilityCommandModule extends cmdLib.CommandModule {
this.template.rotate_presence,
new cmdLib.Answer(() => {
try {
this.bot.client.clearInterval(this.rotator);
this.bot.rotatePresence();
this.bot.rotator = this.bot.client.setInterval(() => this.bot.rotatePresence(),
this.config.presence_duration);
this._bot.client.clearInterval(this._bot.rotator);
this._bot.rotatePresence();
this._bot.rotator = this._bot.client.setInterval(() => this._bot.rotatePresence(),
this._config.presence_duration);
} catch (error) {
this.logger.warn(error.message);
this._logger.warn(error.message);
}
})
);
@ -56,25 +56,25 @@ class UtilityCommandModule extends cmdLib.CommandModule {
new cmdLib.Answer(async (m) => {
try {
await m.reply('Shutting down...');
this.logger.debug('Destroying client...');
this._logger.debug('Destroying client...');
await this._bot.client.destroy();
} catch (err) {
this.logger.error(err.message);
this.logger.debug(err.stack);
this._logger.error(err.message);
this._logger.debug(err.stack);
}
try {
await this.bot.client.destroy();
this.logger.debug('Exiting server...');
this._logger.debug('Exiting server...');
await this._bot.webServer.stop();
} catch (err) {
this.logger.error(err.message);
this.logger.debug(err.stack);
this._logger.error(err.message);
this._logger.debug(err.stack);
}
try {
await this.bot.webServer.stop();
this.logger.debug(`Exiting Process...`);
this._logger.debug(`Exiting Process...`);
process.exit(0);
} catch (err) {
this.logger.error(err.message);
this.logger.debug(err.stack);
this._logger.error(err.message);
this._logger.debug(err.stack);
}
})
);
@ -83,8 +83,8 @@ class UtilityCommandModule extends cmdLib.CommandModule {
this.template.create_user,
new cmdLib.Answer(async (m, k) => {
if (k.username &&k.password && k.scope) {
this.logger.debug(`Creating user entry for ${k.username}`);
let token = await this.bot.webServer.createUser(
this._logger.debug(`Creating user entry for ${k.username}`);
let token = await this._bot.webServer.createUser(
k.username, k.password, k.scope, false);
return `${k.username}'s token is ${token}`;
}
@ -100,11 +100,11 @@ class UtilityCommandModule extends cmdLib.CommandModule {
);
// register commands
commandHandler.registerCommand(addPresence);
commandHandler.registerCommand(rotatePresence);
commandHandler.registerCommand(shutdown);
commandHandler.registerCommand(createUser);
commandHandler.registerCommand(bugReport);
commandHandler.registerCommand(addPresence)
.registerCommand(rotatePresence)
.registerCommand(shutdown)
.registerCommand(createUser)
.registerCommand(bugReport);
}
}

@ -1,49 +1,45 @@
const cmd = require('./cmd'),
music = require('./music'),
const music = require('./MusicLib'),
utils = require('./utils'),
config = require('../config.json'),
sqliteAsync = require('./sqliteAsync'),
fs = require('fs-extra'),
servercmd = require('../commands/servercommands'),
Discord = require('discord.js'),
waterfall = require('promise-waterfall'),
dataDir = config.dataPath || './data';
let logger = require('winston');
exports.setLogger = function (newLogger) {
logger = newLogger;
music.setLogger(logger);
cmd.setLogger(logger);
};
/**
* Server-Specific commands, music and more
* The Guild Handler handles guild settings and data.
* @type {GuildHandler}
*/
exports.GuildHandler = class {
constructor(guild, prefix) {
class GuildHandler {
constructor(guild) {
this.guild = guild;
this.dj = null;
this.mention = false;
this.prefix = prefix || config.prefix;
this.servant = new cmd.Servant(this.prefix);
this.musicPlayer = new music.MusicPlayer(null);
}
/**
* Initializes the database
* @returns {Promise<void>}
*/
async initDatabase() {
await fs.ensureDir(dataDir + '/gdb');
this.db = new sqliteAsync.Database(`${dataDir}/gdb/${this.guild}.db`);
await this.db.init();
logger.debug(`Connected to the database for ${this.guild}`);
await this.createTables();
// register commands
this.registerCommands();
}
/**
* Destroys the guild handler
*/
destroy() {
this.dj.stop();
this.musicPlayer.stop();
this.db.close();
}
@ -72,283 +68,8 @@ exports.GuildHandler = class {
command VARCHAR(255) NOT NULL
)`);
}
}
/**
* Answers a message via mention if mentioning is active or with just sending it to the same channel.
* @param msg
* @param answer
*/
async answerMessage(msg, answer) {
if (answer instanceof Promise || answer)
if (answer instanceof Discord.RichEmbed) {
(this.mention) ? msg.reply('', answer) : msg.channel.send('', answer);
} else if (answer instanceof Promise) {
let resolvedAnswer = await answer;
await this.answerMessage(msg, resolvedAnswer);
} else if (answer instanceof Array) {
await waterfall(answer.map((x) => async () => await this.answerMessage(msg, x)));
} else if ({}.toString.call(answer) === '[object Function]') { // check if the answer is of type function
await this.answerMessage(msg, answer());
} else {
(this.mention) ? msg.reply(answer) : msg.channel.send(answer);
}
}
/**
* handles the message by letting the servant parse the command. Depending on the message setting it
* replies or just sends the answer.
* @param msg
*/
async handleMessage(msg) {
if (this.db)
await this.db.run(
'INSERT INTO messages (author, creation_timestamp, author_name, content) values (?, ?, ?, ?)',
[msg.author.id, msg.createdTimestamp, msg.author.username, msg.content]
);
await this.answerMessage(msg, this.servant.parseCommand(msg));
}
/**
* Connect to a voice-channel if not connected and play the url
* @param vc
* @param url
* @param next
*/
async connectAndPlay(vc, url, next) {
if (!this.dj.connected) {
await this.dj.connect(vc);
await this.dj.playYouTube(url, next);
} else {
await this.dj.playYouTube(url, next);
}
}
/**
* registers all music commands and initializes a dj
*/
registerCommands() {
this.dj = new music.DJ();
let playCb = async (msg, kwargs, argv, template, next) => {
let vc = this.dj.voiceChannel || msg.member.voiceChannel;
let url = kwargs['url'];
if (!vc)
return template.response.no_voicechannel;
if (!url)
return template.response.no_url;
if (!utils.YouTube.isValidEntityUrl(url)) {
if (argv && argv.length > 0)
url += ' ' + argv.join(' '); // join to get the whole expression behind the command
let row = await this.db.get('SELECT url FROM playlists WHERE name = ?', [url]);
if (!row) {
logger.debug('Got invalid url for play command.');
return template.response.url_invalid;
} else {
await this.connectAndPlay(vc, row.url, next);
return template.response.success;
}
} else {
await this.connectAndPlay(vc, url, next);
return template.response.success;
}
};
// play command
this.servant.createCommand(servercmd.music.play, async (msg, kwargs, argv) => {
return await playCb(msg, kwargs, argv, servercmd.music.play, false);
});
// playnext command
this.servant.createCommand(servercmd.music.playnext, async (msg, kwargs, argv) => {
return await playCb(msg, kwargs, argv, servercmd.music.playnext, true);
});
// join command
this.servant.createCommand(servercmd.music.join, (msg) => {
if (msg.member.voiceChannel)
this.dj.connect(msg.member.voiceChannel);
else
return servercmd.music.join.response.not_connected;
});
// stop command
this.servant.createCommand(servercmd.music.stop, () => {
if (this.dj.connected) {
this.dj.stop();
return servercmd.music.stop.response.success;
} else {
return servercmd.music.stop.response.not_playing;
}
});
// pause command
this.servant.createCommand(servercmd.music.pause, () => {
if (this.dj.playing) {
this.dj.pause();
return servercmd.music.pause.response.success;
} else {
return servercmd.music.pause.response.not_playing;
}
});
// resume command
this.servant.createCommand(servercmd.music.resume, () => {
if (this.dj.playing) {
this.dj.resume();
return servercmd.music.resume.response.success;
} else {
return servercmd.music.resume.response.not_playing;
}
});
// skip command
this.servant.createCommand(servercmd.music.skip, () => {
if (this.dj.playing) {
this.dj.skip();
return servercmd.music.skip.response.success;
} else {
return servercmd.music.skip.response.not_playing;
}
});
// clear command
this.servant.createCommand(servercmd.music.clear, () => {
this.dj.clear();
return servercmd.music.clear.response.success;
});
// playlist command
this.servant.createCommand(servercmd.music.playlist, () => {
logger.debug(`found ${this.dj.queue.length} songs`);
let describtion = '';
for (let i = 0; i < Math.min(this.dj.queue.length, 9); i++) {
let entry = this.dj.queue[i];
describtion += `[${entry.title}](${entry.url})\n`;
}
return new Discord.RichEmbed()
.setTitle(`${this.dj.queue.length} songs in queue`)
.setDescription(describtion);
});
// np command
this.servant.createCommand(servercmd.music.current, () => {
let song = this.dj.song;
if (song)
return new Discord.RichEmbed()
.setTitle('Now playing:')
.setDescription(`[${song.title}](${song.url})`)
.setImage(utils.YouTube.getVideoThumbnailUrlFromUrl(song.url))
.setColor(0x00aaff);
else
return servercmd.music.current.response.not_playing;
});
// shuffle command
this.servant.createCommand(servercmd.music.shuffle, () => {
this.dj.shuffle();
return servercmd.music.shuffle.response.success;
});
// repeat command
this.servant.createCommand(servercmd.music.repeat, () => {
if (this.dj) {
this.dj.repeat = !this.dj.repeat;
if (this.dj.repeat)
return servercmd.music.repeat.response.repeat_true;
else
return servercmd.music.repeat.response.repeat_false;
}
});
// saves playlists and videos
this.servant.createCommand(servercmd.music.savemedia, async (msg, kwargs, argv) => {
let saveName = argv.join(' ');
let row = await this.db.get('SELECT COUNT(*) count FROM playlists WHERE name = ?', [saveName]);
if (!row || row.count === 0)
await this.db.run('INSERT INTO playlists (name, url) VALUES (?, ?)', [saveName, kwargs.url]);
else
await this.db.run('UPDATE playlists SET url = ? WHERE name = ?', [kwargs.url, saveName]);
return `Saved song/playlist as ${saveName}`;
});
// savedmedia command - prints out saved playlists and videos
this.servant.createCommand(servercmd.music.savedmedia, async () => {
let response = '';
let rows = await this.db.all('SELECT name, url FROM playlists');
for (let row of rows)
response += `[${row.name}](${row.url})\n`;
if (rows.length === 0)
return servercmd.music.savedmedia.response.no_saved;
else
return new Discord.RichEmbed()
.setTitle('Saved Songs and Playlists')
.setDescription(response)
.setFooter(`Play a saved entry with ${this.prefix}play [Entryname]`)
.setTimestamp();
});
this.servant.createCommand(servercmd.music.deletemedia, async (msg, kwargs, argv) => {
let saveName = argv.join(' ');
if (!saveName) {
return servercmd.music.deletemedia.response.no_name;
} else {
await this.db.run('DELETE FROM playlists WHERE name = ?', [saveName]);
return `Deleted ${saveName} from saved media`;
}
});
// savecmd - saves a command sequence with a name
this.servant.createCommand(servercmd.utils.savecmd, async (msg, kwargs, argv) => {
let saveName = argv.pop();
let cmdsequence = argv.join(' ').replace(/\\/g, '');
if (argv.includes(this.prefix + servercmd.utils.execute.name)) {
return servercmd.utils.savecmd.response.no_recursion;
} else if (cmdsequence.split(';').length < (config.maxCmdSequenceLength || 5)){
let row = await this.db.get('SELECT COUNT(*) count FROM commands WHERE name = ?', [saveName]);
if (!row || row.count === 0)
await this.db.run('INSERT INTO commands (name, command) VALUES (?, ?)', [saveName, cmdsequence]);
else
await this.db.run('UPDATE commands SET sequence = ? WHERE name = ?', [cmdsequence, saveName]);
return `saved command sequence as ${saveName}`;
} else {
return servercmd.utils.savecmd.response.sequence_too_long;
}
});
// savedcmd - prints saved commands
this.servant.createCommand(servercmd.utils.savedcmd, async () => {
let response = new Discord.RichEmbed()
.setTitle('Saved Commands')
.setFooter(`Execute a saved entry with ${this.prefix}execute [Entryname]`)
.setTimestamp();
let rows = await this.db.all('SELECT name, command FROM commands');
if (rows.length === 0)
return servercmd.utils.savedcmd.response.no_commands;
else
for (let row of rows)
response.addField(row.name, '`' + row.command + '`');
return response;
});
// deletecmd - deletes a command from saved commands
this.servant.createCommand(servercmd.utils.deletecmd, async (msg, kwargs) => {
await this.db.run('DELETE FROM commands WHERE name = ?', [kwargs.cmdname]);
return `Deleted command ${kwargs.cmdname}`;
});
// execute - executes a saved command
this.servant.createCommand(servercmd.utils.execute, async (msg, kwargs) => {
let row = await this.db.get('SELECT command FROM commands WHERE name = ?', [kwargs.cmdname]);
if (row) {
msg.content = row.command;
await this.handleMessage(msg);
} else {
return servercmd.utils.execute.response.not_found;
}
});
}
};
Object.assign(exports, {
GuildHandler: GuildHandler
});

@ -201,7 +201,7 @@ describe('lib/music', function() {
"api": {}
});
describe('#DJ', function () {
describe('#MusicPlayer', function () {
it('connects to a VoiceChannel', function (done) {
let dj = new music.DJ(mockobjects.mockVoicechannel);
@ -456,7 +456,7 @@ describe('lib/guilding', function*() { // deactivated because of problems with s
let gh = new guilding.GuildHandler('test', '~');
gh.db = new mockobjects.MockDatabase('', ()=>{});
gh.ready = true;
gh.dj = new music.DJ(mockobjects.mockVoicechannel);
gh.musicPlayer = new music.DJ(mockobjects.mockVoicechannel);
gh.connectAndPlay(mockobjects.mockVoicechannel, 'test', false).then(() => {
done();
});

@ -50,30 +50,30 @@ head
span.label.text-right Member Count:
span#guild-memberCount.text-left
.space
h3.cell DJ
h3.cell MusicPlayer
.cell
span.label.text-right State:
span#guild-djStatus.text-left
span#guild-mpStatus.text-left
.cell
span.label.text-right Repeat:
span#dj-repeat.text-left
span#mp-repeat.text-left
.cell
span.label.text-right Voice Channel:
span#dj-voiceChannel.text-left
#dj-songinfo.listContainer(style='display: none')
span#mp-voiceChannel.text-left
#mp-songinfo.listContainer(style='display: none')
a#songinfo-container
span#dj-songname
img#dj-songImg(src='' alt='')
#dj-songProgress(style='display:none')
span#dj-songCurrentTS
#dj-queue-container
span#mp-songname
img#mp-songImg(src='' alt='')
#mp-songProgress(style='display:none')
span#mp-songCurrentTS
#mp-queue-container
span.cell.label(id='Queue Song count')
span#dj-queueCount
span#mp-queueCount
| Songs in Queue
span.cell
| Next
span#dj-queueDisplayCount 0
span#mp-queueDisplayCount 0
| Songs:
#dj-songQueue
#mp-songQueue
script.
startUpdating();

@ -247,7 +247,7 @@ div.cell > *
#guild-nameAndIcon
width: 50%
#dj-songinfo
#mp-songinfo
background-color: $cBackgroundVariant
border-radius: 20px
overflow-x: hidden
@ -259,15 +259,15 @@ div.cell > *
padding: 10px
width: calc(100% - 20px)
#dj-queue-container
#mp-queue-container
display: grid
padding: 0 5px 5px
#dj-songname
#mp-songname
font-weight: bold
font-size: 120%
#dj-songImg
#mp-songImg
align-self: center
width: 80%
height: auto
@ -281,6 +281,6 @@ div.cell > *
#guildinfo:hover
overflow-y: auto
#dj-songQueue
#mp-songQueue
display: grid
max-height: 100%

@ -60,7 +60,7 @@ function queryGuilds() {
guilds {
id
name
dj {
musicPlayer {
playing
}
}
@ -71,8 +71,8 @@ function queryGuilds() {
if ($(`option[value=${guild.id}]`).length === 0) {
let option = document.createElement('option');
option.setAttribute('value', guild.id);
if (guild.dj)
option.innerText = guild.dj.playing? guild.name + ' 🎶' : guild.name;
if (guild.musicPlayer)
option.innerText = guild.musicPlayer.playing? guild.name + ' 🎶' : guild.name;
let guildSelect = document.querySelector('#guild-select');
guildSelect.appendChild(option);
}
@ -118,7 +118,7 @@ function queryGuildStatus(guildId) {
let query = `{
client {
guilds(id: "${guildId}") {
dj {
musicPlayer {
playing
connected
repeat
@ -144,29 +144,29 @@ function queryGuildStatus(guildId) {
}`;
postQuery(query).then((res) => {
let guild = res.data.client.guilds[0];
document.querySelector('#dj-repeat').innerText = guild.dj.repeat? 'on': 'off';
document.querySelector('#guild-djStatus').innerText = guild.dj.connected? 'connected' : 'disconnected';
if (guild.dj.connected) {
let songinfoContainer = $('#dj-songinfo');
document.querySelector('#mp-repeat').innerText = guild.musicPlayer.repeat? 'on': 'off';
document.querySelector('#guild-mpStatus').innerText = guild.musicPlayer.connected? 'connected' : 'disconnected';
if (guild.musicPlayer.connected) {
let songinfoContainer = $('#mp-songinfo');
songinfoContainer.show();
document.querySelector('#guild-djStatus').innerText = guild.dj.playing? 'playing' : 'connected';
document.querySelector('#dj-voiceChannel').innerText = guild.dj.voiceChannel;
document.querySelector('#guild-mpStatus').innerText = guild.musicPlayer.playing? 'playing' : 'connected';
document.querySelector('#mp-voiceChannel').innerText = guild.musicPlayer.voiceChannel;
if (guild.dj.playing) {
if (guild.musicPlayer.playing) {
if (songinfoContainer.is(':hidden'))
songinfoContainer.show();
document.querySelector('#guild-djStatus').innerText = guild.dj.paused? 'paused' : 'playing';
document.querySelector('#songinfo-container').setAttribute('href', guild.dj.currentSong.url);
document.querySelector('#dj-songname').innerText = guild.dj.currentSong.name;
document.querySelector('#dj-songImg').setAttribute('src', guild.dj.currentSong.thumbnail.replace('maxresdefault', 'mqdefault'));
let songSd = getSplitDuration(Date.now() - guild.dj.songStartTime);
document.querySelector('#dj-songCurrentTS').innerText = `${songSd.minutes}:${songSd.seconds.toString().padStart(2, '0')}`;
document.querySelector('#dj-songCurrentTS').setAttribute('start-ts', guild.dj.songStartTime);
document.querySelector('#dj-queueCount').innerText = guild.dj.queueCount;
let songContainer = document.querySelector('#dj-songQueue');
document.querySelector('#guild-mpStatus').innerText = guild.musicPlayer.paused? 'paused' : 'playing';
document.querySelector('#songinfo-container').setAttribute('href', guild.musicPlayer.currentSong.url);
document.querySelector('#mp-songname').innerText = guild.musicPlayer.currentSong.name;
document.querySelector('#mp-songImg').setAttribute('src', guild.musicPlayer.currentSong.thumbnail.replace('maxresdefault', 'mqdefault'));
let songSd = getSplitDuration(Date.now() - guild.musicPlayer.songStartTime);
document.querySelector('#mp-songCurrentTS').innerText = `${songSd.minutes}:${songSd.seconds.toString().padStart(2, '0')}`;
document.querySelector('#mp-songCurrentTS').setAttribute('start-ts', guild.musicPlayer.songStartTime);
document.querySelector('#mp-queueCount').innerText = guild.musicPlayer.queueCount;
let songContainer = document.querySelector('#mp-songQueue');
$('.songEntry').remove();
for (let song of guild.dj.queue) {
for (let song of guild.musicPlayer.queue) {
let songEntry = document.createElement('a');
songEntry.setAttribute('href', song.url);
songEntry.setAttribute('class', 'songEntry');
@ -179,14 +179,14 @@ function queryGuildStatus(guildId) {
songEntry.appendChild(nameEntry);
songContainer.appendChild(songEntry);
}
document.querySelector('#dj-queueDisplayCount').innerText = document.querySelectorAll('.songEntry').length;
document.querySelector('#mp-queueDisplayCount').innerText = document.querySelectorAll('.songEntry').length;
} else {
if (songinfoContainer.is(':not(:hidden)'))
songinfoContainer.hide();
}
} else {
$('#dj-songinfo').hide();
document.querySelector('#dj-voiceChannel').innerText = 'None';
$('#mp-songinfo').hide();
document.querySelector('#mp-voiceChannel').innerText = 'None';
}
});
}
@ -302,7 +302,7 @@ function startUpdating() {
queryGuild(guildId);
});
setInterval(() => {
let songSd = getSplitDuration(Date.now() - $('#dj-songCurrentTS').attr('start-ts'));
document.querySelector('#dj-songCurrentTS').innerText = `${songSd.minutes}:${songSd.seconds.toString().padStart(2, '0')}`;
let songSd = getSplitDuration(Date.now() - $('#mp-songCurrentTS').attr('start-ts'));
document.querySelector('#mp-songCurrentTS').innerText = `${songSd.minutes}:${songSd.seconds.toString().padStart(2, '0')}`;
}, 500);
}

Loading…
Cancel
Save