Major changes to command api

- changed return type of command parsing to response
- changed Response to extend the EventEmitter
- changed MusicPlayer to extend the EvenEmitter
- added continuos update to now playing if the message is the latest in the channel
- started redesigning the graphql api
feature/api-rewrite
Trivernis 5 years ago
parent 3da2d098c4
commit ed1cb1a812

@ -19,10 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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
### Added
- state lib with `EventRouter` and `EventGroup` and `Event` classes
- Subclasses of EventRouter for client events groupes `Client`, `Channel`, `Message` and `Guild`
- Utility classes for generic SQL Statements
- logging of unrejected promises
- database class for database abstraction (lib/database)
@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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
## [0.11.0-beta] - 2019-03-03
### Changed

@ -228,6 +228,28 @@ class MusicCommandModule extends cmdLib.CommandModule {
.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.off('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));
}
})
);

@ -3,22 +3,42 @@ const Discord = require('discord.js'),
fsx = require('fs-extra'),
logging = require('../utils/logging'),
config = require('../../config.json'),
xevents = require('../utils/extended-events'),
utils = require('../utils');
const scopes = {
'Global': 0,
'User': 1,
'Guild': 2
};
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
* @param func {function} - the function to evaluate the answer
* @param [onSent] {function} - executed when the response was sent
*/
constructor(func) {
constructor(func, onSent) {
this._func = func;
this.listeners = onSent? {sent: onSent} : {};
this.lastResponse = null;
}
/**
@ -27,14 +47,28 @@ class Answer {
* @param message
* @param kwargs
* @param argsString
* @returns {Promise<*>}
* @returns {Promise<Response>}
*/
async evaluate(message, kwargs, argsString) {
let result = this._func(message, kwargs, argsString);
if (result instanceof Promise)
return await utils.resolveNestedPromise(result);
return this._getResponseInstance(await utils.resolveNestedPromise(result));
else
return result;
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;
}
}
@ -66,7 +100,7 @@ class Command {
* @param message {Discord.Message}
* @param kwargs {JSON}
* @param argsString {String} The raw argument string.
* @returns {String}
* @returns {Response}
*/
async answer(message, kwargs, argsString) {
return await this.answObj.evaluate(message, kwargs, argsString);
@ -104,7 +138,7 @@ class CommandHandler {
* Handles the command and responds to the message.
* @param commandMessage {String}
* @param message {Discord.Message}
* @returns {Boolean | String | Promise<String|Discord.RichEmbed>}
* @returns {Response | Promise<Response>}
*/
handleCommand(commandMessage, message) {
this._logger.debug(`Handling command ${commandMessage}`);
@ -129,14 +163,14 @@ class CommandHandler {
return command.answer(message, kwargs, argsString);
} else if (command) {
this._logger.silly(`Permission ${command.permission} denied for command ${commandName}`);
return "You don't have permission for this command";
return new Response("You don't have permission for this command");
} else {
this._logger.silly(`Command ${commandName} not found.`);
return false;
return null;
}
} else {
this._logger.silly(`No prefix found in command ${commandName}`);
return false;
return null;
}
}

@ -44,6 +44,39 @@ class GuildDatabase extends dblib.Database {
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

@ -23,6 +23,7 @@ class MessageHandler {
this.guildCmdHandler = new cmdLib.CommandHandler(config.prefix,
cmdLib.CommandScopes.Guild);
this.userRates = {};
this.registeredResponses = {};
this._registerEvents();
}
@ -68,7 +69,8 @@ class MessageHandler {
* @private
*/
_registerEvents() {
this.logger.debug('Registering message event...');
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
@ -81,6 +83,32 @@ class MessageHandler {
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];
}
});
}
/**
@ -124,9 +152,9 @@ class MessageHandler {
this.logger.silly(`globalResult: ${globalResult}, scopeResult: ${scopeResult}`);
if (scopeResult)
this._answerMessage(message, scopeResult);
await this._answerMessage(message, scopeResult);
else if (globalResult)
this._answerMessage(message, globalResult);
await this._answerMessage(message, globalResult);
} catch (err) {
this.logger.verbose(err.message);
this.logger.silly(err.stack);
@ -149,16 +177,25 @@ class MessageHandler {
/**
* Answers
* @param message {Discord.Message}
* @param answer {String | Discord.RichEmbed}
* @param response {Response}
* @private
*/
_answerMessage(message, answer) {
this.logger.debug(`Sending answer ${answer}`);
if (answer)
if (answer instanceof Discord.RichEmbed)
message.channel.send('', answer);
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
message.channel.send(answer);
responseMessage = await message.channel.send(response.content);
if (response.hasListeners)
this.registeredResponses[responseMessage] = response;
response.message = responseMessage;
response.emit('sent', response);
}
}
/**

@ -2,7 +2,8 @@ const ytdl = require("ytdl-core"),
ypi = require('youtube-playlist-info'),
yttl = require('get-youtube-title'),
config = require('../../config.json'),
utils = require('../utils/index.js'),
utils = require('../utils'),
xevents = require('../utils/extended-events'),
logging = require('../utils/logging'),
ytapiKey = config.api.youTubeApiKey;
@ -10,8 +11,13 @@ const ytdl = require("ytdl-core"),
* The Music Player class is used to handle music playing tasks on Discord Servers (Guilds).
* @type {MusicPlayer}
*/
class MusicPlayer {
class MusicPlayer extends xevents.ExtendedEventEmitter{
/**
* Constructor
* @param [voiceChannel] {Discord.VoiceChannel}
*/
constructor(voiceChannel) {
super();
this.conn = null;
this.disp = null;
this.queue = [];
@ -46,6 +52,7 @@ class MusicPlayer {
let connection = await this.voiceChannel.join();
this._logger.info(`Connected to Voicechannel ${this.voiceChannel.name}`);
this.conn = connection;
this.emit('connected');
}
/**
@ -56,6 +63,7 @@ class MusicPlayer {
this.repeat = value;
if (this.current)
this.queue.push(this.current);
this.emit('listenOnRepeat', this.repeat);
}
/**
@ -182,6 +190,7 @@ class MusicPlayer {
this.current = this.queue.shift();
if (this.repeat) // listen on repeat
this.queue.push(this.current);
this.emit('next', this.current);
this.playYouTube(this.current.url).catch((err) => this._logger.warn(err.message));
} else {
this.stop();
@ -280,6 +289,7 @@ class MusicPlayer {
} catch (error) {
this._logger.verbose(JSON.stringify(error));
}
this.emit('stop');
}
/**
@ -303,6 +313,7 @@ class MusicPlayer {
this.stop();
}
}
this.emit('skip', this.current);
}
/**
@ -318,6 +329,7 @@ class MusicPlayer {
*/
shuffle() {
this.queue = utils.shuffleArray(this.queue);
this.emit('shuffle');
}
/**
@ -325,6 +337,7 @@ class MusicPlayer {
*/
clear() {
this.queue = [];
this.emit('clear');
}
}

@ -1,163 +0,0 @@
let stateLib = require("index.js");
class DiscordGuildEvents extends EventGroup {
constructor(client) {
super();
this._registerClientEvents(client);
}
/**
* Registeres the client events to the EventGroup
* @param client {Discord.Client}
* @private
*/
_registerClientEvents(client) {
this.registerEvent(new stateLib.Event('clientUserGuildSettingsUpdate'))
.registerEvent(new stateLib.Event('clientUserSettingsUpdate'))
.registerEvent(new stateLib.Event('emojiCreate'))
.registerEvent(new stateLib.Event('emojiDelete'))
.registerEvent(new stateLib.Event('emojiUpdate'))
.registerEvent(new stateLib.Event('guildBanAdd'))
.registerEvent(new stateLib.Event('guildBanRemove'))
.registerEvent(new stateLib.Event('guildCreate'))
.registerEvent(new stateLib.Event('guildDelete'))
.registerEvent(new stateLib.Event('guildMemberAdd'))
.registerEvent(new stateLib.Event('guildMemberAvailable'))
.registerEvent(new stateLib.Event('guildMemberRemove'))
.registerEvent(new stateLib.Event('guildMemberChunk'))
.registerEvent(new stateLib.Event('guildMemberSpeaking'))
.registerEvent(new stateLib.Event('guildMemberUpdate'))
.registerEvent(new stateLib.Event('guildUnavailable'))
.registerEvent(new stateLib.Event('guildUpdate'))
.registerEvent(new stateLib.Event('presenceUpdate'))
.registerEvent(new stateLib.Event('roleCreate'))
.registerEvent(new stateLib.Event('roleDelete'))
.registerEvent(new stateLib.Event('roleUpdate'))
.registerEvent(new stateLib.Event('userNoteUpdate'))
.registerEvent(new stateLib.Event('userUpdate'))
.registerEvent(new stateLib.Event('voiceStateUpdate'));
client.on('clientUserGuildSettingsUpdate', (...o) => this.events.clientUserGuildSettingsUpdate.fire(o));
client.on('clientUserSettingsUpdate', (...o) => this.events.clientUserSettingsUpdate.fire(o));
client.on('emojiCreate', (...o) => this.events.emojiCreate.fire(o));
client.on('emojiDelete', (...o) => this.events.emojiDelete.fire(o));
client.on('emojiUpdate', (...o) => this.events.emojiUpdate.fire(o));
client.on('guildBanAdd', (...o) => this.events.guildBanAdd.fire(o));
client.on('guildBanRemove', (...o) => this.events.guildBanRemove.fire(o));
client.on('guildCreate', (...o) => this.events.guildCreate.fire(o));
client.on('guildDelete', (...o) => this.events.guildDelete.fire(o));
client.on('guildMemberAdd', (...o) => this.events.guildMemberAdd.fire(o));
client.on('guildMemberAvailable', (...o) => this.events.guildMemberAvailable.fire(o));
client.on('guildMemberRemove', (...o) => this.events.guildMemberRemove.fire(o));
client.on('guildMemberChunk', (...o) => this.events.guildMemberChunk.fire(o));
client.on('guildMemberSpeaking', (...o) => this.events.guildMemberSpeaking.fire(o));
client.on('guildMemberUpdate', (...o) => this.events.guildMemberUpdate.fire(o));
client.on('guildUnavailable', (...o) => this.events.guildUnavailable.fire(o));
client.on('guildUpdate', (...o) => this.events.guildUpdate.fire(o));
client.on('presenceUpdate', (...o) => this.events.presenceUpdate.fire(o));
client.on('roleCreate', (...o) => this.events.roleCreate.fire(o));
client.on('roleDelete', (...o) => this.events.roleDelete.fire(o));
client.on('roleUpdate', (...o) => this.events.roleUpdate.fire(o));
client.on('userNoteUpdate', (...o) => this.events.userNoteUpdate.fire(o));
client.on('userUpdate', (...o) => this.events.userUpdate.fire(o));
client.on('voiceStateUpdate', (...o) => this.events.voiceStateUpdate.fire(o));
}
}
class DiscordMessageEvents extends stateLib.EventGroup {
constructor(client) {
super();
this._registerMessageEvents(client);
}
/**
* Registeres all client message events
* @param client {Discord.Client}
* @private
*/
_registerMessageEvents(client) {
this.registerEvent(new stateLib.Event('messageDelete'))
.registerEvent(new stateLib.Event('messageDeleteBulk'))
.registerEvent(new stateLib.Event('messageReactionAdd'))
.registerEvent(new stateLib.Event('messageReactionRemove'))
.registerEvent(new stateLib.Event('messageReactionRemoveAll'))
.registerEvent(new stateLib.Event('messageUpdate'))
.registerEvent(new stateLib.Event('message'));
client.on('messageDelete', (...o) => this.events.messageDelete.fire(o));
client.on('messageDeleteBulk', (...o) => this.events.messageDeleteBulk.fire(o));
client.on('messageReactionAdd', (...o) => this.events.messageReactionAdd.fire(o));
client.on('messageReactionRemove', (...o) => this.events.messageReactionRemove.fire(o));
client.on('messageReactionRemoveAll', (...o) => this.events.messageReactionRemoveAll.fire(o));
client.on('messageUpdate', (...o) => this.events.messageUpdate.fire(o));
client.on('message', (...o) => this.events.message.fire(o));
}
}
class DiscordChannelEvents extends stateLib.EventGroup {
constructor(client) {
super();
this._registerChannelEvents(client);
}
/**
* Registers all events for discord channels.
* @param client {Discord.Client}
* @private
*/
_registerChannelEvents(client) {
this.registerEvent(new stateLib.Event('channelCreate'))
.registerEvent(new stateLib.Event('channelDelete'))
.registerEvent(new stateLib.Event('channelPinsUpdate'))
.registerEvent(new stateLib.Event('channelUpdate'))
.registerEvent(new stateLib.Event('typingStart'))
.registerEvent(new stateLib.Event('typingStop'));
client.on('channelCreate', (...o) => this.events.channelCreate.fire(o));
client.on('channelDelete', (...o) => this.events.channelDelete.fire(o));
client.on('channelPinsUpdate', (...o) => this.events.channelPinsUpdate.fire(o));
client.on('channelUpdate', (...o) => this.events.channelUpdate.fire(o));
client.on('typingStart', (...o) => this.events.typingStart.fire(o));
client.on('typingStop', (...o) => this.events.typingStop.fire(o));
}
}
class DiscordClientEvents extends stateLib.EventGroup {
constructor(client) {
super();
this._registerClientEvents(client);
}
/**
* Registers Discord client events
* @param client {Discord.Client}
* @private
*/
_registerClientEvents(client) {
this.registerEvent(new stateLib.Event('debug'))
.registerEvent(new stateLib.Event('warn'))
.registerEvent(new stateLib.Event('error'))
.registerEvent(new stateLib.Event('ready'))
.registerEvent(new stateLib.Event('resume'))
.registerEvent(new stateLib.Event('disconnect'))
.registerEvent(new stateLib.Event('reconnecting'))
.registerEvent(new stateLib.Event('rateLimit'));
client.on('debug', (...o) => this.events.debug.fire(o));
client.on('warn', (...o) => this.events.warn.fire(o));
client.on('error', (...o) => this.events.error.fire(o));
client.on('ready', (...o) => this.events.ready.fire(o));
client.on('resume', (...o) => this.events.resume.fire(o));
client.on('disconnect', (...o) => this.events.disconnect.fire(o));
client.on('reconnecting', (...o) => this.events.reconnecting.fire(o));
client.on('rateLimit', (...o) => this.events.rateLimit.fire(o));
client.on('presenceUpdate', (...o) => this.events.presenceUpdate.fire(o));
}
}

@ -1,99 +0,0 @@
const logging = require('../utils/logging');
class EventRouter {
constructor() {
this._logger = new logging.Logger(this);
this.eventGroups = {};
}
/**
* Fires an event of an event group with event data.
* @param eventGroup {String}
* @param eventName {String}
* @param eventData {Object}
*/
fireEvent(eventGroup, eventName, eventData) {
if (this.eventGroups[eventGroup] instanceof EventGroup)
this.eventGroups[eventGroup].fireEvent(eventName, eventData);
return this;
}
/**
* Adds an EventRoute to the EventRouter
* @param group {EventGroup}
*/
registerEventGroup(group) {
this.eventGroups[group.name] = name;
}
}
class EventGroup {
/**
* Creates a new EventGroup with the given name.
* @param [name] {String}
*/
constructor(name) {
this._logger = new logging.Logger(this);
this.name = name || this.constructor.name;
this.events = {};
}
fireEvent(eventName, eventData) {
if (this.events[eventName] instanceof Event)
this.events[eventName].fire(eventData);
return this;
}
/**
* Registeres an Event to the EventGroup
* @param event {Event}
*/
registerEvent(event) {
this.events[event.name] = event;
}
}
class Event {
/**
* Creates a new Event with the given name.
* @param name
*/
constructor(name) {
this._logger = new logging.Logger(this);
this.name = name;
this.handlers = [];
}
/**
* Adds an event handler to the Event
* @param handler {Function}
*/
addHandler(handler) {
this.handlers.push(handler);
return this;
}
/**
* Fires the event with the given data.
* @param data {Object}
*/
fire(data) {
for (let handler in this.handlers)
try {
handler(data);
} catch (err) {
this._logger.verbose(err.message);
this._logger.silly(err.stack);
}
}
}
Object.assign(exports, {
EventRouter: EventRouter,
EventGroup: EventGroup,
Event: Event
});

@ -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,
});

