Merge branch 'develop' into dependency-updates
commit
6cf3b8661d
@ -0,0 +1,89 @@
|
||||
# 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).
|
||||
|
||||
## [Unreleased]
|
||||
### Fixed
|
||||
- bug where the bot counts itself when calculating needed votes to skip/stop music
|
||||
- bug on the `ExtendedRichEmbed` where `addField` and `setDescription` throws an error when the value is null or undefined
|
||||
- bug on `AnilistApiCommands` where the `RichCharacterInfo` uses a nonexistent function of the `ExtendedRichEmbed`
|
||||
- bug on`AnilistApi` where the `.gql` files couldn't be found.
|
||||
- Typo in changelog
|
||||
- bug on `~np` message that causes the player to crash
|
||||
|
||||
### Changed
|
||||
- name of MiscCommands module from `TemplateCommandModule` to `MiscCommandModule`
|
||||
- moved everything in `lib` to subfolders with the same name as the files and renamed the files to `index.js`
|
||||
- renamed libfolders to lowercase and removed the lib suffix
|
||||
- moved commands outside of `lib`
|
||||
- switched from opusscript to node-opus for voice
|
||||
- all hard coded sql statements to generic sql generation
|
||||
- MusicPlayer to extend the default EventEmitter
|
||||
- MessageHandler to accept instances of Response and redirect events to it
|
||||
- switched to `ytdl-core-discord` for youtube audio playback
|
||||
|
||||
### Added
|
||||
- Utility classes for generic SQL Statements
|
||||
- logging of unrejected promises
|
||||
- database class for database abstraction (lib/database)
|
||||
- config entry for `database` with supported values `postgresql` or `sqlite`
|
||||
- config entry for `databaseConnection` for postgresql (`user`, `host`, `password`, `database`, `port`)
|
||||
- table `settings` to each guild to store guild specific settings
|
||||
- table `messages` to main database where messages are stored for statistical analysis and bug handling
|
||||
- ExtendedEventEmitter class in lib/utils/extended-events.js
|
||||
- Response object that allows the registration of events for messages
|
||||
- Handling of error event for every VoiceConnection
|
||||
|
||||
### Removed
|
||||
- `~volume` command because volume can't be controlled anymore
|
||||
- volume functions and properties from the MusicPlayer
|
||||
|
||||
## [0.11.0-beta] - 2019-03-03
|
||||
### Changed
|
||||
- template Files to name `template.yaml`
|
||||
- loading template file form CommandModule property `templateFile` to loading the `template.yaml` file from the `_templateDir` property (still supporting loading form templateFile)
|
||||
- ExtendedRichEmbed checks if fields are empty again after replacing values
|
||||
|
||||
### Added
|
||||
- `.template` to commands as a template for a command module with help comments
|
||||
- *METADATA* property to `template.yaml` files that is used as an anchor for shared command metadata (like `category`)
|
||||
- `CommandModule` **Misc** with command that are not really fitting into any other module
|
||||
- option to query this CHANGELOG with `_changes [version]` and `_versions` in the `CommandModule` **Info**
|
||||
|
||||
### Removed
|
||||
- `ExtendedRichEmbed.addNonemptyField` because the overide of `.addField` does the same
|
||||
|
||||
## [0.10.1]-beta - 2019-03-03
|
||||
### Changed
|
||||
- Bugfix on RichEmbed not returning itself on addField and setDescription because of method overide
|
||||
- AniList CommandModule bug fix on `~alCharacter` not returning voice actor names
|
||||
|
||||
## [0.10.0-beta] - 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
|
@ -0,0 +1,45 @@
|
||||
/* template index.js. Doesn't implement actual commands */
|
||||
const cmdLib = require('../../lib/command'); // required for command objects
|
||||
|
||||
/**
|
||||
* A description what the command module includes and why. Doesn't need to list commands but explains
|
||||
* category of the defined commands aswell as the scope.
|
||||
*/
|
||||
|
||||
class TemplateCommandModule extends cmdLib.CommandModule {
|
||||
|
||||
/**
|
||||
* @param opts {Object} properties: --- define the properties the opts object needs aswell as the type
|
||||
* bot - the instance of the bot
|
||||
*/
|
||||
constructor(opts) {
|
||||
super(cmdLib.CommandScopes.Global); // call constructor of superclass with the scope of the module
|
||||
this._templateDir = __dirname; // define the current directory as directory for the template.yaml file
|
||||
|
||||
this._bot = opts.bot; // define opts attributes as private properties of the module class
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines and registers commands to the commandHandler.
|
||||
* @param commandHandler {CommandHandler}
|
||||
*/
|
||||
async register(commandHandler) {
|
||||
await this._loadTemplate(); // loads the template file to the property this.template.
|
||||
|
||||
let templateCommand = new cmdLib.Command( // create a new instance of Command
|
||||
this.template.template_command, // pass the template to the constructor
|
||||
new cmdLib.Answer(() => { // pass a new instance of Answer to the constructor
|
||||
/* Command Logic */
|
||||
return this.template.response.not_implemented; // this command just returns the answer not_implemented
|
||||
})
|
||||
);
|
||||
|
||||
// register the commands on the commandHandler
|
||||
commandHandler.registerCommand(templateCommand); // register the command to the handler
|
||||
}
|
||||
}
|
||||
|
||||
// set the export properties
|
||||
Object.assign(exports, {
|
||||
module: TemplateCommandModule // Export the commandModule as module property. This is the default.
|
||||
});
|
@ -0,0 +1,16 @@
|
||||
# see yaml references (learnxinyminutes.com/docs/yaml/)
|
||||
|
||||
METADATA: &METADATA
|
||||
category: template # [optional if defined in commands]
|
||||
permission: all # [optional if defined in commands]
|
||||
|
||||
template_command:
|
||||
<<: *METADATA # include the predefined metadata for the command
|
||||
name: templateCommand # [required] the name of the command for execution
|
||||
usage: _templateCommand [templateArg] # [optional] overides the default help that generates from name and args
|
||||
permission: owner # [optional if in METADATA] overiedes the metadata value for permission
|
||||
description: > # [required] the description entry for the command help.
|
||||
A template for a command
|
||||
response: # [optional] predefine responses that can be used in the command logic
|
||||
not_implemented: >
|
||||
This command is not implemented.
|
@ -0,0 +1,305 @@
|
||||
const cmdLib = require('../../lib/command'),
|
||||
anilistApi = require('../../lib/api/AniListApi');
|
||||
|
||||
/**
|
||||
* 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.addField(
|
||||
'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._templateDir = __dirname;
|
||||
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,57 @@
|
||||
METADATA: &METADATA
|
||||
category: AniList
|
||||
permission: all
|
||||
|
||||
anime_search:
|
||||
<<: *METADATA
|
||||
name: alAnime
|
||||
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.
|
||||
response:
|
||||
not_found: >
|
||||
I couldn't find the anime you were searching for :(
|
||||
|
||||
anime_staff_search:
|
||||
<<: *METADATA
|
||||
name: alAnimeStaff
|
||||
usage: alAnimeStaff [search query]
|
||||
description: >
|
||||
Searches [AniList.co](https://anilist.co) for the anime *title* or *id* and returns all staff members.
|
||||
response:
|
||||
not_found: >
|
||||
I couldn't find the anime you were searching for :(
|
||||
|
||||
manga_search:
|
||||
<<: *METADATA
|
||||
name: alManga
|
||||
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.
|
||||
response:
|
||||
not_found: >
|
||||
I couldn't find the manga you were searching for :(
|
||||
|
||||
staff_search:
|
||||
<<: *METADATA
|
||||
name: alStaff
|
||||
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.
|
||||
response:
|
||||
not_found: >
|
||||
I couldn't find the staff member you were searching for :(
|
||||
|
||||
character_search:
|
||||
<<: *METADATA
|
||||
name: alCharacter
|
||||
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.
|
||||
response:
|
||||
not_found: >
|
||||
I couldn't find the character member you were searching for :(
|
@ -0,0 +1,203 @@
|
||||
const cmdLib = require('../../lib/command'),
|
||||
fsx = require('fs-extra'),
|
||||
utils = require('../../lib/utils');
|
||||
|
||||
/**
|
||||
* 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._templateDir = __dirname;
|
||||
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 _loadChangelog() {
|
||||
try {
|
||||
let changelog = (await fsx.readFile('CHANGELOG.md', {encoding: 'utf-8'})).replace(/\r\n/g, '\n');
|
||||
let entries = changelog.split(/\n## /);
|
||||
let changes = {};
|
||||
let latestVersion = null;
|
||||
this._logger.debug(`Found ${entries.length} changelog entries`);
|
||||
for (let entry of entries) {
|
||||
let title = '';
|
||||
let version = '';
|
||||
let date = '';
|
||||
let titleMatch = entry.match(/^.*?\n/g);
|
||||
|
||||
if (titleMatch && titleMatch.length > 0)
|
||||
title = titleMatch[0].replace(/\n/, '');
|
||||
let versionMatch = title.match(/\[.*?]/);
|
||||
|
||||
if (versionMatch && versionMatch.length > 0)
|
||||
version = versionMatch[0].replace(/^\[|]$/g, '');
|
||||
if (!latestVersion && version && version.length > 0)
|
||||
latestVersion = version;
|
||||
let dateMatch = title.match(/\d{4}-\d{2}-\d{2}/);
|
||||
|
||||
if (version && version.length > 0) {
|
||||
changes[version] = {
|
||||
date: date,
|
||||
title: title,
|
||||
segments: {}
|
||||
};
|
||||
if (dateMatch && dateMatch.length > 0)
|
||||
date = dateMatch[0];
|
||||
let segments = entry.replace(title.replace(/\n/, ''), '').split(/\n### /);
|
||||
for (let segment of segments) {
|
||||
let segmentTitle = '';
|
||||
let titleMatch = segment.match(/^.*?\n/);
|
||||
if (titleMatch && titleMatch.length > 0)
|
||||
segmentTitle = titleMatch[0].replace(/\n/, '');
|
||||
changes[version].segments[segmentTitle] = segment.replace(segmentTitle, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
changes.latest = changes[latestVersion];
|
||||
this._changes = changes;
|
||||
} catch (err) {
|
||||
this._logger.warn(err.message);
|
||||
this._logger.debug(err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
async register(commandHandler) {
|
||||
await this._loadTemplate();
|
||||
await this._loadChangelog();
|
||||
|
||||
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);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let changes = new cmdLib.Command(
|
||||
this.template.changes,
|
||||
new cmdLib.Answer((m, k) => {
|
||||
try {
|
||||
if (!k.version)
|
||||
return new cmdLib.ExtendedRichEmbed(this._changes.latest.title)
|
||||
.addFields(this._changes.latest.segments)
|
||||
.setColor(this.template.changes.embed_color)
|
||||
.attachFile('CHANGELOG.md');
|
||||
else
|
||||
return new cmdLib.ExtendedRichEmbed(this._changes[k.version].title)
|
||||
.addFields(this._changes[k.version].segments)
|
||||
.setColor(this.template.changes.embed_color)
|
||||
.attachFile('CHANGELOG.md');
|
||||
} catch (err) {
|
||||
this._logger.verbose(err.message);
|
||||
this._logger.silly(err.stack);
|
||||
return this.template.changes.response.not_found;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let versions = new cmdLib.Command(
|
||||
this.template.versions,
|
||||
new cmdLib.Answer(() => {
|
||||
try {
|
||||
return new cmdLib.ExtendedRichEmbed('CHANGELOG.md Versions')
|
||||
.setDescription(Object.keys(this._changes).join('\n'))
|
||||
.setColor(this.template.versions.embed_color);
|
||||
} catch (err) {
|
||||
this._logger.verbose(err.message);
|
||||
this._logger.silly(err.stack);
|
||||
return this.template.versions.response.not_found;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// register commands
|
||||
commandHandler
|
||||
.registerCommand(about)
|
||||
.registerCommand(ping)
|
||||
.registerCommand(uptime)
|
||||
.registerCommand(guilds)
|
||||
.registerCommand(help)
|
||||
.registerCommand(changes)
|
||||
.registerCommand(versions);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(exports, {
|
||||
'module': InfoCommandModule
|
||||
});
|
@ -0,0 +1,65 @@
|
||||
METADATA: &METADATA
|
||||
category: Info
|
||||
permission: all
|
||||
|
||||
about:
|
||||
<<: *METADATA
|
||||
name: about
|
||||
description: >
|
||||
Shows information about this Discord Bot.
|
||||
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:
|
||||
<<: *METADATA
|
||||
name: ping
|
||||
description: >
|
||||
Answers with the current average ping of the bot.
|
||||
|
||||
uptime:
|
||||
<<: *METADATA
|
||||
name: uptime
|
||||
description: >
|
||||
Answers with the uptime of the bot.
|
||||
guilds:
|
||||
<<: *METADATA
|
||||
name: guilds
|
||||
description: >
|
||||
Answers with the number of guilds the bot has joined
|
||||
permission: owner
|
||||
|
||||
help:
|
||||
<<: *METADATA
|
||||
name: help
|
||||
description: >
|
||||
Shows help for bot ocmmands.
|
||||
embed_color: 0xffffff
|
||||
args:
|
||||
- command
|
||||
|
||||
changes:
|
||||
<<: *METADATA
|
||||
name: changes
|
||||
description: >
|
||||
Shows the changes of the current release or a specific previous.
|
||||
embed_color: 0xaabbcc
|
||||
args:
|
||||
- version
|
||||
response:
|
||||
not_found: >
|
||||
I could not find the changelog for the version you were looking for.
|
||||
|
||||
versions:
|
||||
<<: *METADATA
|
||||
name: versions
|
||||
description: >
|
||||
Shows all versions present in the CHANGELOG.
|
||||
embed_color: 0xaabbcc
|
||||
response:
|
||||
not_found: >
|
||||
I could not find any versions.
|
@ -0,0 +1,82 @@
|
||||
/* template index.js. Doesn't implement actual commands */
|
||||
const cmdLib = require('../../lib/command');
|
||||
|
||||
/**
|
||||
* Several commands that are that special that they can't be included in any other module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Async delay
|
||||
* @param seconds {Number}
|
||||
*/
|
||||
function delay(seconds) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, seconds * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
class MiscCommandModule extends cmdLib.CommandModule {
|
||||
|
||||
constructor() {
|
||||
super(cmdLib.CommandScopes.Global);
|
||||
this._templateDir = __dirname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines and registers commands to the commandHandler.
|
||||
* @param commandHandler {CommandHandler}
|
||||
*/
|
||||
async register(commandHandler) {
|
||||
await this._loadTemplate();
|
||||
|
||||
let sayCommand = new cmdLib.Command(
|
||||
this.template.say,
|
||||
new cmdLib.Answer((m, k, s) => {
|
||||
return s.replace(/^"|"$/g, '');
|
||||
})
|
||||
);
|
||||
|
||||
let delayCommand = new cmdLib.Command(
|
||||
this.template.delay,
|
||||
new cmdLib.Answer(async (m, k) => {
|
||||
this._logger.silly(`Delaying for ${k.seconds} seconds`);
|
||||
await delay(k.seconds);
|
||||
})
|
||||
);
|
||||
|
||||
let chooseCommand = new cmdLib.Command(
|
||||
this.template.choose,
|
||||
new cmdLib.Answer(async (m, k, s) => {
|
||||
let options = s.split(',').map(x => {
|
||||
if (x) {
|
||||
let strippedValue = x.replace(/^\s+|\s+$/, '');
|
||||
if (strippedValue.length === 0)
|
||||
return null;
|
||||
else
|
||||
return strippedValue;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).filter(x => x);
|
||||
if (options.length === 0) {
|
||||
return this.template.choose.response.no_options;
|
||||
} else {
|
||||
this._logger.silly(`Choosing from ${options.join(', ')}`);
|
||||
let item = options[Math.floor(Math.random() * options.length)];
|
||||
return `I've chosen ${item.replace(/^"|"$|^\s+|\s+$/g, '')}`;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/* Register commands to handler */
|
||||
commandHandler
|
||||
.registerCommand(sayCommand)
|
||||
.registerCommand(delayCommand)
|
||||
.registerCommand(chooseCommand);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Object.assign(exports, {
|
||||
module: MiscCommandModule
|
||||
});
|
@ -0,0 +1,29 @@
|
||||
METADATA: &METADATA
|
||||
category: Misc
|
||||
permission: all
|
||||
|
||||
say:
|
||||
<<: *METADATA
|
||||
name: say
|
||||
usage: say [...message]
|
||||
description: >
|
||||
The bot says what you defined in the message argument
|
||||
|
||||
delay:
|
||||
<<: *METADATA
|
||||
name: delay
|
||||
usage: delay
|
||||
args:
|
||||
- seconds
|
||||
description: >
|
||||
Set a delay in seconds. Useful for command sequences.
|
||||
|
||||
choose:
|
||||
<<: *METADATA
|
||||
name: choose
|
||||
usage: choose [opt-1], [opt-2], ..., [opt-n]
|
||||
description: >
|
||||
Chooses randomly from one of the options
|
||||
response:
|
||||
no_options: >
|
||||
You need to define options for me to choose from.
|
@ -0,0 +1,379 @@
|
||||
const cmdLib = require('../../lib/command'),
|
||||
utils = require('../../lib/utils'),
|
||||
config = require('../../config');
|
||||
|
||||
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._templateDir = __dirname;
|
||||
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);
|
||||
return await gh.musicPlayer.playYouTube(url, next);
|
||||
} else {
|
||||
return 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(gh.db.sql.select('playlists', false, ['url'],
|
||||
gh.db.sql.where('name', '=', gh.db.sql.parameter(1))), [url]);
|
||||
if (!row) {
|
||||
this._logger.debug('Got invalid url for play command.');
|
||||
return t.response.url_invalid;
|
||||
} else {
|
||||
let songcount = await this._connectAndPlay(gh, vc, row.url, n);
|
||||
if (songcount)
|
||||
return `Added ${songcount} songs to the queue.`;
|
||||
else
|
||||
return t.response.success;
|
||||
}
|
||||
} else {
|
||||
let songcount = await this._connectAndPlay(gh, vc, url, n);
|
||||
if (songcount)
|
||||
return `Added ${songcount} songs to the queue.`;
|
||||
else
|
||||
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 - 1) / 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 - 1) / 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;
|
||||
}, async (response) => {
|
||||
let message = response.message;
|
||||
let gh = await this._getGuildHandler(message.guild);
|
||||
|
||||
if (message.editable && gh.musicPlayer) {
|
||||
let next = (song) => {
|
||||
message.edit('', new cmdLib.ExtendedRichEmbed('Now playing:')
|
||||
.setDescription(`[${song.title}](${song.url})`)
|
||||
.setImage(utils.YouTube.getVideoThumbnailUrlFromUrl(song.url))
|
||||
.setColor(0x00aaff));
|
||||
if (message.id !== message.channel.lastMessageID) {
|
||||
gh.musicPlayer.removeListener('next', next);
|
||||
message.delete();
|
||||
}
|
||||
};
|
||||
gh.musicPlayer.on('next', next);
|
||||
gh.musicPlayer.on('stop', () => {
|
||||
gh.musicPlayer.off('next', next);
|
||||
message.delete();
|
||||
});
|
||||
response.on('delete', () => gh.musicPlayer.off('next', next));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
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(gh.db.sql.select('playlists', false,
|
||||
[gh.db.sql.count('*')], gh.db.sql.where('name', '=', gh.db.sql.parameter(1))), [saveName]);
|
||||
if (!row || Number(row.count) === 0)
|
||||
await gh.db.run(gh.db.sql.insert('playlists',
|
||||
{name: gh.db.sql.parameter(1), url: gh.db.sql.parameter(2)}), [saveName, k.url]);
|
||||
else
|
||||
await gh.db.run(gh.db.sql.update('playlists',
|
||||
{url: gh.db.sql.parameter(1)},
|
||||
gh.db.sql.where('name', '=', gh.db.sql.parameter(2))), [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(gh.db.sql.delete('playlists', gh.db.sql.where('name', '=', gh.db.sql.parameter(1))), [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(gh.db.sql.select('playlists', false, ['name', 'url']));
|
||||
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]`);
|
||||
})
|
||||
);
|
||||
|
||||
/* TODO: Delete completely on release
|
||||
let volume = new cmdLib.Command(
|
||||
this.template.volume,
|
||||
new cmdLib.Answer(async (m, k) => {
|
||||
let volume = Number(k.volume);
|
||||
if (volume && volume <= 100 && volume >= 0) {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
gh.musicPlayer.setVolume(Math.round(volume)/100);
|
||||
await gh.db.setSetting('musicPlayerVolume', Math.round(volume)/100);
|
||||
return `Set music volume to **${volume}**`;
|
||||
} else {
|
||||
return this.template.volume.response.invalid;
|
||||
}
|
||||
})
|
||||
);*/
|
||||
|
||||
let quality = new cmdLib.Command(
|
||||
this.template.quality,
|
||||
new cmdLib.Answer(async (m, k) => {
|
||||
let allowed = ['highest', 'lowest', 'highestaudio', 'lowestaudio'];
|
||||
if (allowed.includes(k.quality)) {
|
||||
let gh = await this._getGuildHandler(m.guild);
|
||||
gh.musicPlayer.quality = k.quality;
|
||||
await gh.db.setSetting('musicPlayerQuality', k.quality);
|
||||
return `Set music quality to **${k.quality}**`;
|
||||
} else {
|
||||
return this.template.quality.response.invalid;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 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)
|
||||
.registerCommand(quality);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(exports, {
|
||||
module: MusicCommandModule
|
||||
});
|
@ -0,0 +1,195 @@
|
||||
METADATA: &METADATA
|
||||
category: Music
|
||||
permission: all
|
||||
|
||||
play:
|
||||
<<: *METADATA
|
||||
name: play
|
||||
description: >
|
||||
Adds the url to the YouTube video or YouTube playlist into the queue.
|
||||
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 video or Playlist.
|
||||
no_voicechannel: >
|
||||
You need to join a VoiceChannel to request media playback.
|
||||
|
||||
play_next:
|
||||
<<: *METADATA
|
||||
name: playnext
|
||||
description: >
|
||||
Adds the url to the YouTube video or YouTube playlist into the queue as
|
||||
next playing song.
|
||||
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 video or Playlist.
|
||||
no_voicechannel: >
|
||||
You need to join a VoiceChannel to request media playback.
|
||||
|
||||
join:
|
||||
<<: *METADATA
|
||||
name: join
|
||||
description: >
|
||||
Joins the VoiceChannel you are in.
|
||||
response:
|
||||
no_voicechannel: >
|
||||
You need to join a VoiceChannel for me to join.
|
||||
|
||||
stop:
|
||||
<<: *METADATA
|
||||
name: stop
|
||||
description: >
|
||||
Stops the media playback and leaves the VoiceChannel.
|
||||
response:
|
||||
success: >
|
||||
Stopped music playback.
|
||||
not_playing: >
|
||||
I'm not playing music at the moment. What do you want me to stop?
|
||||
|
||||
pause:
|
||||
<<: *METADATA
|
||||
name: pause
|
||||
description: >
|
||||
Pauses the media playback.
|
||||
response:
|
||||
success: >
|
||||
Paused playback.
|
||||
not_playing: >
|
||||
I'm not playing music at the moment.
|
||||
|
||||
resume:
|
||||
<<: *METADATA
|
||||
name: resume
|
||||
description: >
|
||||
Resumes the media playback.
|
||||
response:
|
||||
success: >
|
||||
Resumed playback.
|
||||
not_playing: >
|
||||
I'm not playing music at the moment.
|
||||
|
||||
skip:
|
||||
<<: *METADATA
|
||||
name: skip
|
||||
description: >
|
||||
Skips the currently playing song.
|
||||
response:
|
||||
success: >
|
||||
Skipped to the next song.
|
||||
not_playing: >
|
||||
I'm not playing music at the moment.
|
||||
|
||||
clear:
|
||||
<<: *METADATA
|
||||
name: clear
|
||||
description: >
|
||||
Clears the media queue.
|
||||
permission: dj
|
||||
response:
|
||||
success: >
|
||||
The media queue has been cleared.
|
||||
|
||||
media_queue:
|
||||
<<: *METADATA
|
||||
name: queue
|
||||
descriptions: >
|
||||
Shows the next ten songs in the media queue.
|
||||
|
||||
media_current:
|
||||
<<: *METADATA
|
||||
name: np
|
||||
description: >
|
||||
Shows the currently playing song.
|
||||
response:
|
||||
not_playing: >
|
||||
I'm not playing music at the moment.
|
||||
|
||||
shuffle:
|
||||
<<: *METADATA
|
||||
name: shuffle
|
||||
description: >
|
||||
Shuffles the media queue
|
||||
response:
|
||||
success: >
|
||||
The queue has been shuffled.
|
||||
|
||||
toggle_repeat:
|
||||
<<: *METADATA
|
||||
name: repeat
|
||||
description: >
|
||||
Toggles listening o repeat.
|
||||
response:
|
||||
repeat_true: >
|
||||
Listening on repeat now!
|
||||
repeat_false: >
|
||||
Not listening on repeat anymore.
|
||||
|
||||
save_media:
|
||||
<<: *METADATA
|
||||
name: savemedia
|
||||
description: >
|
||||
Saves the YouTube URL with a specific name.
|
||||
permission: dj
|
||||
args:
|
||||
- url
|
||||
usage: savemedia [url] [name...]
|
||||
|
||||
delete_media:
|
||||
<<: *METADATA
|
||||
name: deletemedia
|
||||
description: >
|
||||
Deletes a saved YouTube URL from saved media.
|
||||
permission: dj
|
||||
usage: deletemedia [name]
|
||||
response:
|
||||
no_name: >
|
||||
You must provide a name for the media to delete.
|
||||
|
||||
saved_media:
|
||||
<<: *METADATA
|
||||
name: savedmedia
|
||||
description: >
|
||||
Shows all saved YouTube URLs.
|
||||
response:
|
||||
no_saved: >
|
||||
There are no saved YouTube URLs :(
|
||||
|
||||
volume:
|
||||
<<: *METADATA
|
||||
name: volume
|
||||
permission: dj
|
||||
args:
|
||||
- volume
|
||||
description: >
|
||||
Sets the volume of the Music Player.
|
||||
response:
|
||||
invalid: >
|
||||
The value you entered is an invalid volume.
|
||||
|
||||
quality:
|
||||
<<: *METADATA
|
||||
name: quality
|
||||
permission: owner
|
||||
args:
|
||||
- quality
|
||||
description: >
|
||||
Sets the quality of the music of the Music Player.
|
||||
The setting will be applied on the next song.
|
||||
response:
|
||||
invalid: >
|
||||
You entered an invalid quality value.
|
@ -0,0 +1,133 @@
|
||||
const cmdLib = require('../../lib/command');
|
||||
|
||||
/**
|
||||
* 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._templateDir = __dirname;
|
||||
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 sql = gh.db.sql;
|
||||
let row = await gh.db.get(sql.select('commands', false, [sql.count('*')],
|
||||
sql.where('name', '=', sql.parameter(1))), [k.name]);
|
||||
if (!row || Number(row.count) === 0)
|
||||
await gh.db.run(sql.insert('commands', {name: sql.parameter(1), command: sql.parameter(2)}),
|
||||
[k.name, JSON.stringify(sequence)]);
|
||||
else
|
||||
await gh.db.run(sql.update('commands', {command: sql.parameter(1)}, sql.where('name', '=', sql.parameter(2))),
|
||||
[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(gh.db.sql.delete('commands', gh.db.sql.where('name', '=', gh.db.sql.parameter(1)), ), [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(gh.db.sql.select('commands', ['name', 'command']));
|
||||
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(gh.db.sql.select('commands',false, ['command'],
|
||||
gh.db.sql.where('name', '=', gh.db.sql.parameter(1))), [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,50 @@
|
||||
METADATA: &METADATA
|
||||
category: Server Utility
|
||||
permission: all
|
||||
|
||||
save_cmd:
|
||||
<<: *METADATA
|
||||
name: savecmd
|
||||
description: >
|
||||
Saves a sequence of commands under a new name.
|
||||
permission: moderator
|
||||
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:
|
||||
<<: *METADATA
|
||||
name: deletecmd
|
||||
description: >
|
||||
Deletes a saved command.
|
||||
permission: moderator
|
||||
args:
|
||||
- name
|
||||
|
||||
saved_cmd:
|
||||
<<: *METADATA
|
||||
name: savedcmd
|
||||
description: >
|
||||
Lists all saved commands.
|
||||
response:
|
||||
no_commands: >
|
||||
There are no saved commands.
|
||||
|
||||
execute:
|
||||
<<: *METADATA
|
||||
name: execute
|
||||
description: >
|
||||
Executes a saved command.
|
||||
args:
|
||||
- name
|
||||
response:
|
||||
not_found: >
|
||||
The command to be executed couldn't be found.
|
@ -0,0 +1,111 @@
|
||||
const cmdLib = require('../../lib/command');
|
||||
|
||||
/**
|
||||
* 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._templateDir = __dirname;
|
||||
this._bot = opts.bot;
|
||||
this._config = opts.config;
|
||||
}
|
||||
|
||||
async register(commandHandler) {
|
||||
await this._loadTemplate();
|
||||
let sql = this._bot.maindb.sql;
|
||||
|
||||
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(sql.insert('presences', {text: sql.parameter(1)}), [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
|
||||
});
|
@ -0,0 +1,44 @@
|
||||
METADATA: &METADATA
|
||||
category: Utility
|
||||
permission: owner
|
||||
|
||||
shutdown:
|
||||
<<: *METADATA
|
||||
name: shutdown
|
||||
description: >
|
||||
Shuts down the bot.
|
||||
|
||||
add_presence:
|
||||
<<: *METADATA
|
||||
name: addpresence
|
||||
description: >
|
||||
Adds a Rich Presence to the bot.
|
||||
usage: addpresence [presence]
|
||||
|
||||
rotate_presence:
|
||||
<<: *METADATA
|
||||
name: rotatepresence
|
||||
description: >
|
||||
Forces a presence rotation
|
||||
|
||||
create_user:
|
||||
<<: *METADATA
|
||||
name: createuser
|
||||
description: >
|
||||
Creates a user for the webinterface.
|
||||
args:
|
||||
- username
|
||||
- password
|
||||
- scope
|
||||
|
||||
bugreport:
|
||||
<<: *METADATA
|
||||
name: bug
|
||||
permission: all
|
||||
description: >
|
||||
Get information about where to report bugs.
|
||||
response:
|
||||
title: >
|
||||
You want to report a bug?
|
||||
bug_report: >
|
||||
Please report your bugs [here](https://github.com/Trivernis/discordbot.js/issues)
|
@ -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,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
|
||||
}
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
const fetch = require('node-fetch'),
|
||||
fsx = require('fs-extra'),
|
||||
yaml = require('js-yaml'),
|
||||
queryPath = __dirname + '/graphql',
|
||||
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
|
||||
});
|
@ -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,318 @@
|
||||
const Discord = require('discord.js'),
|
||||
yaml = require('js-yaml'),
|
||||
fsx = require('fs-extra'),
|
||||
logging = require('../utils/logging'),
|
||||
config = require('../../config.json'),
|
||||
xevents = require('../utils/extended-events'),
|
||||
utils = require('../utils');
|
||||
|
||||
const scopes = new utils.Enum([
|
||||
'Global',
|
||||
'User',
|
||||
'Guild'
|
||||
]);
|
||||
|
||||
/**
|
||||
* The answer message object that is used for easyer access to events.
|
||||
*/
|
||||
class Response extends xevents.ExtendedEventEmitter {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param content
|
||||
*/
|
||||
constructor(content) {
|
||||
super();
|
||||
this.content = content;
|
||||
this.message = null;
|
||||
}
|
||||
}
|
||||
|
||||
class Answer {
|
||||
|
||||
/**
|
||||
* Creates an new Answer object with _func as answer logic.
|
||||
* @param func {function} - the function to evaluate the answer
|
||||
* @param [onSent] {function} - executed when the response was sent
|
||||
*/
|
||||
constructor(func, onSent) {
|
||||
this._func = func;
|
||||
this.listeners = onSent? {sent: onSent} : {};
|
||||
this.lastResponse = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Response>}
|
||||
*/
|
||||
async evaluate(message, kwargs, argsString) {
|
||||
let result = this._func(message, kwargs, argsString);
|
||||
if (result instanceof Promise)
|
||||
return this._getResponseInstance(await utils.resolveNestedPromise(result));
|
||||
else
|
||||
return this._getResponseInstance(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a response instance with listeners attached if defined.
|
||||
* @param responseContent
|
||||
* @returns {Response}
|
||||
* @private
|
||||
*/
|
||||
_getResponseInstance(responseContent) {
|
||||
this.lastResponse = new Response(responseContent);
|
||||
|
||||
if (this.listeners)
|
||||
this.lastResponse.addListeners(this.listeners);
|
||||
return this.lastResponse;
|
||||
}
|
||||
}
|
||||
|
||||
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 {Response}
|
||||
*/
|
||||
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 {Response | Promise<Response>}
|
||||
*/
|
||||
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 new Response("You don't have permission for this command");
|
||||
} else {
|
||||
this._logger.silly(`Command ${commandName} not found.`);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
this._logger.silly(`No prefix found in command ${commandName}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 module from the this._templateDir directory.
|
||||
* Loads the template from this.templateFile if the attribute exists.
|
||||
* @param dir {String} Overides the this._templateDir with this directory.
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
async _loadTemplate(dir) {
|
||||
if (!this.templateFile)
|
||||
this.templateFile = (dir || this._templateDir) + '/template.yaml';
|
||||
let templateString = await fsx.readFile(this.templateFile, {encoding: 'utf-8'});
|
||||
this._logger.silly(`Loaded Template file ${this.templateFile}`);
|
||||
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 the fields defined in the fields JSON
|
||||
* @param fields {JSON}
|
||||
* @returns {ExtendedRichEmbed}
|
||||
*/
|
||||
addFields(fields) {
|
||||
for (let [name, value] of Object.entries(fields))
|
||||
this.addField(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description by shortening the value string to a fitting length for discord.
|
||||
* @param value
|
||||
*/
|
||||
setDescription(value) {
|
||||
if (value) {
|
||||
let croppedValue = value;
|
||||
if (value.substring)
|
||||
croppedValue = value.substring(0, 1024);
|
||||
if (croppedValue.length < value.length && croppedValue.replace)
|
||||
croppedValue = croppedValue.replace(/\n.*$/g, '');
|
||||
if (croppedValue && croppedValue.replace
|
||||
&& croppedValue.replace(/\n/g, '').length > 0)
|
||||
super.setDescription(croppedValue);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the field by shortening the value stirn to a fitting length for discord.
|
||||
* @param name
|
||||
* @param value
|
||||
*/
|
||||
addField(name, value) {
|
||||
if (name && value) {
|
||||
let croppedValue = value;
|
||||
if (value.substring)
|
||||
croppedValue = value.substring(0, 1024);
|
||||
if (croppedValue && croppedValue.length < value.length && croppedValue.replace)
|
||||
croppedValue = croppedValue.replace(/\n.*$/g, '');
|
||||
if (croppedValue && croppedValue.replace
|
||||
&& croppedValue.replace(/\n/g, '').length > 0 && name.replace(/\n/g, '').length > 0)
|
||||
super.addField(name, croppedValue);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// -- exports -- //
|
||||
|
||||
Object.assign(exports, {
|
||||
Answer: Answer,
|
||||
Command: Command,
|
||||
CommandHandler: CommandHandler,
|
||||
CommandModule: CommandModule,
|
||||
ExtendedRichEmbed: ExtendedRichEmbed,
|
||||
CommandScopes: scopes
|
||||
});
|
@ -0,0 +1,177 @@
|
||||
const genericSql = require('../utils/genericSql'),
|
||||
logging = require('../utils/logging'),
|
||||
config = require('../../config.json');
|
||||
|
||||
class Database {
|
||||
/**
|
||||
* Creates a new database.
|
||||
* @param name {String} - the name of the database.
|
||||
*/
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this._logger = new logging.Logger(`Database@${name}`);
|
||||
this._dbType = config.database? config.database : 'sqlite';
|
||||
if (this._dbType === 'sqlite')
|
||||
this.database = new (require('../utils/sqliteAsync')).Database(`./data/${this.name}.db`);
|
||||
else if (this._dbType === 'postgresql')
|
||||
this.database = new (require('pg')).Pool({
|
||||
user: config.databaseConnection.user,
|
||||
host: config.databaseConnection.host,
|
||||
database: config.databaseConnection.database,
|
||||
password: config.databaseConnection.password,
|
||||
port: config.databaseConnection.port
|
||||
});
|
||||
this.sql = new genericSql.GenericSql(this._dbType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the database.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async initDatabase() {
|
||||
if (this._dbType === 'sqlite') {
|
||||
await this.database.init();
|
||||
} else if (this._dbType === 'postgresql') {
|
||||
await this.database.connect();
|
||||
await this.begin();
|
||||
await this.database.query(`CREATE SCHEMA IF NOT EXISTS ${this.name.replace(/\W/g, '')}`);
|
||||
await this.database.query(`SET search_path TO ${this.name.replace(/\W/g, '')}`);
|
||||
await this.commit();
|
||||
}
|
||||
this._logger.verbose(`Connected to ${this._dbType} database ${this.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a sql statement with seperate values and no return.
|
||||
* Autocommit.
|
||||
* @param sql {String}
|
||||
* @param [values] {Array<String|Number>}
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async run(sql, values) {
|
||||
this._logger.debug(`Running SQL "${sql}" with values ${values}`);
|
||||
if (this._dbType === 'sqlite')
|
||||
await this.database.run(sql, values);
|
||||
else if (this._dbType === 'postgresql')
|
||||
try {
|
||||
await this.begin();
|
||||
await this.database.query(sql, values);
|
||||
await this.commit();
|
||||
} catch (err) {
|
||||
this._logger.error(err.message);
|
||||
this._logger.verbose(err.stack);
|
||||
await this.rollback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin. Part of Postgresqls BEGIN / COMMIT / ROLLBACK
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async begin() {
|
||||
if (this._dbType === 'postgresql') {
|
||||
await this.database.query('BEGIN');
|
||||
await this.database.query(`SET search_path TO ${this.name.replace(/\W/g, '')};`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a query to the current changes. No autocommit (except on sqlite).
|
||||
* @param sql
|
||||
* @param values
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async query(sql, values) {
|
||||
if (this._dbType === 'sqlite') {
|
||||
await this.run(sql, values);
|
||||
} else if (this._dbType === 'postgresql') {
|
||||
await this.database.query(sql, values);
|
||||
this._logger.debug(`Running SQL "${sql}" with values ${values}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit. Part of Postgresqls BEGIN / COMMIT / ROLLBACK.
|
||||
* Writes data to the database, ROLLBACK on error. (has no effect on sqlite)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async commit() {
|
||||
if (this._dbType === 'postgresql')
|
||||
try {
|
||||
await this.database.query('COMMIT');
|
||||
} catch (err) {
|
||||
await this.database.query('ROLLBACK');
|
||||
this._logger.error(err.message);
|
||||
this._logger.verbose(err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback. Part of Postgresqls BEGIN / COMMIT / ROLLBACK.
|
||||
* Reverts changes done in the current commit. (has no effect on sqlite)
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async rollback() {
|
||||
if (this._dbType === 'postgresql')
|
||||
this.database.query('ROLLBACK');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Run a sql statement with seperate values and first result row as return.
|
||||
* @param sql {String} - the sql statement with escaped values ($1, $2... for postgres, ? for sqlite)
|
||||
* @param [values] {Array<String|Number>}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async get(sql, values) {
|
||||
this._logger.debug(`Running SQL "${sql}" with values ${values}`);
|
||||
let result = null;
|
||||
if (this._dbType === 'sqlite') {
|
||||
result = await this.database.get(sql, values);
|
||||
} else if (this._dbType === 'postgresql') {
|
||||
await this.database.query(`SET search_path TO ${this.name.replace(/\W/g, '')};`);
|
||||
result = (await this.database.query({
|
||||
text: sql,
|
||||
values: values
|
||||
})).rows;
|
||||
}
|
||||
if (result instanceof Array && result.length > 0)
|
||||
return result[0];
|
||||
else
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a sql statement with seperate values and all result rows as return.
|
||||
* @param sql {String} - the sql statement with escaped values ($1, $2... for postgres, ? for sqlite)
|
||||
* @param [values] {Array<String|Number>} - the seperate values
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async all(sql, values) {
|
||||
this._logger.debug(`Running SQL "${sql}" with values ${values}`);
|
||||
if (this._dbType === 'sqlite') {
|
||||
return await this.database.all(sql, values);
|
||||
} else if (this._dbType === 'postgresql') {
|
||||
await this.database.query(`SET search_path TO ${this.name.replace(/\W/g, '')};`);
|
||||
return (await this.database.query({
|
||||
text: sql,
|
||||
values: values
|
||||
})).rows;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the connection to the database.
|
||||
*/
|
||||
close() {
|
||||
if (this._dbType === 'sqlite')
|
||||
this.database.close();
|
||||
else if (this._dbType === 'postgresql')
|
||||
this.database.release();
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(exports, {
|
||||
Column: genericSql.Column,
|
||||
Database: Database
|
||||
});
|
@ -1,354 +0,0 @@
|
||||
const cmd = require('./cmd'),
|
||||
music = require('./music'),
|
||||
utils = require('./utils'),
|
||||
config = require('../config.json'),
|
||||
sqliteAsync = require('./sqliteAsync'),
|
||||
fs = require('fs-extra'),
|
||||
servercmd = require('../commands/servercommands'),
|
||||
Discord = require('discord.js'),
|
||||
waterfall = require('promise-waterfall'),
|
||||
dataDir = config.dataPath || './data';
|
||||
let logger = require('winston');
|
||||
|
||||
exports.setLogger = function (newLogger) {
|
||||
logger = newLogger;
|
||||
music.setLogger(logger);
|
||||
cmd.setLogger(logger);
|
||||
};
|
||||
|
||||
/**
|
||||
* Server-Specific commands, music and more
|
||||
* @type {GuildHandler}
|
||||
*/
|
||||
exports.GuildHandler = class {
|
||||
constructor(guild, prefix) {
|
||||
this.guild = guild;
|
||||
this.dj = null;
|
||||
this.mention = false;
|
||||
this.prefix = prefix || config.prefix;
|
||||
this.servant = new cmd.Servant(this.prefix);
|
||||
}
|
||||
|
||||
async initDatabase() {
|
||||
await fs.ensureDir(dataDir + '/gdb');
|
||||
this.db = new sqliteAsync.Database(`${dataDir}/gdb/${this.guild}.db`);
|
||||
await this.db.init();
|
||||
logger.debug(`Connected to the database for ${this.guild}`);
|
||||
await this.createTables();
|
||||
// register commands
|
||||
this.registerCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the guild handler
|
||||
*/
|
||||
destroy() {
|
||||
this.dj.stop();
|
||||
this.db.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates all tables needed in the Database.
|
||||
* These are at the moment:
|
||||
* messages - logs all messages send on the server
|
||||
* playlists - save playlists to play them later
|
||||
*/
|
||||
async createTables() {
|
||||
await this.db.run(`${utils.sql.tableExistCreate} messages (
|
||||
${utils.sql.pkIdSerial},
|
||||
creation_timestamp DATETIME NOT NULL,
|
||||
author VARCHAR(128) NOT NULL,
|
||||
author_name VARCHAR(128),
|
||||
content TEXT NOT NULL
|
||||
)`);
|
||||
await this.db.run(`${utils.sql.tableExistCreate} playlists (
|
||||
${utils.sql.pkIdSerial},
|
||||
name VARCHAR(32) UNIQUE NOT NULL,
|
||||
url VARCHAR(255) NOT NULL
|
||||
)`);
|
||||
await this.db.run(`${utils.sql.tableExistCreate} commands (
|
||||
${utils.sql.pkIdSerial},
|
||||
name VARCHAR(32) UNIQUE NOT NULL,
|
||||
command VARCHAR(255) NOT NULL
|
||||
)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Answers a message via mention if mentioning is active or with just sending it to the same channel.
|
||||
* @param msg
|
||||
* @param answer
|
||||
*/
|
||||
async answerMessage(msg, answer) {
|
||||
if (answer instanceof Promise || answer)
|
||||
if (answer instanceof Discord.RichEmbed) {
|
||||
(this.mention) ? msg.reply('', answer) : msg.channel.send('', answer);
|
||||
} else if (answer instanceof Promise) {
|
||||
let resolvedAnswer = await answer;
|
||||
await this.answerMessage(msg, resolvedAnswer);
|
||||
} else if (answer instanceof Array) {
|
||||
await waterfall(answer.map((x) => async () => await this.answerMessage(msg, x)));
|
||||
} else if ({}.toString.call(answer) === '[object Function]') { // check if the answer is of type function
|
||||
await this.answerMessage(msg, answer());
|
||||
} else {
|
||||
(this.mention) ? msg.reply(answer) : msg.channel.send(answer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handles the message by letting the servant parse the command. Depending on the message setting it
|
||||
* replies or just sends the answer.
|
||||
* @param msg
|
||||
*/
|
||||
async handleMessage(msg) {
|
||||
if (this.db)
|
||||
await this.db.run(
|
||||
'INSERT INTO messages (author, creation_timestamp, author_name, content) values (?, ?, ?, ?)',
|
||||
[msg.author.id, msg.createdTimestamp, msg.author.username, msg.content]
|
||||
);
|
||||
await this.answerMessage(msg, this.servant.parseCommand(msg));
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a voice-channel if not connected and play the url
|
||||
* @param vc
|
||||
* @param url
|
||||
* @param next
|
||||
*/
|
||||
async connectAndPlay(vc, url, next) {
|
||||
if (!this.dj.connected) {
|
||||
await this.dj.connect(vc);
|
||||
await this.dj.playYouTube(url, next);
|
||||
} else {
|
||||
await this.dj.playYouTube(url, next);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* registers all music commands and initializes a dj
|
||||
*/
|
||||
registerCommands() {
|
||||
this.dj = new music.DJ();
|
||||
|
||||
let playCb = async (msg, kwargs, argv, template, next) => {
|
||||
let vc = this.dj.voiceChannel || msg.member.voiceChannel;
|
||||
let url = kwargs['url'];
|
||||
if (!vc)
|
||||
return template.response.no_voicechannel;
|
||||
if (!url)
|
||||
return template.response.no_url;
|
||||
if (!utils.YouTube.isValidEntityUrl(url)) {
|
||||
if (argv && argv.length > 0)
|
||||
url += ' ' + argv.join(' '); // join to get the whole expression behind the command
|
||||
let row = await this.db.get('SELECT url FROM playlists WHERE name = ?', [url]);
|
||||
if (!row) {
|
||||
logger.debug('Got invalid url for play command.');
|
||||
return template.response.url_invalid;
|
||||
} else {
|
||||
await this.connectAndPlay(vc, row.url, next);
|
||||
return template.response.success;
|
||||
}
|
||||
} else {
|
||||
await this.connectAndPlay(vc, url, next);
|
||||
return template.response.success;
|
||||
}
|
||||
};
|
||||
|
||||
// play command
|
||||
this.servant.createCommand(servercmd.music.play, async (msg, kwargs, argv) => {
|
||||
return await playCb(msg, kwargs, argv, servercmd.music.play, false);
|
||||
});
|
||||
|
||||
// playnext command
|
||||
this.servant.createCommand(servercmd.music.playnext, async (msg, kwargs, argv) => {
|
||||
return await playCb(msg, kwargs, argv, servercmd.music.playnext, true);
|
||||
});
|
||||
|
||||
// join command
|
||||
this.servant.createCommand(servercmd.music.join, (msg) => {
|
||||
if (msg.member.voiceChannel)
|
||||
this.dj.connect(msg.member.voiceChannel);
|
||||
else
|
||||
return servercmd.music.join.response.not_connected;
|
||||
|
||||
});
|
||||
|
||||
// stop command
|
||||
this.servant.createCommand(servercmd.music.stop, () => {
|
||||
if (this.dj.connected) {
|
||||
this.dj.stop();
|
||||
return servercmd.music.stop.response.success;
|
||||
} else {
|
||||
return servercmd.music.stop.response.not_playing;
|
||||
}
|
||||
});
|
||||
|
||||
// pause command
|
||||
this.servant.createCommand(servercmd.music.pause, () => {
|
||||
if (this.dj.playing) {
|
||||
this.dj.pause();
|
||||
return servercmd.music.pause.response.success;
|
||||
} else {
|
||||
return servercmd.music.pause.response.not_playing;
|
||||
}
|
||||
});
|
||||
|
||||
// resume command
|
||||
this.servant.createCommand(servercmd.music.resume, () => {
|
||||
if (this.dj.playing) {
|
||||
this.dj.resume();
|
||||
return servercmd.music.resume.response.success;
|
||||
} else {
|
||||
return servercmd.music.resume.response.not_playing;
|
||||
}
|
||||
});
|
||||
|
||||
// skip command
|
||||
this.servant.createCommand(servercmd.music.skip, () => {
|
||||
if (this.dj.playing) {
|
||||
this.dj.skip();
|
||||
return servercmd.music.skip.response.success;
|
||||
} else {
|
||||
return servercmd.music.skip.response.not_playing;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// clear command
|
||||
this.servant.createCommand(servercmd.music.clear, () => {
|
||||
this.dj.clear();
|
||||
return servercmd.music.clear.response.success;
|
||||
});
|
||||
|
||||
// playlist command
|
||||
this.servant.createCommand(servercmd.music.playlist, () => {
|
||||
logger.debug(`found ${this.dj.queue.length} songs`);
|
||||
let describtion = '';
|
||||
for (let i = 0; i < Math.min(this.dj.queue.length, 9); i++) {
|
||||
let entry = this.dj.queue[i];
|
||||
describtion += `[${entry.title}](${entry.url})\n`;
|
||||
}
|
||||
return new Discord.RichEmbed()
|
||||
.setTitle(`${this.dj.queue.length} songs in queue`)
|
||||
.setDescription(describtion);
|
||||
});
|
||||
|
||||
// np command
|
||||
this.servant.createCommand(servercmd.music.current, () => {
|
||||
let song = this.dj.song;
|
||||
if (song)
|
||||
return new Discord.RichEmbed()
|
||||
.setTitle('Now playing:')
|
||||
.setDescription(`[${song.title}](${song.url})`)
|
||||
.setImage(utils.YouTube.getVideoThumbnailUrlFromUrl(song.url))
|
||||
.setColor(0x00aaff);
|
||||
else
|
||||
return servercmd.music.current.response.not_playing;
|
||||
|
||||
});
|
||||
|
||||
// shuffle command
|
||||
this.servant.createCommand(servercmd.music.shuffle, () => {
|
||||
this.dj.shuffle();
|
||||
return servercmd.music.shuffle.response.success;
|
||||
});
|
||||
|
||||
// repeat command
|
||||
this.servant.createCommand(servercmd.music.repeat, () => {
|
||||
if (this.dj) {
|
||||
this.dj.repeat = !this.dj.repeat;
|
||||
if (this.dj.repeat)
|
||||
return servercmd.music.repeat.response.repeat_true;
|
||||
else
|
||||
return servercmd.music.repeat.response.repeat_false;
|
||||
}
|
||||
});
|
||||
|
||||
// saves playlists and videos
|
||||
this.servant.createCommand(servercmd.music.savemedia, async (msg, kwargs, argv) => {
|
||||
let saveName = argv.join(' ');
|
||||
let row = await this.db.get('SELECT COUNT(*) count FROM playlists WHERE name = ?', [saveName]);
|
||||
if (!row || row.count === 0)
|
||||
await this.db.run('INSERT INTO playlists (name, url) VALUES (?, ?)', [saveName, kwargs.url]);
|
||||
else
|
||||
await this.db.run('UPDATE playlists SET url = ? WHERE name = ?', [kwargs.url, saveName]);
|
||||
return `Saved song/playlist as ${saveName}`;
|
||||
});
|
||||
|
||||
// savedmedia command - prints out saved playlists and videos
|
||||
this.servant.createCommand(servercmd.music.savedmedia, async () => {
|
||||
let response = '';
|
||||
let rows = await this.db.all('SELECT name, url FROM playlists');
|
||||
for (let row of rows)
|
||||
response += `[${row.name}](${row.url})\n`;
|
||||
|
||||
if (rows.length === 0)
|
||||
return servercmd.music.savedmedia.response.no_saved;
|
||||
else
|
||||
return new Discord.RichEmbed()
|
||||
.setTitle('Saved Songs and Playlists')
|
||||
.setDescription(response)
|
||||
.setFooter(`Play a saved entry with ${this.prefix}play [Entryname]`)
|
||||
.setTimestamp();
|
||||
});
|
||||
|
||||
this.servant.createCommand(servercmd.music.deletemedia, async (msg, kwargs, argv) => {
|
||||
let saveName = argv.join(' ');
|
||||
if (!saveName) {
|
||||
return servercmd.music.deletemedia.response.no_name;
|
||||
} else {
|
||||
await this.db.run('DELETE FROM playlists WHERE name = ?', [saveName]);
|
||||
return `Deleted ${saveName} from saved media`;
|
||||
}
|
||||
});
|
||||
|
||||
// savecmd - saves a command sequence with a name
|
||||
this.servant.createCommand(servercmd.utils.savecmd, async (msg, kwargs, argv) => {
|
||||
let saveName = argv.pop();
|
||||
let cmdsequence = argv.join(' ').replace(/\\/g, '');
|
||||
if (argv.includes(this.prefix + servercmd.utils.execute.name)) {
|
||||
return servercmd.utils.savecmd.response.no_recursion;
|
||||
} else if (cmdsequence.split(';').length < (config.maxCmdSequenceLength || 5)){
|
||||
let row = await this.db.get('SELECT COUNT(*) count FROM commands WHERE name = ?', [saveName]);
|
||||
if (!row || row.count === 0)
|
||||
await this.db.run('INSERT INTO commands (name, command) VALUES (?, ?)', [saveName, cmdsequence]);
|
||||
else
|
||||
await this.db.run('UPDATE commands SET sequence = ? WHERE name = ?', [cmdsequence, saveName]);
|
||||
return `saved command sequence as ${saveName}`;
|
||||
} else {
|
||||
return servercmd.utils.savecmd.response.sequence_too_long;
|
||||
}
|
||||
});
|
||||
|
||||
// savedcmd - prints saved commands
|
||||
this.servant.createCommand(servercmd.utils.savedcmd, async () => {
|
||||
let response = new Discord.RichEmbed()
|
||||
.setTitle('Saved Commands')
|
||||
.setFooter(`Execute a saved entry with ${this.prefix}execute [Entryname]`)
|
||||
.setTimestamp();
|
||||
let rows = await this.db.all('SELECT name, command FROM commands');
|
||||
if (rows.length === 0)
|
||||
return servercmd.utils.savedcmd.response.no_commands;
|
||||
else
|
||||
for (let row of rows)
|
||||
response.addField(row.name, '`' + row.command + '`');
|
||||
return response;
|
||||
});
|
||||
|
||||
// deletecmd - deletes a command from saved commands
|
||||
this.servant.createCommand(servercmd.utils.deletecmd, async (msg, kwargs) => {
|
||||
await this.db.run('DELETE FROM commands WHERE name = ?', [kwargs.cmdname]);
|
||||
return `Deleted command ${kwargs.cmdname}`;
|
||||
});
|
||||
|
||||
// execute - executes a saved command
|
||||
this.servant.createCommand(servercmd.utils.execute, async (msg, kwargs) => {
|
||||
let row = await this.db.get('SELECT command FROM commands WHERE name = ?', [kwargs.cmdname]);
|
||||
if (row) {
|
||||
msg.content = row.command;
|
||||
await this.handleMessage(msg);
|
||||
} else {
|
||||
return servercmd.utils.execute.response.not_found;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
@ -0,0 +1,198 @@
|
||||
const music = require('../music'),
|
||||
utils = require('../utils'),
|
||||
config = require('../../config.json'),
|
||||
dblib = require('../database'),
|
||||
logging = require('../utils/logging'),
|
||||
fs = require('fs-extra'),
|
||||
dataDir = config.dataPath || './data';
|
||||
|
||||
/**
|
||||
* GuildDatabase class has abstraction for some sql statements.
|
||||
*/
|
||||
class GuildDatabase extends dblib.Database {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param name
|
||||
*/
|
||||
constructor(name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates all tables needed in the guilds Database.
|
||||
*/
|
||||
async createTables() {
|
||||
let sql = this.sql;
|
||||
await this.run(sql.createTableIfNotExists('playlists', [
|
||||
sql.templates.idcolumn,
|
||||
new dblib.Column('name', sql.types.getVarchar(32), [sql.constraints.unique, sql.constraints.notNull]),
|
||||
new dblib.Column('url', sql.types.getVarchar(255), [sql.constraints.notNull])
|
||||
]));
|
||||
this._logger.silly('Created Table playlists.');
|
||||
await this.run(sql.createTableIfNotExists('commands', [
|
||||
sql.templates.idcolumn,
|
||||
new dblib.Column('name', sql.types.getVarchar(32), [sql.constraints.unique, sql.constraints.notNull]),
|
||||
new dblib.Column('command', sql.types.getVarchar(255), [sql.constraints.notNull])
|
||||
]));
|
||||
this._logger.silly('Created Table commands.');
|
||||
await this.run(sql.createTableIfNotExists('settings', [
|
||||
sql.templates.idcolumn,
|
||||
new dblib.Column('key', sql.types.getVarchar(32), [sql.constraints.unique, sql.constraints.notNull]),
|
||||
new dblib.Column('value', sql.types.getVarchar(32), [])
|
||||
]));
|
||||
this._logger.silly('Created Table settings.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the column where the key has the value keyvalue
|
||||
* @param table {String} - the table name
|
||||
* @param column {String} - the name of the column
|
||||
* @param keyname {String} - the name of the key
|
||||
* @param keyvalue {*} - the value of the key
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async getSingleValue(table, column, keyname, keyvalue) {
|
||||
let result = await this.get(this.sql.select(table, false, column,
|
||||
this.sql.where(this.sql.parameter(1), '=', this.sql.parameter(2))),
|
||||
[keyname, keyvalue]);
|
||||
if (result)
|
||||
return result[column];
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns either the whole table or a limited version
|
||||
* @param tablename
|
||||
* @param limit
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async getTableContent(tablename, limit) {
|
||||
if (limit)
|
||||
return await this.all(this.sql.select(tablename, false, ['*'], [], [
|
||||
this.sql.limit(limit)
|
||||
]));
|
||||
else
|
||||
return await this.all(this.sql.select(tablename, false, ['*'], [], []));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a setting
|
||||
* @param name
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async getSetting(name) {
|
||||
let result = await this.get(this.sql.select('settings', false, 'value',
|
||||
this.sql.where('key', '=', this.sql.parameter(1))), [name]);
|
||||
if (result)
|
||||
return result.value;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all settings as object.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async getSettings() {
|
||||
let rows = await this.all(this.sql.select('settings', false, ['key', 'value'], [], []));
|
||||
let retObj = {};
|
||||
if (rows)
|
||||
for (let row of rows)
|
||||
retObj[row.key] = row.value;
|
||||
return retObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert or update a setting parameter in the settings database.
|
||||
* @param name
|
||||
* @param value
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async setSetting(name, value) {
|
||||
let row = await this.get(this.sql.select('settings', false, [this.sql.count('*')],
|
||||
this.sql.where('key', '=', this.sql.parameter(1))), [name]);
|
||||
if (!row || Number(row.count) === 0)
|
||||
await this.run(this.sql.insert('settings', {key: this.sql.parameter(1), value: this.sql.parameter(2)}),
|
||||
[name, value]);
|
||||
else
|
||||
await this.run(this.sql.update('settings', {value: this.sql.parameter(1)},
|
||||
this.sql.where('key', '=', this.sql.parameter(2))), [value, name]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Guild Handler handles guild settings and data.
|
||||
* @type {GuildHandler}
|
||||
*/
|
||||
class GuildHandler {
|
||||
|
||||
constructor(guild) {
|
||||
this.guild = guild;
|
||||
this._logger = new logging.Logger(`${this.constructor.name}@${this.guild}`);
|
||||
this.musicPlayer = new music.MusicPlayer(null);
|
||||
this._logger.silly('Initialized Guild Handler');
|
||||
this._votes = {};
|
||||
this.settings = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the database
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async initDatabase() {
|
||||
this._logger.silly('Initializing Database');
|
||||
this.db = new GuildDatabase(`guild_${this.guild.name.replace(/\s/g, '_').replace(/\W/g, '')}`);
|
||||
await this.db.initDatabase();
|
||||
this._logger.debug(`Connected to the database for ${this.guild}`);
|
||||
this._logger.debug('Creating Databases');
|
||||
await this.db.createTables();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all relevant guild settings.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async applySettings() {
|
||||
this.settings = await this.db.getSettings();
|
||||
this.musicPlayer.quality = this.settings.musicPlayerQuality || 'lowest';
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the guild handler
|
||||
*/
|
||||
destroy() {
|
||||
this._logger.debug('Ending musicPlayer');
|
||||
this.musicPlayer.stop();
|
||||
this._logger.debug('Ending Database');
|
||||
this.db.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the vote counter for a command up and adds the user.
|
||||
* @param command {String}
|
||||
* @param user {String}
|
||||
*/
|
||||
updateCommandVote(command, user) {
|
||||
if (!this._votes[command])
|
||||
this._votes[command] = {count: 0, users: []};
|
||||
if (!this._votes[command].users.includes(user)) {
|
||||
this._votes[command].count++;
|
||||
this._votes[command].users.push(user);
|
||||
}
|
||||
return this._votes[command];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the vote counter and voted users for a command.
|
||||
* @param command {String}
|
||||
*/
|
||||
resetCommandVote(command) {
|
||||
this._votes[command] = {count: 0, users: []};
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(exports, {
|
||||
GuildHandler: GuildHandler
|
||||
});
|
@ -1,62 +0,0 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
const winston = require('winston'),
|
||||
DailyRotateFile = require('winston-daily-rotate-file'),
|
||||
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
|
||||
}),
|
||||
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
|
||||
}),
|
||||
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}
|
||||
*/
|
||||
exports.getLogger = function () {
|
||||
logger.exceptions.handle(
|
||||
new winston.transports.File({
|
||||
filename: './.log/exceptions.log'
|
||||
})
|
||||
);
|
||||
return logger;
|
||||
};
|
@ -0,0 +1,238 @@
|
||||
const cmdLib = require('../command'),
|
||||
config = require('../../config.json'),
|
||||
Discord = require('discord.js'),
|
||||
logging = require('../utils/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.registeredResponses = {};
|
||||
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 events...');
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
this.discordClient.on('messageReactionAdd', (messageReaction, user) => {
|
||||
let responseInstance = this.registeredResponses[messageReaction.message];
|
||||
if (responseInstance)
|
||||
responseInstance.emit('reactionAdd', messageReaction, user);
|
||||
});
|
||||
|
||||
this.discordClient.on('messageReactionRemove', (messageReaction, user) => {
|
||||
let responseInstance = this.registeredResponses[messageReaction.message];
|
||||
if (responseInstance)
|
||||
responseInstance.emit('reactionRemove', messageReaction, user);
|
||||
});
|
||||
|
||||
this.discordClient.on('messageReactionRemoveAll', (message) => {
|
||||
let responseInstance = this.registeredResponses[message];
|
||||
if (responseInstance)
|
||||
responseInstance.emit('reactionRemoveAll', message);
|
||||
});
|
||||
|
||||
this.discordClient.on('messageDelete', (message) => {
|
||||
let responseInstance = this.registeredResponses[message];
|
||||
if (responseInstance) {
|
||||
responseInstance.on('delete', message);
|
||||
delete this.registeredResponses[message];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
await this._answerMessage(message, scopeResult);
|
||||
else if (globalResult)
|
||||
await 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 response {Response}
|
||||
* @private
|
||||
*/
|
||||
async _answerMessage(message, response) {
|
||||
this.logger.debug(`Sending answer ${response.content}`);
|
||||
if (response && response.content) {
|
||||
let responseMessage = null;
|
||||
|
||||
if (response.content instanceof Discord.RichEmbed)
|
||||
responseMessage = await message.channel.send('', response.content);
|
||||
else
|
||||
responseMessage = await message.channel.send(response.content);
|
||||
|
||||
if (response.hasListeners)
|
||||
this.registeredResponses[responseMessage] = response;
|
||||
|
||||
response.message = responseMessage;
|
||||
response.emit('sent', response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,76 @@
|
||||
const logging = require('../utils/logging'),
|
||||
EventEmitter = require('events');
|
||||
|
||||
/**
|
||||
* Extends the event emitter with some useful features.
|
||||
*/
|
||||
class ExtendedEventEmitter extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param [name] {String}
|
||||
*/
|
||||
constructor(name) {
|
||||
super();
|
||||
this._logger = new logging.Logger(`${name}-${this.constructor.name}`);
|
||||
this._registerDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registeres the error event to the logger so it won't crash the bot.
|
||||
* @private
|
||||
*/
|
||||
_registerDefault() {
|
||||
this.on('error', (err) => {
|
||||
this._logger.error(err.message);
|
||||
this._logger.debug(err.stack);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an object of events with listeners to the bot.
|
||||
* @param eventListenerObject
|
||||
* @returns {ExtendedEventEmitter}
|
||||
*/
|
||||
addListeners(eventListenerObject) {
|
||||
for (let [event, listener] of Object.entries(eventListenerObject))
|
||||
this.on(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all registered events.
|
||||
* @returns {*|Array<string | symbol>|string[]}
|
||||
*/
|
||||
get events() {
|
||||
return this.eventNames();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around getMaxListeners function
|
||||
* @returns {*|number}
|
||||
*/
|
||||
get maxListeners() {
|
||||
return this.getMaxListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around setMaxListeners function.
|
||||
* @param n
|
||||
* @returns {this | this | Cluster | *}
|
||||
*/
|
||||
set maxListeners(n) {
|
||||
return this.setMaxListeners(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the emitter has additional listeners apart from the error listener.
|
||||
*/
|
||||
get hasListeners() {
|
||||
return this.events.count > 1;
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(exports, {
|
||||
ExtendedEventEmitter: ExtendedEventEmitter,
|
||||
});
|
@ -0,0 +1,413 @@
|
||||
/**
|
||||
* Returns types based on the database.
|
||||
*/
|
||||
class GenericTypes {
|
||||
/**
|
||||
* Constructor.
|
||||
* @param database {String}
|
||||
*/
|
||||
constructor(database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
get null() {
|
||||
switch(this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
default:
|
||||
return 'NULL';
|
||||
}
|
||||
}
|
||||
|
||||
get integer() {
|
||||
switch(this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
default:
|
||||
return 'INTEGER';
|
||||
}
|
||||
}
|
||||
|
||||
get real() {
|
||||
switch(this.database) {
|
||||
case 'sqlite':
|
||||
return 'REAL';
|
||||
case 'postgresql':
|
||||
default:
|
||||
return 'FLOAT';
|
||||
}
|
||||
}
|
||||
|
||||
get text() {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
default:
|
||||
return 'TEXT';
|
||||
}
|
||||
}
|
||||
|
||||
get varchar() {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
default:
|
||||
return 'VARCHAR';
|
||||
}
|
||||
}
|
||||
|
||||
get date() {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
default:
|
||||
return 'DATE';
|
||||
}
|
||||
}
|
||||
|
||||
get datetime() {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
return 'TIMESTAMP';
|
||||
case 'sqlite':
|
||||
default:
|
||||
return 'DATETIME';
|
||||
}
|
||||
}
|
||||
|
||||
get serial() {
|
||||
switch (this.database) {
|
||||
case 'sqlite':
|
||||
return 'INTEGER AUTOINCREMENT NOT NULL';
|
||||
case 'postgresql':
|
||||
default:
|
||||
return 'SERIAL';
|
||||
}
|
||||
}
|
||||
|
||||
get serialPK() {
|
||||
switch (this.database) {
|
||||
case 'sqlite':
|
||||
return 'INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL';
|
||||
case 'postgresql':
|
||||
default:
|
||||
return 'SERIAL PRIMARY KEY UNIQUE';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the VARCHAR type with the specified length.
|
||||
* @param length {Number}
|
||||
*/
|
||||
getVarchar(length) {
|
||||
return `${this.varchar}(${length})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sql statements based on the database.
|
||||
*/
|
||||
class GenericSql {
|
||||
/**
|
||||
* Constructor.
|
||||
* @param database {String}
|
||||
*/
|
||||
constructor(database) {
|
||||
this.database = database;
|
||||
this.types = new GenericTypes(database);
|
||||
this.constraints = {
|
||||
primaryKey: 'PRIMARY KEY',
|
||||
notNull: 'NOT NULL',
|
||||
unique: 'UNIQUE',
|
||||
like: 'LIKE',
|
||||
exists: 'EXISTS',
|
||||
and: 'AND',
|
||||
or: 'OR',
|
||||
in: 'IN',
|
||||
any: 'ANY',
|
||||
all: 'ALL'
|
||||
};
|
||||
this.templates = {
|
||||
idcolumn: new Column('id', this.types.serialPK, [])
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a value placeholder for the specified number.
|
||||
* @param number {Number} - the variables position.
|
||||
*/
|
||||
parameter(number) {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
return `$${number}`;
|
||||
case 'sqlite':
|
||||
return '?';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A sum selector - calculates the sum of all values of the column
|
||||
* @param colname {String} - the name of the column where the sum is selected.
|
||||
* @returns {string}
|
||||
*/
|
||||
sum(colname) {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `SUM(${colname})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A avg selector - selects the average
|
||||
* @param colname {String} - the name of the column where the avg value is selected.
|
||||
* @returns {string}
|
||||
*/
|
||||
avg(colname) {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `AVG(${colname})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A min selector - selects the minimum
|
||||
* @param colname {String} - the name of the column where the min value is selected.
|
||||
* @returns {string}
|
||||
*/
|
||||
min(colname) {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `MIN(${colname})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A max selector - selects the maximum
|
||||
* @param colname {String} - the name of the column where the max value is selected.
|
||||
* @returns {string}
|
||||
*/
|
||||
max(colname) {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `MAX(${colname})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A count selector - counts the results
|
||||
* @param colname {String} - the name of the column to be counted.
|
||||
* @returns {string}
|
||||
*/
|
||||
count(colname) {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `COUNT(${colname}) count`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A default constraint
|
||||
* @param expression {String} - the expression to generate the default value.
|
||||
* @returns {string}
|
||||
*/
|
||||
default(expression) {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `DEFAULT ${expression}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A where statement
|
||||
* @param row {String} - the row
|
||||
* @param operator {String} - the comparison operator
|
||||
* @param comparator {String} the value or row to compare to
|
||||
*/
|
||||
and(row, operator, comparator) {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `AND ${row} ${operator} ${comparator}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A or statement
|
||||
* @param row {String} - the row
|
||||
* @param operator {String} - the comparison operator
|
||||
* @param comparator {String} the value or row to compare to
|
||||
*/
|
||||
or(row, operator, comparator) {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `OR ${row} ${operator} ${comparator}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A where statement
|
||||
* @param row {String} - the row
|
||||
* @param operator {String} - the comparison operator
|
||||
* @param comparator {String} the value or row to compare to
|
||||
*/
|
||||
where(row, operator, comparator) {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `WHERE ${row} ${operator} ${comparator}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A limit statement.
|
||||
* @param count {Number} - the number of rows to return
|
||||
* @returns {string}
|
||||
*/
|
||||
limit(count) {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `LIMIT ${count}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Table statement
|
||||
* @param table {String}
|
||||
* @param rows {Array<Column>}
|
||||
* @returns {string}
|
||||
*/
|
||||
createTable(table, rows) {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `CREATE TABLE ${table} (${rows.map(x => x.sql).join(',')})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Table if it doesn't exist statement
|
||||
* @param table {String}
|
||||
* @param columns {Array<Column>}
|
||||
* @returns {string}
|
||||
*/
|
||||
createTableIfNotExists(table, columns) {
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `CREATE TABLE IF NOT EXISTS ${table} (${columns.map(x => x.sql).join(',')})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert into the table.
|
||||
* @param table {String} - the table name
|
||||
* @param colValueObj {Object} - an object with keys as columnnames and values as columnvalues
|
||||
* @returns {string}
|
||||
*/
|
||||
insert(table, colValueObj) {
|
||||
let rownames = Object.keys(colValueObj);
|
||||
let values = Object.values(colValueObj);
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `INSERT INTO ${table} (${rownames.join(',')}) values (${values.join(',')})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the table with the rowValueObject.
|
||||
* @param table {String} - the table name
|
||||
* @param colValueObj {Object} - an object with keys as columnnames and values as columnvalues
|
||||
* @param conditions {Array<String>|String} - conditions for the update row selection (WHERE ... [OR ...][AND ...]
|
||||
* @returns {string}
|
||||
*/
|
||||
update(table, colValueObj, conditions) {
|
||||
if (!(conditions instanceof Array))
|
||||
conditions = [conditions];
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `UPDATE ${table} SET ${Object.entries(colValueObj).map(x => `${x[0]} = ${x[1]}`).join(',')} ${conditions.join(' ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects from a table
|
||||
* @param table {String} - the tablename
|
||||
* @param distinct {String|boolean} - should distinct values be selected? If yes provide distinct keyword.
|
||||
* @param colnames {Array<String>|String} - the rows to select
|
||||
* @param conditions {Array<String>|String} - conditions for the row selection (WHERE ... [OR ...][AND ...]
|
||||
* @param operations {Array<String>|String} - operations on the selected rows
|
||||
* @returns {String}
|
||||
*/
|
||||
select(table, distinct, colnames, conditions, operations) {
|
||||
if (!(colnames instanceof Array))
|
||||
colnames = [colnames];
|
||||
if (!(conditions instanceof Array))
|
||||
conditions = [conditions];
|
||||
if (!(operations instanceof Array))
|
||||
operations = [operations];
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `SELECT${distinct? ' ' + distinct : ''} ${colnames.join(', ')} FROM ${table} ${conditions.join(' ')} ${operations.join(' ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes from a table
|
||||
* @param table {String} - the table name
|
||||
* @param conditions {Array<String>|String} - conditions for the row selection (WHERE ... [OR ...][AND ...]
|
||||
*/
|
||||
delete(table, conditions) {
|
||||
if (!(conditions instanceof Array))
|
||||
conditions = [conditions];
|
||||
switch (this.database) {
|
||||
case 'postgresql':
|
||||
case 'sqlite':
|
||||
return `DELETE FROM ${table} ${conditions.join(' ')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Column {
|
||||
/**
|
||||
* Create a column for usage in the generic sql statements
|
||||
* @param name {String}
|
||||
* @param [type] {String}
|
||||
* @param [constraints] {Array<String>}
|
||||
*/
|
||||
constructor(name, type, constraints) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.constraints = constraints || [];
|
||||
if (!(constraints instanceof Array))
|
||||
this.constraints = [constraints];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the datatype of the row.
|
||||
* @param constraint {String}
|
||||
*/
|
||||
addConstraint(constraint) {
|
||||
this.constraints.push(constraint);
|
||||
}
|
||||
|
||||
get sql() {
|
||||
return `${this.name} ${this.type} ${this.constraints.join(' ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(exports, {
|
||||
GenericSql: GenericSql,
|
||||
GenericTypes: GenericTypes,
|
||||
Column: Column
|
||||
});
|
@ -0,0 +1,122 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
const winston = require('winston'),
|
||||
DailyRotateFile = require('winston-daily-rotate-file'),
|
||||
args = require('args-parser')(process.argv);
|
||||
|
||||
/**
|
||||
* 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'
|
||||
}),
|
||||
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'
|
||||
}),
|
||||
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
|
||||
})
|
||||
];
|
||||
|
||||
/**
|
||||
* Define the logger
|
||||
* @type {winston.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