Skip to content

Instantly share code, notes, and snippets.

@Khaaz
Created July 29, 2020 13:14
Show Gist options
  • Select an option

  • Save Khaaz/588fd5775fc3b71a700951579a9f7ed1 to your computer and use it in GitHub Desktop.

Select an option

Save Khaaz/588fd5775fc3b71a700951579a9f7ed1 to your computer and use it in GitHub Desktop.

Framework description

COMMAND FLOW

  • regular execution
  • admin execution: ignore permissions / blacklisted guild etc (debugging purpose)
  • owner execution:

Command execution

User type !ping.
AxonClient catch messageCreate event emitted by eris and AxonCore.CommandDispatcher dispatches to the correct command.
Dispatcher

AxonCore.Executor executes the command. and catch the result.
Executor

⚠️ Guild-disable for modules and command are checked upon resolving command.

You can only disable whole command and module. Which mean that if a module is disabled, all command in that module will also be disabled. If a Command is disabled all commands will be disabled.
With a few tweaks to GuildConfig it would be feasible to allow subcommand disable.
Additionally, command can be globally disabled easily. This is checked upon Command resolve as well.

AxonCore.Executor calls command._process. This methods always returns a CommandContext Object or throws an AxonCommandError error.
An AxonCommandError will only be thrown if an unexpected error happens. Developper is expected to catch all other errors and respond accordingly to the user. EG:

  • External API error are expected and should be handled in the Command.execute function. It should respond to the user with the appropriate answer.
  • typo is unexpected and will raise an AxonCommandError. It will be catched at Executor level and logged.
  • Internal Database error could either be handled or not. If not handled and let spread, these errors will also raise an AxonCommandError.
    AxonCommandError will basically wrap the error with additional context (guild, user, time etc of execution).

AxonCore.Command process the command and check for permissions. It will returns a CommandContext with the exact info of the execution, why it didn't execute etc...
Command._process

Inside the Command.execute method, you write code so the bot sends pong!

Structure:

/** @prop {String} raw - The raw message content
  * @prop {Eris.Message} msg - The message object from the lib
  * @prop {Array<String>} args - The array of arguments
  * @prop {GuildConfig} guildConfig - The GuildConfig data-structure with all DB saved settings
  * @prop {String} prefix - The prefix used for this command
  * @prop {String} command - The full label of the command being executed
  * @prop {COMMAND_EXECUTION_TYPES} executionType - Execution type: admin, owner, regular
  */

This is the object passed in command.execute used by the developer in the execute function. Its 3 primary attributes are msg, args and guildConfig.

Structure:

/**
  * @prop {Boolean} success
  * @prop {Boolean} triggerCooldown
  * @prop {Error|null} error
  */

A Command always should returns a Promise wrapping a CommandResponse. Either explicitely by creating a CommandResponse and wrapping it in a Promise (CommandResponse.resolve()), or implicitely via

  • sendMessage
  • sendSuccess : sucess = true, triggerCooldown = true
  • sendError : success = false, triggerCooldown = false

CommandResponse in these last cases is filled with appropriate information regarding command execution.
triggerCooldown can be put at true or false depending whether you want or not trigger Command Cooldown.

Structure:

/** @prop {String} raw - Raw input
  * @prop {String} commandLabel - The command full label
  * @prop {String} moduleLabel - The module name
  * STATUS
  * @prop {Boolean} [executed=true] - Whether the command was actually executed or not
  * @prop {Boolean} [data.helpExecution=false]
  * @prop {COMMAND_EXECUTION_STATE} executionState - The state of execution (no error, cooldown, invalid usage, invalid permission)
  * @prop {COMMAND_EXECUTION_TYPES} executionType - The type of execution (Owner, Admin, Regular)
  * @prop {Boolean} [success=true] - Whether the command was successfully executed or not
  * @prop {Object|String} [error=null] - Optional error object in case of bad command execution
  * CONTEXT
  * @prop {Boolean} [dm=false] - Whether the command was executed in DM or not
  * @prop {String} [guildID=null] - Context: guild where the command was executed ID
  * @prop {String} [guildName=null] - Context: guild where the command was executed name
  * @prop {String} [channelID=null] - Context: channel where the command was executed ID
  * @prop {String} [channelName=null] - Context: channel where the command was executed name
  * @prop {String} [callerID=null] - Context: user that called the command ID
  * @prop {String} [callerName=null] - Context: user that called the command name
  * @prop {Date} [calledAt=Date.now()] - The execution time
  */

This is the object returned by the command._process method. It is used for logging and is also emitted in the commandExecution event.

Bypass / needed

Permissions are split with a few levels for full customability.
A user will need to fullfill ALL needed requirements in order to have the permission to execute the command.
A user will need to fullfill AT LEAST ONE bypass requirements in order to have the permission to execute the command.

EG:
A command has a roleID needed requirement. Anyone that don't have this role ID can't use the command.
The command also has a userID bypass requirement. Anyone that has this user ID will be able to use the command no matter what. All other users will need to fullfill all requirements in the needed states.