@ -265,6 +265,19 @@ class GenericSql {
}
}
/**
* 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}

@ -129,6 +129,18 @@ async function resolveNestedPromise (promise) {
/* Classes */
class Enum {
/**
* Constructor.
* @param symbols {Array<String>}
*/
constructor(symbols) {
for (let symbol in symbols)
this[symbol] = symbols;
Object.freeze(this);
}
}
class YouTube {
/**
* returns if an url is a valid youtube url (without checking for an entity id)
@ -279,5 +291,6 @@ Object.assign(exports, {
getExtension: getFileExtension,
getFileExtension: getFileExtension,
objectDeepFind: objectDeepFind,
Cleanup: Cleanup
Cleanup: Cleanup,
Enum: Enum
});

@ -0,0 +1,145 @@
const graphql = require('graphql'),
fs = require('fs');
class BotGraphql {
constructor(bot) {
this.schema = graphql.buildSchema(fs.readFileSync('.lib/web/schema.graphqls', 'utf-8'));
this.root = {
Query: new Query(bot)
};
}
}
/**
* Easyer managing of page info
*/
class PageInfo {
/**
* constructor.
* @param total {Number} - the total number of entries
* @param perPage {Number} - the number of entries per page
* @param currentPage {Number} - the current page's index
* @param lastPage {Number} - the index of the last page
* @param hasNext {Boolean} - is there a next page?
*/
constructor(total, perPage, currentPage, lastPage, hasNext) {
this.total = total;
this.perPage = perPage;
this.currentPage = currentPage;
this.lastPage = lastPage;
this.hasNext = hasNext;
}
}
/**
* Generic edge
*/
class Edge {
/**
* contructor.
* @param node {Object} - the node belonging to the edge
* @param edgeProps {Object} - additional properties of the edge
*/
constructor(node, edgeProps) {
this.node = node;
Object.assign(this, edgeProps);
}
}
/**
* Generic connection
*/
class Connection {
/**
* constructor.
* @param edges {Array<Edge>} - the edges of the connection
* @param pageInfo {PageInfo} - page info for the connection
*/
constructor(edges, pageInfo) {
this.edges = edges;
this.nodes = this.edges.map(x => x.node);
this.pageInfo = pageInfo;
}
}
/**
* Manages pagination
*/
class Paginator {
/**
* constructor.
* @param edges {Array<Object>} - the edges for the pages
* @param perPage {Number} - the number of entries per page
*/
constructor(edges, perPage) {
this._entries = edges;
this.perPage = perPage;
}
/**
* Get the specific page
* @param page {Number} - the page's number
* @param [perPage] {Number} - the number of entries per page
* @returns {Connection}
*/
getPage(page, perPage) {
perPage = perPage || this.perPage;
let startIndex = (page - 1) * perPage;
let endIndex = startIndex + perPage;
let lastPage = Math.ceil(this._entries.length / perPage);
return new Connection(
this._entries.slice(startIndex, endIndex) || [],
new PageInfo(this._entries.length, perPage, page, lastPage, page !== lastPage)
);
}
/**
* Updates the entries of the Paginator.
* @param entries
*/
updateEntries(entries) {
this._entries = entries;
}
}
class MusicPlayer {
constructor(guildHandler) {
}
}
class Guild {
constructor(guild) {
}
}
class Client {
constructor(bot) {
this._bot = bot;
this._client = bot.client;
this.guildPaginator = new Paginator()
}
_getGuildEdges() {
let guildHandlerPaginator = Array.from(this._client.guilds.values()).map(x => new Edge(
));
return Array.from(this._client.guilds.values()).map(x => new Edge(
new Guild(x),
{
musicPlayer: new MusicPlayer(this._bot.getGuildHandler(x)),
new Connection(
bot.getGuildHandler(x).savedMedia
)
}
));
}
}
class Query {
constructor(bot) {
this._bot = bot;
this.client = new Client(bot);
}
}

@ -0,0 +1,440 @@
const graphql = require('graphql');
let pageInfoType = new graphql.GraphQLObjectType({
name: 'PageInfo',
fields: {
total: {
type: graphql.assertNonNullType(graphql.GraphQLInt),
description: 'total number of pages'
},
perPage: {
type: graphql.assertNonNullType(graphql.GraphQLInt),
description: 'number of entries per page'
},
currentPage: {
type: graphql.assertNonNullType(graphql.GraphQLInt),
description: 'current page'
},
lastPage: {
type: graphql.assertNonNullType(graphql.GraphQLInt),
description: 'last page'
},
hasNextPage: {
type: graphql.assertNonNullType(graphql.GraphQLBoolean),
description: 'does the connection have a next page'
}
}
});
let mediaEntryType = new graphql.GraphQLObjectType({
name: 'MediaEntry',
fields: {
id: {
type: graphql.GraphQLID,
description: 'id of the media entry'
},
url: {
type: graphql.GraphQLString,
description: 'url to the YouTube video'
},
name: {
type: graphql.GraphQLString,
description: 'title of the YouTube video'
},
thumbnail: {
type: graphql.GraphQLString,
description: 'thumbnail of the YouTube video'
}
}
});
let mediaEntryEdgeType = new graphql.GraphQLObjectType({
name: 'MediaEntryEdge',
fields: {
id: {
type: graphql.GraphQLID,
description: 'the connection id'
},
position: {
type: graphql.GraphQLInt,
description: 'position in the queue'
},
node: {
type: mediaEntryType,
description: 'the media entry node'
}
}
});
let mediaEntryConnectionType = new graphql.GraphQLObjectType({
name: 'MediaEntryConnection',
fields: {
edges: {
type: graphql.GraphQLList(mediaEntryEdgeType)
},
nodes: {
type: graphql.GraphQLList(mediaEntryType)
},
pageInfo: {
type: graphql.assertNonNullType(pageInfoType),
description: 'pagination information'
}
}
});
let musicPlayerType = new graphql.GraphQLObjectType({
name: 'MusicPlayer',
fields: {
queue: {
type: new graphql.GraphQLList(mediaEntryConnectionType),
description: 'media entries in the music queue'
},
queueCount: {
type: graphql.GraphQLInt,
description: 'number of media entries in the queue'
},
songStartTime: {
type: graphql.GraphQLString
},
playing: {
type: graphql.GraphQLBoolean
},
volume: {
type: graphql.GraphQLFloat
},
repeat: {
type: graphql.GraphQLBoolean
},
currentSong: {
type: mediaEntryType
},
quality: {
type: graphql.GraphQLString
},
voiceChannel: {
type: graphql.GraphQLString
},
connected: {
type: graphql.GraphQLBoolean
},
paused: {
type: graphql.GraphQLBoolean
}
}
});
let presenceType = new graphql.GraphQLObjectType({
name: 'Presence',
fields: {
game: {
type: graphql.GraphQLString
},
status: {
type: graphql.GraphQLString
}
}
});
let userType = new graphql.GraphQLObjectType({
name: 'User',
fields: {
id: {
type: graphql.GraphQLID
},
discordId: {
type: graphql.GraphQLID
},
name: {
type: graphql.GraphQLString
},
avatar: {
type: graphql.GraphQLString
},
bot: {
type: graphql.GraphQLBoolean
},
tag: {
type: graphql.GraphQLString
},
presence: {
type: presenceType
}
}
});
let guildMemberType = new graphql.GraphQLObjectType({
name: 'GuildMember',
fields: {
id: {
type: graphql.assertNonNullType(graphql.GraphQLID),
description: 'id of the guild member'
},
discordId: {
type: graphql.GraphQLID
},
user: {
type: userType,
description: 'the user instance of the guild member'
},
nickname: {
type: graphql.GraphQLString,
description: 'the nickname of the guild member'
},
roles: {
type: graphql.GraphQLList(roleType),
description: 'the roles of the guild member'
},
highestRole: {
type: roleType,
description: 'the highest role of the guild member'
}
}
});
let userRoleEdgeType = new graphql.GraphQLObjectType({
name: 'userRoleEdge',
fields: {
id: {
type: graphql.GraphQLID,
description: 'the connection id'
},
node: {
type: guildMemberType,
description: 'guild member edge of the role'
},
isHighest: {
type: graphql.GraphQLBoolean,
description: 'is the role the highest of the guild member'
}
}
});
let userRoleConnectionType = new graphql.GraphQLObjectType({
name: 'UserRoleConnection',
fields: {
edges: {
type: graphql.GraphQLList(userRoleEdgeType)
},
nodes: {
type: graphql.GraphQLList(userType)
},
pageInfoType: {
type: graphql.assertNonNullType(pageInfoType),
description: 'pagination information'
}
}
});
let roleType = new graphql.GraphQLObjectType({
name: 'Role',
fields: {
id: {
type: graphql.GraphQLID
},
discordId: {
type: graphql.GraphQLID
},
name: {
type: graphql.GraphQLString
},
color: {
type: graphql.GraphQLString
},
members: {
type: userRoleConnectionType
}
}
});
let userGuildConnectionType = new graphql.GraphQLObjectType({
name: 'UserGuildConnection',
fields: {
edges: {
type: graphql.GraphQLList(guildMemberType)
},
nodes: {
type: graphql.GraphQLList(userType)
},
pageInfoType: {
type: graphql.assertNonNullType(pageInfoType),
description: 'pagination information'
}
}
});
let guildType = new graphql.GraphQLObjectType({
name: 'Guild',
fields: {
id: {
type: graphql.GraphQLID
},
discordId: {
type: graphql.GraphQLID
},
name: {
type: graphql.GraphQLString
},
owner: {
type: guildMemberType
},
members: {
type: userGuildConnectionType
},
memberCount: {
type: graphql.GraphQLInt
},
roles: {
type: graphql.GraphQLList(roleType),
description: 'the roles of the guild'
},
icon: {
type: graphql.GraphQLString
}
}
});
let guildEdgeType = new graphql.GraphQLObjectType({
name: 'GuildEdge',
fields: {
id: {
type: graphql.GraphQLID,
description: 'id of the connection'
},
node: {
type: guildType
},
musicPlayer: {
type: musicPlayerType,
description: 'guilds music player'
},
savedMedia: {
type: mediaEntryConnectionType,
description: 'saved media entries'
}
}
});
let guildConnectionType = new graphql.GraphQLObjectType({
name: 'GuildConnection',
edges: {
type: guildEdgeType
},
nodes: {
type: graphql.GraphQLList(guildType)
},
pageInfo: {
type: pageInfoType,
description: 'pagination information'
}
});
let clientType = new graphql.GraphQLObjectType({
name: 'Client',
fields: {
guilds: {
type: [guildType]
},
guildCount: {
type: graphql.GraphQLInt
},
voiceConnectionCount: {
type: graphql.GraphQLInt
},
user: {
type: userType
},
ping: {
type: graphql.GraphQLFloat
},
status: {
type: graphql.GraphQLInt
},
uptime: {
type: graphql.GraphQLInt
}
}
});
let logLevelEnum = new graphql.GraphQLEnumType({
name: 'LogLevel',
description: 'log levels of log entries',
values: {
SILLY: {
value: 'silly'
},
DEBUG: {
value: 'debug'
},
VERBOSE: {
value: 'verbose'
},
INFO: {
value: 'info'
},
WARN: {
value: 'warn'
},
ERROR: {
value: 'error'
}
}
});
let logEntryType = new graphql.GraphQLObjectType({
name: 'LogEntry',
fields: {
id: {
type: graphql.assertNonNullType(graphql.GraphQLID),
description: 'id of the log entry'
},
message: {
type: graphql.GraphQLString,
description: 'log entry content'
},
level: {
type: logLevelEnum,
description: 'log level of the log entry'
},
timestamp: {
type: graphql.GraphQLString,
description: 'timestamp of the log entry'
},
module: {
type: graphql.GraphQLString,
description: 'module that logged the entry'
}
}
});
const queryType = new graphql.GraphQLObjectType({
name: 'Query',
fields: {
client: {
type: clientType,
description: 'client instance of the bot'
},
presences: {
type: graphql.assertNonNullType(graphql.GraphQLList(presenceType)),
description: 'presences of the bot'
},
prefix: {
type: graphql.GraphQLString,
description: 'prefix of the bot'
},
logs: {
type: graphql.GraphQLList(logEntryType),
description: 'log entries of the bot'
}
}
});
Object.assign(exports, {
queryType: queryType
});

@ -0,0 +1,429 @@
enum LogLevel {
SILLY
DEBUG
VERBOSE
INFO
WARN
ERROR
}
type PageInfo {
# the total number of entries on all pages
total: Int
# the number of entries per page
perPage: Int
# the current page
currentPage: Int
# the last page
lastPage: Int
# If there is a next page
hasNextPage: Boolean
}
type MediaEntry {
# the id of the media entry
id: ID!
# the url to the YouTube video
url: String!
# the title of the YouTube video
name: String!
# the url of the YouTube video's thumbnail
thumbnail: String!
}
type MediaEntryEdge {
# the id of the edge
id: ID!
node: MediaEntry
# the position of the entry in the queue
position: Int
}
type MediaEntryConnection {
edges: [MediaEntryEdge]
nodes: [MediaEntry]
# the pagination information
pageInfo: PageInfo
}
type MusicPlayer {
# the content of the music players queue
#
# Arguments
# id: get the media entry by id
# page: get the page by number
# perPage: the number of entries per page
queue(
id: ID,
page: Int,
perPage: Int
): MediaEntryConnection
# the current position in the song
songPosition: Int
# if the music player is currently playing
playing: Boolean!
# the volume of the music player
volume: Float
# if the music player plays on repeat
repeat: Boolean
# the currently playing song
currentSong: MediaEntry
# the quality of the music that is played (YouTube quality)
quality: String
# the name of the voice channel the music player is playing in
voiceChannel: String
# if the music player is connected to a voice channel
connected: Boolean!
# if the music player is paused
paused: Boolean!
}
type User {
# the id of the user
id: ID!
# the discord id of the user
discordId: String
# the name of the user
name: String!
# the url of the users avatar
avatar: String
# if the user is a bot
bot: Boolean
# the discord tag of the user
tag: String!
# the current presence of the user
presence: Presence
}
type Role {
# the id of the role
id: ID!
# the discord id of the role
discordId: String
# the name of the role
name: String
# the color of the role
color: String
}
type GuildMemberRoleEdge {
# the id of the edge
id: ID!
node: GuildMember
# if the role is the highest of the guild member
isHighest: Boolean
}
type GuildMemberRoleConnection {
edges: [GuildMemberRoleEdge]
nodes: [GuildMember]
# the pagination information
pageInfo: PageInfo
}
type GuildRoleEdge {
# the id of the edge
id: ID!
node: Role
# the members in the role
#
# Arguments
# id: get the member by id
# page: get the page by number
# perPage: the number of entries per page
members(
id: ID,
page: Int,
perPage: Int
): GuildMemberRoleConnection
}
type GuildRoleConnection{
edges: [GuildRoleEdge]
nodes: [Role]
# the pagination information
pageInfo: PageInfo
}
type GuildMember {
# the id of the guild member
id: ID!
# the discord id of the guild member
discordId: String
# the user associated with the guild member
user: User
# the nickname of the guild member
nickname: String
# the roles of the guild member
roles(
first: Int = 10,
offset: Int = 0,
id: String
): [Role]
# the highest role of the guild member
highestRole: Role
}
type GuildMemberEdge {
# the id of the edge
id: ID!
node: GuildMember
# if the guild member is the server owner
isOwner: Boolean
}
type GuildMemberConnection{
edges: [GuildMemberEdge]
nodes: [GuildMember]
# the pagination information
pageInfo: PageInfo
}
type Guild {
# the id of the guild
id: ID!
# the discord id of the guild
discordId: ID
# the guild's name
name: String
# the owner of the guild
owner: GuildMember
# the members in the guild
#
# Arguments
# id: get the member by id
# page: get the page by number
# perPage: the number of entries per page
members(
id: ID,
page: Int,
perPage: Int
): GuildMemberConnection
# the roles of the guild
#
# Arguments
# id: get the role by id
# page: get the page by number
# perPage: the number of entries per page
roles(
id: ID,
page: Int,
perPage: Int
): GuildRoleConnection
# the url of the guild icon
icon: String
}
type GuildEdge {
# the id of the edge
id: ID!
node: Guild
# the music player associated with the guild
musicPlayer: MusicPlayer
# the saved media of the guild
savedMedia: mediaEntryConnection
}
type GuildConnection {
edges: [GuildEdge]
nodes: [Guild]
# the pagination information
pageInfo: PageInfo
}
type Client {
# the guilds the client has joined
#
# Arguments
# id: get the guild by id
# page: get the page by number
# perPage: the number of entries per page
guilds (
id: ID,
page: Int,
perPage: Int
): GuildConnection
# the number of voice connections
voiceConnectionCount: Int
# the bot user
user: User
# the current average ping
ping: Float
# the websocket status
status: Int
# the total uptime
uptime: Int
}
type LogEntry {
# the id of the log entry
id: ID!
# the message of the log entry
message: String
# the level of the log entry
level: Level
# the timestamp of the log entry
timestamp: String
# the module that created the log entry
module: String
}
type Query {
# The bots client
client: Client
# the presences in the presence rotation
presences: [String]!
# the prefix for commands
prefix: String
# The log entries generated in the current runtime.
#
# Arguments
# first: the number of entries to get
# offset: the offset of the entries
# id: get the log entry by id
# last: oposite of first - the latest entries
# level: filter by loglevel
logs(
first: Int,
offset: Int = 0,
id: String,
last: Int = 10,
level: LogLevel
): [LogEntry]
}
type Mutation {
# adds media to the queue
#
# Arguments
# guildId: the id of the guild
# url: the url to the media YouTube video
addMediaToQueue(
guildId: ID!,
url: String!
): MusicPlayer
# removes media from the queue
#
# Arguments
# guildId: the id of the guild
# entryId: the id of the media entry to remove
removeMediaFromQueue(
guildId: ID!,
entryId: ID!
): MusicPlayer
# skips to the next song
#
# Arguments
# guildId: the id of the guild
skipSong(guildId: ID!): MusicPlayer
# toggles between pause and play
#
# Arguments
# guildId: the id of the guild
togglePause(guildId: ID!): MusicPlayer
# toggles repeat
#
# Arguments
# guildId: the id of the guild
toggleRepeat(guildId: ID!): MusicPlayer
# stops the music
#
# Arguments
# guildId: the id of the guild
stopMusic(guildId: ID!): MusicPlayer
}
Loading…
Cancel
Save