commit
e930d0ef61
@ -0,0 +1,33 @@
|
||||
# Changelog
|
||||
All notable changes to the discord bot will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.10.0] - 2019-03-03
|
||||
### Added
|
||||
- AniList api commands powered by [AniList.co](https://www.anilist.co)
|
||||
- MessageHandler - handles all incoming messages, parses the syntax, executes the syntax and handles rate limits
|
||||
- CommandHandler - handles all single commands, checks command Permission and executes the command
|
||||
- Command - represents a single command with the necessary metadata and answer instance
|
||||
- Answer - represents a commands answer with own syntax parsing (can be overwritten)
|
||||
- CommandModule - represents a single module of a command with the initialization and registring of command to the command handler. Each module owns an instance of the logger
|
||||
- ExtendedRichEmbed - extends the functinality of the default discord.js RichEmbed with auto cropping of too long field values, functions to add an Object with fields that are not empty and automatic timestamp addition
|
||||
|
||||
### Changed
|
||||
- Command Syntax now orients more on linux/unix style with `&&` and `;`
|
||||
- GuildHandler now doesn't handle commands anymore
|
||||
- the default logger is now a wrapper around the winston.js logger that loggs the current module's name
|
||||
- all commands are now defined in the lib/commands folder with a folder for each command that contains a `index.js` and a `CommandTemplate.yaml`.
|
||||
- Rate Limits now only affect commands
|
||||
- Music commands `~skip` and `~stop` now are votable when the user doesn't have the role *dj* or *botcommander*
|
||||
- renamed the lib/music to lib/MusicLib and the DJ class to MusicHandler class
|
||||
- renamed the lib/weblib to lib/WebLib
|
||||
- changed graphql schema to fit the new internal names
|
||||
- changed interface to fit the new graphql schema
|
||||
- changed module export definition to `Object.assign(exports, {...})` at the end of the module file
|
||||
- added section `commandSettings` to config.js file
|
||||
- added module information to webinterface log
|
||||
|
||||
### Removed
|
||||
- removed lib/cmd because all functionalities are now adapted to the MessageHandler and CommadnHandlers
|
@ -1,87 +0,0 @@
|
||||
{
|
||||
"utils": {
|
||||
"help": {
|
||||
"name": "help",
|
||||
"permission": "all",
|
||||
"description": "Shows this help command.",
|
||||
"category": "Utility",
|
||||
"args": [
|
||||
"command"
|
||||
]
|
||||
},
|
||||
"say": {
|
||||
"name": "say",
|
||||
"permission": "all",
|
||||
"description": "Says something. ~say [String].",
|
||||
"category": "Utility"
|
||||
},
|
||||
"addpresence": {
|
||||
"name": "addpresence",
|
||||
"permission": "owner",
|
||||
"description": "Adds a presence to presences.",
|
||||
"category": "Utility"
|
||||
},
|
||||
"shutdown": {
|
||||
"name": "shutdown",
|
||||
"description": "Shuts the bot down.",
|
||||
"permission": "owner",
|
||||
"category": "Utility"
|
||||
},
|
||||
"rotate": {
|
||||
"name": "rotate",
|
||||
"description": "Forces a presence rotation",
|
||||
"permission": "owner",
|
||||
"category": "Utility"
|
||||
},
|
||||
"createUser": {
|
||||
"name": "createUser",
|
||||
"permission": "owner",
|
||||
"description": "Creates a new user for the webinterface.",
|
||||
"category": "Utility",
|
||||
"args": [
|
||||
"username",
|
||||
"password",
|
||||
"scope"
|
||||
]
|
||||
},
|
||||
"bugreport": {
|
||||
"name": "bug",
|
||||
"permission": "all",
|
||||
"description": "Get info about how to report a bug",
|
||||
"category": "Utility",
|
||||
"response": {
|
||||
"bug_report": "Please report your bugs [here](https://github.com/Trivernis/discordbot.js/issues)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"about": {
|
||||
"name": "about",
|
||||
"permission": "all",
|
||||
"description": "Shows information about this bot.",
|
||||
"category": "Info",
|
||||
"response": {
|
||||
"about_icon": "This icon war created by [blackrose14344](https://www.deviantart.com/blackrose14344). \n [Original](https://www.deviantart.com/blackrose14344/art/2B-Chi-B-685771489)",
|
||||
"about_creator": "This bot was created by Trivernis. More about this bot [here](https://github.com/Trivernis/discordbot.js)."
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"name": "ping",
|
||||
"permission": "owner",
|
||||
"description": "Answers with the current average ping of the bot.",
|
||||
"category": "Info"
|
||||
},
|
||||
"uptime": {
|
||||
"name": "uptime",
|
||||
"permission": "owner",
|
||||
"description": "Answers with the current uptime of the bot.",
|
||||
"category": "Info"
|
||||
},
|
||||
"guilds": {
|
||||
"name": "guilds",
|
||||
"permission": "owner",
|
||||
"description": "Answers with the number of guilds the bot has joined",
|
||||
"category": "Info"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,203 +0,0 @@
|
||||
{
|
||||
"utils": {
|
||||
"roles": {
|
||||
"name": "roles",
|
||||
"permission": "all",
|
||||
"description": "Shows the roles used for commands on the server.",
|
||||
"category": "Utility"
|
||||
},
|
||||
"savecmd": {
|
||||
"name": "savecmd",
|
||||
"permission": "moderator",
|
||||
"description": "Saves a sequence of commands under a new name. ~save [cmdsequence] [cmdname]. Semicoli must be escaped with \\ (Backslash)",
|
||||
"category": "Utility",
|
||||
"response": {
|
||||
"no_recursion": "You are **not** allowed to execute another saved command in this sequence. This is a safety measure to avoid endlessly recursive calls.",
|
||||
"sequence_too_long": "This command sequence is too long!"
|
||||
}
|
||||
},
|
||||
"savedcmd": {
|
||||
"name": "savedcmd",
|
||||
"permission": "all",
|
||||
"description": "Displays the saved commands.",
|
||||
"category": "Utility",
|
||||
"response": {
|
||||
"no_commands": "There are no saved commands."
|
||||
}
|
||||
},
|
||||
"deletecmd": {
|
||||
"name": "deletecmd",
|
||||
"permission": "moderator",
|
||||
"description": "Delete a saved command.",
|
||||
"args": [
|
||||
"cmdname"
|
||||
],
|
||||
"category": "Utility"
|
||||
},
|
||||
"execute": {
|
||||
"name": "execute",
|
||||
"permission": "all",
|
||||
"args": [
|
||||
"cmdname"
|
||||
],
|
||||
"description": "Execute saved commands.",
|
||||
"category": "Utility",
|
||||
"response": {
|
||||
"not_found": "This command could not be found."
|
||||
}
|
||||
}
|
||||
},
|
||||
"music": {
|
||||
"play": {
|
||||
"name": "play",
|
||||
"permission": "all",
|
||||
"args": [
|
||||
"url"
|
||||
],
|
||||
"description": "Adds the url to the YouTube video/playlist into the queue.",
|
||||
"category": "Music",
|
||||
"response": {
|
||||
"success": "Added Song/Playlist to the queue.",
|
||||
"failure": "Failed adding Song/Playlist to the queue.",
|
||||
"url_invalid": "This is not a valid url!",
|
||||
"no_url": "I need an url to a video to play!",
|
||||
"no_voicechannel": "You need to join a voicechannel to do that!"
|
||||
}
|
||||
},
|
||||
"playnext": {
|
||||
"name": "playnext",
|
||||
"permission": "all",
|
||||
"args": [
|
||||
"url"
|
||||
],
|
||||
"description": "Adds the url to the YouTube video as next song to the queue.",
|
||||
"category": "Music",
|
||||
"response": {
|
||||
"success": "Added Song as next Song to the queue.",
|
||||
"failure": "Failed adding Song as next Song to the queue.",
|
||||
"url_invalid": "This is not a valid url!",
|
||||
"no_url": "I need an url to a video to play",
|
||||
"no_voicechannel": "You need to join a voicechannel to do that!"
|
||||
}
|
||||
},
|
||||
"join": {
|
||||
"name": "join",
|
||||
"permission": "all",
|
||||
"description": "Joins the VC you are in.",
|
||||
"category": "Music",
|
||||
"response": {
|
||||
"not_connected": "You are not connected to a Voice Channel."
|
||||
}
|
||||
},
|
||||
"stop": {
|
||||
"name": "stop",
|
||||
"permission": "dj",
|
||||
"description": "Stops playing music and leaves.",
|
||||
"category": "Music",
|
||||
"response": {
|
||||
"success": "Stopping now...",
|
||||
"not_playing": "I'm not playing music at the moment."
|
||||
}
|
||||
},
|
||||
"pause": {
|
||||
"name": "pause",
|
||||
"permission": "all",
|
||||
"description": "Pauses playing.",
|
||||
"category": "Music",
|
||||
"response": {
|
||||
"success": "Pausing playback.",
|
||||
"not_playing": "I'm not playing music at the moment."
|
||||
}
|
||||
},
|
||||
"resume": {
|
||||
"name": "resume",
|
||||
"permission": "all",
|
||||
"description": "Resumes playing.",
|
||||
"category": "Music",
|
||||
"response": {
|
||||
"success": "Resuming playback.",
|
||||
"not_playing": "I'm not playing music at the moment."
|
||||
}
|
||||
},
|
||||
"skip": {
|
||||
"name": "skip",
|
||||
"permission": "dj",
|
||||
"description": "Skips the current song.",
|
||||
"category": "Music",
|
||||
"response": {
|
||||
"success": "Skipping to the next song.",
|
||||
"not_playing": "I'm not playing music at the moment."
|
||||
}
|
||||
},
|
||||
"clear": {
|
||||
"name": "clear",
|
||||
"permission": "dj",
|
||||
"description": "Clears the queue.",
|
||||
"category": "Music",
|
||||
"response": {
|
||||
"success": "The Queue has been cleared."
|
||||
}
|
||||
},
|
||||
"playlist": {
|
||||
"name": "queue",
|
||||
"permission": "all",
|
||||
"description": "Shows the next ten songs.",
|
||||
"category": "Music"
|
||||
},
|
||||
"current": {
|
||||
"name": "np",
|
||||
"permission": "all",
|
||||
"description": "Shows the currently playing song.",
|
||||
"category": "Music",
|
||||
"response": {
|
||||
"not_playing": "I'm not playing music at the moment."
|
||||
}
|
||||
},
|
||||
"shuffle": {
|
||||
"name": "shuffle",
|
||||
"permission": "all",
|
||||
"description": "Shuffles the playlist.",
|
||||
"category": "Music",
|
||||
"response": {
|
||||
"success": "The Queue has been shuffled."
|
||||
}
|
||||
},
|
||||
"repeat": {
|
||||
"name": "repeat",
|
||||
"permission": "all",
|
||||
"description": "Toggle listening on repeat.",
|
||||
"category": "Music",
|
||||
"response": {
|
||||
"repeat_true": "Listening on repeat now!",
|
||||
"repeat_false": "Not listening on repeat anymore."
|
||||
}
|
||||
},
|
||||
"savemedia": {
|
||||
"name": "savemedia",
|
||||
"permission": "dj",
|
||||
"args": [
|
||||
"url"
|
||||
],
|
||||
"description": "Saves the YouTube song/playlist with a specific name",
|
||||
"category": "Music"
|
||||
},
|
||||
"savedmedia": {
|
||||
"name": "savedmedia",
|
||||
"permission": "all",
|
||||
"description": "Prints out all saved playlists and songs.",
|
||||
"category": "Music",
|
||||
"response": {
|
||||
"no_saved": "There are no saved songs/playlists :("
|
||||
}
|
||||
},
|
||||
"deletemedia": {
|
||||
"name": "deletemedia",
|
||||
"permission": "dj",
|
||||
"description": "Deletes a saved media entry. ~deletemedia [name]",
|
||||
"category": "Music",
|
||||
"response": {
|
||||
"no_name": "You must provide a name for the media that shall be deleted."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,282 @@
|
||||
const Discord = require('discord.js'),
|
||||
yaml = require('js-yaml'),
|
||||
fsx = require('fs-extra'),
|
||||
logging = require('./logging'),
|
||||
config = require('../config.json'),
|
||||
utils = require('./utils');
|
||||
|
||||
const scopes = {
|
||||
'Global': 0,
|
||||
'User': 1,
|
||||
'Guild': 2
|
||||
};
|
||||
|
||||
class Answer {
|
||||
|
||||
/**
|
||||
* Creates an new Answer object with _func as answer logic.
|
||||
* @param func
|
||||
*/
|
||||
constructor(func) {
|
||||
this._func = func;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the answer string for the answer object.
|
||||
* If the logic function returns a promise all nested promises get resolved.
|
||||
* @param message
|
||||
* @param kwargs
|
||||
* @param argsString
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async evaluate(message, kwargs, argsString) {
|
||||
let result = this._func(message, kwargs, argsString);
|
||||
if (result instanceof Promise)
|
||||
return await utils.resolveNestedPromise(result);
|
||||
else
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class Command {
|
||||
|
||||
/**
|
||||
* Creates a new command object where the answer function needs
|
||||
* to be implemented for it to work.
|
||||
* @param template {JSON:{}}
|
||||
* @param answer {Answer}
|
||||
*/
|
||||
constructor(template, answer) {
|
||||
this.name = template.name;
|
||||
this.prefix = '';
|
||||
this.description = template.description;
|
||||
this.args = template.args || [];
|
||||
this.permission = template.permission;
|
||||
this.category = template.category || 'Other';
|
||||
this.usage = template.usage ||
|
||||
`${this.name} [${this.args.join('][')}]`.replace('[]', '');
|
||||
this.answObj = answer;
|
||||
if (!template.name)
|
||||
throw new Error("Template doesn't define a name.");
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is meant to be replaced by logic.
|
||||
* @abstract
|
||||
* @param message {Discord.Message}
|
||||
* @param kwargs {JSON}
|
||||
* @param argsString {String} The raw argument string.
|
||||
* @returns {String}
|
||||
*/
|
||||
async answer(message, kwargs, argsString) {
|
||||
return await this.answObj.evaluate(message, kwargs, argsString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns rich help embed for this command.
|
||||
* @returns {*|Discord.RichEmbed}
|
||||
*/
|
||||
get help() {
|
||||
return new ExtendedRichEmbed(`Help for ${this.name}`)
|
||||
.addFields({
|
||||
'Usage': `\`${this.prefix}${this.usage}\``,
|
||||
'Description': this.description,
|
||||
'Permission Role': this.permission
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CommandHandler {
|
||||
|
||||
/**
|
||||
* Initializes the CommandHandler
|
||||
* @param prefix {String} The prefix of all commands.
|
||||
* @param scope {Number} A scope from the CommandScopes (scopes)
|
||||
*/
|
||||
constructor(prefix, scope) {
|
||||
this.prefix = prefix;
|
||||
this.scope = scope;
|
||||
this.commands = {};
|
||||
this._logger = new logging.Logger(`${this.constructor.name}@${Object.keys(scopes)[this.scope]}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the command and responds to the message.
|
||||
* @param commandMessage {String}
|
||||
* @param message {Discord.Message}
|
||||
* @returns {Boolean | String | Promise<String|Discord.RichEmbed>}
|
||||
*/
|
||||
handleCommand(commandMessage, message) {
|
||||
this._logger.debug(`Handling command ${commandMessage}`);
|
||||
let commandName = commandMessage.match(/^\S+/);
|
||||
if (commandName.length > 0)
|
||||
commandName = commandName[0];
|
||||
this._logger.silly(`Command name is ${commandName}`);
|
||||
if (commandName.indexOf(this.prefix) >= 0) {
|
||||
commandName = commandName.replace(this.prefix, '');
|
||||
let argsString = commandMessage.replace(/^\S+/, '');
|
||||
argsString = argsString
|
||||
.replace(/^\s+/, '') // leading whitespace
|
||||
.replace(/\s+$/, ''); // trailing whitespace
|
||||
let args = argsString.match(/\S+/g);
|
||||
let command = this.commands[commandName];
|
||||
if (command && this._checkPermission(message, command.permission)) {
|
||||
this._logger.silly(`Permission ${command.permission} granted for command ${commandName}`);
|
||||
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) {
|
||||
this._logger.silly(`Permission ${command.permission} denied for command ${commandName}`);
|
||||
return "You don't have permission for this command";
|
||||
} else {
|
||||
this._logger.silly(`Command ${commandName} not found.`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
this._logger.silly(`No prefix found in command ${commandName}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the command so that the handler can use it.
|
||||
* @param command {Command}
|
||||
*/
|
||||
registerCommand(command) {
|
||||
command.prefix = this.prefix;
|
||||
this.commands[command.name] = command;
|
||||
this._logger.debug(`Registered ${command.name} on handler`);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
*/
|
||||
class CommandModule {
|
||||
|
||||
/**
|
||||
* Initializes a CommandModule instance.
|
||||
* @param scope
|
||||
*/
|
||||
constructor(scope) {
|
||||
this.scope = scope;
|
||||
this._logger = new logging.Logger(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a template for the object property templateFile or the given argument file.
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
async _loadTemplate(file) {
|
||||
let templateString = await fsx.readFile(this.templateFile || file, {encoding: 'utf-8'});
|
||||
this._logger.silly(`Loaded Template file ${this.templateFile || file}`);
|
||||
this.template = yaml.safeLoad(templateString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registering commands after loading a template
|
||||
* @param commandHandler {CommandHandler}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async register(commandHandler) { // eslint-disable-line no-unused-vars
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class ExtendedRichEmbed extends Discord.RichEmbed {
|
||||
|
||||
/**
|
||||
* Constructor that automatically set's the Title and Timestamp.
|
||||
* @param title {String}
|
||||
*/
|
||||
constructor(title) {
|
||||
super();
|
||||
this.setTitle(title);
|
||||
this.setTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Field when a name is given or adds a blank Field otherwise
|
||||
* @param name {String}
|
||||
* @param content {String}
|
||||
* @returns {ExtendedRichEmbed}
|
||||
*/
|
||||
addNonemptyField(name, content) {
|
||||
if (name && name.length > 0 && content && content.length > 0)
|
||||
this.addField(name, content);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the fields defined in the fields JSON
|
||||
* @param fields {JSON}
|
||||
* @returns {ExtendedRichEmbed}
|
||||
*/
|
||||
addFields(fields) {
|
||||
for (let [name, value] of Object.entries(fields))
|
||||
this.addNonemptyField(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description by shortening the value string to a fitting length for discord.
|
||||
* @param value
|
||||
*/
|
||||
setDescription(value) {
|
||||
let croppedValue = value;
|
||||
if (value.substring)
|
||||
croppedValue = value.substring(0, 1024);
|
||||
if (croppedValue.length < value.length)
|
||||
croppedValue = croppedValue.replace(/\n.*$/g, '');
|
||||
super.setDescription(croppedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field by shortening the value stirn to a fitting length for discord.
|
||||
* @param name
|
||||
* @param value
|
||||
*/
|
||||
addField(name, value) {
|
||||
let croppedValue = value;
|
||||
if (value.substring)
|
||||
croppedValue = value.substring(0, 1024);
|
||||
if (croppedValue.length < value.length)
|
||||
croppedValue = croppedValue.replace(/\n.*$/g, '');
|
||||
super.addField(name, croppedValue);
|
||||
}
|
||||
}
|
||||
|
||||
// -- exports -- //
|
||||
|
||||
Object.assign(exports, {
|
||||
Answer: Answer,
|
||||
Command: Command,
|
||||
CommandHandler: CommandHandler,
|
||||
CommandModule: CommandModule,
|
||||
ExtendedRichEmbed: ExtendedRichEmbed,
|
||||
CommandScopes: scopes
|
||||
});
|
@ -0,0 +1,201 @@
|
||||
const cmdLib = require('./CommandLib'),
|
||||
config = require('../config.json'),
|
||||
Discord = require('discord.js'),
|
||||
logging = require('./logging'),
|
||||
promiseWaterfall = require('promise-waterfall');
|
||||
|
||||
/* eslint no-useless-escape: 0 */
|
||||
|
||||
class MessageHandler {
|
||||
|
||||
/**
|
||||
* Message Handler to handle messages. Listens on the
|
||||
* _client message event.
|
||||
* @param client {Discord.Client}
|
||||
*/
|
||||
constructor (client) {
|
||||
this.discordClient = client;
|
||||
this.logger = new logging.Logger(this);
|
||||
this.globalCmdHandler = new cmdLib.CommandHandler(config.prefix,
|
||||
cmdLib.CommandScopes.Global);
|
||||
this.userCmdHandler = new cmdLib.CommandHandler(config.prefix,
|
||||
cmdLib.CommandScopes.User);
|
||||
this.guildCmdHandler = new cmdLib.CommandHandler(config.prefix,
|
||||
cmdLib.CommandScopes.Guild);
|
||||
this.userRates = {};
|
||||
this._registerEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the handler fitting the scope
|
||||
* @param scope {Number}
|
||||
* @returns {cmdLib.CommandHandler}
|
||||
*/
|
||||
getHandler(scope) {
|
||||
switch (scope) {
|
||||
case cmdLib.CommandScopes.Global:
|
||||
return this.globalCmdHandler;
|
||||
case cmdLib.CommandScopes.Guild:
|
||||
return this.guildCmdHandler;
|
||||
case cmdLib.CommandScopes.User:
|
||||
return this.userCmdHandler;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a command module to a command handler.
|
||||
* @param CommandModule {cmdLib.CommandModule}
|
||||
* @param options {Object} Options passed to the module constructor.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async registerCommandModule(CommandModule, options) {
|
||||
this.logger.info(`Registering command module ${CommandModule.name}...`);
|
||||
let cmdModule = new CommandModule(options);
|
||||
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
|
||||
*/
|
||||
_registerEvents() {
|
||||
this.logger.debug('Registering message event...');
|
||||
this.discordClient.on('message', async (msg) => {
|
||||
this.logger.verbose(`<${msg.guild? msg.channel.name+'@'+msg.guild.name : 'PRIVATE'}> ${msg.author.tag}: ${msg.content}`);
|
||||
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);
|
||||
this.logger.debug('Executed command sequence');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the syntax of a message into a command array.
|
||||
* @param message
|
||||
* @returns {Array<Array<String>>}
|
||||
* @private
|
||||
*/
|
||||
_parseSyntax(message) {
|
||||
this.logger.silly('Parsing command sequence...');
|
||||
let commandSequence = [];
|
||||
let content = message.content;
|
||||
let strings = content.match(/".+?"/g) || [];
|
||||
|
||||
for (let string of strings)
|
||||
content = content.replace(string, string // escape all special chars
|
||||
.replace(/;/g, '\\;')
|
||||
.replace(/&/g, '\\&'));
|
||||
let independentCommands = content // independent command sequende with ;
|
||||
.split(/(?<!\\);/g)
|
||||
.map(x => x.replace(/^ +/, ''));
|
||||
for (let indepCommand of independentCommands)
|
||||
commandSequence.push(indepCommand
|
||||
.split(/(?<!\\)&&/g) // dependend sequence with && (like unix)
|
||||
.map(x => x.replace(/^ +/, ''))
|
||||
);
|
||||
return commandSequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a sequence of commands
|
||||
*/
|
||||
async executeCommandSequence(cmdSequence, message) {
|
||||
this.logger.silly(`Executing command sequence: ${JSON.stringify(cmdSequence)} ...`);
|
||||
let scopeCmdHandler = this.getScopeHandler(message);
|
||||
await Promise.all(cmdSequence.map((sq) => promiseWaterfall(sq.map((cmd) => async () => {
|
||||
try {
|
||||
this.logger.silly(`Executing command ${cmd}`);
|
||||
let globalResult = await this.globalCmdHandler.handleCommand(cmd, message);
|
||||
let scopeResult = await scopeCmdHandler.handleCommand(cmd, message);
|
||||
this.logger.silly(`globalResult: ${globalResult}, scopeResult: ${scopeResult}`);
|
||||
|
||||
if (scopeResult)
|
||||
this._answerMessage(message, scopeResult);
|
||||
else if (globalResult)
|
||||
this._answerMessage(message, globalResult);
|
||||
} catch (err) {
|
||||
this.logger.verbose(err.message);
|
||||
this.logger.silly(err.stack);
|
||||
}
|
||||
}))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns two commandHandlers for the messages scope.
|
||||
* @param message
|
||||
* @private
|
||||
*/
|
||||
getScopeHandler(message) {
|
||||
if (message && message.guild)
|
||||
return this.guildCmdHandler;
|
||||
else
|
||||
return this.userCmdHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Answers
|
||||
* @param message {Discord.Message}
|
||||
* @param answer {String | Discord.RichEmbed}
|
||||
* @private
|
||||
*/
|
||||
_answerMessage(message, answer) {
|
||||
this.logger.debug(`Sending answer ${answer}`);
|
||||
if (answer)
|
||||
if (answer instanceof Discord.RichEmbed)
|
||||
message.channel.send('', answer);
|
||||
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, {
|
||||
MessageHandler: MessageHandler
|
||||
});
|
@ -0,0 +1,177 @@
|
||||
const fetch = require('node-fetch'),
|
||||
fsx = require('fs-extra'),
|
||||
yaml = require('js-yaml'),
|
||||
queryPath = './lib/api/graphql/AnilistApi',
|
||||
alApiEndpoint = 'https://graphql.anilist.co';
|
||||
|
||||
async function getFragments() {
|
||||
let fragments = await fsx.readFile(`${queryPath}/Fragments.yaml`, {encoding: 'utf-8'});
|
||||
return yaml.safeLoad(fragments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a graphql query read from a file from a configured path.
|
||||
* @param name
|
||||
* @returns {Promise<String>}
|
||||
*/
|
||||
async function getGraphqlQuery(name) {
|
||||
let query = await fsx.readFile(`${queryPath}/${name}.gql`, {encoding: 'utf-8'});
|
||||
let fragments = await getFragments();
|
||||
for (let [key, value] of Object.entries(fragments))
|
||||
if (query.includes(`...${key}`))
|
||||
query += '\n' + value;
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post a query read from a file to the configured graphql endpoint and return the data.
|
||||
* @param queryName
|
||||
* @param queryVariables
|
||||
* @returns {Promise<JSON>}
|
||||
*/
|
||||
function postGraphqlQuery(queryName, queryVariables) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
fetch(alApiEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: (await getGraphqlQuery(queryName)),
|
||||
variables: queryVariables
|
||||
})
|
||||
}).then(async (response) => {
|
||||
let json = await response.json();
|
||||
return response.ok ? json: Promise.reject(json);
|
||||
}).then((data) => resolve(data.data)).catch((err) => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an anime by id.
|
||||
* @param id {Number}
|
||||
* @param withStaff {Boolean} Include Staff information?
|
||||
* @param withMetadata {Boolean} Include Metadata?
|
||||
* @returns {Promise<JSON>}
|
||||
*/
|
||||
async function getAnimeById(id, withStaff, withMoreData) {
|
||||
let data = await postGraphqlQuery('AnimeQuery',
|
||||
{id: id, withStaff: withStaff, withMoreData: withMoreData});
|
||||
if (data && data.Media)
|
||||
return data.Media;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a manga by id.
|
||||
* @param id {Number}
|
||||
* @param withStaff {Boolean} Include Staff information?
|
||||
* @param withMoreData {Boolean} Include Metadata?
|
||||
* @returns {Promise<JSON>}
|
||||
*/
|
||||
async function getMangaById(id, withStaff, withMoreData) {
|
||||
let data = await postGraphqlQuery('MangaQuery',
|
||||
{id: id, withStaff: withStaff, withMoreData: withMoreData});
|
||||
if (data && data.Media)
|
||||
return data.Media;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a staff member by id.
|
||||
* @param id {Number}
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async function getStaffById(id) {
|
||||
let data = await postGraphqlQuery('StaffQuery', {id: id});
|
||||
if (data && data.Staff)
|
||||
return data.Staff;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a character by id.
|
||||
* @param id {Number}
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async function getCharacterById(id) {
|
||||
let data = await postGraphqlQuery('CharacterQuery', {id: id});
|
||||
if (data && data.Character)
|
||||
return data.Character;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an anime by name and get it by id.
|
||||
* @param name {String}
|
||||
* @param withStaff {Boolean} Include Staff information?
|
||||
* @param withMoreData {Boolean} Include Metadata?
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async function searchAnimeByName(name, withStaff, withMoreData) {
|
||||
let data = await postGraphqlQuery('AnimeQuery',
|
||||
{name: name, withStaff: withStaff, withMoreData: withMoreData});
|
||||
if (data && data.Media)
|
||||
return data.Media;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a manga by name and get it by id.
|
||||
* @param name {String}
|
||||
* @param withStaff {Boolean} Include Staff information?
|
||||
* @param withMoreData {Boolean} Include Metadata?
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async function searchMangaByName(name, withStaff, withMoreData) {
|
||||
let data = await postGraphqlQuery('MangaQuery',
|
||||
{name: name, withStaff: withStaff, withMoreData: withMoreData});
|
||||
if (data && data.Media)
|
||||
return data.Media;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a staff member by name and get information.
|
||||
* @param name {String} The name of the staff member
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async function searchStaffByName(name) {
|
||||
let data = await postGraphqlQuery('StaffQuery', {name: name});
|
||||
if (data && data.Staff)
|
||||
return data.Staff;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seach for a character by name and get information.
|
||||
* @param name {String} Character Name
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async function searchCharacterByName(name) {
|
||||
let data = await postGraphqlQuery('CharacterQuery', {name: name});
|
||||
if (data && data.Character)
|
||||
return data.Character;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
// exports
|
||||
Object.assign(exports, {
|
||||
getAnimeById: getAnimeById,
|
||||
getMangaById: getMangaById,
|
||||
getStaffById: getStaffById,
|
||||
getCharacterById: getCharacterById,
|
||||
searchAnimeByName: searchAnimeByName,
|
||||
searchMangaByName: searchMangaByName,
|
||||
searchStaffByName: searchStaffByName,
|
||||
searchCharacterByName: searchCharacterByName
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
query AnimeData($name: String, $id: Int, $withStaff: Boolean = false, $withMoreData: Boolean = true) {
|
||||
Media (id: $id, search: $name, type: ANIME) {
|
||||
...mediaMetadata
|
||||
...mediaAdditionalMetadata @include(if: $withMoreData)
|
||||
...staffFields @include(if: $withStaff)
|
||||
season @include(if: $withMoreData)
|
||||
episodes @include(if: $withMoreData)
|
||||
duration @include(if: $withMoreData)
|
||||
studios(isMain: true) @include(if: $withMoreData) {
|
||||
studioList: nodes {
|
||||
id
|
||||
name
|
||||
siteUrl
|
||||
}
|
||||
}
|
||||
nextAiringEpisode @include(if: $withMoreData) {
|
||||
id
|
||||
airingAt
|
||||
episode
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
query ($name: String, $id: Int) {
|
||||
Character(search: $name, id: $id) {
|
||||
id
|
||||
name {
|
||||
first
|
||||
last
|
||||
native
|
||||
}
|
||||
description
|
||||
image {
|
||||
large
|
||||
medium
|
||||
}
|
||||
siteUrl
|
||||
media {
|
||||
edges {
|
||||
characterRole
|
||||
voiceActors(language: JAPANESE) {
|
||||
...staffMetadata
|
||||
}
|
||||
node {
|
||||
...mediaMetadata
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
mediaMetadata: |
|
||||
fragment mediaMetadata on Media {
|
||||
id
|
||||
siteUrl
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
large
|
||||
medium
|
||||
color
|
||||
}
|
||||
}
|
||||
|
||||
mediaAdditionalMetadata: |
|
||||
fragment mediaAdditionalMetadata on Media {
|
||||
status
|
||||
description(asHtml: false)
|
||||
format
|
||||
genres
|
||||
averageScore
|
||||
favourites
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
}
|
||||
|
||||
staffFields: |
|
||||
fragment staffFields on Media {
|
||||
staff {
|
||||
edges {
|
||||
node {
|
||||
...staffMetadata
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
staffMetadata: |
|
||||
fragment staffMetadata on Staff {
|
||||
id
|
||||
name {
|
||||
first
|
||||
last
|
||||
native
|
||||
}
|
||||
image {
|
||||
large
|
||||
medium
|
||||
}
|
||||
language
|
||||
siteUrl
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
query MangaData($name: String, $id: Int, $withStaff: Boolean = false, $withMoreData: Boolean = true) {
|
||||
Media (id: $id, search: $name, type: MANGA) {
|
||||
...mediaMetadata
|
||||
...mediaAdditionalMetadata @include(if: $withMoreData)
|
||||
...staffFields @include(if: $withStaff)
|
||||
chapters @include(if: $withMoreData)
|
||||
volumes @include(if: $withMoreData)
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
query StaffData($name: String, $id: Int) {
|
||||
Staff(id: $id, search: $name) {
|
||||
id
|
||||
name {
|
||||
first
|
||||
last
|
||||
native
|
||||
}
|
||||
language
|
||||
image {
|
||||
large
|
||||
medium
|
||||
}
|
||||
staffMedia(page: 0, perPage: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
siteUrl
|
||||
}
|
||||
characters {
|
||||
id
|
||||
name {
|
||||
first
|
||||
last
|
||||
}
|
||||
siteUrl
|
||||
image {
|
||||
large
|
||||
medium
|
||||
}
|
||||
}
|
||||
staffRole
|
||||
}
|
||||
}
|
||||
characters(page: 0, perPage: 10) {
|
||||
nodes {
|
||||
id
|
||||
name {
|
||||
first
|
||||
last
|
||||
}
|
||||
siteUrl
|
||||
image {
|
||||
large
|
||||
medium
|
||||
}
|
||||
}
|
||||
}
|
||||
description(asHtml: false)
|
||||
siteUrl
|
||||
}
|
||||
}
|
@ -1,325 +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');
|
||||
|
||||
let logger = require('winston'),
|
||||
globCommands = {};
|
||||
|
||||
/**
|
||||
* @type {Servant}
|
||||
*/
|
||||
exports.Servant = class {
|
||||
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 helpEmbed = new Discord.RichEmbed()
|
||||
.setTitle('Commands')
|
||||
.setDescription('Create a sequence of commands with `;` (semicolon).')
|
||||
.setTimestamp();
|
||||
let globHelp = '';
|
||||
Object.entries(globCommands).sort().forEach(([key, value]) => {
|
||||
if (value.role !== 'owner' || checkPermission(msg, 'owner'))
|
||||
globHelp += `\`${key}\` \t`;
|
||||
|
||||
});
|
||||
helpEmbed.addField('Global Commands', globHelp);
|
||||
let categories = [];
|
||||
let catCommands = {};
|
||||
Object.entries(this.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;
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
*/
|
||||
exports.setLogger = function (newLogger) {
|
||||
logger = newLogger;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a global command that can be executed in every channel.
|
||||
* @param prefix
|
||||
* @param template
|
||||
* @param call
|
||||
*/
|
||||
exports.createGlobalCommand = function (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
|
||||
*/
|
||||
exports.init = function (prefix) {
|
||||
logger.verbose("Created help command");
|
||||
this.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 {
|
||||
let helpEmbed = new Discord.RichEmbed()
|
||||
.setTitle('Global Commands')
|
||||
.setDescription('Create a sequence of commands with `;` (semicolon).')
|
||||
.setTimestamp();
|
||||
let description = '';
|
||||
Object.entries(globCommands).sort().forEach(([key, value]) => {
|
||||
if (value.role === 'owner' && checkPermission(msg, 'owner'))
|
||||
description += `\`${key}\` \t`;
|
||||
else if (value.role !== 'owner')
|
||||
description += `\`${key}\` \t`;
|
||||
|
||||
});
|
||||
helpEmbed.setFooter(prefix + 'help [command] for more info to each command');
|
||||
helpEmbed.setDescription(description);
|
||||
return helpEmbed;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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!';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
anime_search:
|
||||
name: alAnime
|
||||
permission: all
|
||||
usage: alAnime [search query]
|
||||
description: >
|
||||
Searches [AniList.co](https://anilist.co) for the anime *title* or *id* and returns information about
|
||||
it if there is a result. The staff members are not included because the message would grow too big.
|
||||
category: AniList
|
||||
response:
|
||||
not_found: >
|
||||
I couldn't find the anime you were searching for :(
|
||||
|
||||
anime_staff_search:
|
||||
name: alAnimeStaff
|
||||
permission: all
|
||||
usage: alAnimeStaff [search query]
|
||||
description: >
|
||||
Searches [AniList.co](https://anilist.co) for the anime *title* or *id* and returns all staff members.
|
||||
category: AniList
|
||||
response:
|
||||
not_found: >
|
||||
I couldn't find the anime you were searching for :(
|
||||
|
||||
manga_search:
|
||||
name: alManga
|
||||
permission: all
|
||||
usage: alManga [search query]
|
||||
description: >
|
||||
Searches [AniList.co](https://anilist.co) for the manga *title* or *id* and returns information about
|
||||
it if there is a result.
|
||||
category: AniList
|
||||
response:
|
||||
not_found: >
|
||||
I couldn't find the manga you were searching for :(
|
||||
|
||||
staff_search:
|
||||
name: alStaff
|
||||
permission: all
|
||||
usage: alStaff [search query]
|
||||
description: >
|
||||
Searches [AniList.co](https://anilist.co) for the staff member *name* or *id* and returns information about
|
||||
the member aswell as roles in media.
|
||||
category: AniList
|
||||
response:
|
||||
not_found: >
|
||||
I couldn't find the staff member you were searching for :(
|
||||
|
||||
character_search:
|
||||
name: alCharacter
|
||||
permission: all
|
||||
usage: alCharacter [search query]
|
||||
description: >
|
||||
Searches [AniList.co](https://anilist.co) for the character *name* or *id* and returns information about
|
||||
the character aswell as media roles.
|
||||
category: AniList
|
||||
response:
|
||||
not_found: >
|
||||
I couldn't find the character member you were searching for :(
|
@ -0,0 +1,306 @@
|
||||
const cmdLib = require('../../CommandLib'),
|
||||
anilistApi = require('../../api/AnilistApi'),
|
||||
location = './lib/commands/AnilistApiCommands';
|
||||
|
||||
/**
|
||||
* The AniList commands are all commands that interact with the anilist api.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a string for a name.
|
||||
* @param nameNode {String} The AniList name node in format {first, last, native}
|
||||
*/
|
||||
function getNameString(nameNode) {
|
||||
let name = '';
|
||||
if (nameNode.first)
|
||||
name = nameNode.first;
|
||||
if (nameNode.last)
|
||||
name += ' ' + nameNode.last;
|
||||
if (name.length === 0)
|
||||
name = nameNode.native;
|
||||
return name;
|
||||
}
|
||||
|
||||
class RichMediaInfo extends cmdLib.ExtendedRichEmbed {
|
||||
|
||||
/**
|
||||
* Creates a rich embed with info for AniListApi Media.
|
||||
* @param mediaInfo
|
||||
*/
|
||||
constructor(mediaInfo) {
|
||||
super(mediaInfo.title.romaji);
|
||||
this.setThumbnail(mediaInfo.coverImage.large || mediaInfo.coverImage.medium)
|
||||
.setURL(mediaInfo.siteUrl)
|
||||
.setColor(mediaInfo.coverImage.color)
|
||||
.setFooter('Powered by AniList.co');
|
||||
if (mediaInfo.description)
|
||||
this.setDescription(mediaInfo.description
|
||||
.replace(/<\/?.*?>/g, '')
|
||||
.replace(/~!.*?!~/g, '')
|
||||
.replace(/\n\n\n/g, ''));
|
||||
let fields = {
|
||||
'Genres': mediaInfo.genres? mediaInfo.genres.join(' ') : null,
|
||||
'Studios': mediaInfo.studios? mediaInfo.studios.studioList.map(x => `[${x.name}](${x.siteUrl})`) : null,
|
||||
'Scoring': mediaInfo.averageScore? `**AverageScore**: ${mediaInfo.averageScore}\n**Favourites:** ${mediaInfo.favourites}`: null,
|
||||
'Episodes': mediaInfo.episodes,
|
||||
'Volumes': mediaInfo.volumes,
|
||||
'Chapters': mediaInfo.chapters,
|
||||
'Duration': null,
|
||||
'Season': mediaInfo.season,
|
||||
'Status': mediaInfo.status,
|
||||
'Format': mediaInfo.format
|
||||
};
|
||||
if (mediaInfo.duration)
|
||||
fields['Episode Duration'] = `${mediaInfo.duration} min`;
|
||||
if (mediaInfo.startDate && mediaInfo.startDate.day)
|
||||
fields['Start Date'] = `${mediaInfo.startDate.day}.${mediaInfo.startDate.month}.${mediaInfo.startDate.year}`;
|
||||
if (mediaInfo.nextAiringEpisode) {
|
||||
let epInfo = mediaInfo.nextAiringEpisode;
|
||||
fields['Next Episode'] = `**Episode** ${epInfo.episode}\n**Airing at:** ${new Date(epInfo.airingAt * 1000).toUTCString()}`;
|
||||
}
|
||||
if (mediaInfo.endDate && mediaInfo.endDate.day)
|
||||
fields['End Date'] = `${mediaInfo.endDate.day}.${mediaInfo.endDate.month}.${mediaInfo.endDate.year}`;
|
||||
this.addStaffInfo(mediaInfo);
|
||||
this.addFields(fields);
|
||||
}
|
||||
|
||||
addStaffInfo(mediaInfo) {
|
||||
let fields = {};
|
||||
if (mediaInfo.staff && mediaInfo.staff.edges) {
|
||||
let staffContent = mediaInfo.staff.edges.map((x) => {
|
||||
let url = x.node.siteUrl;
|
||||
let name = getNameString(x.node.name);
|
||||
return `**${x.role}:** [${name}](${url})`;
|
||||
});
|
||||
let staffFieldValue = staffContent.join('\n');
|
||||
if (staffFieldValue.length > 1024) {
|
||||
let staffValues = [];
|
||||
let currentValue = '';
|
||||
|
||||
for (let staffLine of staffContent) {
|
||||
let concatValue = currentValue + '\n' + staffLine;
|
||||
if (concatValue.length > 1024) {
|
||||
staffValues.push(currentValue);
|
||||
currentValue = staffLine;
|
||||
} else {
|
||||
currentValue = concatValue;
|
||||
}
|
||||
}
|
||||
staffValues.push(currentValue);
|
||||
for (let i = 0; i < staffValues.length; i++)
|
||||
fields[`Staff part ${i + 1}`] = staffValues[i];
|
||||
} else {
|
||||
fields['Staff'] = staffFieldValue;
|
||||
}
|
||||
}
|
||||
this.addFields(fields);
|
||||
}
|
||||
}
|
||||
|
||||
class RichStaffInfo extends cmdLib.ExtendedRichEmbed {
|
||||
|
||||
/**
|
||||
* A Rich Embed with informatin about an AniList staff member.
|
||||
* @param staffInfo
|
||||
*/
|
||||
constructor(staffInfo) {
|
||||
super(getNameString(staffInfo.name));
|
||||
this.setThumbnail(staffInfo.image.large || staffInfo.image.medium)
|
||||
.setURL(staffInfo.siteUrl);
|
||||
let fields = {
|
||||
'Language': staffInfo.language
|
||||
};
|
||||
if (staffInfo.staffMedia && staffInfo.staffMedia.edges)
|
||||
fields['Staff Media Roles (first 10)'] = staffInfo.staffMedia.edges.map(x => {
|
||||
let node = x.node;
|
||||
let title = node.title.romaji;
|
||||
let url = node.siteUrl;
|
||||
return `[**${title}**](${url}): ${x.staffRole}`;
|
||||
}).join('\n');
|
||||
if (staffInfo.characters && staffInfo.characters.nodes)
|
||||
fields['Staff Character Roles (first 10)'] = staffInfo.characters.nodes.map(x => {
|
||||
let name = getNameString(x.name);
|
||||
let url = x.siteUrl;
|
||||
return `[${name}](${url})`;
|
||||
}).join('\n');
|
||||
|
||||
|
||||
this.addFields(fields);
|
||||
}
|
||||
}
|
||||
|
||||
class RichCharacterInfo extends cmdLib.ExtendedRichEmbed {
|
||||
|
||||
/**
|
||||
* A RichEmbed with information about an AniList character.
|
||||
* @param characterInfo {Object}
|
||||
*/
|
||||
constructor(characterInfo) {
|
||||
super(getNameString(characterInfo.name));
|
||||
this.setURL(characterInfo.siteUrl)
|
||||
.setThumbnail(characterInfo.image.large || characterInfo.image.medium);
|
||||
if (characterInfo.description)
|
||||
this.setDescription(characterInfo.description
|
||||
.replace(/<\/?.*?>/g, '')
|
||||
.replace(/~!.*?!~/g, '')
|
||||
.replace(/\n\n\n/g, ''));
|
||||
if (characterInfo.media && characterInfo.media.edges)
|
||||
this.addNonemptyField(
|
||||
'Media Appeareance',
|
||||
characterInfo.media.edges.map(x => {
|
||||
let media = x.node;
|
||||
let informationString = `**[${media.title.romaji}](${media.siteUrl})**: ${x.characterRole}`;
|
||||
if (x.voiceActors && x.voiceActors.length > 0)
|
||||
informationString += ` voice by ${x.voiceActors.map(y => {
|
||||
return `[${getNameString(y.name)}](${y.siteUrl})`;
|
||||
}).join(', ')}`;
|
||||
return informationString;
|
||||
}).join('\n')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// -- initialize -- //
|
||||
|
||||
/**
|
||||
* Implementing the AniList commands module.
|
||||
*/
|
||||
class AniListCommandModule extends cmdLib.CommandModule {
|
||||
|
||||
constructor() {
|
||||
super(cmdLib.CommandScopes.Global);
|
||||
this.templateFile = location + '/AniListCommandsTemplate.yaml';
|
||||
this.template = null;
|
||||
}
|
||||
|
||||
async register(commandHandler) {
|
||||
await this._loadTemplate();
|
||||
|
||||
let animeSearch = new cmdLib.Command(
|
||||
this.template.anime_search,
|
||||
new cmdLib.Answer(async (m, k, s) => {
|
||||
try {
|
||||
let animeData = {};
|
||||
if (/^\d+$/.test(s))
|
||||
animeData = await anilistApi.getAnimeById(s, false, true);
|
||||
else
|
||||
animeData = await anilistApi.searchAnimeByName(s, false, true);
|
||||
this._logger.silly(`Anime Query returned ${JSON.stringify(animeData)}`);
|
||||
return new RichMediaInfo(animeData);
|
||||
} catch (err) {
|
||||
if (err.message) {
|
||||
this._logger.verbose(err.message);
|
||||
this._logger.silly(err.stack);
|
||||
} else if (err.errors) {
|
||||
this._logger.silly(`Graphql Errors ${JSON.stringify(err.errors)}`);
|
||||
}
|
||||
return this.template.anime_search.response.not_found;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let animeStaffSearch = new cmdLib.Command(
|
||||
this.template.anime_staff_search,
|
||||
new cmdLib.Answer(async (m, k, s) => {
|
||||
try {
|
||||
let animeData = {};
|
||||
if (/^\d+$/.test(s))
|
||||
animeData = await anilistApi.getAnimeById(s, true, false);
|
||||
else
|
||||
animeData = await anilistApi.searchAnimeByName(s, true, false);
|
||||
this._logger.silly(`Anime Query returned ${JSON.stringify(animeData)}`);
|
||||
return new RichMediaInfo(animeData);
|
||||
} catch (err) {
|
||||
if (err.message) {
|
||||
this._logger.verbose(err.message);
|
||||
this._logger.silly(err.stack);
|
||||
} else if (err.errors) {
|
||||
this._logger.silly(`Graphql Errors ${JSON.stringify(err.errors)}`);
|
||||
}
|
||||
return this.template.anime_staff_search.response.not_found;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let mangaSearch = new cmdLib.Command(
|
||||
this.template.manga_search,
|
||||
new cmdLib.Answer(async (m, k, s) => {
|
||||
try {
|
||||
let mangaData = {};
|
||||
if (/^\d+$/.test(s))
|
||||
mangaData = await anilistApi.getMangaById(s, true, true);
|
||||
else
|
||||
mangaData= await anilistApi.searchMangaByName(s, true, true);
|
||||
this._logger.silly(`Manga Query returned ${JSON.stringify(mangaData)}`);
|
||||
return new RichMediaInfo(mangaData);
|
||||
} catch (err) {
|
||||
if (err.message) {
|
||||
this._logger.verbose(err.message);
|
||||
this._logger.silly(err.stack);
|
||||
} else if (err.errors) {
|
||||
this._logger.silly(`Graphql Errors ${JSON.stringify(err.errors)}`);
|
||||
}
|
||||
return this.template.manga_search.response.not_found;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let staffSearch = new cmdLib.Command(
|
||||
this.template.staff_search,
|
||||
new cmdLib.Answer(async (m, k, s) => {
|
||||
try {
|
||||
let staffData = {};
|
||||
if (/^\d+$/.test(s))
|
||||
staffData = await anilistApi.getStaffById(s);
|
||||
else
|
||||
staffData = await anilistApi.searchStaffByName(s);
|
||||
this._logger.silly(`Staff Query returned ${JSON.stringify(staffData)}`);
|
||||
return new RichStaffInfo(staffData);
|
||||
} catch (err) {
|
||||
if (err.message) {
|
||||
this._logger.verbose(err.message);
|
||||
this._logger.silly(err.stack);
|
||||
} else if (err.errors) {
|
||||
this._logger.silly(`Graphql Errors ${JSON.stringify(err.errors)}`);
|
||||
}
|
||||
return this.template.staff_search.response.not_found;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let characterSearch = new cmdLib.Command(
|
||||
this.template.character_search,
|
||||
new cmdLib.Answer(async (m, k, s) => {
|
||||
try {
|
||||
let characterData = {};
|
||||
if (/^\d+$/.test(s))
|
||||
characterData = await anilistApi.getCharacterById(s);
|
||||
else
|
||||
characterData = await anilistApi.searchCharacterByName(s);
|
||||
this._logger.silly(`Character Query returned ${JSON.stringify(characterData)}`);
|
||||
return new RichCharacterInfo(characterData);
|
||||
} catch (err) {
|
||||
if (err.message) {
|
||||
this._logger.verbose(err.message);
|
||||
this._logger.silly(err.stack);
|
||||
} else if (err.errors) {
|
||||
this._logger.silly(`Graphql Errors ${JSON.stringify(err.errors)}`);
|
||||
}
|
||||
return this.template.character_search.response.not_found;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// registering commands
|
||||
commandHandler
|
||||
.registerCommand(animeSearch)
|
||||
.registerCommand(mangaSearch)
|
||||
.registerCommand(staffSearch)
|
||||
.registerCommand(animeStaffSearch)
|
||||
.registerCommand(characterSearch);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(exports, {
|
||||
'module': AniListCommandModule
|
||||
});
|
@ -0,0 +1,44 @@
|
||||
about:
|
||||
name: about
|
||||
description: >
|
||||
Shows information about this Discord Bot.
|
||||
permission: all
|
||||
category: Info
|
||||
response:
|
||||
about_icon: |
|
||||
This icon war created by [blackrose14344](https://www.deviantart.com/blackrose14344).
|
||||
[Original](https://www.deviantart.com/blackrose14344/art/2B-Chi-B-685771489)
|
||||
about_creator: |
|
||||
This bot was created by Trivernis.
|
||||
More about this bot [here](https://github.com/Trivernis/discordbot.js).
|
||||
|
||||
ping:
|
||||
name: ping
|
||||
description: >
|
||||
Answers with the current average ping of the bot.
|
||||
permission: all
|
||||
category: Info
|
||||
|
||||
uptime:
|
||||
name: uptime
|
||||
description: >
|
||||
Answers with the uptime of the bot.
|
||||
permission: all
|
||||
category: Info
|
||||
|
||||
guilds:
|
||||
name: guilds
|
||||
description: >
|
||||
Answers with the number of guilds the bot has joined
|
||||
permission: owner
|
||||
category: Info
|
||||
|
||||
help:
|
||||
name: help
|
||||
description: >
|
||||
Shows help for bot ocmmands.
|
||||
permission: all
|
||||
category: Info
|
||||
embed_color: 0xffffff
|
||||
args:
|
||||
- command
|
@ -0,0 +1,114 @@
|
||||
const cmdLib = require('../../CommandLib'),
|
||||
utils = require('../../utils'),
|
||||
location = './lib/commands/InfoCommands';
|
||||
|
||||
/**
|
||||
* Info commands provide information about the bot. These informations are
|
||||
* not process specific but access the discord _client instance of the bot.
|
||||
*/
|
||||
|
||||
class InfoCommandModule extends cmdLib.CommandModule {
|
||||
|
||||
/**
|
||||
* @param opts {Object} properties:
|
||||
* client - the instance of the discord client.
|
||||
* messageHandler - the instance of the Message Handler
|
||||
*/
|
||||
constructor(opts) {
|
||||
super(cmdLib.CommandScopes.Global);
|
||||
this.templateFile = location + '/InfoCommandsTemplate.yaml';
|
||||
this._client = opts.client;
|
||||
this._messageHandler = opts.messageHandler;
|
||||
}
|
||||
|
||||
_createHelpEmbed(commands, msg, prefix, embedColor = 0xfff) {
|
||||
let helpEmbed = new cmdLib.ExtendedRichEmbed('Commands')
|
||||
.setDescription('Create a sequence of commands with `;` and `&&`.')
|
||||
.setColor(embedColor);
|
||||
let categories = [];
|
||||
let catCommands = {};
|
||||
Object.entries(commands).sort().forEach(([key, value]) => {
|
||||
if (!categories.includes(value.category)) {
|
||||
categories.push(value.category);
|
||||
catCommands[value.category] = `\`${prefix}${key}\` \t`;
|
||||
} else {
|
||||
catCommands[value.category] += `\`${prefix}${key}\` \t`;
|
||||
}
|
||||
});
|
||||
for (let cat of categories)
|
||||
helpEmbed.addField(cat, catCommands[cat]);
|
||||
|
||||
helpEmbed.setFooter(prefix + 'help [command] for more info to each command');
|
||||
this._logger.silly('Created help embed');
|
||||
return helpEmbed;
|
||||
}
|
||||
|
||||
async register(commandHandler) {
|
||||
await this._loadTemplate();
|
||||
|
||||
let about = new cmdLib.Command(
|
||||
this.template.about,
|
||||
new cmdLib.Answer(() => {
|
||||
return new cmdLib.ExtendedRichEmbed('About')
|
||||
.setDescription(this.template.about.response.about_creator)
|
||||
.addField('Icon', this.template.about.response.about_icon);
|
||||
})
|
||||
);
|
||||
|
||||
let ping = new cmdLib.Command(
|
||||
this.template.ping,
|
||||
new cmdLib.Answer(() => {
|
||||
return `Current average ping: \`${this._client.ping} ms\``;
|
||||
})
|
||||
);
|
||||
|
||||
let uptime = new cmdLib.Command(
|
||||
this.template.uptime,
|
||||
new cmdLib.Answer(() => {
|
||||
let uptime = utils.getSplitDuration(this._client.uptime);
|
||||
return new cmdLib.ExtendedRichEmbed('Uptime').setDescription(`
|
||||
**${uptime.days}** days
|
||||
**${uptime.hours}** hours
|
||||
**${uptime.minutes}** minutes
|
||||
**${uptime.seconds}** seconds
|
||||
**${uptime.milliseconds}** milliseconds
|
||||
`).setTitle('Uptime');
|
||||
})
|
||||
);
|
||||
|
||||
let guilds = new cmdLib.Command(
|
||||
this.template.guilds,
|
||||
new cmdLib.Answer(() => {
|
||||
return `Number of guilds: \`${this._client.guilds.size}\``;
|
||||
})
|
||||
);
|
||||
|
||||
let help = new cmdLib.Command(
|
||||
this.template.help,
|
||||
new cmdLib.Answer((m, k) => {
|
||||
let globH = this._messageHandler.globalCmdHandler;
|
||||
let scopeH = this._messageHandler.getScopeHandler(m);
|
||||
if (k.command) {
|
||||
k.command = k.command.replace(globH.prefix, '');
|
||||
let commandInstance = globH.commands[k.command] || scopeH.commands[k.command];
|
||||
return commandInstance.help.setColor(this.template.help.embed_color);
|
||||
} else {
|
||||
let commandObj = {...globH.commands, ...scopeH.commands};
|
||||
return this._createHelpEmbed(commandObj, m, globH.prefix, this.template.help.embed_color);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// register commands
|
||||
commandHandler
|
||||
.registerCommand(about)
|
||||
.registerCommand(ping)
|
||||
.registerCommand(uptime)
|
||||
.registerCommand(guilds)
|
||||
.registerCommand(help);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(exports, {
|
||||
'module': InfoCommandModule
|
||||
});
|
@ -0,0 +1,178 @@
|
||||
play:
|
||||
name: play
|
||||
description: >
|
||||
Adds the url to the YouTube video or YouTube playlist into the queue.
|
||||
permission: all
|
||||
category: Music
|
||||
args:
|
||||
- url
|
||||
response:
|
||||
success: >
|
||||
Added URL to the media queue.
|
||||
failure: >
|
||||
Failed adding the URL to the media queue.
|
||||
url_invalid: >
|
||||
The URL you provided is not a valid YouTube video or Playlist URL.
|
||||
no_url: >
|
||||
You need to provide an URL to a YouTube viceo or Playlist.
|
||||
no_voicechannel: >
|
||||
You need to join a VoiceChannel to request media playback.
|
||||
|
||||
play_next:
|
||||
name: playnext
|
||||
description: >
|
||||
Adds the url to the YouTube video or YouTube playlist into the queue as
|
||||
next playing song.
|
||||
permission: all
|
||||
category: Music
|
||||
args:
|
||||
- url
|
||||
response:
|
||||
success: >
|
||||
Added URL as next media to the media queue.
|
||||
failure: >
|
||||
Failed adding the URL to the media queue.
|
||||
url_invalid: >
|
||||
The URL you provided is not a valid YouTube video or Playlist URL.
|
||||
no_url: >
|
||||
You need to provide an URL to a YouTube viceo or Playlist.
|
||||
no_voicechannel: >
|
||||
You need to join a VoiceChannel to request media playback.
|
||||
|
||||
join:
|
||||
name: join
|
||||
description: >
|
||||
Joins the VoiceChannel you are in.
|
||||
permission: all
|
||||
category: Music
|
||||
response:
|
||||
no_voicechannel: >
|
||||
You need to join a VoiceChannel for me to join.
|
||||
|
||||
stop:
|
||||
name: stop
|
||||
description: >
|
||||
Stops the media playback and leaves the VoiceChannel.
|
||||
permission: all
|
||||
category: Music
|
||||
response:
|
||||
success: >
|
||||
Stopped music playback.
|
||||
not_playing: >
|
||||
I'm not playing music at the moment. What do you want me to stop?
|
||||
|
||||
pause:
|
||||
name: pause
|
||||
description: >
|
||||
Pauses the media playback.
|
||||
permission: all
|
||||
category: Music
|
||||
response:
|
||||
success: >
|
||||
Paused playback.
|
||||
not_playing: >
|
||||
I'm not playing music at the moment.
|
||||
|
||||
resume:
|
||||
name: resume
|
||||
description: >
|
||||
Resumes the media playback.
|
||||
permission: all
|
||||
category: Music
|
||||
response:
|
||||
success: >
|
||||
Resumed playback.
|
||||
not_playing: >
|
||||
I'm not playing music at the moment.
|
||||
|
||||
skip:
|
||||
name: skip
|
||||
description: >
|
||||
Skips the currently playing song.
|
||||
permission: all
|
||||
category: Music
|
||||
response:
|
||||
success: >
|
||||
Skipped to the next song.
|
||||
not_playing: >
|
||||
I'm not playing music at the moment.
|
||||
|
||||
clear:
|
||||
name: clear
|
||||
description: >
|
||||
Clears the media queue.
|
||||
permission: musicPlayer
|
||||
category: Music
|
||||
response:
|
||||
success: >
|
||||
The media queue has been cleared.
|
||||
|
||||
media_queue:
|
||||
name: queue
|
||||
descriptions: >
|
||||
Shows the next ten songs in the media queue.
|
||||
permission: all
|
||||
category: Music
|
||||
|
||||
media_current:
|
||||
name: np
|
||||
description: >
|
||||
Shows the currently playing song.
|
||||
permission: all
|
||||
category: Music
|
||||
response:
|
||||
not_playing: >
|
||||
I'm not playing music at the moment.
|
||||
|
||||
shuffle:
|
||||
name: shuffle
|
||||
description: >
|
||||
Shuffles the media queue
|
||||
permission: all
|
||||
category: Music
|
||||
response:
|
||||
success: >
|
||||
The queue has been shuffled.
|
||||
|
||||
toggle_repeat:
|
||||
name: repeat
|
||||
description: >
|
||||
Toggles listening o repeat.
|
||||
permission: all
|
||||
category: Music
|
||||
response:
|
||||
repeat_true: >
|
||||
Listening on repeat now!
|
||||
repeat_false: >
|
||||
Not listening on repeat anymore.
|
||||
|
||||
save_media:
|
||||
name: savemedia
|
||||
description: >
|
||||
Saves the YouTube URL with a specific name.
|
||||
permission: dj
|
||||
category: Music
|
||||
args:
|
||||
- url
|
||||
usage: savemedia [url] [name...]
|
||||
|
||||
delete_media:
|
||||
name: deletemedia
|
||||
description: >
|
||||
Deletes a saved YouTube URL from saved media.
|
||||
permission: dj
|
||||
category: Music
|
||||
usage: deletemedia [name]
|
||||
response:
|
||||
no_name: >
|
||||
You must provide a name for the media to delete.
|
||||
|
||||
saved_media:
|
||||
name: savedmedia
|
||||
description: >
|
||||
Shows all saved YouTube URLs.
|
||||
permission: all
|
||||
category: Music
|
||||
response:
|
||||
no_saved: >
|
||||
There are no saved YouTube URLs :(
|
@ -0,0 +1,318 @@
|
||||
const cmdLib = require('../../CommandLib'),
|
||||
utils = require('../../utils'),
|
||||
config = require('../../../config'),
|
||||
location = './lib/commands/MusicCommands';
|
||||
|
||||
function 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Music commands provide commands to control the bots music functions.
|
||||
* These commands are for server music functionalities.
|
||||
*/
|
||||
class MusicCommandModule extends cmdLib.CommandModule {
|
||||
|
||||
/**
|
||||
* @param opts {Object} properties:
|
||||
* getGuildHandler - a function to get the guild handler for a guild.
|
||||
*/
|
||||
constructor(opts) {
|
||||
super(cmdLib.CommandScopes.Guild);
|
||||
this.templateFile = location + '/MusicCommandsTemplate.yaml';
|
||||
this._getGuildHandler = opts.getGuildHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to a voice-channel if not connected and plays the url
|
||||
* @param gh {guilding.GuildHandler}
|
||||
* @param vc {Discord.VoiceChannel}
|
||||
* @param url {String} The url to the YouTube media
|
||||
* @param next {Boolean} Should the song be played next
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
async _connectAndPlay(gh, vc, url, next) {
|
||||
if (!gh.musicPlayer.connected) {
|
||||
await gh.musicPlayer.connect(vc);
|
||||
await gh.musicPlayer.playYouTube(url, next);
|
||||
} else {
|
||||
await gh.musicPlayer.playYouTube(url, next);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The play function for the music commands play and playnext
|
||||
* @param m {Discord.Message}
|
||||
* @param k {Object} kwargs
|
||||
* @param s {String} argsString
|
||||
* @param t {Object} template
|
||||
* @param n {Boolean} play next
|
||||
* @returns {Promise<*>}
|
||||
* @private
|
||||
*/
|
||||
async _playFunction(m, k, s, t, n) {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
let vc = gh.musicPlayer.voiceChannel || m.member.voiceChannel;
|
||||
let url = k['url'];
|
||||
if (!vc)
|
||||
return t.response.no_voicechannel;
|
||||
if (!url)
|
||||
return t.response.no_url;
|
||||
if (!utils.YouTube.isValidEntityUrl(url)) {
|
||||
url = s;
|
||||
let row = await gh.db.get('SELECT url FROM playlists WHERE name = ?', [url]);
|
||||
if (!row) {
|
||||
this._logger.debug('Got invalid url for play command.');
|
||||
return t.response.url_invalid;
|
||||
} else {
|
||||
await this._connectAndPlay(gh, vc, row.url, n);
|
||||
return t.response.success;
|
||||
}
|
||||
} else {
|
||||
await this._connectAndPlay(gh, vc, url, n);
|
||||
return t.response.success;
|
||||
}
|
||||
}
|
||||
|
||||
async register(commandHandler) {
|
||||
await this._loadTemplate();
|
||||
|
||||
let play = new cmdLib.Command(
|
||||
this.template.play,
|
||||
new cmdLib.Answer(async (m, k, s) => {
|
||||
return await this._playFunction(m, k, s, this.template.play, false);
|
||||
})
|
||||
);
|
||||
|
||||
let playNext = new cmdLib.Command(
|
||||
this.template.play_next,
|
||||
new cmdLib.Answer(async (m, k, s) => {
|
||||
return await this._playFunction(m, k, s, this.template.play_next, true);
|
||||
})
|
||||
);
|
||||
|
||||
let join = new cmdLib.Command(
|
||||
this.template.join,
|
||||
new cmdLib.Answer(async (m) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
if (m.member.voiceChannel)
|
||||
await gh.musicPlayer.connect(m.member.voiceChannel);
|
||||
else
|
||||
return this.template.join.response.no_voicechannel;
|
||||
})
|
||||
);
|
||||
|
||||
let stop = new cmdLib.Command(
|
||||
this.template.stop,
|
||||
new cmdLib.Answer(async (m) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
let vc = gh.musicPlayer.voiceChannel || m.member.voiceChannel;
|
||||
if (gh.musicPlayer.connected && vc) {
|
||||
let votes = gh.updateCommandVote(stop.name, m.author.tag);
|
||||
let neededVotes = Math.ceil(vc.members.size/2);
|
||||
|
||||
if (neededVotes <= votes.count || checkPermission(m, 'dj')) {
|
||||
this._logger.debug(`Vote passed. ${votes.count} out of ${neededVotes} for stop or permission granted`);
|
||||
gh.musicPlayer.stop();
|
||||
gh.resetCommandVote(stop.name);
|
||||
return this.template.stop.success;
|
||||
} else {
|
||||
this._logger.silly(`Vote count increased. ${votes.count} out of ${neededVotes} for stop`);
|
||||
return `${votes.count} out of ${neededVotes} needed voted to stop.`;
|
||||
}
|
||||
} else {
|
||||
return this.template.stop.not_playing;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let pause = new cmdLib.Command(
|
||||
this.template.pause,
|
||||
new cmdLib.Answer(async (m) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
if (gh.musicPlayer.playing) {
|
||||
gh.musicPlayer.pause();
|
||||
return this.template.pause.response.success;
|
||||
} else {
|
||||
return this.template.pause.response.not_playing;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let resume = new cmdLib.Command(
|
||||
this.template.resume,
|
||||
new cmdLib.Answer(async (m) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
if (gh.musicPlayer.playing) {
|
||||
gh.musicPlayer.resume();
|
||||
return this.template.resume.response.success;
|
||||
} else {
|
||||
return this.template.resume.response.not_playing;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let skip = new cmdLib.Command(
|
||||
this.template.skip,
|
||||
new cmdLib.Answer(async (m) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
let vc = gh.musicPlayer.voiceChannel || m.member.voiceChannel;
|
||||
if (gh.musicPlayer.playing && vc) {
|
||||
let votes = gh.updateCommandVote(skip.name, m.author.tag);
|
||||
let neededVotes = Math.ceil(vc.members.size/2);
|
||||
|
||||
if (neededVotes <= votes.count || checkPermission(m, 'dj')) {
|
||||
this._logger.debug(`Vote passed. ${votes.count} out of ${neededVotes} for skip or permission granted`);
|
||||
gh.musicPlayer.skip();
|
||||
gh.resetCommandVote(skip.name);
|
||||
return this.template.skip.response.success;
|
||||
} else {
|
||||
this._logger.silly(`Vote count increased. ${votes.count} out of ${neededVotes} for skip`);
|
||||
return `${votes.count} out of ${neededVotes} needed voted to skip.`;
|
||||
}
|
||||
} else {
|
||||
return this.template.skip.response.not_playing;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let clear = new cmdLib.Command(
|
||||
this.template.clear,
|
||||
new cmdLib.Answer(async (m) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
gh.musicPlayer.clear();
|
||||
return this.template.clear.response.success;
|
||||
})
|
||||
);
|
||||
|
||||
let mediaQueue = new cmdLib.Command(
|
||||
this.template.media_queue,
|
||||
new cmdLib.Answer(async (m) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
this._logger.debug(`Found ${gh.musicPlayer.queue.length} songs.`);
|
||||
let description = '';
|
||||
|
||||
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.musicPlayer.queue.length} songs in queue`)
|
||||
.setDescription(description);
|
||||
})
|
||||
);
|
||||
|
||||
let mediaCurrent = new cmdLib.Command(
|
||||
this.template.media_current,
|
||||
new cmdLib.Answer(async (m) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
let song = gh.musicPlayer.song;
|
||||
if (song)
|
||||
return new cmdLib.ExtendedRichEmbed('Now playing:')
|
||||
.setDescription(`[${song.title}](${song.url})`)
|
||||
.setImage(utils.YouTube.getVideoThumbnailUrlFromUrl(song.url))
|
||||
.setColor(0x00aaff);
|
||||
else
|
||||
return this.template.media_current.response.not_playing;
|
||||
})
|
||||
);
|
||||
|
||||
let shuffle = new cmdLib.Command(
|
||||
this.template.shuffle,
|
||||
new cmdLib.Answer(async (m) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
gh.musicPlayer.shuffle();
|
||||
return this.template.shuffle.response.success;
|
||||
})
|
||||
);
|
||||
|
||||
let toggleRepeat = new cmdLib.Command(
|
||||
this.template.toggle_repeat,
|
||||
new cmdLib.Answer(async (m) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
gh.musicPlayer.repeat = !gh.musicPlayer.repeat;
|
||||
return gh.musicPlayer.repeat?
|
||||
this.template.toggle_repeat.response.repeat_true :
|
||||
this.template.toggle_repeat.response.repeat_false;
|
||||
})
|
||||
);
|
||||
|
||||
let saveMedia = new cmdLib.Command(
|
||||
this.template.save_media,
|
||||
new cmdLib.Answer(async (m, k, s) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
let saveName = s.replace(k.url + ' ', '');
|
||||
let row = await gh.db
|
||||
.get('SELECT COUNT(*) count FROM playlists WHERE name = ?', [saveName]);
|
||||
if (!row || row.count === 0)
|
||||
await gh.db.run('INSERT INTO playlists (name, url) VALUES (?, ?)',
|
||||
[saveName, k.url]);
|
||||
else
|
||||
await gh.db.run('UPDATE playlists SET url = ? WHERE name = ?',
|
||||
[k.url, saveName]);
|
||||
return `Saved song/playlist as ${saveName}`;
|
||||
})
|
||||
);
|
||||
|
||||
let deleteMedia = new cmdLib.Command(
|
||||
this.template.delete_media,
|
||||
new cmdLib.Answer(async (m, k, s) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
if (!s) {
|
||||
return this.template.delete_media.response.no_name;
|
||||
} else {
|
||||
await gh.db.run('DELETE FROM playlists WHERE name = ?', [s]);
|
||||
return `Deleted ${s} from saved media`;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let savedMedia = new cmdLib.Command(
|
||||
this.template.saved_media,
|
||||
new cmdLib.Answer(async (m) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
let response = '';
|
||||
let rows = await gh.db.all('SELECT name, url FROM playlists');
|
||||
for (let row of rows)
|
||||
response += `[${row.name}](${row.url})\n`;
|
||||
|
||||
if (rows.length === 0)
|
||||
return this.template.saved_media.response.no_saved;
|
||||
else
|
||||
return new cmdLib.ExtendedRichEmbed('Saved Songs and Playlists')
|
||||
.setDescription(response)
|
||||
.setFooter(`Play a saved entry with play [Entryname]`);
|
||||
})
|
||||
);
|
||||
|
||||
// register commands
|
||||
commandHandler
|
||||
.registerCommand(play)
|
||||
.registerCommand(playNext)
|
||||
.registerCommand(join)
|
||||
.registerCommand(stop)
|
||||
.registerCommand(pause)
|
||||
.registerCommand(resume)
|
||||
.registerCommand(skip)
|
||||
.registerCommand(clear)
|
||||
.registerCommand(mediaQueue)
|
||||
.registerCommand(mediaCurrent)
|
||||
.registerCommand(shuffle)
|
||||
.registerCommand(toggleRepeat)
|
||||
.registerCommand(saveMedia)
|
||||
.registerCommand(deleteMedia)
|
||||
.registerCommand(savedMedia);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(exports, {
|
||||
'module': MusicCommandModule
|
||||
});
|
@ -0,0 +1,48 @@
|
||||
save_cmd:
|
||||
name: savecmd
|
||||
description: >
|
||||
Saves a sequence of commands under a new name.
|
||||
permission: moderator
|
||||
category: Server Utility
|
||||
usage: savecmd [cmdname] [cmdsequence]
|
||||
args:
|
||||
- name
|
||||
response:
|
||||
no_recursion: >
|
||||
You are **not** allowed to execute another saved command in this sequence.
|
||||
This is to prevent recursion that could break the bot.
|
||||
sequence_too_many_parallel: >
|
||||
This sequence executes too many commands in parallel.
|
||||
sequence_too_many_serial: >
|
||||
This sequence executes too long serial chains.
|
||||
|
||||
delete_cmd:
|
||||
name: deletecmd
|
||||
description: >
|
||||
Deletes a saved command.
|
||||
permission: moderator
|
||||
category: Server Utility
|
||||
args:
|
||||
- name
|
||||
|
||||
saved_cmd:
|
||||
name: savedcmd
|
||||
description: >
|
||||
Lists all saved commands.
|
||||
category: Server Utility
|
||||
permission: all
|
||||
response:
|
||||
no_commands: >
|
||||
There are no saved commands.
|
||||
|
||||
execute:
|
||||
name: execute
|
||||
description: >
|
||||
Executes a saved command.
|
||||
permission: all
|
||||
category: Server Utility
|
||||
args:
|
||||
- name
|
||||
response:
|
||||
not_found: >
|
||||
The command to be executed couldn't be found.
|
@ -0,0 +1,133 @@
|
||||
const cmdLib = require('../../CommandLib'),
|
||||
location = './lib/commands/ServerUtilityCommands';
|
||||
|
||||
/**
|
||||
* This command module includes utility commands for the server.
|
||||
*/
|
||||
class ServerUtilityCommandModule extends cmdLib.CommandModule {
|
||||
|
||||
/**
|
||||
* @param opts {Object} properties:
|
||||
* getGuildHandler - a function to get the guild handler for the guild
|
||||
* messagehandler - the MessageHandler instance
|
||||
* config - the config object
|
||||
*/
|
||||
constructor(opts) {
|
||||
super(cmdLib.CommandScopes.Guild);
|
||||
this.templateFile = location + '/ServerUtilityCommandsTemplate.yaml';
|
||||
this._messageHandler = opts.messageHandler;
|
||||
this._getGuildHandler = opts.getGuildHandler;
|
||||
this._config = opts.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a command sequence to string.
|
||||
* @param sqArray
|
||||
* @returns {*}
|
||||
* @private
|
||||
*/
|
||||
_serializeCmdSequence(sqArray) {
|
||||
this._logger.debug(sqArray);
|
||||
return sqArray.map((x) => x.join(' && ')).join('; ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the utility commands.
|
||||
* @param commandHandler
|
||||
*/
|
||||
async register(commandHandler) {
|
||||
await this._loadTemplate();
|
||||
|
||||
let saveCmd = new cmdLib.Command(
|
||||
this.template.save_cmd,
|
||||
new cmdLib.Answer(async (m, k, s) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
let sequenceString = s
|
||||
.replace(new RegExp(`^${k.name}\\s`), '')
|
||||
.replace(/\\&/g, '&')
|
||||
.replace(/\\;/g, ';');
|
||||
let innerStrings = sequenceString.match(/'.+?'/g) || [];
|
||||
|
||||
for (let innerString of innerStrings)
|
||||
sequenceString.replace(innerString, innerString
|
||||
.replace(/&/g, '\\&'))
|
||||
.replace(/;/g, '\\;');
|
||||
sequenceString = sequenceString
|
||||
.replace(/"/g, '')
|
||||
.replace(/'/g, '"');
|
||||
let sequence = this._messageHandler.parseSyntaxString(sequenceString);
|
||||
let execCommand = this._config.prefix + this.template.execute.name;
|
||||
let maxSqPar = this._config.commandSettings.maxSequenceParallel;
|
||||
let maxSqSer = this._config.commandSettings.maxSequenceSerial;
|
||||
|
||||
if (sequenceString.includes(execCommand)) {
|
||||
return this.template.save_cmd.response.no_recursion;
|
||||
} else if (sequence.length > maxSqPar) {
|
||||
return this.template.save_cmd.response.sequence_too_many_parallel;
|
||||
} else if (sequence.find(x => x.length > maxSqSer)) {
|
||||
return this.template.save_cmd.response.sequence_too_many_serial;
|
||||
} else {
|
||||
let row = await gh.db
|
||||
.get('SELECT COUNT(*) count FROM commands WHERE name = ?', [k.name]);
|
||||
if (!row || row.count === 0)
|
||||
await gh.db
|
||||
.run('INSERT INTO commands (name, command) VALUES (?, ?)', [k.name, JSON.stringify(sequence)]);
|
||||
else
|
||||
await await gh.db
|
||||
.run('UPDATE commands SET command = ? WHERE name = ?', [JSON.stringify(sequence), k.name]);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let deleteCmd = new cmdLib.Command(
|
||||
this.template.delete_cmd,
|
||||
new cmdLib.Answer(async (m, k) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
await gh.db.run('DELETE FROM commands WHERE name = ?', [k.name]);
|
||||
return `Deleted command ${k.name}`;
|
||||
})
|
||||
);
|
||||
|
||||
let savedCmd = new cmdLib.Command(
|
||||
this.template.saved_cmd,
|
||||
new cmdLib.Answer(async (m) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
let response = new cmdLib.ExtendedRichEmbed('Saved Commands')
|
||||
.setFooter(`Execute a saved entry with ${this._config.prefix}execute [Entryname]`);
|
||||
let rows = await gh.db.all('SELECT name, command FROM commands');
|
||||
if (rows.length === 0)
|
||||
return this.template.saved_cmd.response.no_commands;
|
||||
else
|
||||
for (let row of rows)
|
||||
response.addField(row.name, '`' + this._serializeCmdSequence(JSON.parse(row.command)) + '`');
|
||||
return response;
|
||||
})
|
||||
);
|
||||
|
||||
let execute = new cmdLib.Command(
|
||||
this.template.execute,
|
||||
new cmdLib.Answer(async (m, k) => {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
let row = await gh.db
|
||||
.get('SELECT command FROM commands WHERE name = ?', [k.name]);
|
||||
if (row)
|
||||
await this._messageHandler
|
||||
.executeCommandSequence(JSON.parse(row.command), m);
|
||||
else
|
||||
return this.template.execute.response.not_found;
|
||||
|
||||
})
|
||||
);
|
||||
|
||||
// register commands
|
||||
commandHandler
|
||||
.registerCommand(saveCmd)
|
||||
.registerCommand(deleteCmd)
|
||||
.registerCommand(savedCmd)
|
||||
.registerCommand(execute);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(exports, {
|
||||
'module': ServerUtilityCommandModule
|
||||
});
|
@ -0,0 +1,44 @@
|
||||
shutdown:
|
||||
name: shutdown
|
||||
description: >
|
||||
Shuts down the bot.
|
||||
permission: owner
|
||||
category: Utility
|
||||
|
||||
add_presence:
|
||||
name: addpresence
|
||||
description: >
|
||||
Adds a Rich Presence to the bot.
|
||||
permission: owner
|
||||
category: Utility
|
||||
usage: addpresence [presence]
|
||||
|
||||
rotate_presence:
|
||||
name: rotatepresence
|
||||
description: >
|
||||
Forces a presence rotation
|
||||
permission: owner
|
||||
category: Utility
|
||||
|
||||
create_user:
|
||||
name: createuser
|
||||
description: >
|
||||
Creates a user for the webinterface.
|
||||
permission: owner
|
||||
category: Utility
|
||||
args:
|
||||
- username
|
||||
- password
|
||||
- scope
|
||||
|
||||
bugreport:
|
||||
name: bug
|
||||
description: >
|
||||
Get information about where to report bugs.
|
||||
permission: all
|
||||
category: Utility
|
||||
response:
|
||||
title: >
|
||||
You want to report a bug?
|
||||
bug_report: >
|
||||
Please report your bugs [here](https://github.com/Trivernis/discordbot.js/issues)
|
@ -0,0 +1,111 @@
|
||||
const cmdLib = require('../../CommandLib'),
|
||||
location = './lib/commands/UtilityCommands';
|
||||
|
||||
/**
|
||||
* Utility commands are all commands that allow the user to control the behaviour of the
|
||||
* bot. Utility commands for example are allowed to:
|
||||
* - manipulate the main database
|
||||
* - manipulate the bot's presences
|
||||
* - manipulate the process (e.g. shutdown)
|
||||
*/
|
||||
|
||||
class UtilityCommandModule extends cmdLib.CommandModule {
|
||||
|
||||
/**
|
||||
* @param opts {Object} properties:
|
||||
* bot - the instance of the bot.
|
||||
* config - the config object
|
||||
*/
|
||||
constructor(opts) {
|
||||
super(cmdLib.CommandScopes.User);
|
||||
this.templateFile = location + '/UtilityCommandsTemplate.yaml';
|
||||
this._bot = opts.bot;
|
||||
this._config = opts.config;
|
||||
}
|
||||
|
||||
async register(commandHandler) {
|
||||
await this._loadTemplate();
|
||||
|
||||
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]);
|
||||
return `Added Presence \`${s}\``;
|
||||
})
|
||||
);
|
||||
|
||||
let rotatePresence = new cmdLib.Command(
|
||||
this.template.rotate_presence,
|
||||
new cmdLib.Answer(() => {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let shutdown = new cmdLib.Command(
|
||||
this.template.shutdown,
|
||||
new cmdLib.Answer(async (m) => {
|
||||
try {
|
||||
await m.reply('Shutting down...');
|
||||
this._logger.debug('Destroying client...');
|
||||
await this._bot.client.destroy();
|
||||
} catch (err) {
|
||||
this._logger.error(err.message);
|
||||
this._logger.debug(err.stack);
|
||||
}
|
||||
try {
|
||||
this._logger.debug('Exiting server...');
|
||||
await this._bot.webServer.stop();
|
||||
} catch (err) {
|
||||
this._logger.error(err.message);
|
||||
this._logger.debug(err.stack);
|
||||
}
|
||||
try {
|
||||
this._logger.debug(`Exiting Process...`);
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
this._logger.error(err.message);
|
||||
this._logger.debug(err.stack);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let createUser = new cmdLib.Command(
|
||||
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(
|
||||
k.username, k.password, k.scope, false);
|
||||
return `${k.username}'s token is ${token}`;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let bugReport = new cmdLib.Command(
|
||||
this.template.bugreport,
|
||||
new cmdLib.Answer(() => {
|
||||
return new cmdLib.ExtendedRichEmbed(this.template.bugreport.response.title)
|
||||
.setDescription(this.template.bugreport.response.bug_report);
|
||||
})
|
||||
);
|
||||
|
||||
// register commands
|
||||
commandHandler.registerCommand(addPresence)
|
||||
.registerCommand(rotatePresence)
|
||||
.registerCommand(shutdown)
|
||||
.registerCommand(createUser)
|
||||
.registerCommand(bugReport);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(exports, {
|
||||
'module': UtilityCommandModule
|
||||
});
|
@ -1,62 +1,122 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
const winston = require('winston'),
|
||||
DailyRotateFile = require('winston-daily-rotate-file'),
|
||||
args = require('args-parser')(process.argv),
|
||||
args = require('args-parser')(process.argv);
|
||||
|
||||
fileLoggingFormat = winston.format.printf(info => {
|
||||
return `${info.timestamp} ${info.level.toUpperCase()}: ${JSON.stringify(info.message)}`; // the logging format for files
|
||||
}),
|
||||
consoleLoggingFormat = winston.format.printf(info => {
|
||||
return `${info.timestamp} [${info.level}] ${JSON.stringify(info.message)}`; //the logging format for the console
|
||||
/**
|
||||
* Set console format to simple string format
|
||||
* @type {Format}
|
||||
*/
|
||||
const consoleLoggingFormat = winston.format.printf(info => {
|
||||
return `${info.timestamp} {${info.module || info.m || 'DEFAULT'}} [${info.level}] ${JSON.stringify(info.message)}`; //the logging format for the console
|
||||
});
|
||||
|
||||
/**
|
||||
* Set full format to combination of formats
|
||||
* @type {Format}
|
||||
*/
|
||||
const loggingFullFormat = winston.format.combine(
|
||||
winston.format.timestamp({
|
||||
format: 'YY-MM-DD HH:mm:ss.SSS'
|
||||
}),
|
||||
loggingFullFormat = winston.format.combine(
|
||||
winston.format.splat(),
|
||||
winston.format.timestamp({
|
||||
format: 'YY-MM-DD HH:mm:ss.SSS'
|
||||
}),
|
||||
winston.format.json()
|
||||
),
|
||||
logger = winston.createLogger({
|
||||
level: winston.config.npm.levels, // logs with npm levels
|
||||
format: loggingFullFormat,
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.splat(),
|
||||
winston.format.timestamp({
|
||||
format: 'YY-MM-DD HH:mm:ss.SSS'
|
||||
}),
|
||||
consoleLoggingFormat
|
||||
),
|
||||
level: args.loglevel || 'info'
|
||||
}),
|
||||
new winston.transports.File({
|
||||
level: 'debug',
|
||||
filename: './.log/latest.log',
|
||||
options: {flags: 'w'} // overwrites the file on restart
|
||||
winston.format.json()
|
||||
);
|
||||
/**
|
||||
* Define all transports used.
|
||||
* @type {any[]}
|
||||
*/
|
||||
let transports = [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.splat(),
|
||||
winston.format.timestamp({
|
||||
format: 'YY-MM-DD HH:mm:ss.SSS'
|
||||
}),
|
||||
new DailyRotateFile({
|
||||
level: 'verbose',
|
||||
filename: './.log/%DATE%.log',
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: true,
|
||||
maxSize: '32m',
|
||||
maxFiles: '30d',
|
||||
json: true
|
||||
})
|
||||
]
|
||||
});
|
||||
winston.format.label({label: ''}),
|
||||
consoleLoggingFormat
|
||||
),
|
||||
level: args.loglevel || 'info'
|
||||
}),
|
||||
new winston.transports.File({
|
||||
level: 'debug',
|
||||
filename: './.log/latest.log',
|
||||
options: {flags: 'w'} // overwrites the file on restart
|
||||
}),
|
||||
new DailyRotateFile({
|
||||
level: 'verbose',
|
||||
filename: './.log/%DATE%.log',
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: true,
|
||||
maxSize: '32m',
|
||||
maxFiles: '30d',
|
||||
json: true
|
||||
})
|
||||
];
|
||||
|
||||
/**
|
||||
* A function to return the logger that has been created after appending an exception handler
|
||||
* @returns {Object}
|
||||
* Define the logger
|
||||
* @type {winston.Logger}
|
||||
*/
|
||||
exports.getLogger = function () {
|
||||
logger.exceptions.handle(
|
||||
new winston.transports.File({
|
||||
filename: './.log/exceptions.log'
|
||||
})
|
||||
);
|
||||
return logger;
|
||||
};
|
||||
let logger = winston.createLogger({
|
||||
level: winston.config.npm.levels,
|
||||
format: loggingFullFormat,
|
||||
transports: transports
|
||||
});
|
||||
|
||||
|
||||
// Define exception handling
|
||||
logger.exceptions.handle(
|
||||
new winston.transports.File({
|
||||
filename: './.log/exceptions.log'
|
||||
})
|
||||
);
|
||||
|
||||
class ModuleLogger {
|
||||
|
||||
constructor(moduleInstance) {
|
||||
this.logger = logger;
|
||||
if (moduleInstance.constructor)
|
||||
switch (moduleInstance.constructor.name) {
|
||||
case 'String':
|
||||
this.logName = moduleInstance;
|
||||
break;
|
||||
case 'Number':
|
||||
this.logName = moduleInstance.toString();
|
||||
break;
|
||||
default:
|
||||
this.logName = moduleInstance.constructor.name;
|
||||
}
|
||||
else
|
||||
this.logName = moduleInstance.toString();
|
||||
|
||||
}
|
||||
|
||||
silly(msg, meta) {
|
||||
logger.silly(msg, {module: this.logName, ...meta});
|
||||
}
|
||||
|
||||
debug(msg, meta) {
|
||||
logger.debug(msg, {module: this.logName, ...meta});
|
||||
}
|
||||
|
||||
verbose(msg, meta) {
|
||||
logger.verbose(msg, {module: this.logName, ...meta});
|
||||
}
|
||||
|
||||
info(msg, meta) {
|
||||
logger.info(msg, {module: this.logName, ...meta});
|
||||
}
|
||||
warn(msg, meta) {
|
||||
logger.warn(msg, {module: this.logName, ...meta});
|
||||
}
|
||||
|
||||
error(msg, meta) {
|
||||
logger.error(msg, {module: this.logName, ...meta});
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(exports, {
|
||||
logger: logger,
|
||||
Logger: ModuleLogger
|
||||
});
|
||||
|
Loading…
Reference in New Issue