Permission levels

  • owners - Wether the command is limited to the server owner
  • administrators - Wether the command is limited to server administrator [administrator] or above
  • manager - Wehther the command is limited to server manager [manageServer]
  • moderator - Wether the command is limited to server moderator [moderator (user or role) in guildConfig]

Those are the basic level of permission for a command. Full fledged permission system is explained below.

Moderators

Moderators are custom permissions made by the framework.
A Moderator is a user that is a moderator user or has a moderator role. These 2 things are guild-specific settings that can be found in the guildConfig.

Structure:

/**
  * @prop {Array} [bot=[]] - Discord permissions that the bot needs to have in order to execute the command
  * @prop {Boolean} [serverMod=false] - Axoncore server moderator
  * @prop {Boolean} [serverManager=false] - Discord server manager (manageServer)
  * @prop {Boolean} [serverAdmin=false] - Discord server administrator (administrator)
  * @prop {Boolean} [serverOwner=false] - Discord server owner
  * @prop {Array<String>} [author.needed=[]] - Discord permissions that the user needs to have in order to execute the command
  * @prop {Array<String>} [author.bypass=[]] - Discord permissions that will allow the user to execute the command no matter what
  * @prop {Array<String>} [users.needed=[]] - Discord user ids that the user needs to have in order to execute the command
  * @prop {Array<String>} [users.bypass=[]] - Discord user ids that will allow the user to execute the command no matter what
  * @prop {Array<String>} [roles.needed=[]] - Discord role ids that the user needs to have in order to execute the command
  * @prop {Array<String>} [roles.bypass=[]] - Discord role ids that will allow the user to execute the command no matter what
  * @prop {Array<String>} [channels.needed=[]] - Discord channel ids that the user needs to have in order to execute the command
  * @prop {Array<String>} [channels.bypass=[]] - Discord channel ids that will allow the user to execute the command no matter what
  * @prop {Array<String>} [guilds.needed=[]] - Discord guild ids that the user needs to have in order to execute the command
  * @prop {Array<String>} [guilds.bypass=[]] - Discord guild ids that will allow the user to execute the command no matter what
  * @prop {Array<String>} [staff.needed=[]] - Axoncore staff ids that the user needs to have in order to execute the command
  * @prop {Array<String>} [staff.bypass=[]] - Axoncore staff ids that will allow the user to execute the command no matter what
  * @prop {Function} [custom=()=>true] Custom function that returns a boolean. True will let the command execute, False will prevent the command from executing
  */

ERROR HANDLING / USAGE TRACKING

Execution events

Two types of events will then be emitted, depending on the scenario:

  • The command was executed entirely and successfully. A commandExecution event is emitted by AxonClient with status = true.
  • The command was called but something blocked the execution and it was not successfully executed. This can be due to a missing permission, invalid usage, or even a condition in the command.execute method (via the sendError method for instance). In this case a commandExecution event is emitted with status = false.
  • The command was executed but an error occured in the execution (API error, code error...). A commandError event is then emitted.

You can listen to these events according to the following example.

axonClient.on('commandExecution', (status: Boolean, commandLabel: String, { msg: Message, command: Command, guildConfig: GuildConfig, context: CommandContext }) => {} );
axonClient.on('commandError', (commandLabel: String, { msg: Message, command: Command, guildConfig: GuildConfig, err: AxonCommandError }) => {} ); // err.context = CommandContext

For listener execution, listenerExecution and listenerError events are also emitted by AxonClient.

axonClient.on('listenerExecution', (status: Boolean, eventName: String, listenerName: String, { listener: Listener, guildConfig: GuildConfig }) => {} ); // event: the event name, listener: the Listener object
axonClient.on('listenerError', (eventName: String, listenerName: String, { listener: Listener, guildConfig: GuildConfig, err: Error }) => {} );

Error structure

  • AxonError

  • AxonCommandError

Emitted when a Command fails unexpectedly.
Created with the context (CommandContext) and the optional error.

/**
  * @prop {CommandContext} context - Command Context containing all informations about the command execution
  * @prop {String} short - original error short filled with context info
  * @prop {String} message - original error message filled with context info
  * @prop {String} stack - original error stack filled with context info
  */

CLIENT LIFECYCLE

AxonClient and Bot (eris) client are decloupled. So you create your eris client yourself and pass that in AxonClient parameters. This allows you to have full control on the client options.

During AxonClient creation, there are several lifecycle method that you can override to customise the Client.

  • onInit(): called in AxonClient contructor, just before initialising modules and commands.
  • onStart(): called at the beginning of AxonCore.start() method
  • onReady(): called after the AxonClient ready log. Right after everything is initialised (after events are bound)
  • onBotReady(): called when the botClient is ready.

There are also other methods (not lifecycle methods) you can override to change how the Client will behave such as:

  • initErrorListeners: Init global errors listeners (eris error and warn events, process uncaughtException and unhandledRejection).
  • initStatus: Setup the bot status, override this method to customise the bot status.

AxonClient will emit a debug event, during command and modules initialisation and on each command execution.
With debugMode enabled, AxonCore will automatically listen to this event via the onDebug method. Overriding this method will allow to customise how it logs information.

Additionally, overriding sendFullHelp will allow to entirely customise the help command.
To customise Command help, you can do it once globally by creating a sendHelp method in the client. Or by overriding the sendHelp method in the Command class directly.

HOOKS

TODO

Create a Command by extending Command and overriding the execute method.

Creating a Subcommand is exactly like creating a command. In the parent command, you just need to add the command in the init method (see ping pong example here)

EVENT MANAGER

An event is a Discord event.
A Listener is a function that is run when a Discord-specific event occurs. Many listeners can be bound to one Discord event.
A Handler is an object responsible of running all listeners for a specific Discord event.

You will create Listeners bound to specific discord events. That will allow you to split your listeners in different functions.

Create a Listener by extending Listener and overriding the execute method.
Listeners allow you to split your code in separate funciton instead of filling one event listener with a lot of code.
Listeners can be disabled, and they are also part of a module which mean module disable will also work out of the box.
Additionally any event happening in a guild will transport the guildConfig object, allowing to use guild specific config easily.

DATABASE

GuildConfig

GuildConfig is an object used internally that holds all guild settings.
GuildConfig object is created upon fetching guildSettings from the DB.
It has a set of method used internally to GET info from that object. It means that GuildConfig doesn't depend upon property names but upon the GET methods access.
It has a set of methods used to SET properties. This will have for benefits to at the same time update the DB and the cache by one method call directly on the GuildConfig object.
The cache will by consequence always be kept sync with the DB.

AxonConfig

AxonConfig behave the same way. However AxonConfig is a global document used for all AxonCore settings (eg: global blacklist etc). It will have a lot of less WRITE. And will only ever exists as one document in the DB.

Cache management

GuildConfig cache is implemented as LRUCache. LRUCache has a limit, and it will drop the least recently used element when said cache hits the limits.
GuildConfigs are fetched on messageCreate event like:
GuildConfig cache

Database override

Overriding and customising the Database can be done quite easily. You need to extends GuildConfig and customise it like you want. Lot of the times that can be enough.
It is also possible to extends DBProvider for more customisation.
The two extended class are passed to AxonClient on startup which will then use these instead of default values.

TRANSLATIONS

The idea behind Translations and message management is always the same. You shouldn't have any String hardcoded except for log messages.
AxonCore MessageManagement method is inspired from Android this.R class.

Native solution

There is a native solution for message management and translation support.
All message can be written in json files with String key as identifier. accessible through this.l. Upon calling this.l.KEY, the manager returns a string in the default language for said key. Language can optionally be passed as parameter in order to obtain the message for this specific language.
This gets easily appliable to guild-lang or even channel-lang.

The major advantage of the translation manager is that it supports {{tag}} that can be replaced on the go. This allows to have language-specific string with an invariant being the parameter.
A live example of usage with cooldown for instance would be:

// ERR_COOLDOWN: "Hey {{user}}, you need to wait {{cooldown}} seconds"
 
const cd = Math.ceil( (this.options.cooldown - time) / 100) / 10; // 3.4
const str = this.l.ERR_COOLDOWN( { cooldown: cd, user: msg.channel.author.username } ); 
createMessage(channel.id, str); // "Hey Khaaz, you need to wait 3.4 seconds"

The translation / message manager has 2 major utility:

  • an easy to use message manager with input parsing. Prevents to have hardcoded values and manage strings more easily.
  • an easy to use translation manager with custom lang on the fly.

i18n framework support

An implementation with another i18n framework is definitely possible. You will get guild-language via for instance guildConfig. The you can call directly any other translation framework. There is literally nothing in the framework preventing from using a different implementation and anything should work flawlessly.

LOGGING

Logger is decoupled from AxonCore, and can be customised as will, with a few default options to choose.

Default behavior

There are 7 log levels (higher priority to lower priority) such as:

  • fatal
  • error
  • warn
  • debug
  • notice
  • info
  • verbose

It is advised to use the Chalk logger for a simple colored logging. For more complex behaviour, the Logger can be customised and passed as a custom class in AxonOptions as long as it has these 7 methods.

Additionally, AxonClient has a log method that centralise every internal logging. This method will also trigger the webhook according to the log level. This allows to easily log to discord as well.
Overriding this method will allow to customise this behaviour (eg: sentry support etc).

Transportable winston logger

Although you can customise the logger, there is an already existing Winston logger brought up in the framework.
Winston allow for transportable logs directly in files as well as in the console. With a few plugins you can do a lot more. If a complex logging behaviour is needed, it's advised to use AxonCore winston logger as base and customise it from there.

UTILITIES

(not exhaustive list)

  • Collectors
  • Resolver (adaptable to fit no cache behaviour)
  • Queues and cache structures
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment