Repository: 1Computer1/discord-akairo
Branch: master
Commit: 905f69382957
Files: 92
Total size: 340.5 KB
Directory structure:
gitextract_ns2ldn2j/
├── .eslintrc.json
├── .gitattributes
├── .gitignore
├── .jsdoc.json
├── .npmignore
├── .npmrc
├── .prettierignore
├── .travis.yml
├── LICENSE
├── README.md
├── docs/
│ ├── arguments/
│ │ ├── arguments.md
│ │ ├── compose.md
│ │ ├── custom.md
│ │ ├── functions.md
│ │ ├── generators.md
│ │ ├── matches.md
│ │ ├── prompts.md
│ │ ├── prompts2.md
│ │ ├── types.md
│ │ └── unordered.md
│ ├── basics/
│ │ ├── commands.md
│ │ ├── inhibitors.md
│ │ ├── listeners.md
│ │ └── setup.md
│ ├── commands/
│ │ ├── commandutil.md
│ │ ├── conditional.md
│ │ ├── cooldowns.md
│ │ ├── permissions.md
│ │ ├── prefixes.md
│ │ ├── regex.md
│ │ └── restrictions.md
│ ├── general/
│ │ └── welcome.md
│ ├── index.yml
│ ├── inhibitors/
│ │ ├── inhibtypes.md
│ │ └── priority.md
│ ├── listeners/
│ │ └── emitters.md
│ ├── other/
│ │ ├── clientutil.md
│ │ ├── handlers.md
│ │ ├── handling.md
│ │ ├── mongoose.md
│ │ ├── providers.md
│ │ └── updating.md
│ └── snippets/
│ └── ping.md
├── package.json
├── src/
│ ├── index.d.ts
│ ├── index.js
│ ├── providers/
│ │ ├── MongooseProvider.js
│ │ ├── Provider.js
│ │ ├── SQLiteProvider.js
│ │ └── SequelizeProvider.js
│ ├── struct/
│ │ ├── AkairoClient.js
│ │ ├── AkairoHandler.js
│ │ ├── AkairoModule.js
│ │ ├── ClientUtil.js
│ │ ├── commands/
│ │ │ ├── Command.js
│ │ │ ├── CommandHandler.js
│ │ │ ├── CommandUtil.js
│ │ │ ├── ContentParser.js
│ │ │ ├── Flag.js
│ │ │ └── arguments/
│ │ │ ├── Argument.js
│ │ │ ├── ArgumentRunner.js
│ │ │ └── TypeResolver.js
│ │ ├── inhibitors/
│ │ │ ├── Inhibitor.js
│ │ │ └── InhibitorHandler.js
│ │ └── listeners/
│ │ ├── Listener.js
│ │ └── ListenerHandler.js
│ └── util/
│ ├── AkairoError.js
│ ├── Category.js
│ ├── Constants.js
│ └── Util.js
├── test/
│ ├── bot.js
│ ├── commands/
│ │ ├── args.js
│ │ ├── ayy.js
│ │ ├── condition.js
│ │ ├── condition.promise.js
│ │ ├── embed.js
│ │ ├── eval.js
│ │ ├── f.js
│ │ ├── generate.js
│ │ ├── lock.js
│ │ ├── p.js
│ │ ├── q.js
│ │ ├── separate.js
│ │ ├── sub.js
│ │ ├── test.js
│ │ ├── test2.js
│ │ └── unordered.js
│ ├── listeners/
│ │ ├── invalidMessage.js
│ │ └── message.js
│ └── struct/
│ └── TestClient.js
├── tsconfig.json
└── tslint.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.json
================================================
{
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 9
},
"env": {
"es6": true,
"node": true
},
"rules": {
"no-await-in-loop": "off",
"no-extra-parens": [
"warn",
"all",
{
"nestedBinaryExpressions": false
}
],
"no-template-curly-in-string": "error",
"no-unsafe-negation": "error",
"valid-jsdoc": [
"error",
{
"requireReturn": true,
"requireReturnDescription": false,
"prefer": {
"return": "returns",
"arg": "param"
},
"preferType": {
"String": "string",
"Number": "number",
"Boolean": "boolean",
"object": "Object",
"function": "Function",
"array": "Array",
"date": "Date",
"error": "Error",
"null": "void"
}
}
],
"accessor-pairs": "warn",
"array-callback-return": "error",
"complexity": [
"warn",
25
],
"consistent-return": "error",
"curly": [
"error",
"multi-line",
"consistent"
],
"dot-location": [
"error",
"property"
],
"dot-notation": "error",
"eqeqeq": [
"error",
"smart"
],
"no-console": "warn",
"no-empty-function": "error",
"no-floating-decimal": "error",
"no-implied-eval": "error",
"no-invalid-this": "error",
"no-lone-blocks": "error",
"no-multi-spaces": "error",
"no-new-func": "error",
"no-new-wrappers": "error",
"no-new": "error",
"no-octal-escape": "error",
"no-return-assign": "error",
"no-return-await": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-throw-literal": "error",
"no-unmodified-loop-condition": "error",
"no-unused-expressions": "error",
"no-useless-call": "error",
"no-useless-concat": "error",
"no-useless-escape": "error",
"no-useless-return": "error",
"no-void": "error",
"no-warning-comments": "warn",
"require-await": "warn",
"wrap-iife": "error",
"yoda": "error",
"no-label-var": "error",
"no-shadow": "error",
"no-undef-init": "error",
"callback-return": "error",
"handle-callback-err": "error",
"no-mixed-requires": "error",
"no-new-require": "error",
"no-path-concat": "error",
"array-bracket-spacing": "error",
"block-spacing": "error",
"brace-style": [
"error",
"1tbs",
{
"allowSingleLine": true
}
],
"capitalized-comments": [
"error",
"always",
{
"ignoreConsecutiveComments": true
}
],
"comma-dangle": [
"error",
"never"
],
"comma-spacing": "error",
"comma-style": "error",
"computed-property-spacing": "error",
"consistent-this": [
"error",
"$this"
],
"eol-last": "error",
"func-names": "error",
"func-name-matching": "error",
"func-style": [
"error",
"declaration",
{
"allowArrowFunctions": true
}
],
"indent": [
"error",
4,
{
"MemberExpression": 1
}
],
"key-spacing": "error",
"keyword-spacing": "error",
"max-depth": [
"error",
7
],
"max-nested-callbacks": [
"error",
{
"max": 4
}
],
"max-statements-per-line": [
"error",
{
"max": 2
}
],
"new-cap": "error",
"no-array-constructor": "error",
"no-inline-comments": "error",
"no-lonely-if": "error",
"no-mixed-operators": "error",
"no-multiple-empty-lines": [
"error",
{
"max": 2,
"maxEOF": 1,
"maxBOF": 0
}
],
"no-new-object": "error",
"no-spaced-func": "error",
"no-trailing-spaces": "error",
"no-unneeded-ternary": "error",
"no-whitespace-before-property": "error",
"object-curly-spacing": [
"error",
"always"
],
"operator-assignment": "error",
"operator-linebreak": [
"error",
"before"
],
"padded-blocks": [
"error",
"never"
],
"quote-props": [
"error",
"as-needed"
],
"quotes": [
"error",
"single"
],
"semi-spacing": "error",
"semi": "error",
"space-before-blocks": "error",
"space-before-function-paren": [
"error",
{
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}
],
"space-in-parens": "error",
"space-infix-ops": "error",
"space-unary-ops": "error",
"spaced-comment": "error",
"unicode-bom": "error",
"arrow-parens": [
"error",
"as-needed"
],
"arrow-spacing": "error",
"no-duplicate-imports": "error",
"no-useless-computed-key": "error",
"no-useless-constructor": "error",
"prefer-const": "error",
"prefer-arrow-callback": "error",
"prefer-numeric-literals": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "error",
"rest-spread-spacing": "error",
"template-curly-spacing": "error",
"yield-star-spacing": "error"
}
}
================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
================================================
FILE: .gitignore
================================================
node_modules
.git
.vscode
test/auth.json
test/db.sqlite
================================================
FILE: .jsdoc.json
================================================
{
"tags": {
"allowUnknownTags": true,
"dictionaries": ["jsdoc"]
},
"source": {
"include": ["README.md", "src", "package.json"],
"includePattern": ".js$",
"excludePattern": "node_modules/"
},
"opts": {
"encoding": "utf8",
"private": true,
"recurse": true,
"sort": true
}
}
================================================
FILE: .npmignore
================================================
# NPM
node_modules
.git
.vscode
.eslintrc.json
.gitattributes
.gitignore
.travis.yml
test/
docs/
tsconfig.json
tslint.json
================================================
FILE: .npmrc
================================================
package-lock=false
================================================
FILE: .prettierignore
================================================
*
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- "12"
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 1Computer1
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
## Features
#### Completely modular commands, inhibitors, and listeners.
- Reading files recursively from directories.
- Adding, removing, and reloading modules.
- Creating your own handlers and module types.
#### Flexible command handling and creation.
- Command aliases.
- Command throttling and cooldowns.
- Client and user permission checks.
- Running commands on edits and editing previous responses.
- Multiple prefixes and mention prefixes.
- Regular expression and conditional triggers.
#### Complex and highly customizable arguments.
- Support for quoted arguments.
- Arguments based on previous arguments.
- Several ways to match arguments, such as flag arguments.
- Casting input into certain types.
- Simple types such as string, integer, float, url, date, etc.
- Discord-related types such as user, member, message, etc.
- Types that you can add yourself.
- Asynchronous type casting.
- Prompting for input for arguments.
- Customizable prompts with embeds, files, etc.
- Easily include dynamic data such as the incorrect input.
- Infinite argument prompting.
#### Blocking and monitoring messages with inhibitors.
- Run at various stages of command handling.
- On all messages.
- On messages that are from valid users.
- On messages before commands.
#### Helpful events and modular listeners.
- Events for handlers, such as loading modules.
- Events for various stages of command handling.
- Reloadable listeners to easily separate your event handling.
#### Useful utilities and database providers.
- Resolvers for members, users, and others that can filter by name.
- Shortcut methods for making embeds and collections.
- Simple to use database providers.
- Built-in support for `sqlite` and `sequelize`.
- Works on entire table or single JSON column.
- Caching data from databases.
## Installation
Requires Node 16.6.0+ and Discord.js v13.
*discord-akairo*
`npm install discord-akairo`
*discord.js*
`npm install discord.js`
*sqlite (optional)*
`npm install sqlite`
*sequelize (optional)*
`npm install sequelize`
## Links
- [Website](https://discord-akairo.github.io)
- [Repository](https://github.com/discord-akairo/discord-akairo)
- [Changelog](https://github.com/discord-akairo/discord-akairo/releases)
- [Discord](https://discord.gg/arTauDY)
## Contributing
Open an issue or a pull request!
Everyone is welcome to do so.
Make sure to run `npm test` before committing.
================================================
FILE: docs/arguments/arguments.md
================================================
# Basic Arguments
### Adding Numbers
Commands should also have some user input, in the form of arguments.
In Akairo, arguments are the most complex things ever, so this tutorial will only go through the basics.
Let's make a command that takes three numbers and adds them up.
```js
const { Command } = require('discord-akairo');
class AddCommand extends Command {
constructor() {
super('add', {
aliases: ['add']
});
}
exec(message) {
// This doesn't work!
return message.reply(a + b + c);
}
}
module.exports = AddCommand;
```
Now we will add arguments in the command options with the `args` option.
This option must be an array of objects, containing info for parsing.
```js
const { Command } = require('discord-akairo');
class AddCommand extends Command {
constructor() {
super('add', {
aliases: ['add'],
args: [
{
id: 'numOne',
type: 'number',
default: 0
},
{
id: 'numTwo',
type: 'number',
default: 0
},
{
id: 'numThree',
type: 'number',
default: 0
}
]
});
}
exec(message, args) {
const sum = args.numOne + args.numTwo + args.numThree;
return message.reply(`The sum is ${sum}!`);
}
}
module.exports = AddCommand;
```
Arguments must always have an `id`, it will be what you use to refer to them in `args`.
The `type` options is optional, but since we want numbers, it is set to `number`.
The `default` is what is used if there are no input or no number input.
By default, arguments are able to be quoted (you can disable this by having the `quoted` option set to false).
So, technically, this works (although it won't be an actual number input): `?add "hello world" 2 3`.
================================================
FILE: docs/arguments/compose.md
================================================
# Composing Types
### Union Types
Akairo allows the creation of union types, where the input can match one of many types.
You can import the `Argument` class, where there is the `Argument.union` static method.
```js
{
id: 'numOrName',
type: Argument.union('integer', 'string')
}
```
The above argument will try matching using `integer` first, then `string`.
So, it is recommended that you go from most to least specific types.
### Product Types
A product type in Akairo casts the input to multiple types.
The static method `Argument.product` lets us create one of these.
The type will parse the input into an array containing the values respective to the given types.
```js
{
id: 'numAndName',
type: Argument.product('integer', 'string')
}
```
The above argument will give an array where the first element was parsed using `integer`, and the second using `string`.
If any of the types fail, the entire argument fails.
### Validation
Extra validation can be done on the parsed value using `Argument.validate`.
For numbers and things with a length or size, `Argument.range` is a convenient method as well.
```js
{
id: 'content',
type: Argument.validate('string', (m, p, str) => str.length < 2000)
}
```
This argument ensures that the input is less than 2000 characters in length.
If it is over 2000 characters, the input is considered invalid.
```js
{
id: 'number',
type: Argument.range('number', 0, 100)
}
```
The `range` method ensures that the parsed value is within a certain range.
Here, `number` will be between 0 and 100, exclusive.
To make the upper bound inclusive, simply pass `true` to the 4th argument in the range function.
### We're Going Functional
Types can be composed together using `Argument.compose`.
For example, the result of `Argument.compose(type1, type2)` is a type that uses the first type, then the result of that is passed the second.
A use case of this function is for preprocessing before casting:
```js
{
id: 'lowercaseChars',
type: Argument.compose('lowercase', 'charCodes')
}
```
For more complicated types compositions and validations, it will be a lot easier to use type functions.
See the [Using Functions](./functions.md) section for more information.
================================================
FILE: docs/arguments/custom.md
================================================
# Custom Types
### New Type
We have to access the command handler's `TypeResolver` in order to add new types for our arguments.
To add a new type:
```js
this.commandHandler = new CommandHandler(this, { /* Options here */ });
this.commandHandler.resolver.addType('pokemon', (message, phrase) => {
if (!phrase) return null;
for (const pokemon of pokemonList) {
if (pokemon.name.toLowerCase() === phrase.toLowerCase()) {
return pokemon;
}
}
return null;
});
```
We now have a new type called `pokemon` which we can use in a command!
Simply do `type: 'pokemon'` for an argument and everything will work as expected.
### With Message
Let's say we want to add a type that would get a role based on the input.
This means we need access to the guild through the message.
Good thing the first parameter is the message!
```js
this.commandHandler.resolver.addType('colorRole', (message, phrase) => {
if (!phrase) return null;
const roles = {
red: '225939226628194304',
blue: '225939219841810432',
green: '225939232512802816'
};
const role = message.guild.roles.cache.get(roles[phrase.toLowerCase()]);
return role || null;
});
```
So now, using the type `colorRole`, we can get either red, blue, or green from the input and end up with corresponding role object!
### Accessing Another Type
To get another type for use, you use the `type` method on `TypeResolver`.
The following gives the `member` type and we can use as part of another type.
```js
this.commandHandler.resolver.addType('moderator', (message, phrase) => {
if (!phrase) return null;
const memberType = this.commandHandler.resolver.type('member');
const member = memberType(message, phrase);
if (!member.roles.cache.has('222089067028807682')) return null;
return member;
});
```
================================================
FILE: docs/arguments/functions.md
================================================
# Using Functions
### Dynamic Defaults
When you are doing default values for certain arguments, you could really only do what JavaScript has to offer: numbers, strings, etc.
What if we want to use a default such as the author's username or the guild's owner?
This is where you can use a function.
```js
const { Command } = require('discord-akairo');
class HighestRoleCommand extends Command {
constructor() {
super('highestRole', {
aliases: ['highestRole'],
args: [
{
id: 'member',
type: 'member',
default: message => message.member
}
],
channel: 'guild'
});
}
exec(message, args) {
return message.reply(args.member.roles.highest.name);
}
}
module.exports = HighestRoleCommand;
```
The command above gives the name of the inputted member's highest role.
If there were no member or an incorrect member provided, the `default` function is called, giving us the message member.
### Dynamic Types
Let's go to using a function for types.
Take a look at the roll command below.
```js
const { Command } = require('discord-akairo');
class RollCommand extends Command {
constructor() {
super('roll', {
aliases: ['roll'],
args: [
{
id: 'amount',
type: 'integer',
default: 100
}
]
});
}
exec(message, args) {
const res = Math.floor(Math.random() * args.amount));
return message.reply(`You rolled ${res}!`);
}
}
module.exports = RollCommand;
```
Let's say we want to limit the user to between 1 and 100, so that there are no giant numbers.
While we could do it in the execution function, let's stick it straight into the type as a function.
This is much easier with a validation type (see [Composing Types](./compose.md)), but for the sake of example, let's do it anyways.
```js
const { Command } = require('discord-akairo');
class RollCommand extends Command {
constructor() {
super('roll', {
aliases: ['roll'],
args: [
{
id: 'amount',
type: (message, phrase) => {
if (!phrase || isNaN(phrase)) return null;
const num = parseInt(phrase);
if (num < 1 || num > 100) return null;
return num;
},
default: 100
}
]
});
}
exec(message, args) {
const res = Math.floor(Math.random() * args.amount));
return message.reply(`You rolled ${res}!`);
}
}
module.exports = RollCommand;
```
The type function follows these steps:
1. Check if there was input.
2. Check if input is not a number.
3. Parse input to an integer.
4. Check if the integer is out of bounds.
5. Return the integer.
Whenever a `null` or `undefined` value is returned, it means the type casting failed, and the default will be used.
Otherwise, whatever you return is the result.
Promise are awaited and the resolved value will go through the same process.
### A Bit Further
Type functions can be used almost anywhere a type is expected.
This includes the types builders in the [Composing Types](./compose.md) section.
Take a look at this slightly exaggerated example for a type for a page argument:
```js
args: [
{
id: 'page',
type: Argument.compose(Argument.range('integer', 0, Infinity), n => n - 1)
}
]
```
This casts the input to an integer, ensure it is at least 0, and decrements it by 1.
================================================
FILE: docs/arguments/generators.md
================================================
# Generator Arguments
## Yield!
The most powerful aspect of Akairo's argument parsing is the fact that it is implemented using generators.
With this, you can do things such as:
- Have an argument depend on the previous argument
- Branch your argument parsing
- Run an argument multiple times
- Inject code in between arguments
- And more!
To get started, take this command:
```js
const { Command } = require('discord-akairo');
class GeneratorCommand extends Command {
constructor() {
super('generator', {
aliases: ['generator']
});
}
*args() {
// Here!
}
exec(message, args) {
// Do whatever.
}
}
module.exports = GeneratorCommand;
```
Note that instead of an `args` object in the `super` call, we have a generator method, `*args`.
We will focus on this method.
(You can put it in the `super` call if you want, but it is cleaner this way.)
To run an argument:
```js
*args() {
// Notice: no `id` necessary!
// Also notice: `yield` must be used.
const x = yield { type: 'integer' };
const y = yield {
type: 'integer',
prompt: {
// Everything you know still works.
}
};
// When finished.
return { x, y };
}
```
But more things are possible because you have access to all of JavaScript's syntax!
```js
*args(message) {
const x = yield { type: 'integer' };
// Use previous arguments by referring to the identifier.
const y = yield (x > 10 ? { type: 'integer' } : { type: 'string' });
// Debug in between your arguments!
console.log('debug', message.id, x, y);
return { x, y };
}
```
================================================
FILE: docs/arguments/matches.md
================================================
# Matching Input
### Entire Content
Let's say you have a command that picks from a list inputted.
Obviously, you won't know how many things there are.
So, we need a different way of matching input instead of phrase by phrase.
```js
const { Command } = require('discord-akairo');
class PickCommand extends Command {
constructor() {
super('pick', {
aliases: ['pick'],
args: [
{
// Only takes one phrase!
id: 'items'
}
]
});
}
exec(message, args) {
const picked = args.items; // ???
return message.reply(`I picked ${picked}`);
}
}
module.exports = PickCommand;
```
To remedy this, we will use the `match` option.
```js
const { Command } = require('discord-akairo');
class PickCommand extends Command {
constructor() {
super('pick', {
aliases: ['pick'],
args: [
{
id: 'items',
match: 'content'
}
]
});
}
exec(message, args) {
const items = args.items.split('|');
const picked = items[Math.floor(Math.random() * items.length)]
return message.reply(`I picked ${picked.trim()}!`);
}
}
module.exports = PickCommand;
```
Now, the entire content, excluding the prefix and command of course, is matched.
### Flags
If you had a command with lots of argument that can be true or false, you may forget the order.
This is where `flag` match comes in handy.
Here is a command where the user can change the output with a flag:
```js
const { Command } = require('discord-akairo');
const exampleAPI = require('example-api');
class StatsCommand extends Command {
constructor() {
super('stats', {
aliases: ['stats'],
args: [
{
id: 'username'
},
{
id: 'advanced',
match: 'flag',
flag: '--advanced'
}
]
});
}
exec(message, args) {
const user = exampleAPI.getUser(args.username);
if (args.advanced) {
return message.reply(user.advancedInfo);
}
return message.reply(user.basicInfo);
}
}
module.exports = StatsCommand;
```
Now, if a user does `?stats 1Computer` they will get the `basicInfo`, but if they do `?stats 1Computer --advanced`, they will get the `advancedInfo`.
It can be out of order too, so `?stats --advanced 1Computer` will work.
### Option Flag
The above example shows `flag`, which does only a boolean value, there or not there.
Here, we will use `option` for unordered input.
Similar to the above example, but this time, we have many different possibilities.
```js
const { Command } = require('discord-akairo');
const exampleAPI = require('example-api');
class StatsCommand extends Command {
constructor() {
super('stats', {
aliases: ['stats'],
args: [
{
id: 'username'
},
{
id: 'color',
match: 'option',
flag: 'color:',
default: 'red'
}
]
});
}
exec(message, args) {
const team = exampleAPI.getTeam(args.color);
const user = team.getUser(args.username);
return message.reply(user.info);
}
}
module.exports = StatsCommand;
```
So now, all of these inputs can be valid:
- `?stats 1Computer`
- `?stats 1Computer color:blue`
- `?stats color:green 1Computer`
It's also whitespace insensitive between the flag and the input:
- `?stats 1Computer color: blue`
- `?stats color: green 1Computer`
If you would like to use multiple flags, you can use an array.
So, if you did `prefix: ['color:', 'colour:']`, both will be valid for the user.
Note that for both flag match type, you can have flags with whitespace or using an empty string.
It will work, but will be extremely weird for the end users, so don't do it!
### Separate
Let's say that we want our pick command to only work on numbers.
This would mean having to deal with splitting then casting the types within the args!
We can do this with a custom separator using `separator` option alongside the `separate` match.
```js
const { Command } = require('discord-akairo');
class PickCommand extends Command {
constructor() {
super('pick', {
aliases: ['pick'],
separator: '|',
args: [
{
id: 'items',
match: 'separate',
type: 'number'
}
]
});
}
exec(message, args) {
const picked = args.items[Math.floor(Math.random() * args.items.length)]
return message.reply(`I picked ${picked} which is ${picked % 2 === 0 ? 'even' : 'odd'}!`);
}
}
module.exports = PickCommand;
```
The `separate` match matches the phrases individually into an array where each element is type casted one by one.
The `separator` option simply makes it so that all the input is separated via a certain character rather than by whitespace.
Note that with the `separator` option, quotes will not work.
Flags will also have to be contained individually:
- `!foo a, --flag, c` recognizes `--flag`
- `!foo a, x --flag y, c` does not
- `!foo a, --option y, c` recognizes `--option`
- `!foo a, x --option y, c` does not
### Summary
Here are all the match types available in Akairo.
- `phrase` (default) matches one by one (where a phrase is either a word or something in quotes).
- `rest` matches the rest of the content, minus things matched by `flag` and `option`.
- `separate` matches the same way as `rest`, but works on each phrase separately.
- `flag` matches a flag.
- `option` matches a flag with additional input.
- `content` matches the content.
- `text` matches the content, minus things matched by `flag` and `option`.
- `none` matches nothing at all.
The different match types have the following behavior with border whitespaces, quotes, and separators:
- `phrase`, `separate`, and `option` do not preserve any of the three.
- `rest`, `content`, `text` do preserve all three.
================================================
FILE: docs/arguments/prompts.md
================================================
# Argument Prompting
### Please Try Again
You may notice prompting for arguments in other bots (Tatsumaki) or bot frameworks (Commando).
Akairo has a flexible way for you to do them too!
It allows you to set the following properties:
- How many times the user can retry.
- How long they can stall the prompt for.
- The input to use to cancel a prompt (default is `cancel`).
- Whether or not the prompt is optional.
- The message to send on start, on retry, on timeout, on maximum retries, and on cancel.
- There can be embeds or files too!
- Or you can have no message at all!
Let's start with a basic prompt.
We will be reusing this command:
```js
const { Command } = require('discord-akairo');
class HighestRoleCommand extends Command {
constructor() {
super('highestRole', {
aliases: ['highestRole'],
args: [
{
id: 'member',
type: 'member',
default: message => message.member
}
],
channel: 'guild'
});
}
exec(message, args) {
return message.reply(args.member.roles.highest.name);
}
}
module.exports = HighestRoleCommand;
```
First, remove the `default`.
Since prompting will have the user retry until it is finished, `default` won't do anything.
Now, add the `prompt` property with the options you want.
```js
const { Command } = require('discord-akairo');
class HighestRoleCommand extends Command {
constructor() {
super('highestRole', {
aliases: ['highestRole'],
args: [
{
id: 'member',
type: 'member',
prompt: {
start: 'Who would you like to get the highest role of?',
retry: 'That\'s not a valid member! Try again.'
}
}
],
channel: 'guild'
});
}
exec(message, args) {
return message.reply(args.member.roles.highest.name);
}
}
module.exports = HighestRoleCommand;
```
Simple as that, you have a prompt.
Guess what, you can use a function for those messages too!
```js
prompt: {
start: message => `Hey ${message.author}, who would you like to get the highest role of?`,
retry: message => `That\'s not a valid member! Try again, ${message.author}.`
}
```
More complex structures can also be returned as well.
This includes embeds, attachments, anything that can be sent.
```js
prompt: {
start: message => {
const embed = new MessageEmbed().setDescription('Please input a member!');
const content = 'Please!';
return { embed, content };
}
}
```
### Cascading
Prompts can also "cascade" from three places: the command handler, then the command, then the argument.
For the command handler or the command, we would set the `argumentDefaults` option.
```js
this.commandHandler = new CommandHandler(this, {
directory: './commands/',
prefix: '?',
argumentDefaults: {
prompt: {
timeout: 'Time ran out, command has been cancelled.',
ended: 'Too many retries, command has been cancelled.',
cancel: 'Command has been cancelled.',
retries: 4,
time: 30000
}
}
});
```
Those prompt options would now be applied to all prompts that do not have those options already.
Or, with a command with similar arguments:
```js
const { Command } = require('discord-akairo');
class AddCommand extends Command {
constructor() {
super('add', {
aliases: ['add'],
args: [
{
id: 'numOne',
type: 'number',
prompt: true
},
{
id: 'numTwo',
type: 'number',
prompt: true
}
{
id: 'numThree',
type: 'number',
prompt: true
}
],
defaultPrompt: {
start: 'Please input a number!',
retry: 'Please input a number!'
}
});
}
exec(message, args) {
const sum = args.numOne + args.numTwo + args.numThree;
return message.reply(`The sum is ${sum}!`);
}
}
module.exports = AddCommand;
```
Rather than repeating the text for all three arguments, there is a default prompt that applies to all three.
Their `prompt` property still has to be truthy in order to actually prompt, of course.
### Modifying
Prompts can then be modified with a modify function.
It is most useful inside the `argumentDefaults` option, such as on the command handler.
```js
argumentDefaults: {
prompt: {
modifyStart: (message, text) => `${text}\nType cancel to cancel this command.`,
modifyRetry: (message, text) => `${text}\nType cancel to cancel this command.`,
timeout: 'Time ran out, command has been cancelled.',
ended: 'Too many retries, command has been cancelled.',
cancel: 'Command has been cancelled.',
retries: 4,
time: 30000
}
}
```
The options `modifyStart`, `modifyRetry`, etc. are used to modify those types of prompts.
With the above options, all `start` and `retry` prompts will have "Type cancel to cancel this command." appended after it.
================================================
FILE: docs/arguments/prompts2.md
================================================
# More Prompting
### Optional Prompts
Optional prompts are prompts that run if there was input, but the type casting failed.
If there was no input, it would go on as normal.
```js
const { Command } = require('discord-akairo');
class HighestRoleCommand extends Command {
constructor() {
super('highestRole', {
aliases: ['highestRole'],
args: [
{
id: 'member',
type: 'member',
prompt: {
start: 'Who would you like to get the highest role of?',
retry: 'That\'s not a valid member! Try again.',
optional: true
},
default: message => message.member
}
],
channel: 'guild'
});
}
exec(message, args) {
return message.reply(args.member.roles.highest.name);
}
}
module.exports = HighestRoleCommand;
```
With it, `default` is now used again.
- `?highestRole` would give the name for the message author.
- `?highestRole 1Computer` would give the name for 1Computer.
- `?highestRole someone-non-existant` would start up the prompts.
### Infinite Prompts
Infinite prompts are prompts that go on and on until the user says stop.
(You can customize the input, but by default it is `stop`.)
```js
const { Command } = require('discord-akairo');
class PickCommand extends Command {
constructor() {
super('pick', {
aliases: ['pick'],
args: [
{
id: 'items',
match: 'none',
prompt: {
start: [
'What items would you like to pick from?',
'Type them in separate messages.',
'Type `stop` when you are done.'
],
infinite: true
}
}
]
});
}
exec(message, args) {
const picked = args.items[Math.floor(Math.random() * args.items.length)];
return message.reply(`I picked ${picked.trim()}!`);
}
}
module.exports = PickCommand;
```
And with that, `args.items` is now an array of responses from the user.
Note that the `none` match is used, meaning nothing is matched in the original message.
Because this is an infinite prompt that goes across multiple messages, we don't want it to take input from the original message.
If you wish to allow a hybrid of matching and prompting multiple phrases, try using `separate` match with infinite prompts.
================================================
FILE: docs/arguments/types.md
================================================
# Argument Types
### Basic Types
As seen in the previous tutorials, there was the `type` option for type casting.
You've only seen the type `number`, so here are the rest of them:
- `string` (default)
- This type does not do anything.
- `lowercase`
- Transform input to all lowercase.
- `uppercase`
- Transform input to all uppercase.
- `charCodes`
- Transform the input to an array of char codes.
- `number`
- Casts to a number.
- `integer`
- Casts to a integer.
- `bigint`
- Casts to a big integer.
- `url`
- Parses to an URL object.
- `date`
- Parses to a Date object.
- `color`
- Parses a hex code to an color integer.
- `commandAlias`
- Finds a command by alias.
- `command`
- Finds a command by ID.
- `inhibitor`
- Finds an inhibitor by ID.
- `listener`
- Finds a listener by ID.
### Discord Types
Of course, since this is a framework for Discord.js, there are Discord-related types.
- `user`
- Resolves a user from the client's collection.
- `member`
- Resolves a member from the guild's collection.
- `relevant`
- Resolves a user from the relevant place.
- Works in both guilds and DMs.
- `channel`
- Resolves a channel from the guild's collection.
- `textChannel`
- Resolves a text channel from the guild's collection.
- `voiceChannel`
- Resolves a voice channel from the guild's collection.
- `role`
- Resolves a role from the guild's collection.
- `emoji`
- Resolves an emoji from the guild's collection.
- `guild`
- Resolves a guild from the client's collection.
All of the above types also have plural forms.
So if you do `users` instead of `user`, you will receive a Collection of resolved users.
The types below are also Discord-related, but have no plural form.
- `message`
- Fetches a message from an ID within the channel.
- `guildMessage`
- Fetches a message from an ID within the guild.
- `invite`
- Fetches an invite from a link.
- `userMention`
- Matches the user from a mention.
- `memberMention`
- Matches the member from a mention.
- `channelMention`
- Matches the channel from a mention.
- `roleMention`
- Matches the role from a mention.
- `emojiMention`
- Matches the emoji from a mention.
### Array Types
There are other ways to do type-casting instead of a string literal too.
The first way is with an array:
```js
const { Command } = require('discord-akairo');
class PokemonCommand extends Command {
constructor() {
super('pokemon', {
aliases: ['pokemon'],
args: [
{
id: 'option',
type: ['grass', 'fire', 'water', 'electric'],
default: 'electric'
}
]
});
}
exec(message, args) {
if (args.option === 'grass') return message.reply('bulbasaur');
if (args.option === 'fire') return message.reply('charmander');
if (args.option === 'water') return message.reply('squirtle');
if (args.option === 'electric') return message.reply('pikachu');
}
}
module.exports = PokemonCommand;
```
With the above, the user can only enter one of the entries in the array.
It is also case-insensitive for input, but not for output.
This means that if the array was `['GrasS', 'FIrE']` and the input was `grass`, you will get `GrasS`.
You can also do aliases with the array type like so:
```js
const { Command } = require('discord-akairo');
class PokemonCommand extends Command {
constructor() {
super('pokemon', {
aliases: ['pokemon'],
args: [
{
id: 'option',
type: [
['grass', 'leaf', 'green'],
['fire', 'red'],
['water', 'blue'],
['electric', 'electricity', 'lightning', 'yellow']
],
default: 'electric'
}
]
});
}
exec(message, args) {
if (args.option === 'grass') return message.reply('bulbasaur');
if (args.option === 'fire') return message.reply('charmander');
if (args.option === 'water') return message.reply('squirtle');
if (args.option === 'electric') return message.reply('pikachu');
}
}
module.exports = PokemonCommand;
```
If the user inputs anything from the arrays, the first entry will be used.
So, the input of `leaf` will give you `grass`, `blue` will give you `water`, etc.
### Regex Types
You can also use a regular expression as a type.
```js
const { Command } = require('discord-akairo');
class AskCommand extends Command {
constructor() {
super('ask', {
aliases: ['ask'],
args: [
{
id: 'yesOrNo',
type: /^(yes|no)$/i
}
]
});
}
exec(message, args) {
// {
// match: [...],
// matches: null
// }
console.log(args.yesOrNo);
}
}
module.exports = AskCommand;
```
This will match `yes` or `no`, case-insensitive and `args.yesOrNo` will give you the result from `word.match(/^(yes|no)$/i`.
If using a global regex, the `matches` property will be filled for the matches.
================================================
FILE: docs/arguments/unordered.md
================================================
# Unordered Arguments
### Any Order!
Arguments can be made to be unordered.
For example, if you want a command where the arguments are a role and a member in any order:
```js
const { Command } = require('discord-akairo');
class AddRoleCommand extends Command {
constructor() {
super('addrole', {
aliases: ['addrole'],
args: [
{
id: 'member',
type: 'member',
unordered: true
},
{
id: 'role',
type: 'role',
unordered: true
}
],
userPermissions: ['ADMINISTRATOR'],
channel: 'guild'
});
}
async exec(message, args) {
await args.member.roles.add(args.role);
return message.reply('Done!');
}
}
module.exports = AddRoleCommand;
```
The above command would work as `!addrole member role` and `!addrole role member`.
No phrase will be parsed twice, for example, if the first phrase matched as a member, the role argument will ignore the first phrase and start with the second.
Only the match type `phrase` (which is by default) works with the `unordered` option.
Other match types will ignore this behavior.
To choose a index to be unordered from (e.g. from the second phrase onwards) use a number, e.g. `unordered: 1`.
To choose specific indices to be unordered on, use an array, e.g. `unordered: [0, 1, 2]`.
### With Defaults or Prompts
Unordered arguments have a slightly different behavior when used with a default value and/or a prompt.
If an unordered argument has a default and nothing matches, the default is used.
If there is a prompt and nothing matches:
- If the prompt is optional, the default value is used.
- If not, the prompt is started as if no input was given.
So, if you do have a prompt, make sure the `optional` option is not used or else it will prompt not at all.
================================================
FILE: docs/basics/commands.md
================================================
# Basic Commands
### The Command Handler
In Akairo, the hierachy is that there are handlers which contains modules.
The handlers deals with loading modules and executing them.
For commands, we will import and instantiate the `CommandHandler`.
```js
const { AkairoClient, CommandHandler } = require('discord-akairo');
class MyClient extends AkairoClient {
constructor() {
super({
ownerID: '123992700587343872', // or ['123992700587343872', '86890631690977280']
}, {
disableMentions: 'everyone'
});
this.commandHandler = new CommandHandler(this, {
// Options for the command handler goes here.
});
}
}
const client = new MyClient();
client.login('TOKEN');
```
Now, for some options.
The `directory` option tells the handler where the main set of commands modules are at.
The `prefix` option is simply the prefixes you want to use, you can have multiple too!
```js
this.commandHandler = new CommandHandler(this, {
directory: './commands/',
prefix: '?' // or ['?', '!']
});
```
And now that the command handler has been setup, we simply have to tell it to load the modules.
```js
this.commandHandler.loadAll();
```
### Ping Command
Our first order of business is to make a ping command.
No bot is complete without one!
We specified that the `directory` is in `./commands/`.
So, go there, make a new file, and require Akairo.
```js
const { Command } = require('discord-akairo');
```
Here is a basic ping command:
```js
const { Command } = require('discord-akairo');
class PingCommand extends Command {
constructor() {
super('ping', {
aliases: ['ping']
});
}
exec(message) {
return message.reply('Pong!');
}
}
module.exports = PingCommand;
```
The first parameter of `super` is the unique ID of the command.
It is not seen nor used by users, but you should keep it the same as one of the aliases.
The second parameter is the options.
The only option there right now are the aliases, which are the names of the command for the users to call.
Note that the ID of the command is not an alias!
The exec method is the execution function, ran when the command is called.
You should try to always return a value such as a Promise with it, so that the framework can tell when a command finishes.
If everything was done correctly, your command should now work!
Because there are a lot of things that can be changed for commands, they will be explained further in other tutorials.
================================================
FILE: docs/basics/inhibitors.md
================================================
# Basic Inhibitors
### Setup
Inhibitors are a way to monitor or block messages coming into the command handler.
Because inhibitors are another kind of module, we need another kind of handler.
To set it up, simply import and instantiate the `InhibitorHandler`, just like with the command handler.
```js
const { AkairoClient, CommandHandler, InhibitorHandler } = require('discord-akairo');
class MyClient extends AkairoClient {
constructor() {
super({
ownerID: '123992700587343872',
}, {
disableMentions: 'everyone'
});
this.commandHandler = new CommandHandler(this, {
directory: './commands/',
prefix: '?'
});
this.inhibitorHandler = new InhibitorHandler(this, {
directory: './inhibitors/'
});
}
}
const client = new MyClient();
client.login('TOKEN');
```
Then, tell it to load all the modules.
But, since inhibitors are a part of the command handling process, the command handler has to know about the inhibitor handler, so:
```js
this.commandHandler.useInhibitorHandler(this.inhibitorHandler);
this.inhibitorHandler.loadAll();
```
### Blacklist
Create a folder named `inhibitors`, then a file there to make one.
```js
const { Inhibitor } = require('discord-akairo');
class BlacklistInhibitor extends Inhibitor {
constructor() {
super('blacklist', {
reason: 'blacklist'
})
}
exec(message) {
// He's a meanie!
const blacklist = ['81440962496172032'];
return blacklist.includes(message.author.id);
}
}
module.exports = BlacklistInhibitor;
```
The first parameter in `super` is the unique ID of the inhibitor.
The second parameter are the options.
The option `reason` is what will get emitted to an event, but we can worry about that later.
The exec method is ran on testing.
It should return `true` in order to block the message.
Promise are awaited and the resolved value will be checked.
================================================
FILE: docs/basics/listeners.md
================================================
# Basic Listeners
### Setup
Listeners are a basic concept in Node.js.
Problem is, you usually end up with loooooong files attaching listeners on your client.
And plus, you can't reload them as easily!
Let's add some listeners.
You have to setup a `ListenerHandler` just like with commands and inhibitors.
```js
const { AkairoClient, CommandHandler, InhibitorHandler, ListenerHandler } = require('discord-akairo');
class MyClient extends AkairoClient {
constructor() {
super({
ownerID: '123992700587343872',
}, {
disableMentions: 'everyone'
});
this.commandHandler = new CommandHandler(this, {
directory: './commands/',
prefix: '?'
});
this.inhibitorHandler = new InhibitorHandler(this, {
directory: './inhibitors/'
});
this.listenerHandler = new ListenerHandler(this, {
directory: './listeners/'
});
}
}
const client = new MyClient();
client.login('TOKEN');
```
Then, tell it to load all the modules.
The command handler may need to use the listener handler for some operations later on, so it should use it as well:
```js
this.commandHandler.useListenerHandler(this.listenerHandler);
this.listenerHandler.loadAll();
```
### I'm Ready!
And now, we can make a listener!
Let's start with a simple client `ready` event.
```js
const { Listener } = require('discord-akairo');
class ReadyListener extends Listener {
constructor() {
super('ready', {
emitter: 'client',
event: 'ready'
});
}
exec() {
console.log('I\'m ready!');
}
}
module.exports = ReadyListener;
```
The first parameter in `super` is the listener's unique ID.
The second parameter are the options.
First, we have the emitter's name.
Then, we have the event we want to listen to.
Then the exec method, whose parameters are the event's.
### Custom Emitters
By default, the `client` emitter is the only one available.
Handlers in Akairo are also EventEmitters, so we can have our listener handler listen to our handlers.
Using `setEmitters`, we can set custom emitters:
```js
this.listenerHandler.setEmitters({
commandHandler: this.commandHandler,
inhibitorHandler: this.inhibitorHandler,
listenerHandler: this.listenerHandler
});
```
Note: You have to call `setEmitters` before `loadAll` or Akairo will not be able to resolve your emitters.
### Blocked Commands
Remember the `reason` for inhibitors in previous tutorial?
They are emitted to the `messageBlocked` (anything with `pre` type or before) or `commandBlocked` (everything after) event by the command handler.
Since we set the command handler to the key `commandHandler` up above, we have to use that as the `emitter` option.
```js
const { Listener } = require('discord-akairo');
class CommandBlockedListener extends Listener {
constructor() {
super('commandBlocked', {
emitter: 'commandHandler',
event: 'commandBlocked'
});
}
exec(message, command, reason) {
console.log(`${message.author.username} was blocked from using ${command.id} because of ${reason}!`);
}
}
module.exports = CommandBlockedListener;
```
And if you want your listeners to run only once, you add the option `type` with the value of `'once'`.
================================================
FILE: docs/basics/setup.md
================================================
# Setting Up
### Installation
Before even doing anything else, you of course have to install the Discord.js and Akairo.
`npm i discord.js`
`npm i discord-akairo`
If you feel like working with SQLite or Sequelize later, install them too.
`npm i sqlite`
`npm i sequelize`
Once everything has been installed, your working directory should look something like this:
```
mybot
|____ node_modules
bot.js
```
### Main File
Inside `bot.js`, require `discord-akairo` and extend the `AkairoClient` class to customize your client.
As your bot gets more complicated, you may want to separate this client class from your main file.
```js
const { AkairoClient } = require('discord-akairo');
class MyClient extends AkairoClient {
constructor() {
super({
// Options for Akairo go here.
}, {
// Options for discord.js goes here.
});
}
}
const client = new MyClient();
client.login('TOKEN');
```
There are some options you may want to setup first, for example, the owner of the bot.
If you would like to have multiple owners simply add those with an array.
We want to use Discord.js's `disableMentions` option too.
```js
const { AkairoClient } = require('discord-akairo');
class MyClient extends AkairoClient {
constructor() {
super({
ownerID: '123992700587343872', // or ['123992700587343872', '86890631690977280']
}, {
disableMentions: 'everyone'
});
}
}
const client = new MyClient();
client.login('TOKEN');
```
Your bot should now login, and you are ready to make commands.
================================================
FILE: docs/commands/commandutil.md
================================================
# CommandUtil
### Handling Edits
The CommandUtil class is a utility class for working with responses.
In order to make it available, you must enable `commandUtil`.
```js
this.commandHandler = new CommandHandler(this, {
directory: './commands/',
prefix: '?',
handleEdits: true,
commandUtil: true
});
```
Now, CommandUtil is available on messages with the property `util`.
An instance is kept for each message that go through command handling, but they have a lifetime of 5 minutes from then.
To keep them alive longer, set a larger time in milliseconds using the `commandUtilLifetime` option.
Note that this can build up memory usage really fast on larger bots, so it is recommended you give it a reasonable lifetime.
You can CommandUtil methods such as `send` in order to send responses.
With `handleEdits` on, the `send` methods will edit responses accordingly.
This works for prompts as well.
```js
const { Command } = require('discord-akairo');
class HelloCommand extends Command {
constructor() {
super('hello', {
aliases: ['hello']
});
}
exec(message) {
// Also available: util.reply()
return message.util.send('Hello!');
}
}
module.exports = HelloCommand;
```
As an example of what that means:
- User sends `?ping` (message A).
- Bot replies with `Pong!` (message B).
- User edits message A to `?hello`.
- Bot edits message B to `Hello!`.
### Raw Input
CommandUtil can also be used to view the prefix, command alias, and arguments used.
The format for command is almost always ` `.
CommandUtil stores all three of that and more for you.
```js
const { Command } = require('discord-akairo');
class HelloCommand extends Command {
constructor() {
super('hello', {
aliases: ['hello', 'hi', 'konnichiha', 'bonjour', 'heyo']
});
}
exec(message) {
if (message.util.parsed.alias === 'konnichiha') {
return message.util.send('こんにちは!');
}
if (message.util.parsed.alias === 'bonjour') {
return message.util.send('Bonjour!');
}
return message.util.send('Hello!');
}
}
module.exports = HelloCommand;
```
With that, you can see which alias was used by the user.
You can see the prefix as well.
For example, if you have two prefixes, `?` and `!`, `message.util.parsed.prefix` will be either `?` or `!`.
The content can also be viewed, for example, in `!command xyz abc`, `message.util.parsed.content` would be `xyz abc`.
CommandUtil, if enabled, is available on all messages just after built-in pre-inhibitors.
This means an invalid input, e.g. `?not-a-command` will still be parsed with prefix of `?` and alias of `not-a-command`.
### Stored Messages
If you set the command handler option `storeMessages` to true, CommandUtil instances will start storing messages from prompts.
This means that prompts from the client as well as the user replies are stored within `message.util.messages`.
See the prompting sections under Arguments for more information about prompts.
================================================
FILE: docs/commands/conditional.md
================================================
# Conditional Commands
### Run Whenever
Conditional commands are commands that run if the following conditions are true:
- The command was not invoked normally.
- The command's `condition` option is true.
Multiple conditional commands/regex commands can be triggered on one message.
```js
const { Command } = require('discord-akairo');
class ComplimentCommand extends Command {
constructor() {
super('compliment', {
category: 'random'
});
}
condition(message) {
return message.author.id === '126485019500871680';
}
exec(message) {
return message.reply('You are a great person!');
}
}
module.exports = ComplimentCommand;
```
This command, whenever a certain person sends any message, will execute.
================================================
FILE: docs/commands/cooldowns.md
================================================
# Cooldowns
### No Spam!
Cooldowns are how you make sure that troublemakers don't spam your bot.
Akairo allows you to set cooldowns in uses per milliseconds.
```js
const { Command } = require('discord-akairo');
const exampleAPI = require('example-api');
class RequestCommand extends Command {
constructor() {
super('request', {
aliases: ['request'],
cooldown: 10000,
ratelimit: 2
});
}
async exec(message) {
const info = await exampleAPI.fetchInfo();
return message.reply(info);
}
}
module.exports = RequestCommand;
```
`cooldown` is the amount of time a user would be in cooldown for.
`ratelimit` is the amount of uses a user can do before they are denied usage.
In simple terms, this means 2 uses every 10000 milliseconds.
If you wish to set a default cooldown for all commands, the `defaultCooldown` option is available:
```js
this.commandHandler = new CommandHandler(this, {
directory: './commands/',
prefix: '?',
defaultCooldown: 1000
});
```
When someone uses a command while in cooldown, the event `cooldown` will be emitted on the command handler with the remaining time in milliseconds.
### Ignoring Cooldown
By default, cooldowns are ignored by the client owners.
This is actually done through the option `ignoreCooldown`.
To change this, simply pass in an ID or an array of IDs:
```js
this.commandHandler = new CommandHandler(this, {
directory: './commands/',
prefix: '?',
defaultCooldown: 1000,
ignoreCooldown: ['123992700587343872', '130175406673231873']
});
```
Note that you should pass the owner ID in as well, since it overrides the default.
That is, unless you actually want to be ratelimited yourself.
Also, a function could also be used to check who should be ignored.
================================================
FILE: docs/commands/permissions.md
================================================
# Permissions
### Permission Flags
Some commands should only be used by someone with certain permissions.
There are options to help you do this.
The two options to use are `clientPermissions` and `userPermissions`.
```js
const { Command } = require('discord-akairo');
class BanCommand extends Command {
constructor() {
super('ban', {
aliases: ['ban'],
args: [
{
id: 'member',
type: 'member'
}
],
clientPermissions: ['BAN_MEMBERS'],
userPermissions: ['BAN_MEMBERS'],
channel: 'guild'
});
}
async exec(message, args) {
if (!args.member) {
return message.reply('No member found with that name.');
}
await args.member.ban();
return message.reply(`${args.member} was banned!`);
}
}
module.exports = BanCommand;
```
This now checks for the required permissions for the client, then the user.
When blocked, it emits `missingPermissions` on the command handler.
It will pass the message, command, either `client` or `user`, then the missing permissions.
### Dynamic Permissions
Sometimes, you may want to check for a role instead of permission flags.
This means you can use a function instead of an array!
A function can be used on both `clientPermissions` and `userPermissions`.
The return value is the `missing` parameter that is sent to the `missingPermissions` event.
If the return value is null, then that means they're not missing anything.
```js
const { Command } = require('discord-akairo');
class BanCommand extends Command {
constructor() {
super('ban', {
aliases: ['ban'],
args: [
{
id: 'member',
type: 'member'
}
],
clientPermissions: ['BAN_MEMBERS'],
channel: 'guild'
});
}
userPermissions(message) {
if (!message.member.roles.cache.some(role => role.name === 'Moderator')) {
return 'Moderator';
}
return null;
}
async exec(message, args) {
if (!args.member) {
return message.reply('No member found with that name.');
}
await args.member.ban();
return message.reply(`${args.member} was banned!`);
}
}
module.exports = BanCommand;
```
================================================
FILE: docs/commands/prefixes.md
================================================
# Prefixes and Aliases
### Mentioning
Sometimes people can forget or not know the prefix for your bot, so letting them use command with a mention is useful.
This can be enabled with the `allowMention` option.
```js
this.commandHandler = new CommandHandler(this, {
directory: './commands/',
prefix: '?',
allowMention: true
});
```
Now both `?ping` and `@BOT ping` works!
### Changeable Prefixes
A prefix can change based on the message.
Use a function as the `prefix` option to do so.
This is most useful with an actual database to back it up, so check out the [Using Providers](../other/providers.md) section.
```js
this.commandHandler = new CommandHandler(this, {
directory: './commands/',
prefix: msg => {
// Get prefix here...
return prefix;
},
allowMention: true
});
```
### Prefix Overrides
Prefix overrides are command-specific prefixes.
To use them, simply add the `prefix` option.
```js
const { Command } = require('discord-akairo');
class SecretCommand extends Command {
constructor() {
super('secret', {
aliases: ['secret'],
prefix: '???'
});
}
exec(message) {
return message.reply('Woah! How did you find this!?');
}
}
module.exports = SecretCommand;
```
Now, if our prefix was `?`, `?secret` won't work, but `???secret` would.
An array works too, so you can do `prefix: ['???', '??']` and both would work.
### Automatic Aliases
To speed up your development, you can make command aliases automatically.
For example, if you had a command alias that is two words, you might want both `command-name` and `commandname` to be valid.
Use the `aliasReplacement` option, which takes a regular expression to make aliases:
```js
this.commandHandler = new CommandHandler(this, {
directory: './commands/',
prefix: '?',
aliasReplacement: /-/g,
allowMention: true
});
```
The option is passed `/-/g` which means that all dashes are to be removed to make an alias.
So now, in a command, you can pass `aliases: ['command-name']` and both `command-name` and `commandname` would be valid.
================================================
FILE: docs/commands/regex.md
================================================
# Regex Commands
### Memes
Regex or regular expressions, is basically a way to match characters in a string.
Regex commands are commands that run if the following conditions are true:
- The command was not invoked normally.
- The command's `regex` matches the message.
Multiple regex commands/conditional commands can be triggered from one message.
```js
const { Command } = require('discord-akairo');
class AyyCommand extends Command {
constructor() {
super('ayy', {
regex: /^ayy$/i
});
}
exec(message, args) {
return message.reply('lmao');
}
}
module.exports = AyyCommand;
```
This command will trigger on any message with the content `ayy`, case-insensitive.
In `args`, the `match` property will be the result from `message.content.match(/^ayy$/i)`.
The `matches` property will be the matches, if using a global regex.
### As a Function
The `regex` option can also be a function.
```js
const { Command } = require('discord-akairo');
class AyyCommand extends Command {
constructor() {
super('ayy', {
category: 'random'
});
}
regex(message) {
// Do some code...
return /^ayy$/i;
}
exec(message, args) {
return message.reply('lmao');
}
}
module.exports = AyyCommand;
```
================================================
FILE: docs/commands/restrictions.md
================================================
# Restrictions
### Channel Restrictions
If a command requires a guild to be used correctly, you can restrict it to a guild with one option.
```js
const { Command } = require('discord-akairo');
class NicknameCommand extends Command {
constructor() {
super('nickname', {
aliases: ['nickname']
});
}
exec(message) {
return message.reply(`Your nickname is ${message.member.nickname}.`);
}
}
module.exports = NicknameCommand;
```
The above breaks in a DM, so let's add the `channel` option.
```js
const { Command } = require('discord-akairo');
class NicknameCommand extends Command {
constructor() {
super('nickname', {
aliases: ['nickname'],
channel: 'guild'
});
}
exec(message) {
return message.reply(`Your nickname is ${message.member.nickname}.`);
}
}
module.exports = NicknameCommand;
```
Everything is fixed and you can go on your way!
As a bonus, this will emit `commandBlocked` on the command handler with the reason `guild` if someone tries to use it in a DM.
### Owner Only
Remember the `ownerID` option in your client?
Your commands can be owner-only, restricting them to be used by the owner(s).
Simply add `ownerOnly`.
```js
const { Command } = require('discord-akairo');
class TokenCommand extends Command {
constructor() {
super('token', {
aliases: ['token'],
ownerOnly: true,
channel: 'dm'
});
}
exec(message) {
// Don't actually do this.
return message.reply(this.client.token);
}
}
module.exports = TokenCommand;
```
This will emit `commandBlocked` with the reason `owner` if someone else uses it.
================================================
FILE: docs/general/welcome.md
================================================
## Welcome!
You are currently looking at the discord-akairo v8 tutorials.
## Links
- [Website](https://discord-akairo.github.io)
- [Repository](https://github.com/discord-akairo/discord-akairo)
- [Changelog](https://github.com/discord-akairo/discord-akairo/releases)
- [Discord](https://discord.gg/arTauDY)
================================================
FILE: docs/index.yml
================================================
- name: General
files:
- name: Welcome
path: welcome.md
- name: Basics
files:
- name: Setting Up
path: setup.md
- name: Basic Commands
path: commands.md
- name: Basic Inhibitors
path: inhibitors.md
- name: Basic Listeners
path: listeners.md
- name: Commands
files:
- name: Restrictions
path: restrictions.md
- name: Permissions
path: permissions.md
- name: Cooldowns
path: cooldowns.md
- name: Regex Commands
path: regex.md
- name: Conditional Commands
path: conditional.md
- name: Prefixes and Aliases
path: prefixes.md
- name: CommandUtil
path: commandutil.md
- name: Arguments
files:
- name: Basic Arguments
path: arguments.md
- name: Matching Input
path: matches.md
- name: Argument Types
path: types.md
- name: Using Functions
path: functions.md
- name: Composing Types
path: compose.md
- name: Custom Types
path: custom.md
- name: Argument Prompting
path: prompts.md
- name: More Prompting
path: prompts2.md
- name: Unordered Arguments
path: unordered.md
- name: Generator Arguments
path: generators.md
- name: Inhibitors
files:
- name: Inhibitor Types
path: inhibtypes.md
- name: Inhibitor Priority
path: priority.md
- name: Listeners
files:
- name: Custom Emitters
path: emitters.md
- name: Other
files:
- name: Updating to v8
path: updating.md
- name: Handling Modules
path: handling.md
- name: Custom Handlers
path: handlers.md
- name: Using Providers
path: providers.md
- name: Using Mongoose Provider
path: mongoose.md
- name: ClientUtil
path: clientutil.md
- name: Snippets
files:
- name: Ping Command
path: ping.md
================================================
FILE: docs/inhibitors/inhibtypes.md
================================================
# Inhibitor Types
### More Coverage
Right now, your inhibitors only runs before a command.
They do not actually run on all messages.
To change that, change the `type` option.
```js
const { Inhibitor } = require('discord-akairo');
class BlacklistInhibitor extends Inhibitor {
constructor() {
super('blacklist', {
reason: 'blacklist',
type: 'all'
});
}
exec(message) {
// Still a meanie!
const blacklist = ['81440962496172032'];
return blacklist.includes(message.author.id);
}
}
module.exports = BlacklistInhibitor;
```
There are three types:
- `all` is run on all messages.
- `pre` is run on messages not blocked by `all` and built-in inhibitors.
- `post` (the default) is run on messages before commands, not blocked by the previous.
The built-in inhibitors are:
- `client` blocks the client (itself).
- `bot` blocks all other bots.
- `owner` blocks non-owners from using owner-only commands.
- `guild` blocks guild-only commands used in DMs.
- `dm` blocks DM-only commands used in guilds.
To make it easier to visualize, here is the order:
- `all` type inhibitors.
- `client`, and `bot`.
- (commands sent when someone is in the middle of being prompted are blocked here)
- `pre` type inhibitors.
- `owner`, `guild`, and `dm`.
- (commands that have missing permissions are blocked here)
- `post` type inhibitors.
- (commands under cooldown are blocked here)
================================================
FILE: docs/inhibitors/priority.md
================================================
# Inhibitor Priority
### Me First!
Sometimes multiple inhibitors can block a message.
For example, you may have an inhibitor for blacklisting within a server, and another for a global blacklist.
By default, inhibitors are ordered by their load order, which is based on the filename.
So, if you had named the inhibitors `blacklist.js` and `globalBlacklist.js`, the former would have a higher priority.
Whenever both inhibitors block a message, the `commandBlocked` event would fire with the blacklist inhibitor's reason.
If you want the global blacklist inhibitor's instead you can use the `priority` option.
```js
const { Inhibitor } = require('discord-akairo');
const globalBlacklist = require('something');
class GlobalBlacklistInhibitor extends Inhibitor {
constructor() {
super('globalBlacklist', {
reason: 'globalBlacklist',
priority: 1
});
}
exec(message) {
return globalBlacklist.has(message.author.id);
}
}
module.exports = BlacklistInhibitor;
```
By default, inhibitors have a priority of 0.
By increasing it, it means that the inhibitor will now have priority over the others.
So when two inhibitors block a message, the one with the higher priority will be used.
If they have the same priority, then it is still by load order.
================================================
FILE: docs/listeners/emitters.md
================================================
# Custom Emitters
### Watching Process
As shown in the first listener tutorial, we can have custom emitters.
Listeners can run on more than Akairo-related things.
To add a custom emitter, use the `setEmitters` method available on the listener handler.
```js
this.listenerHandler.setEmitters({
process: process,
anything: youWant
});
```
Note: You have to call `setEmitters` before `load` or `loadAll` so that Akairo will be able to resolve your emitters.
The key will be the emitter's name, and the value is the emitter itself.
Now, we can use a listener on the process:
```js
const { Listener } = require('discord-akairo');
class UnhandledRejectionListener extends Listener {
constructor() {
super('unhandledRejection', {
event: 'unhandledRejection',
emitter: 'process'
});
}
exec(error) {
console.error(error);
}
}
module.exports = UnhandledRejectionListener;
```
================================================
FILE: docs/other/clientutil.md
================================================
# ClientUtil
### Finding Things
ClientUtil is a class filled with utility methods.
It is available on your client as `client.util`.
There are three "groups" of resolver methods for finding or checking Discord-related things.
They allow you to, for example, find a user named `1Computer` from an input of `comp`.
- `resolve `
- e.g. `resolveUser`, `resolveChannel`, etc.
- Finds an Discord-related object from a collection of those objects.
- `resolve `
- e.g. `resolveUsers`, `resolveChannels`, etc.
- Filters Discord-related objects from a collection of those objects.
- `check `
- e.g. `checkUser`, `checkChannel`, etc.
- Used for the above methods, checks if a string could be referring to the object.
### Other Methods
There are a bunch of other things you may find useful:
- `embed`, `attachment`, and `collection`
- Shortcuts for MessageEmbed, MessageAttachment, and Collection.
- `resolvePermissionNumber`
- Converts a permission number to an array of permission names.
================================================
FILE: docs/other/handlers.md
================================================
# Custom Handlers
### And Custom Modules
Internally, Akairo's handlers all extends AkairoHandler, and all modules extends AkairoModule.
So, you can create your own handlers and module types!
Create a new class for your module.
```js
const { AkairoModule } = require('discord-akairo');
class CustomModule extends AkairoModule {
constructor(id, options = {}) {
super(id, options);
this.color = options.color || 'red';
}
exec() {
throw new Error('Not implemented!');
}
}
module.exports = CustomModule;
```
Note that the `exec` method you see in Command, Inhibitor, and Listener are not native to AkairoModule.
They require you to actually create them within the module type, such as above.
We throw an error there just in case you forget to implement it.
Then, create a new class for your handler:
```js
const { AkairoHandler } = require('discord-akairo');
const CustomModule = require('./CustomModule');
class CustomHandler extends AkairoHandler {
constructor(client, options = {}) {
super(client, {
directory: options.directory,
classToHandle: CustomModule
});
this.customOption = options.customOption || 'something';
}
}
module.exports = CustomHandler;
```
For the handler, the `super()` takes the client, the directory for the handler, and the class of the module type we want to handle.
Now we can add it to our client if we so desire:
```js
const { AkairoClient } = require('discord-akairo');
const CustomHandler = require('./CustomHandler');
class MyClient extends AkairoClient {
constructor() {
super({
ownerID: '123992700587343872',
}, {
disableMentions: 'everyone'
});
this.customHandler = new CustomHandler(this, {
directory: './customs/'
});
this.customHandler.loadAll();
}
}
module.exports = MyClient;
```
And the module:
```js
const CustomModule = require('../CustomModule');
class CustomCustom extends CustomModule {
constructor() {
super('custom', {
color: 'blue'
});
}
exec() {
console.log('I did something!');
}
}
module.exports = CustomCustom;
```
Custom handlers and modules are can get much more complicated than this.
However, it would be out of the scope of this tutorial, so if you want to go there, check out the source code on Github.
================================================
FILE: docs/other/handling.md
================================================
# Handling Modules
### Categorizing
You can categorize a module with the `category` option.
```js
const { Command } = require('discord-akairo');
class PingCommand extends Command {
constructor() {
super('ping', {
aliases: ['ping'],
category: 'stuff'
});
}
exec(message) {
return message.reply('Pong!');
}
}
module.exports = PingCommand;
```
A new category will be created on the handler with the ID of `stuff`.
By default, all modules are in the `default` category.
### Reloading
Everything in Akairo is a module, and all modules are loaded by handlers.
With that said, this means you can add, remove, or reload modules while the bot is running!
Here is a basic command that reloads the inputted ID:
```js
const { Command } = require('discord-akairo');
class ReloadCommand extends Command {
constructor() {
super('reload', {
aliases: ['reload'],
args: [
{
id: 'commandID'
}
],
ownerOnly: true,
category: 'owner'
});
}
exec(message, args) {
// `this` refers to the command object.
this.handler.reload(args.commandID);
return message.reply(`Reloaded command ${args.commandID}!`);
}
}
module.exports = ReloadCommand;
```
Ways you can reload a module includes:
- Individually:
- `.reload(moduleID)`
- `.reload()`
- Many at once:
- `.reloadAll()`
- `.reloadAll()`
### Removing and Adding
For removing, simply change all those `reload` to `remove`.
To add a new module, you can use the `load` method.
With `load`, you will need to specify a full filepath or a module class.
If you load with a class, note that those cannot be reloaded.
================================================
FILE: docs/other/mongoose.md
================================================
# Using Mongoose Provider
### Storing Prefixes
Let's implement per-guild prefixes.
First, create a new MongooseProvider.
```js
// It is better to init mongoose in another file (eg. main.js)
// connect to database and then require this file (eg. bot.js)
const model = require('./path/to/model'); // see Model Example below
const { AkairoClient, MongooseProvider } = require('discord-akairo');
class CustomClient extends AkairoClient {
constructor() {
super({
/* Options here */
});
// Mongoose Provider
this.settings = new MongooseProvider(model);
}
}
```
Before you can actually use the provider, you would have to run the `init` method.
For example:
```js
class CustomClient extends AkairoClient {
/* ... */
async login(token) {
await this.settings.init();
return super.login(token);
}
}
```
Now, the provider can be used like so:
```js
class CustomClient extends AkairoClient {
constructor() {
super({
prefix: (message) => {
if (message.guild) {
// The third param is the default.
return this.settings.get(message.guild.id, 'prefix', '!');
}
return '!';
}
});
/* ... */
}
}
```
Values can be set with the `set` method:
```js
const { Command } = require('discord-akairo');
class PrefixCommand extends Command {
constructor() {
super('prefix', {
aliases: ['prefix'],
category: 'stuff',
args: [
{
id: 'prefix',
default: '!'
}
],
channel: 'guild'
});
}
async exec(message, args) {
// The third param is the default.
const oldPrefix = this.client.settings.get(message.guild.id, 'prefix', '!');
await this.client.settings.set(message.guild.id, 'prefix', args.prefix);
return message.reply(`Prefix changed from ${oldPrefix} to ${args.prefix}`);
}
}
module.exports = PrefixCommand;
```
### Model Example
```js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const guildSchema = new Schema({
id: {
type: String,
required: true
},
settings: {
type: Object,
required: true
}
}, { minimize: false });
module.exports = mongoose.model('model', guildSchema);
```
================================================
FILE: docs/other/providers.md
================================================
# Using Providers
### Storing Prefixes
Akairo comes with SQLiteProvider and SequelizeProvider, optional utility classes for databases.
Note that if you are doing something complicated with databases, you should use SQLite or Sequelize directly.
Let's implement per-guild prefixes.
First, create a new SQLiteProvider or SequelizeProvider.
```js
const sqlite = require('sqlite');
const sequelize = require('sequelize');
const { AkairoClient, SQLiteProvider, SequelizeProvider } = require('discord-akairo');
class CustomClient extends AkairoClient {
constructor() {
super({
/* Options here */
});
// With SQLite
this.settings = new SQLiteProvider(sqlite.open('path/to/db.sqlite'), 'table_name', {
idColumn: 'guild_id',
dataColumn: 'settings'
});
// Or, with sequelize
this.settings = new SequelizeProvider(/* Sequelize model here */, {
idColumn: 'guild_id',
dataColumn: 'settings'
});
}
}
```
The providers only handle one table at a time.
Notice that you can set the `idColumn` and the `dataColumn`.
The `idColumn` defaults to `id` and is the unique key for that table.
The `dataColumn` is optional and will change the behavior of the provider in relation with the database.
When the `dataColumn` is provided, the provider will parse a single column as JSON in order to set values.
For Sequelize, remember to set that column's type to JSON or JSONB.
When `dataColumn` is not provided, the provider will work on all columns of the table instead.
Before you can actually use the provider, you would have to run the `init` method.
For example:
```js
class CustomClient extends AkairoClient {
/* ... */
async login(token) {
await this.settings.init()
return super.login(token);
}
}
```
Now, the provider can be used like so:
```js
class CustomClient extends AkairoClient {
constructor() {
super({
prefix: message => {
if (message.guild) {
// The third param is the default.
return this.settings.get(message.guild.id, 'prefix', '!');
}
return '!';
}
});
/* ... */
}
}
```
Values can be set with the `set` method:
```js
const { Command } = require('discord-akairo');
class PrefixCommand extends Command {
constructor() {
super('prefix', {
aliases: ['prefix'],
category: 'stuff',
args: [
{
id: 'prefix',
default: '!'
}
],
channel: 'guild'
});
}
async exec(message, args) {
// The third param is the default.
const oldPrefix = this.client.settings.get(message.guild.id, 'prefix', '!');
await this.client.settings.set(message.guild.id, 'prefix', args.prefix);
return message.reply(`Prefix changed from ${oldPrefix} to ${args.prefix}`);
}
}
module.exports = PrefixCommand;
```
================================================
FILE: docs/other/updating.md
================================================
# Updating to v8
### Breaking Changes
This tutorial is for updating from Akairo v7 to v8.
Many changes were introduced in v8 so hopefully this guide can help you fix them.
Not only are there changes within the framework, there are also changes with Discord.js v12.
You will have to update to Node 10 in order to use the libraries due to new JavaScript features.
The suggestions below are not an exhaustive list.
For a full changelog, see [here](https://github.com/discord-akairo/discord-akairo/releases).
### Renames
Below are renames that will mostly be find-and-replace.
##### General
The ClientUtil method `fetchMemberFrom` has been renamed to `fetchMember`.
##### Commands
The CommandHandler event `commandCooldown` has been renamed to `cooldown`.
The Command option and property `channelRestriction` has been renamed to just `channel`.
The Command option and property `trigger` has been renamed to `regex`.
##### Arguments
The Argument option and property `prefix` has been renamed to `flag`.
The Argument method `cast` has been renamed to `process`.
Regex types in arguments e.g. `type: /^text$/` used to evaluate to an object with the property `match` and `groups`.
This has been replaced with an object with the property `match` and `matches`.
The match type `word` has been renamed to `phrase`.
The match type `prefix` has been renamed to `option`.
The TypeResolver property `handler` has been renamed to `commandHandler`.
##### Listeners
The Listener option and property `eventName` has been renamed to just `event`.
### Changes
Below are breaking changes that may require some more thought when fixing.
##### General
The structure of the AkairoClient and the various handlers has been changed.
To see what has changed, start at [Setting Up](../basics/setup.md).
Before, in a type function of an arg when returning a Promise, it would only be a cast failure if the Promise rejected.
Now, it will only be cast failure if the Promise resolves with `null` or `undefined`.
This is to make async functions easier to use.
Similarly, in inhibitors, a Promise rejection would be used to signify that the message was to be blocked.
Now, with Promises, the Promise has to resolve with `true` to signify a block.
The following methods are now properties:
- `Argument#default`
- `Command#trigger`
- `CommandHandler#prefix`
- `CommandHandler#allowMention`
This means that you have to check if its a function before using it e.g.
`typeof arg.default === 'function' ? arg.default(message) : arg.default`.
This allows for checking if a value was set, such as the default value of an argument.
Of course, if you already know that the property is or is not a function, then there is no need for changes.
##### Commands
The events `commandStarted` and `commandFinished` have new parameters.
`commandStarted` now passes `(message, command, args)` where `args` are the parsed args.
`commandFinished` now passes `(message, command, args, returnValue)` where `returnValue` is what the command's exec function returned.
The event `commandBlocked` is no longer fired when permissions checks are failed.
Instead, a new event `missingPermissions` is fired.
It will have the params `(message, command, type, missing)` where `type` could be either `client` or `user` and `missing` are the missing permissions.
Regex commands used to pass in the values of `(message, match, groups, edited)`.
Now it has been changed to `(message, args)`.
The `args` object will contain `match` and `matches` property similar to a regex type.
CommandHandler options `handleEdits` will no longer implicitly enable the `commandUtil` option.
The `commandUtilLifetime` option also now defaults to 5 minutes.
All the CommandUtil parse information such as `command`, `prefix`, `alias` etc. are moved to the `parsed` property.
The `defaultPrompt` option has been changed to `argumentDefaults` which allow for more defaults.
You can simply move your options into `argumentDefaults.prompt`.
##### Arguments
Argument parsing now uses a new parser.
Some behavior with whitespace and quotes may have changed.
The argument type function used to have a special behavior for when `true` was returned.
It would use the original user input phrase as the evaluated argument.
Now, it simply is just `true`.
Argument functions `this` binding has also been changed.
They will now all point to the Argument instance rather than being inconsistent, where some would point to the command.
The default value of the argument `default` option is now `null` rather than an empty string.
Type functions previously would be passed `(phrase, message, args)`.
They now pass `(message, phrase)`.
The previous arguments can be accessed by using a generator for arguments.
Previously, prompt functions would be passed `(message, args, retries)`.
They now pass `(message, data)` where `data` is an object.
You can get the retry count, among other properties, with `data.retries`.
The previous arguments can be accessed by using a generator for arguments.
The argument type `invite` used to just match an invite from a string.
Now, it will attempt to fetch an Invite object.
### Removals
All features deprecated in v7.5.x have been removed as well as some unexpected removals.
Suggestions will be made for replacements.
##### General
Loading modules by instances is now unsupported.
This means you cannot do, for example, `module.exports = new Command(...)` or `handler.load(new Listener(...))`.
Subsequently, this also means that the constructor for AkairoModule and related have also changed.
The constructor is now `(id, options)` instead of `(id, exec, options)`.
The `exec` method is now also not on the AkairoModule prototype and should be implemented where needed when extending.
The AkairoHandler method `add` has been removed.
Use the `load` with a full path instead.
The AkairoHandler events `add` and `reload` have been removed.
The `load` event will now take care of both.
When reloaded, the `load` event will pass a second parameter which is a boolean, signifying that is was a reload.
The ability to enable/disable modules have been removed, along with the events.
It is recommended to implement this feature manually instead.
Both ClientUtil prompt methods, `prompt` and `promptIn` were removed.
Alternatives includes your own message collector, using `awaitMessages`, or using `Argument#collect`.
An example of the `collect` method would be `new Argument(command, argOptions).collect(message)`.
Selfbot support has been removed.
##### Commands
The command option `split` is now removed.
Instead, the `quoted` option is added, which can be true or false, defaulting to true.
The `match` option on an argument can no longer be a function.
In the command `exec` function as well as the `commandStarted` event and some other places, the `edited` parameter is removed.
To see if the message was edited, you can check with `message.edited`.
The `dynamic` and `dynamicInt` types were removed.
Instead, use a union type e.g. `Argument.union('integer', 'string')`.
##### SQLiteHandler
SQLiteHandler and related properties in AkairoClient have been removed completely.
Alternatives include `SQLiteProvider` and `SequelizeProvider`.
Or, you can make your own by extending the `Provider` class.
For a guide on how to use the new providers, see [Using Providers](./providers,md).
##### Other Removals
Other removals include the send aliases in CommandUtil, deprecated methods in ClientUtil, and some methods in AkairoClient.
Most of them can now be found in Discord.js itself or implemented yourself if needed.
================================================
FILE: docs/snippets/ping.md
================================================
# Ping Command
```js
const { Command } = require('discord-akairo');
class PingCommand extends Command {
constructor() {
super('ping', {
aliases: ['ping', 'hello']
});
}
async exec(message) {
const sent = await message.util.reply('Pong!');
const timeDiff = (sent.editedAt || sent.createdAt) - (message.editedAt || message.createdAt);
return message.util.reply(
'Pong!\n' +
`🔂 **RTT**: ${timeDiff} ms\n` +
`💟 **Heartbeat**: ${Math.round(this.client.ws.ping)} ms`
);
}
}
module.exports = PingCommand;
```
================================================
FILE: package.json
================================================
{
"name": "discord-akairo",
"version": "8.1.0",
"description": "A highly customizable bot framework for Discord.js.",
"main": "./src/index.js",
"types": "./src/index.d.ts",
"author": "1Computer",
"license": "MIT",
"keywords": [
"discord",
"discord-js",
"discord.js",
"framework",
"bot",
"client",
"modular",
"commands",
"arguments"
],
"dependencies": {},
"devDependencies": {
"@types/node": "^10.14.4",
"discord.js-docgen": "github:discordjs/docgen",
"eslint": "^5.16.0",
"jsdoc": "^3.6.4",
"tslint": "^5.15.0",
"tslint-config-typings": "^0.3.1",
"typescript": "^3.4.2"
},
"scripts": {
"test": "npm run lint",
"lint": "eslint ./src && tslint ./src/index.d.ts"
},
"repository": {
"type": "git",
"url": "https://github.com/discord-akairo/discord-akairo.git"
},
"bugs": {
"url": "https://github.com/discord-akairo/discord-akairo/issues"
},
"homepage": "https://github.com/discord-akairo/discord-akairo"
}
================================================
FILE: src/index.d.ts
================================================
declare module 'discord-akairo' {
import {
BufferResolvable, Client, ClientOptions, Collection,
Message, MessageAttachment, MessageEmbed,
MessageEditOptions, MessageOptions, MessagePayload,
User, UserResolvable, GuildMember,
Channel, Role, Emoji, Guild, ReplyMessageOptions,
PermissionResolvable, Snowflake
} from 'discord.js';
import { EventEmitter } from 'events';
import { Stream } from 'stream';
module 'discord.js' {
export interface Message {
util?: CommandUtil;
}
}
export class AkairoError extends Error {
public code: string;
}
export class AkairoClient extends Client {
public constructor(options?: AkairoOptions & ClientOptions);
public constructor(options: AkairoOptions, clientOptions: ClientOptions);
public ownerID: Snowflake | Snowflake[];
public util: ClientUtil;
public isOwner(user: UserResolvable): boolean;
}
export class AkairoHandler extends EventEmitter {
public constructor(client: AkairoClient, options: AkairoHandlerOptions);
public automateCategories: boolean;
public extensions: Set;
public categories: Collection>;
public classToHandle: Function;
public client: AkairoClient;
public directory: string;
public loadFilter: LoadPredicate;
public modules: Collection;
public deregister(mod: AkairoModule): void;
public findCategory(name: string): Category;
public load(thing: string | Function, isReload?: boolean): AkairoModule;
public loadAll(directory?: string, filter?: LoadPredicate): this;
public register(mod: AkairoModule, filepath?: string): void;
public reload(id: string): AkairoModule;
public reloadAll(): this;
public remove(id: string): AkairoModule;
public removeAll(): this;
public on(event: 'remove', listener: (mod: AkairoModule) => any): this;
public on(event: 'load', listener: (mod: AkairoModule, isReload: boolean) => any): this;
public static readdirRecursive(directory: string): string[];
}
export class AkairoModule {
public constructor(id: string, options?: AkairoModuleOptions);
public category: Category;
public categoryID: string;
public client: AkairoClient;
public filepath: string;
public handler: AkairoHandler;
public id: string;
public reload(): this;
public remove(): this;
}
export class Argument {
public constructor(command: Command, options: ArgumentOptions);
public readonly client: AkairoClient;
public command: Command;
public default: DefaultValueSupplier | any;
public description: string | any;
public readonly handler: CommandHandler;
public index?: number;
public limit: number;
public match: ArgumentMatch;
public multipleFlags: boolean;
public flag?: string | string[];
public otherwise?: string | MessageOptions | OtherwiseContentSupplier;
public prompt?: ArgumentPromptOptions | boolean;
public type: ArgumentType | ArgumentTypeCaster;
public unordered: boolean | number | number[];
public allow(message: Message): boolean;
public cast(message: Message, phrase: string): Promise;
public collect(message: Message, commandInput?: string): Promise;
public process(message: Message, phrase: string): Promise;
public static cast(type: ArgumentType | ArgumentTypeCaster, resolver: TypeResolver, message: Message, phrase: string): Promise;
public static compose(...types: (ArgumentType | ArgumentTypeCaster)[]): ArgumentTypeCaster;
public static composeWithFailure(...types: (ArgumentType | ArgumentTypeCaster)[]): ArgumentTypeCaster;
public static isFailure(value: any): value is null | undefined | Flag & { value: any };
public static product(...types: (ArgumentType | ArgumentTypeCaster)[]): ArgumentTypeCaster;
public static range(type: ArgumentType | ArgumentTypeCaster, min: number, max: number, inclusive?: boolean): ArgumentTypeCaster;
public static tagged(type: ArgumentType | ArgumentTypeCaster, tag?: any): ArgumentTypeCaster;
public static taggedUnion(...types: (ArgumentType | ArgumentTypeCaster)[]): ArgumentTypeCaster;
public static taggedWithInput(type: ArgumentType | ArgumentTypeCaster, tag?: any): ArgumentTypeCaster;
public static union(...types: (ArgumentType | ArgumentTypeCaster)[]): ArgumentTypeCaster;
public static validate(type: ArgumentType | ArgumentTypeCaster, predicate: ParsedValuePredicate): ArgumentTypeCaster;
public static withInput(type: ArgumentType | ArgumentTypeCaster): ArgumentTypeCaster;
}
export class Category extends Collection {
public constructor(id: string, iterable?: Iterable<[K, V][]>);
public id: string;
public reloadAll(): this;
public removeAll(): this;
}
export class ClientUtil {
public constructor(client: AkairoClient);
public readonly client: AkairoClient;
public attachment(file: BufferResolvable | Stream, name?: string): MessageAttachment;
public checkChannel(text: string, channel: Channel, caseSensitive?: boolean, wholeWord?: boolean): boolean;
public checkEmoji(text: string, emoji: Emoji, caseSensitive?: boolean, wholeWord?: boolean): boolean;
public checkGuild(text: string, guild: Guild, caseSensitive?: boolean, wholeWord?: boolean): boolean;
public checkMember(text: string, member: GuildMember, caseSensitive?: boolean, wholeWord?: boolean): boolean;
public checkRole(text: string, role: Role, caseSensitive?: boolean, wholeWord?: boolean): boolean;
public checkUser(text: string, user: User, caseSensitive?: boolean, wholeWord?: boolean): boolean;
public collection(iterable?: Iterable<[K, V][]>): Collection;
public compareStreaming(oldMember: GuildMember, newMember: GuildMember): number;
public embed(data?: object): MessageEmbed;
public fetchMember(guild: Guild, id: string, cache?: boolean): Promise;
public resolveChannel(text: string, channels: Collection, caseSensitive?: boolean, wholeWord?: boolean): Channel;
public resolveChannels(text: string, channels: Collection, caseSensitive?: boolean, wholeWord?: boolean): Collection;
public resolveEmoji(text: string, emojis: Collection, caseSensitive?: boolean, wholeWord?: boolean): Emoji;
public resolveEmojis(text: string, emojis: Collection, caseSensitive?: boolean, wholeWord?: boolean): Collection;
public resolveGuild(text: string, guilds: Collection, caseSensitive?: boolean, wholeWord?: boolean): Guild;
public resolveGuilds(text: string, guilds: Collection, caseSensitive?: boolean, wholeWord?: boolean): Collection;
public resolveMember(text: string, members: Collection, caseSensitive?: boolean, wholeWord?: boolean): GuildMember;
public resolveMembers(text: string, members: Collection, caseSensitive?: boolean, wholeWord?: boolean): Collection;
public resolvePermissionNumber(number: number): string[];
public resolveRole(text: string, roles: Collection, caseSensitive?: boolean, wholeWord?: boolean): Role;
public resolveRoles(text: string, roles: Collection, caseSensitive?: boolean, wholeWord?: boolean): Collection;
public resolveUser(text: string, users: Collection, caseSensitive?: boolean, wholeWord?: boolean): User;
public resolveUsers(text: string, users: Collection, caseSensitive?: boolean, wholeWord?: boolean): Collection;
}
export class Command extends AkairoModule {
public constructor(id: string, options?: CommandOptions);
public aliases: string[];
public argumentDefaults: DefaultArgumentOptions;
public quoted: boolean;
public category: Category;
public channel?: string;
public client: AkairoClient;
public clientPermissions: PermissionResolvable | PermissionResolvable[] | MissingPermissionSupplier;
public cooldown?: number;
public description: string | any;
public editable: boolean;
public filepath: string;
public handler: CommandHandler;
public id: string;
public lock?: KeySupplier;
public locker?: Set;
public ignoreCooldown?: Snowflake | Snowflake[] | IgnoreCheckPredicate;
public ignorePermissions?: Snowflake | Snowflake[] | IgnoreCheckPredicate;
public ownerOnly: boolean;
public prefix?: string | string[] | PrefixSupplier;
public ratelimit: number;
public regex: RegExp | RegexSupplier;
public typing: boolean;
public userPermissions: PermissionResolvable | PermissionResolvable[] | MissingPermissionSupplier;
public before(message: Message): any;
public condition(message: Message): boolean;
public exec(message: Message, args: any): any;
public parse(message: Message, content: string): Promise;
public reload(): this;
public remove(): this;
}
export class CommandHandler extends AkairoHandler {
public constructor(client: AkairoClient, options: CommandHandlerOptions);
public aliasReplacement?: RegExp;
public aliases: Collection;
public allowMention: boolean | MentionPrefixPredicate;
public argumentDefaults: DefaultArgumentOptions;
public blockBots: boolean;
public blockClient: boolean;
public categories: Collection>;
public classToHandle: typeof Command;
public client: AkairoClient;
public commandUtil: boolean;
public commandUtilLifetime: number;
public commandUtils: Collection;
public commandUtilSweepInterval: number;
public cooldowns: Collection;
public defaultCooldown: number;
public directory: string;
public fetchMembers: boolean;
public handleEdits: boolean;
public ignoreCooldown: Snowflake | Snowflake[] | IgnoreCheckPredicate;
public ignorePermissions: Snowflake | Snowflake[] | IgnoreCheckPredicate;
public inhibitorHandler?: InhibitorHandler;
public modules: Collection;
public prefix: string | string[] | PrefixSupplier;
public prefixes: Collection>;
public prompts: Collection>;
public resolver: TypeResolver;
public storeMessage: boolean;
public add(filename: string): Command;
public addPrompt(channel: Channel, user: User): void;
public deregister(command: Command): void;
public emitError(err: Error, message: Message, command?: Command): void;
public findCategory(name: string): Category;
public findCommand(name: string): Command;
public handle(message: Message): Promise;
public handleDirectCommand(message: Message, content: string, command: Command, ignore?: boolean): Promise;
public handleRegexAndConditionalCommands(message: Message): Promise;
public handleRegexCommands(message: Message): Promise;
public handleConditionalCommands(message: Message): Promise;
public hasPrompt(channel: Channel, user: User): boolean;
public load(thing: string | Function, isReload?: boolean): Command;
public loadAll(directory?: string, filter?: LoadPredicate): this;
public parseCommand(message: Message): Promise;
public parseCommandOverwrittenPrefixes(message: Message): Promise;
public parseMultiplePrefixes(message: Message, prefixes: [string, Set | null]): ParsedComponentData;
public parseWithPrefix(message: Message, prefix: string, associatedCommands?: Set): ParsedComponentData;
public register(command: Command, filepath?: string): void;
public reload(id: string): Command;
public reloadAll(): this;
public remove(id: string): Command;
public removeAll(): this;
public removePrompt(channel: Channel, user: User): void;
public runAllTypeInhibitors(message: Message): Promise;
public runPermissionChecks(message: Message, command: Command): Promise;
public runPreTypeInhibitors(message: Message): Promise;
public runPostTypeInhibitors(message: Message, command: Command): Promise;
public runCooldowns(message: Message, command: Command): boolean;
public runCommand(message: Message, command: Command, args: any): Promise;
public useInhibitorHandler(inhibitorHandler: InhibitorHandler): this;
public useListenerHandler(ListenerHandler: ListenerHandler): this;
public on(event: 'remove', listener: (command: Command) => any): this;
public on(event: 'load', listener: (command: Command, isReload: boolean) => any): this;
public on(event: 'commandBlocked', listener: (message: Message, command: Command, reason: string) => any): this;
public on(event: 'commandBreakout', listener: (message: Message, command: Command, breakMessage: Message) => any): this;
public on(event: 'commandCancelled', listener: (message: Message, command: Command, retryMessage?: Message) => any): this;
public on(event: 'commandFinished', listener: (message: Message, command: Command, args: any, returnValue: any) => any): this;
public on(event: 'commandLocked', listener: (message: Message, command: Command) => any): this;
public on(event: 'commandStarted', listener: (message: Message, command: Command, args: any) => any): this;
public on(event: 'cooldown', listener: (message: Message, command: Command, remaining: number) => any): this;
public on(event: 'error', listener: (error: Error, message: Message, command?: Command) => any): this;
public on(event: 'inPrompt' | 'messageInvalid', listener: (message: Message) => any): this;
public on(event: 'messageBlocked', listener: (message: Message, reason: string) => any): this;
public on(event: 'missingPermissions', listener: (message: Message, command: Command, type: 'client' | 'user', missing?: any) => any): this;
}
export class CommandUtil {
public constructor(handler: CommandHandler, message: Message);
public handler: CommandHandler;
public lastResponse?: Message;
public message: Message;
public messages?: Collection;
public parsed?: ParsedComponentData;
public shouldEdit: boolean;
public addMessage(message: Message | Message[]): Message | Message[];
public edit(content: string | MessageEditOptions | MessagePayload): Promise;
public reply(options: string | MessagePayload | ReplyMessageOptions): Promise;
public send(options: string | MessagePayload | MessageOptions): Promise;
public sendNew(options: string | MessagePayload | MessageOptions): Promise;
public setEditable(state: boolean): this;
public setLastResponse(message: Message | Message[]): Message;
public static transformOptions(options?: string | MessageOptions): MessageOptions;
}
export class Flag {
public constructor(type: string, data: object);
public type: string;
public static cancel(): Flag;
public static continue(command: string, ignore?: boolean, rest?: string): Flag & { command: string, ignore: boolean, rest: string };
public static retry(message: Message): Flag & { message: Message };
public static fail(value: any): Flag & { value: any };
public static is(value: any, type: 'cancel'): value is Flag;
public static is(value: any, type: 'continue'): value is Flag & { command: string, ignore: boolean, rest: string };
public static is(value: any, type: 'retry'): value is Flag & { message: Message };
public static is(value: any, type: 'fail'): value is Flag & { value: any };
public static is(value: any, type: string): value is Flag;
}
export class Inhibitor extends AkairoModule {
public constructor(id: string, options?: InhibitorOptions);
public category: Category;
public client: AkairoClient;
public filepath: string;
public handler: InhibitorHandler;
public id: string;
public reason: string;
public type: string;
public exec(message: Message, command?: Command): boolean | Promise;
public reload(): this;
public remove(): this;
}
export class InhibitorHandler extends AkairoHandler {
public constructor(client: AkairoClient, options: AkairoHandlerOptions);
public categories: Collection>;
public classToHandle: typeof Inhibitor;
public client: AkairoClient;
public directory: string;
public modules: Collection;
public deregister(inhibitor: Inhibitor): void;
public findCategory(name: string): Category;
public load(thing: string | Function): Inhibitor;
public loadAll(directory?: string, filter?: LoadPredicate): this;
public register(inhibitor: Inhibitor, filepath?: string): void;
public reload(id: string): Inhibitor;
public reloadAll(): this;
public remove(id: string): Inhibitor;
public removeAll(): this;
public test(type: 'all' | 'pre' | 'post', message: Message, command?: Command): Promise;
public on(event: 'remove', listener: (inhibitor: Inhibitor) => any): this;
public on(event: 'load', listener: (inhibitor: Inhibitor, isReload: boolean) => any): this;
}
export class Listener extends AkairoModule {
public constructor(id: string, options?: ListenerOptions);
public category: Category;
public client: AkairoClient;
public emitter: string | EventEmitter;
public event: string;
public filepath: string;
public handler: ListenerHandler;
public type: string;
public exec(...args: any[]): any;
public reload(): this;
public remove(): this;
}
export class ListenerHandler extends AkairoHandler {
public constructor(client: AkairoClient, options: AkairoHandlerOptions);
public categories: Collection>;
public classToHandle: typeof Listener;
public client: AkairoClient;
public directory: string;
public emitters: Collection;
public modules: Collection;
public add(filename: string): Listener;
public addToEmitter(id: string): Listener;
public deregister(listener: Listener): void;
public findCategory(name: string): Category;
public load(thing: string | Function): Listener;
public loadAll(directory?: string, filter?: LoadPredicate): this;
public register(listener: Listener, filepath?: string): void;
public reload(id: string): Listener;
public reloadAll(): this;
public remove(id: string): Listener;
public removeAll(): this;
public removeFromEmitter(id: string): Listener;
public setEmitters(emitters: { [x: string]: EventEmitter }): void;
public on(event: 'remove', listener: (listener: Listener) => any): this;
public on(event: 'load', listener: (listener: Listener, isReload: boolean) => any): this;
}
export abstract class Provider {
public items: Collection;
public abstract clear(id: string): any;
public abstract delete(id: string, key: string): any;
public abstract get(id: string, key: string, defaultValue: any): any;
public abstract init(): any;
public abstract set(id: string, key: string, value: any): any;
}
export class SequelizeProvider extends Provider {
public constructor(table: any, options?: ProviderOptions);
public dataColumn?: string;
public idColumn: string;
public items: Collection;
public table: any;
public clear(id: string): Promise;
public delete(id: string, key: string): Promise;
public get(id: string, key: string, defaultValue: any): any;
public init(): Promise;
public set(id: string, key: string, value: any): Promise;
}
export class SQLiteProvider extends Provider {
public constructor(db: any | Promise, tableName: string, options?: ProviderOptions);
public dataColumn?: string;
public db: any;
public idColumn: string;
public items: Collection;
public tableName: string;
public clear(id: string): Promise;
public delete(id: string, key: string): Promise;
public get(id: string, key: string, defaultValue: any): any;
public init(): Promise;
public set(id: string, key: string, value: any): Promise;
}
export class MongooseProvider extends Provider {
public constructor(model: any);
public model: any;
public items: Collection;
public clear(id: string): Promise;
public delete(id: string, key: string): Promise;
public get(id: string, key: string, defaultValue: any): any;
public getDocument(id: string): any;
public init(): Promise;
public set(id: string, key: string, value: any): Promise;
}
export class TypeResolver {
public constructor(handler: CommandHandler);
public client: AkairoClient;
public commandHandler: CommandHandler;
public inhibitorHandler?: InhibitorHandler;
public listenerHandler?: ListenerHandler;
public types: Collection;
public addBuiltInTypes(): void;
public addType(name: string, fn: ArgumentTypeCaster): this;
public addTypes(types: { [x: string]: ArgumentTypeCaster }): this;
public type(name: string): ArgumentTypeCaster;
}
export class Util {
public static isEventEmitter(value: any): boolean;
public static isPromise(value: any): boolean;
}
export interface AkairoHandlerOptions {
automateCategories?: boolean;
classToHandle?: Function;
directory?: string;
extensions?: string[] | Set;
loadFilter?: LoadPredicate;
}
export interface AkairoModuleOptions {
category?: string;
}
export interface AkairoOptions {
ownerID?: Snowflake | Snowflake[];
}
export interface DefaultArgumentOptions {
prompt?: ArgumentPromptOptions;
otherwise?: string | MessageOptions | OtherwiseContentSupplier;
modifyOtherwise?: OtherwiseContentModifier;
}
export interface ArgumentOptions {
default?: DefaultValueSupplier | any;
description?: string;
id?: string;
index?: number;
limit?: number;
match?: ArgumentMatch;
modifyOtherwise?: OtherwiseContentModifier;
multipleFlags?: boolean;
flag?: string | string[];
otherwise?: string | MessageOptions | OtherwiseContentSupplier;
prompt?: ArgumentPromptOptions | boolean;
type?: ArgumentType | ArgumentTypeCaster;
unordered?: boolean | number | number[];
}
export interface ArgumentPromptData {
infinite: boolean;
message: Message;
retries: number;
phrase: string;
failure: void | (Flag & { value: any });
}
export interface ArgumentPromptOptions {
breakout?: boolean;
cancel?: string | MessageOptions | PromptContentSupplier;
cancelWord?: string;
ended?: string | MessageOptions | PromptContentSupplier;
infinite?: boolean;
limit?: number;
modifyCancel?: PromptContentModifier;
modifyEnded?: PromptContentModifier;
modifyRetry?: PromptContentModifier;
modifyStart?: PromptContentModifier;
modifyTimeout?: PromptContentModifier;
optional?: boolean;
retries?: number;
retry?: string | MessageOptions | PromptContentSupplier;
start?: string | MessageOptions | PromptContentSupplier;
stopWord?: string;
time?: number;
timeout?: string | MessageOptions | PromptContentSupplier;
}
export interface ArgumentRunnerState {
index: number;
phraseIndex: number;
usedIndices: Set;
}
export interface CommandOptions extends AkairoModuleOptions {
aliases?: string[];
args?: ArgumentOptions[] | ArgumentGenerator;
argumentDefaults?: DefaultArgumentOptions;
before?: BeforeAction;
channel?: 'guild' | 'dm';
clientPermissions?: PermissionResolvable | PermissionResolvable[] | MissingPermissionSupplier;
condition?: ExecutionPredicate;
cooldown?: number;
description?: string | any;
editable?: boolean;
flags?: string[];
ignoreCooldown?: Snowflake | Snowflake[] | IgnoreCheckPredicate;
ignorePermissions?: Snowflake | Snowflake[] | IgnoreCheckPredicate;
lock?: KeySupplier | 'guild' | 'channel' | 'user';
optionFlags?: string[];
ownerOnly?: boolean;
prefix?: string | string[] | PrefixSupplier;
ratelimit?: number;
regex?: RegExp | RegexSupplier;
separator?: string;
typing?: boolean;
userPermissions?: PermissionResolvable | PermissionResolvable[] | MissingPermissionSupplier;
quoted?: boolean;
}
export interface CommandHandlerOptions extends AkairoHandlerOptions {
aliasReplacement?: RegExp;
allowMention?: boolean | MentionPrefixPredicate;
argumentDefaults?: DefaultArgumentOptions;
blockBots?: boolean;
blockClient?: boolean;
commandUtil?: boolean;
commandUtilLifetime?: number;
commandUtilSweepInterval?: number;
defaultCooldown?: number;
fetchMembers?: boolean;
handleEdits?: boolean;
ignoreCooldown?: Snowflake | Snowflake[] | IgnoreCheckPredicate;
ignorePermissions?: Snowflake | Snowflake[] | IgnoreCheckPredicate;
prefix?: string | string[] | PrefixSupplier;
storeMessages?: boolean;
}
export interface ContentParserResult {
all: StringData[];
phrases: StringData[];
flags: StringData[];
optionFlags: StringData[];
}
export interface CooldownData {
end: number;
timer: NodeJS.Timer;
uses: number;
}
export interface FailureData {
phrase: string;
failure: void | (Flag & { value: any });
}
export interface InhibitorOptions extends AkairoModuleOptions {
reason?: string;
type?: string;
priority?: number;
}
export interface ListenerOptions extends AkairoModuleOptions {
emitter: string | EventEmitter;
event: string;
type?: string;
}
export interface ParsedComponentData {
afterPrefix?: string;
alias?: string;
command?: Command;
content?: string;
prefix?: string;
}
export interface ProviderOptions {
dataColumn?: string;
idColumn?: string;
}
export type StringData = {
type: 'Phrase';
value: string;
raw: string;
} | {
type: 'Flag';
key: string;
raw: string;
} | {
type: 'OptionFlag',
key: string;
value: string;
raw: string;
};
export type ArgumentMatch = 'phrase' | 'flag' | 'option' | 'rest' | 'separate' | 'text' | 'content' | 'restContent' | 'none';
export type ArgumentType = 'string' | 'lowercase' | 'uppercase' | 'charCodes'
| 'number' | 'integer' | 'bigint' | 'emojint'
| 'url' | 'date' | 'color'
| 'user' | 'users' | 'member' | 'members' | 'relevant' | 'relevants'
| 'channel' | 'channels' | 'textChannel' | 'textChannels' | 'voiceChannel' | 'voiceChannels' | 'categoryChannel' | 'categoryChannels' | 'newsChannel' | 'newsChannels' | 'storeChannel' | 'storeChannels'
| 'role' | 'roles' | 'emoji' | 'emojis' | 'guild' | 'guilds'
| 'message' | 'guildMessage' | 'relevantMessage' | 'invite'
| 'userMention' | 'memberMention' | 'channelMention' | 'roleMention' | 'emojiMention'
| 'commandAlias' | 'command' | 'inhibitor' | 'listener'
| (string | string[])[]
| RegExp
| string;
export type ArgumentGenerator = (message: Message, parsed: ContentParserResult, state: ArgumentRunnerState) => IterableIterator;
export type ArgumentTypeCaster = (message: Message, phrase: string) => any;
export type BeforeAction = (message: Message) => any;
export type DefaultValueSupplier = (message: Message, data: FailureData) => any;
export type ExecutionPredicate = (message: Message) => boolean;
export type IgnoreCheckPredicate = (message: Message, command: Command) => boolean;
export type KeySupplier = (message: Message, args: any) => string;
export type LoadPredicate = (filepath: string) => boolean;
export type MentionPrefixPredicate = (message: Message) => boolean | Promise;
export type MissingPermissionSupplier = (message: Message) => Promise | any;
export type OtherwiseContentModifier = (message: Message, text: string, data: FailureData)
=> string | MessageOptions | Promise;
export type OtherwiseContentSupplier = (message: Message, data: FailureData)
=> string | MessageOptions | Promise;
export type ParsedValuePredicate = (message: Message, phrase: string, value: any) => boolean;
export type PrefixSupplier = (message: Message) => string | string[] | Promise;
export type PromptContentModifier = (message: Message, text: string, data: ArgumentPromptData)
=> string | MessageOptions | Promise;
export type PromptContentSupplier = (message: Message, data: ArgumentPromptData)
=> string | MessageOptions | Promise;
export type RegexSupplier = (message: Message) => RegExp;
export const Constants: {
ArgumentMatches: {
PHRASE: 'phrase';
FLAG: 'flag';
OPTION: 'option';
REST: 'rest';
SEPARATE: 'separate';
TEXT: 'text';
CONTENT: 'content';
REST_CONTENT: 'restContent';
NONE: 'none';
};
ArgumentTypes: {
STRING: 'string';
LOWERCASE: 'lowercase';
UPPERCASE: 'uppercase';
CHAR_CODES: 'charCodes';
NUMBER: 'number';
INTEGER: 'integer';
BIGINT: 'bigint';
EMOJINT: 'emojint';
URL: 'url';
DATE: 'date';
COLOR: 'color';
USER: 'user';
USERS: 'users';
MEMBER: 'member';
MEMBERS: 'members';
RELEVANT: 'relevant';
RELEVANTS: 'relevants';
CHANNEL: 'channel';
CHANNELS: 'channels';
TEXT_CHANNEL: 'textChannel';
TEXT_CHANNELS: 'textChannels';
VOICE_CHANNEL: 'voiceChannel';
VOICE_CHANNELS: 'voiceChannels';
CATEGORY_CHANNEL: 'categoryChannel';
CATEGORY_CHANNELS: 'categoryChannels';
NEWS_CHANNEL: 'newsChannel';
NEWS_CHANNELS: 'newsChannels';
STORE_CHANNEL: 'storeChannel';
STORE_CHANNELS: 'storeChannels';
ROLE: 'role';
ROLES: 'roles';
EMOJI: 'emoji';
EMOJIS: 'emojis';
GUILD: 'guild';
GUILDS: 'guilds';
MESSAGE: 'message';
GUILD_MESSAGE: 'guildMessage';
INVITE: 'invite';
MEMBER_MENTION: 'memberMention';
CHANNEL_MENTION: 'channelMention';
ROLE_MENTION: 'roleMention';
EMOJI_MENTION: 'emojiMention';
COMMAND_ALIAS: 'commandAlias';
COMMAND: 'command';
INHIBITOR: 'inhibitor';
LISTENER: 'listener';
};
AkairoHandlerEvents: {
LOAD: 'load';
REMOVE: 'remove';
};
CommandHandlerEvents: {
MESSAGE_BLOCKED: 'messageBlocked';
MESSAGE_INVALID: 'messageInvalid';
COMMAND_BLOCKED: 'commandBlocked';
COMMAND_STARTED: 'commandStarted';
COMMAND_FINISHED: 'commandFinished';
COMMAND_CANCELLED: 'commandCancelled';
COMMAND_LOCKED: 'commandLocked';
MISSING_PERMISSIONS: 'missingPermissions';
COOLDOWN: 'cooldown';
IN_PROMPT: 'inPrompt';
ERROR: 'error';
};
BuiltInReasons: {
CLIENT: 'client';
BOT: 'bot';
OWNER: 'owner';
GUILD: 'guild';
DM: 'dm';
};
};
export const version: string;
}
================================================
FILE: src/index.js
================================================
module.exports = {
// Core
AkairoClient: require('./struct/AkairoClient'),
AkairoHandler: require('./struct/AkairoHandler'),
AkairoModule: require('./struct/AkairoModule'),
ClientUtil: require('./struct/ClientUtil'),
// Commands
Command: require('./struct/commands/Command'),
CommandHandler: require('./struct/commands/CommandHandler'),
CommandUtil: require('./struct/commands/CommandUtil'),
Flag: require('./struct/commands/Flag'),
// Arguments
Argument: require('./struct/commands/arguments/Argument'),
TypeResolver: require('./struct/commands/arguments/TypeResolver'),
// Inhibitors
Inhibitor: require('./struct/inhibitors/Inhibitor'),
InhibitorHandler: require('./struct/inhibitors/InhibitorHandler'),
// Listeners
Listener: require('./struct/listeners/Listener'),
ListenerHandler: require('./struct/listeners/ListenerHandler'),
// Providers
Provider: require('./providers/Provider'),
SequelizeProvider: require('./providers/SequelizeProvider'),
SQLiteProvider: require('./providers/SQLiteProvider'),
MongooseProvider: require('./providers/MongooseProvider'),
// Utilities
AkairoError: require('./util/AkairoError'),
Category: require('./util/Category'),
Constants: require('./util/Constants'),
Util: require('./util/Util'),
version: require('../package.json').version
};
================================================
FILE: src/providers/MongooseProvider.js
================================================
const Provider = require('./Provider');
/**
* Provider using the `Mongoose` library.
* @param {Model} model - A Mongoose model.
* @extends {Provider}
*/
class MongooseProvider extends Provider {
constructor(model) {
super();
/**
* Mongoose model.
* @type {Model}
*/
this.model = model;
}
/**
* Initializes the provider.
* @returns {Promise}
*/
async init() {
const guilds = await this.model.find();
for (const i in guilds) {
const guild = guilds[i];
this.items.set(guild.id, guild.settings);
}
}
/**
* Gets a value.
* @param {string} id - guildID.
* @param {string} key - The key to get.
* @param {any} [defaultValue] - Default value if not found or null.
* @returns {any}
*/
get(id, key, defaultValue) {
if (this.items.has(id)) {
const value = this.items.get(id)[key];
return value == null ? defaultValue : value;
}
return defaultValue;
}
/**
* Sets a value.
* @param {string} id - guildID.
* @param {string} key - The key to set.
* @param {any} value - The value.
* @returns {Promise} - Mongoose query object|document
*/
async set(id, key, value) {
const data = this.items.get(id) || {};
data[key] = value;
this.items.set(id, data);
const doc = await this.getDocument(id);
doc.settings[key] = value;
doc.markModified('settings');
return doc.save();
}
/**
* Deletes a value.
* @param {string} id - guildID.
* @param {string} key - The key to delete.
* @returns {Promise} - Mongoose query object|document
*/
async delete(id, key) {
const data = this.items.get(id) || {};
delete data[key];
const doc = await this.getDocument(id);
delete doc.settings[key];
doc.markModified('settings');
return doc.save();
}
/**
* Removes a document.
* @param {string} id - GuildID.
* @returns {Promise}
*/
async clear(id) {
this.items.delete(id);
const doc = await this.getDocument(id);
if (doc) await doc.remove();
}
/**
* Gets a document by guildID.
* @param {string} id - guildID.
* @returns {Promise} - Mongoose query object|document
*/
async getDocument(id) {
const obj = await this.model.findOne({ id });
if (!obj) {
// eslint-disable-next-line new-cap
const newDoc = await new this.model({ id, settings: {} }).save();
return newDoc;
}
return obj;
}
}
module.exports = MongooseProvider;
================================================
FILE: src/providers/Provider.js
================================================
const AkairoError = require('../util/AkairoError');
const { Collection } = require('discord.js');
/**
* A provider for key-value storage.
* Must be implemented.
*/
class Provider {
constructor() {
/**
* Cached entries.
* @type {Collection}
*/
this.items = new Collection();
}
/**
* Initializes the provider.
* @abstract
* @returns {any}
*/
init() {
throw new AkairoError('NOT_IMPLEMENTED', this.constructor.name, 'init');
}
/**
* Gets a value.
* @abstract
* @param {string} id - ID of entry.
* @param {string} key - The key to get.
* @param {any} [defaultValue] - Default value if not found or null.
* @returns {any}
*/
get() {
throw new AkairoError('NOT_IMPLEMENTED', this.constructor.name, 'get');
}
/**
* Sets a value.
* @abstract
* @param {string} id - ID of entry.
* @param {string} key - The key to set.
* @param {any} value - The value.
* @returns {any}
*/
set() {
throw new AkairoError('NOT_IMPLEMENTED', this.constructor.name, 'set');
}
/**
* Deletes a value.
* @abstract
* @param {string} id - ID of entry.
* @param {string} key - The key to delete.
* @returns {any}
*/
delete() {
throw new AkairoError('NOT_IMPLEMENTED', this.constructor.name, 'delete');
}
/**
* Clears an entry.
* @abstract
* @param {string} id - ID of entry.
* @returns {any}
*/
clear() {
throw new AkairoError('NOT_IMPLEMENTED', this.constructor.name, 'clear');
}
}
module.exports = Provider;
/**
* Options to use for providers.
* @typedef {Object} ProviderOptions
* @prop {string} [idColumn='id'] - Column for the unique key, defaults to 'id'.
* @prop {string} [dataColumn] - Column for JSON data.
* If not provided, the provider will use all columns of the table.
* If provided, only one column will be used, but it will be more flexible due to being parsed as JSON.
* For Sequelize, note that the model has to specify the type of the column as JSON or JSONB.
*/
================================================
FILE: src/providers/SQLiteProvider.js
================================================
const Provider = require('./Provider');
/**
* Provider using the `sqlite` library.
* @param {Database|Promise} db - SQLite database from `sqlite`.
* @param {string} tableName - Name of table to handle.
* @param {ProviderOptions} [options={}] - Options to use.
* @extends {Provider}
*/
class SQLiteProvider extends Provider {
constructor(db, tableName, { idColumn = 'id', dataColumn } = {}) {
super();
/**
* SQLite database.
* @type {Database}
*/
this.db = db;
/**
* Name of the table.
* @type {string}
*/
this.tableName = tableName;
/**
* Column for ID.
* @type {string}
*/
this.idColumn = idColumn;
/**
* Column for JSON data.
* @type {?string}
*/
this.dataColumn = dataColumn;
}
/**
* Initializes the provider.
* @returns {Promise}
*/
async init() {
const db = await this.db;
this.db = db;
const rows = await this.db.all(`SELECT * FROM ${this.tableName}`);
for (const row of rows) {
this.items.set(row[this.idColumn], this.dataColumn ? JSON.parse(row[this.dataColumn]) : row);
}
}
/**
* Gets a value.
* @param {string} id - ID of entry.
* @param {string} key - The key to get.
* @param {any} [defaultValue] - Default value if not found or null.
* @returns {any}
*/
get(id, key, defaultValue) {
if (this.items.has(id)) {
const value = this.items.get(id)[key];
return value == null ? defaultValue : value;
}
return defaultValue;
}
/**
* Sets a value.
* @param {string} id - ID of entry.
* @param {string} key - The key to set.
* @param {any} value - The value.
* @returns {Promise}
*/
set(id, key, value) {
const data = this.items.get(id) || {};
const exists = this.items.has(id);
data[key] = value;
this.items.set(id, data);
if (this.dataColumn) {
return this.db.run(exists
? `UPDATE ${this.tableName} SET ${this.dataColumn} = $value WHERE ${this.idColumn} = $id`
: `INSERT INTO ${this.tableName} (${this.idColumn}, ${this.dataColumn}) VALUES ($id, $value)`, {
$id: id,
$value: JSON.stringify(data)
});
}
return this.db.run(exists
? `UPDATE ${this.tableName} SET ${key} = $value WHERE ${this.idColumn} = $id`
: `INSERT INTO ${this.tableName} (${this.idColumn}, ${key}) VALUES ($id, $value)`, {
$id: id,
$value: value
});
}
/**
* Deletes a value.
* @param {string} id - ID of entry.
* @param {string} key - The key to delete.
* @returns {Promise}
*/
delete(id, key) {
const data = this.items.get(id) || {};
delete data[key];
if (this.dataColumn) {
return this.db.run(`UPDATE ${this.tableName} SET ${this.dataColumn} = $value WHERE ${this.idColumn} = $id`, {
$id: id,
$value: JSON.stringify(data)
});
}
return this.db.run(`UPDATE ${this.tableName} SET ${key} = $value WHERE ${this.idColumn} = $id`, {
$id: id,
$value: null
});
}
/**
* Clears an entry.
* @param {string} id - ID of entry.
* @returns {Promise}
*/
clear(id) {
this.items.delete(id);
return this.db.run(`DELETE FROM ${this.tableName} WHERE ${this.idColumn} = $id`, { $id: id });
}
}
module.exports = SQLiteProvider;
================================================
FILE: src/providers/SequelizeProvider.js
================================================
const Provider = require('./Provider');
/**
* Provider using the `sequelize` library.
* @param {Model} table - A Sequelize model.
* @param {ProviderOptions} [options={}] - Options to use.
* @extends {Provider}
*/
class SequelizeProvider extends Provider {
constructor(table, { idColumn = 'id', dataColumn } = {}) {
super();
/**
* Sequelize model.
* @type {Model}
*/
this.table = table;
/**
* Column for ID.
* @type {string}
*/
this.idColumn = idColumn;
/**
* Column for JSON data.
* @type {?string}
*/
this.dataColumn = dataColumn;
}
/**
* Initializes the provider.
* @returns {Bluebird}
*/
async init() {
const rows = await this.table.findAll();
for (const row of rows) {
this.items.set(row[this.idColumn], this.dataColumn ? row[this.dataColumn] : row);
}
}
/**
* Gets a value.
* @param {string} id - ID of entry.
* @param {string} key - The key to get.
* @param {any} [defaultValue] - Default value if not found or null.
* @returns {any}
*/
get(id, key, defaultValue) {
if (this.items.has(id)) {
const value = this.items.get(id)[key];
return value == null ? defaultValue : value;
}
return defaultValue;
}
/**
* Sets a value.
* @param {string} id - ID of entry.
* @param {string} key - The key to set.
* @param {any} value - The value.
* @returns {Bluebird}
*/
set(id, key, value) {
const data = this.items.get(id) || {};
data[key] = value;
this.items.set(id, data);
if (this.dataColumn) {
return this.table.upsert({
[this.idColumn]: id,
[this.dataColumn]: data
});
}
return this.table.upsert({
[this.idColumn]: id,
[key]: value
});
}
/**
* Deletes a value.
* @param {string} id - ID of entry.
* @param {string} key - The key to delete.
* @returns {Bluebird}
*/
delete(id, key) {
const data = this.items.get(id) || {};
delete data[key];
if (this.dataColumn) {
return this.table.upsert({
[this.idColumn]: id,
[this.dataColumn]: data
});
}
return this.table.upsert({
[this.idColumn]: id,
[key]: null
});
}
/**
* Clears an entry.
* @param {string} id - ID of entry.
* @returns {Bluebird}
*/
clear(id) {
this.items.delete(id);
return this.table.destroy({ where: { [this.idColumn]: id } });
}
}
module.exports = SequelizeProvider;
================================================
FILE: src/struct/AkairoClient.js
================================================
const { Client } = require('discord.js');
const ClientUtil = require('./ClientUtil');
/**
* The Akairo framework client.
* Creates the handlers and sets them up.
* @param {AkairoOptions} [options={}] - Options for the client.
* @param {ClientOptions} [clientOptions] - Options for Discord JS client.
* If not specified, the previous options parameter is used instead.
*/
class AkairoClient extends Client {
constructor(options = {}, clientOptions) {
super(clientOptions || options);
const { ownerID = '' } = options;
/**
* The ID of the owner(s).
* @type {Snowflake|Snowflake[]}
*/
this.ownerID = ownerID;
/**
* Utility methods.
* @type {ClientUtil}
*/
this.util = new ClientUtil(this);
}
/**
* Checks if a user is the owner of this bot.
* @param {UserResolvable} user - User to check.
* @returns {boolean}
*/
isOwner(user) {
const id = this.users.resolveId(user);
return Array.isArray(this.ownerID)
? this.ownerID.includes(id)
: id === this.ownerID;
}
}
module.exports = AkairoClient;
/**
* Options for the client.
* @typedef {Object} AkairoOptions
* @prop {Snowflake|Snowflake[]} [ownerID=''] - Discord ID of the client owner(s).
*/
================================================
FILE: src/struct/AkairoHandler.js
================================================
const AkairoError = require('../util/AkairoError');
const { AkairoHandlerEvents } = require('../util/Constants');
const AkairoModule = require('./AkairoModule');
const Category = require('../util/Category');
const { Collection } = require('discord.js');
const EventEmitter = require('events');
const fs = require('fs');
const path = require('path');
/**
* Base class for handling modules.
* @param {AkairoClient} client - The Akairo client.
* @param {AkairoHandlerOptions} options - Options for module loading and handling.
* @extends {EventEmitter}
*/
class AkairoHandler extends EventEmitter {
constructor(client, {
directory,
classToHandle = AkairoModule,
extensions = ['.js', '.json', '.ts'],
automateCategories = false,
loadFilter = (() => true)
}) {
super();
/**
* The Akairo client.
* @type {AkairoClient}
*/
this.client = client;
/**
* The main directory to modules.
* @type {string}
*/
this.directory = directory;
/**
* Class to handle.
* @type {Function}
*/
this.classToHandle = classToHandle;
/**
* File extensions to load.
* @type {Set}
*/
this.extensions = new Set(extensions);
/**
* Whether or not to automate category names.
* @type {boolean}
*/
this.automateCategories = Boolean(automateCategories);
/**
* Function that filters files when loading.
* @type {LoadPredicate}
*/
this.loadFilter = loadFilter;
/**
* Modules loaded, mapped by ID to AkairoModule.
* @type {Collection}
*/
this.modules = new Collection();
/**
* Categories, mapped by ID to Category.
* @type {Collection}
*/
this.categories = new Collection();
}
/**
* Registers a module.
* @param {AkairoModule} mod - Module to use.
* @param {string} [filepath] - Filepath of module.
* @returns {void}
*/
register(mod, filepath) {
mod.filepath = filepath;
mod.client = this.client;
mod.handler = this;
this.modules.set(mod.id, mod);
if (mod.categoryID === 'default' && this.automateCategories) {
const dirs = path.dirname(filepath).split(path.sep);
mod.categoryID = dirs[dirs.length - 1];
}
if (!this.categories.has(mod.categoryID)) {
this.categories.set(mod.categoryID, new Category(mod.categoryID));
}
const category = this.categories.get(mod.categoryID);
mod.category = category;
category.set(mod.id, mod);
}
/**
* Deregisters a module.
* @param {AkairoModule} mod - Module to use.
* @returns {void}
*/
deregister(mod) {
if (mod.filepath) delete require.cache[require.resolve(mod.filepath)];
this.modules.delete(mod.id);
mod.category.delete(mod.id);
}
/**
* Loads a module, can be a module class or a filepath.
* @param {string|Function} thing - Module class or path to module.
* @param {boolean} [isReload=false] - Whether this is a reload or not.
* @returns {AkairoModule}
*/
load(thing, isReload = false) {
const isClass = typeof thing === 'function';
if (!isClass && !this.extensions.has(path.extname(thing))) return undefined;
let mod = isClass
? thing
: function findExport(m) {
if (!m) return null;
if (m.prototype instanceof this.classToHandle) return m;
return m.default ? findExport.call(this, m.default) : null;
}.call(this, require(thing));
if (mod && mod.prototype instanceof this.classToHandle) {
mod = new mod(this); // eslint-disable-line new-cap
} else {
if (!isClass) delete require.cache[require.resolve(thing)];
return undefined;
}
if (this.modules.has(mod.id)) throw new AkairoError('ALREADY_LOADED', this.classToHandle.name, mod.id);
this.register(mod, isClass ? null : thing);
this.emit(AkairoHandlerEvents.LOAD, mod, isReload);
return mod;
}
/**
* Reads all modules from a directory and loads them.
* @param {string} [directory] - Directory to load from.
* Defaults to the directory passed in the constructor.
* @param {LoadPredicate} [filter] - Filter for files, where true means it should be loaded.
* Defaults to the filter passed in the constructor.
* @returns {AkairoHandler}
*/
loadAll(directory = this.directory, filter = this.loadFilter || (() => true)) {
const filepaths = this.constructor.readdirRecursive(directory);
for (let filepath of filepaths) {
filepath = path.resolve(filepath);
if (filter(filepath)) this.load(filepath);
}
return this;
}
/**
* Removes a module.
* @param {string} id - ID of the module.
* @returns {AkairoModule}
*/
remove(id) {
const mod = this.modules.get(id.toString());
if (!mod) throw new AkairoError('MODULE_NOT_FOUND', this.classToHandle.name, id);
this.deregister(mod);
this.emit(AkairoHandlerEvents.REMOVE, mod);
return mod;
}
/**
* Removes all modules.
* @returns {AkairoHandler}
*/
removeAll() {
for (const m of Array.from(this.modules.values())) {
if (m.filepath) this.remove(m.id);
}
return this;
}
/**
* Reloads a module.
* @param {string} id - ID of the module.
* @returns {AkairoModule}
*/
reload(id) {
const mod = this.modules.get(id.toString());
if (!mod) throw new AkairoError('MODULE_NOT_FOUND', this.classToHandle.name, id);
if (!mod.filepath) throw new AkairoError('NOT_RELOADABLE', this.classToHandle.name, id);
this.deregister(mod);
const filepath = mod.filepath;
const newMod = this.load(filepath, true);
return newMod;
}
/**
* Reloads all modules.
* @returns {AkairoHandler}
*/
reloadAll() {
for (const m of Array.from(this.modules.values())) {
if (m.filepath) this.reload(m.id);
}
return this;
}
/**
* Finds a category by name.
* @param {string} name - Name to find with.
* @returns {Category}
*/
findCategory(name) {
return this.categories.find(category => {
return category.id.toLowerCase() === name.toLowerCase();
});
}
/**
* Reads files recursively from a directory.
* @param {string} directory - Directory to read.
* @returns {string[]}
*/
static readdirRecursive(directory) {
const result = [];
(function read(dir) {
const files = fs.readdirSync(dir);
for (const file of files) {
const filepath = path.join(dir, file);
if (fs.statSync(filepath).isDirectory()) {
read(filepath);
} else {
result.push(filepath);
}
}
}(directory));
return result;
}
}
module.exports = AkairoHandler;
/**
* Emitted when a module is loaded.
* @event AkairoHandler#load
* @param {AkairoModule} mod - Module loaded.
* @param {boolean} isReload - Whether or not this was a reload.
*/
/**
* Emitted when a module is removed.
* @event AkairoHandler#remove
* @param {AkairoModule} mod - Module removed.
*/
/**
* Options for module loading and handling.
* @typedef {Object} AkairoHandlerOptions
* @prop {string} [directory] - Directory to modules.
* @prop {Function} [classToHandle=AkairoModule] - Only classes that extends this class can be handled.
* @prop {string[]|Set} [extensions] - File extensions to load.
* By default this is .js, .json, and .ts files.
* @prop {boolean} [automateCategories=false] - Whether or not to set each module's category to its parent directory name.
* @prop {LoadPredicate} [loadFilter] - Filter for files to be loaded.
* Can be set individually for each handler by overriding the `loadAll` method.
*/
/**
* Function for filtering files when loading.
* True means the file should be loaded.
* @typedef {Function} LoadPredicate
* @param {String} filepath - Filepath of file.
* @returns {boolean}
*/
================================================
FILE: src/struct/AkairoModule.js
================================================
/**
* Base class for a module.
* @param {string} id - ID of module.
* @param {AkairoModuleOptions} [options={}] - Options.
*/
class AkairoModule {
constructor(id, { category = 'default' } = {}) {
/**
* ID of the module.
* @type {string}
*/
this.id = id;
/**
* ID of the category this belongs to.
* @type {string}
*/
this.categoryID = category;
/**
* Category this belongs to.
* @type {Category}
*/
this.category = null;
/**
* The filepath.
* @type {string}
*/
this.filepath = null;
/**
* The Akairo client.
* @type {AkairoClient}
*/
this.client = null;
/**
* The handler.
* @type {AkairoHandler}
*/
this.handler = null;
}
/**
* Reloads the module.
* @returns {AkairoModule}
*/
reload() {
return this.handler.reload(this.id);
}
/**
* Removes the module.
* @returns {AkairoModule}
*/
remove() {
return this.handler.remove(this.id);
}
/**
* Returns the ID.
* @returns {string}
*/
toString() {
return this.id;
}
}
module.exports = AkairoModule;
/**
* Options for module.
* @typedef {Object} AkairoModuleOptions
* @prop {string} [category='default'] - Category ID for organization purposes.
*/
================================================
FILE: src/struct/ClientUtil.js
================================================
const { Collection, MessageAttachment, MessageEmbed, Permissions } = require('discord.js');
/**
* Client utilities to help with common tasks.
* @param {AkairoClient} client - The client.
*/
class ClientUtil {
constructor(client) {
/**
* The Akairo client.
* @type {AkairoClient}
*/
this.client = client;
}
/**
* Resolves a user from a string, such as an ID, a name, or a mention.
* @param {string} text - Text to resolve.
* @param {Collection} users - Collection of users to find in.
* @param {boolean} [caseSensitive=false] - Makes finding by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes finding by name match full word only.
* @returns {User}
*/
resolveUser(text, users, caseSensitive = false, wholeWord = false) {
return users.get(text) || users.find(user => this.checkUser(text, user, caseSensitive, wholeWord));
}
/**
* Resolves multiple users from a string, such as an ID, a name, or a mention.
* @param {string} text - Text to resolve.
* @param {Collection} users - Collection of users to find in.
* @param {boolean} [caseSensitive=false] - Makes finding by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes finding by name match full word only.
* @returns {Collection}
*/
resolveUsers(text, users, caseSensitive = false, wholeWord = false) {
return users.filter(user => this.checkUser(text, user, caseSensitive, wholeWord));
}
/**
* Checks if a string could be referring to a user.
* @param {string} text - Text to check.
* @param {User} user - User to check.
* @param {boolean} [caseSensitive=false] - Makes checking by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes checking by name match full word only.
* @returns {boolean}
*/
checkUser(text, user, caseSensitive = false, wholeWord = false) {
if (user.id === text) return true;
const reg = /<@!?(\d{17,19})>/;
const match = text.match(reg);
if (match && user.id === match[1]) return true;
text = caseSensitive ? text : text.toLowerCase();
const username = caseSensitive ? user.username : user.username.toLowerCase();
const discrim = user.discriminator;
if (!wholeWord) {
return username.includes(text)
|| (username.includes(text.split('#')[0]) && discrim.includes(text.split('#')[1]));
}
return username === text
|| (username === text.split('#')[0] && discrim === text.split('#')[1]);
}
/**
* Resolves a member from a string, such as an ID, a name, or a mention.
* @param {string} text - Text to resolve.
* @param {Collection} members - Collection of members to find in.
* @param {boolean} [caseSensitive=false] - Makes finding by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes finding by name match full word only.
* @returns {GuildMember}
*/
resolveMember(text, members, caseSensitive = false, wholeWord = false) {
return members.get(text) || members.find(member => this.checkMember(text, member, caseSensitive, wholeWord));
}
/**
* Resolves multiple members from a string, such as an ID, a name, or a mention.
* @param {string} text - Text to resolve.
* @param {Collection} members - Collection of members to find in.
* @param {boolean} [caseSensitive=false] - Makes finding by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes finding by name match full word only.
* @returns {Collection}
*/
resolveMembers(text, members, caseSensitive = false, wholeWord = false) {
return members.filter(member => this.checkMember(text, member, caseSensitive, wholeWord));
}
/**
* Checks if a string could be referring to a member.
* @param {string} text - Text to check.
* @param {GuildMember} member - Member to check.
* @param {boolean} [caseSensitive=false] - Makes checking by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes checking by name match full word only.
* @returns {boolean}
*/
checkMember(text, member, caseSensitive = false, wholeWord = false) {
if (member.id === text) return true;
const reg = /<@!?(\d{17,19})>/;
const match = text.match(reg);
if (match && member.id === match[1]) return true;
text = caseSensitive ? text : text.toLowerCase();
const username = caseSensitive ? member.user.username : member.user.username.toLowerCase();
const displayName = caseSensitive ? member.displayName : member.displayName.toLowerCase();
const discrim = member.user.discriminator;
if (!wholeWord) {
return displayName.includes(text)
|| username.includes(text)
|| ((username.includes(text.split('#')[0]) || displayName.includes(text.split('#')[0])) && discrim.includes(text.split('#')[1]));
}
return displayName === text
|| username === text
|| ((username === text.split('#')[0] || displayName === text.split('#')[0]) && discrim === text.split('#')[1]);
}
/**
* Resolves a channel from a string, such as an ID, a name, or a mention.
* @param {string} text - Text to resolve.
* @param {Collection} channels - Collection of channels to find in.
* @param {boolean} [caseSensitive=false] - Makes finding by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes finding by name match full word only.
* @returns {Channel}
*/
resolveChannel(text, channels, caseSensitive = false, wholeWord = false) {
return channels.get(text) || channels.find(channel => this.checkChannel(text, channel, caseSensitive, wholeWord));
}
/**
* Resolves multiple channels from a string, such as an ID, a name, or a mention.
* @param {string} text - Text to resolve.
* @param {Collection} channels - Collection of channels to find in.
* @param {boolean} [caseSensitive=false] - Makes finding by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes finding by name match full word only.
* @returns {Collection}
*/
resolveChannels(text, channels, caseSensitive = false, wholeWord = false) {
return channels.filter(channel => this.checkChannel(text, channel, caseSensitive, wholeWord));
}
/**
* Checks if a string could be referring to a channel.
* @param {string} text - Text to check.
* @param {Channel} channel - Channel to check.
* @param {boolean} [caseSensitive=false] - Makes checking by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes checking by name match full word only.
* @returns {boolean}
*/
checkChannel(text, channel, caseSensitive = false, wholeWord = false) {
if (channel.id === text) return true;
const reg = /<#(\d{17,19})>/;
const match = text.match(reg);
if (match && channel.id === match[1]) return true;
text = caseSensitive ? text : text.toLowerCase();
const name = caseSensitive ? channel.name : channel.name.toLowerCase();
if (!wholeWord) {
return name.includes(text)
|| name.includes(text.replace(/^#/, ''));
}
return name === text
|| name === text.replace(/^#/, '');
}
/**
* Resolves a role from a string, such as an ID, a name, or a mention.
* @param {string} text - Text to resolve.
* @param {Collection} roles - Collection of roles to find in.
* @param {boolean} [caseSensitive=false] - Makes finding by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes finding by name match full word only.
* @returns {Role}
*/
resolveRole(text, roles, caseSensitive = false, wholeWord = false) {
return roles.get(text) || roles.find(role => this.checkRole(text, role, caseSensitive, wholeWord));
}
/**
* Resolves multiple roles from a string, such as an ID, a name, or a mention.
* @param {string} text - Text to resolve.
* @param {Collection} roles - Collection of roles to find in.
* @param {boolean} [caseSensitive=false] - Makes finding by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes finding by name match full word only.
* @returns {Collection}
*/
resolveRoles(text, roles, caseSensitive = false, wholeWord = false) {
return roles.filter(role => this.checkRole(text, role, caseSensitive, wholeWord));
}
/**
* Checks if a string could be referring to a role.
* @param {string} text - Text to check.
* @param {Role} role - Role to check.
* @param {boolean} [caseSensitive=false] - Makes checking by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes checking by name match full word only.
* @returns {boolean}
*/
checkRole(text, role, caseSensitive = false, wholeWord = false) {
if (role.id === text) return true;
const reg = /<@&(\d{17,19})>/;
const match = text.match(reg);
if (match && role.id === match[1]) return true;
text = caseSensitive ? text : text.toLowerCase();
const name = caseSensitive ? role.name : role.name.toLowerCase();
if (!wholeWord) {
return name.includes(text)
|| name.includes(text.replace(/^@/, ''));
}
return name === text
|| name === text.replace(/^@/, '');
}
/**
* Resolves a custom emoji from a string, such as a name or a mention.
* @param {string} text - Text to resolve.
* @param {Collection} emojis - Collection of emojis to find in.
* @param {boolean} [caseSensitive=false] - Makes finding by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes finding by name match full word only.
* @returns {Emoji}
*/
resolveEmoji(text, emojis, caseSensitive = false, wholeWord = false) {
return emojis.get(text) || emojis.find(emoji => this.checkEmoji(text, emoji, caseSensitive, wholeWord));
}
/**
* Resolves multiple custom emojis from a string, such as a name or a mention.
* @param {string} text - Text to resolve.
* @param {Collection} emojis - Collection of emojis to find in.
* @param {boolean} [caseSensitive=false] - Makes finding by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes finding by name match full word only.
* @returns {Collection}
*/
resolveEmojis(text, emojis, caseSensitive = false, wholeWord = false) {
return emojis.filter(emoji => this.checkEmoji(text, emoji, caseSensitive, wholeWord));
}
/**
* Checks if a string could be referring to a emoji.
* @param {string} text - Text to check.
* @param {Emoji} emoji - Emoji to check.
* @param {boolean} [caseSensitive=false] - Makes checking by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes checking by name match full word only.
* @returns {boolean}
*/
checkEmoji(text, emoji, caseSensitive = false, wholeWord = false) {
if (emoji.id === text) return true;
const reg = //;
const match = text.match(reg);
if (match && emoji.id === match[1]) return true;
text = caseSensitive ? text : text.toLowerCase();
const name = caseSensitive ? emoji.name : emoji.name.toLowerCase();
if (!wholeWord) {
return name.includes(text)
|| name.includes(text.replace(/:/, ''));
}
return name === text
|| name === text.replace(/:/, '');
}
/**
* Resolves a guild from a string, such as an ID or a name.
* @param {string} text - Text to resolve.
* @param {Collection} guilds - Collection of guilds to find in.
* @param {boolean} [caseSensitive=false] - Makes finding by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes finding by name match full word only.
* @returns {Guild}
*/
resolveGuild(text, guilds, caseSensitive = false, wholeWord = false) {
return guilds.get(text) || guilds.find(guild => this.checkGuild(text, guild, caseSensitive, wholeWord));
}
/**
* Resolves multiple guilds from a string, such as an ID or a name.
* @param {string} text - Text to resolve.
* @param {Collection} guilds - Collection of guilds to find in.
* @param {boolean} [caseSensitive=false] - Makes finding by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes finding by name match full word only.
* @returns {Collection}
*/
resolveGuilds(text, guilds, caseSensitive = false, wholeWord = false) {
return guilds.filter(guild => this.checkGuild(text, guild, caseSensitive, wholeWord));
}
/**
* Checks if a string could be referring to a guild.
* @param {string} text - Text to check.
* @param {Guild} guild - Guild to check.
* @param {boolean} [caseSensitive=false] - Makes checking by name case sensitive.
* @param {boolean} [wholeWord=false] - Makes checking by name match full word only.
* @returns {boolean}
*/
checkGuild(text, guild, caseSensitive = false, wholeWord = false) {
if (guild.id === text) return true;
text = caseSensitive ? text : text.toLowerCase();
const name = caseSensitive ? guild.name : guild.name.toLowerCase();
if (!wholeWord) return name.includes(text);
return name === text;
}
/**
* Array of permission names.
* @returns {string[]}
*/
permissionNames() {
return Object.keys(Permissions.FLAGS);
}
/**
* Resolves a permission number and returns an array of permission names.
* @param {number} number - The permissions number.
* @returns {string[]}
*/
resolvePermissionNumber(number) {
const resolved = [];
for (const key of Object.keys(Permissions.FLAGS)) {
if (number & Permissions.FLAGS[key]) resolved.push(key);
}
return resolved;
}
/**
* Compares two member objects presences and checks if they stopped or started a stream or not.
* Returns `0`, `1`, or `2` for no change, stopped, or started.
* @param {GuildMember} oldMember - The old member.
* @param {GuildMember} newMember - The new member.
* @returns {number}
*/
compareStreaming(oldMember, newMember) {
const s1 = oldMember.presence?.activities.find(c => c.type === 'STREAMING');
const s2 = newMember.presence?.activities.find(c => c.type === 'STREAMING');
if (s1 === s2) return 0;
if (s1) return 1;
if (s2) return 2;
return 0;
}
/**
* Combination of `.users.fetch()` and `.members.fetch()`.
* @param {Guild} guild - Guild to fetch in.
* @param {string} id - ID of the user.
* @param {boolean} cache - Whether or not to add to cache.
* @returns {Promise}
*/
async fetchMember(guild, id, cache) {
const user = await this.client.users.fetch(id, cache);
return guild.members.fetch(user, cache);
}
/**
* Makes a MessageEmbed.
* @param {Object} [data] - Embed data.
* @returns {MessageEmbed}
*/
embed(data) {
return new MessageEmbed(data);
}
/**
* Makes a MessageAttachment.
* @param {BufferResolvable|Stream} file - The file.
* @param {string} [name] - The filename.
* @returns {MessageAttachment}
*/
attachment(file, name) {
return new MessageAttachment(file, name);
}
/**
* Makes a Collection.
* @param {Iterable} [iterable] - Entries to fill with.
* @returns {Collection}
*/
collection(iterable) {
return new Collection(iterable);
}
}
module.exports = ClientUtil;
================================================
FILE: src/struct/commands/Command.js
================================================
const AkairoError = require('../../util/AkairoError');
const AkairoModule = require('../AkairoModule');
const Argument = require('./arguments/Argument');
const ArgumentRunner = require('./arguments/ArgumentRunner');
const ContentParser = require('./ContentParser');
/**
* Represents a command.
* @param {string} id - Command ID.
* @param {CommandOptions} [options={}] - Options for the command.
* @extends {AkairoModule}
*/
class Command extends AkairoModule {
constructor(id, options = {}) {
super(id, { category: options.category });
const {
aliases = [],
args = this.args || [],
quoted = true,
separator,
channel = null,
ownerOnly = false,
editable = true,
typing = false,
cooldown = null,
ratelimit = 1,
argumentDefaults = {},
description = '',
prefix = this.prefix,
clientPermissions = this.clientPermissions,
userPermissions = this.userPermissions,
regex = this.regex,
condition = this.condition || (() => false),
before = this.before || (() => undefined),
lock,
ignoreCooldown,
ignorePermissions,
flags = [],
optionFlags = []
} = options;
/**
* Command names.
* @type {string[]}
*/
this.aliases = aliases;
const { flagWords, optionFlagWords } = Array.isArray(args)
? ContentParser.getFlags(args)
: { flagWords: flags, optionFlagWords: optionFlags };
this.contentParser = new ContentParser({
flagWords,
optionFlagWords,
quoted,
separator
});
this.argumentRunner = new ArgumentRunner(this);
this.argumentGenerator = Array.isArray(args)
? ArgumentRunner.fromArguments(args.map(arg => [arg.id, new Argument(this, arg)]))
: args.bind(this);
/**
* Usable only in this channel type.
* @type {?string}
*/
this.channel = channel;
/**
* Usable only by the client owner.
* @type {boolean}
*/
this.ownerOnly = Boolean(ownerOnly);
/**
* Whether or not this command can be ran by an edit.
* @type {boolean}
*/
this.editable = Boolean(editable);
/**
* Whether or not to type during command execution.
* @type {boolean}
*/
this.typing = Boolean(typing);
/**
* Cooldown in milliseconds.
* @type {?number}
*/
this.cooldown = cooldown;
/**
* Uses allowed before cooldown.
* @type {number}
*/
this.ratelimit = ratelimit;
/**
* Default prompt options.
* @type {DefaultArgumentOptions}
*/
this.argumentDefaults = argumentDefaults;
/**
* Description of the command.
* @type {string|any}
*/
this.description = Array.isArray(description) ? description.join('\n') : description;
/**
* Command prefix overwrite.
* @type {?string|string[]|PrefixSupplier}
*/
this.prefix = typeof prefix === 'function' ? prefix.bind(this) : prefix;
/**
* Permissions required to run command by the client.
* @type {PermissionResolvable|PermissionResolvable[]|MissingPermissionSupplier}
*/
this.clientPermissions = typeof clientPermissions === 'function' ? clientPermissions.bind(this) : clientPermissions;
/**
* Permissions required to run command by the user.
* @type {PermissionResolvable|PermissionResolvable[]|MissingPermissionSupplier}
*/
this.userPermissions = typeof userPermissions === 'function' ? userPermissions.bind(this) : userPermissions;
/**
* The regex trigger for this command.
* @type {RegExp|RegexSupplier}
*/
this.regex = typeof regex === 'function' ? regex.bind(this) : regex;
/**
* Checks if the command should be ran by using an arbitrary condition.
* @method
* @param {Message} message - Message being handled.
* @returns {boolean}
*/
this.condition = condition.bind(this);
/**
* Runs before argument parsing and execution.
* @method
* @param {Message} message - Message being handled.
* @returns {any}
*/
this.before = before.bind(this);
/**
* The key supplier for the locker.
* @type {?KeySupplier}
*/
this.lock = lock;
if (typeof lock === 'string') {
this.lock = {
guild: message => message.guild && message.guild.id,
channel: message => message.channel.id,
user: message => message.author.id
}[lock];
}
if (this.lock) {
/**
* Stores the current locks.
* @type {?Set}
*/
this.locker = new Set();
}
/**
* ID of user(s) to ignore cooldown or a function to ignore.
* @type {?Snowflake|Snowflake[]|IgnoreCheckPredicate}
*/
this.ignoreCooldown = typeof ignoreCooldown === 'function' ? ignoreCooldown.bind(this) : ignoreCooldown;
/**
* ID of user(s) to ignore `userPermissions` checks or a function to ignore.
* @type {?Snowflake|Snowflake[]|IgnoreCheckPredicate}
*/
this.ignorePermissions = typeof ignorePermissions === 'function' ? ignorePermissions.bind(this) : ignorePermissions;
/**
* The ID of this command.
* @name Command#id
* @type {string}
*/
/**
* The command handler.
* @name Command#handler
* @type {CommandHandler}
*/
}
/**
* Executes the command.
* @abstract
* @param {Message} message - Message that triggered the command.
* @param {any} args - Evaluated arguments.
* @returns {any}
*/
exec() {
throw new AkairoError('NOT_IMPLEMENTED', this.constructor.name, 'exec');
}
/**
* Parses content using the command's arguments.
* @param {Message} message - Message to use.
* @param {string} content - String to parse.
* @returns {Promise}
*/
parse(message, content) {
const parsed = this.contentParser.parse(content);
return this.argumentRunner.run(message, parsed, this.argumentGenerator);
}
/**
* Reloads the command.
* @method
* @name Command#reload
* @returns {Command}
*/
/**
* Removes the command.
* @method
* @name Command#remove
* @returns {Command}
*/
}
module.exports = Command;
/**
* Options to use for command execution behavior.
* Also includes properties from AkairoModuleOptions.
* @typedef {AkairoModuleOptions} CommandOptions
* @prop {string[]} [aliases=[]] - Command names.
* @prop {ArgumentOptions[]|ArgumentGenerator} [args=[]] - Argument options or generator.
* @prop {boolean} [quoted=true] - Whether or not to consider quotes.
* @prop {string} [separator] - Custom separator for argument input.
* @prop {string[]} [flags=[]] - Flags to use when using an ArgumentGenerator.
* @prop {string[]} [optionFlags=[]] - Option flags to use when using an ArgumentGenerator.
* @prop {string} [channel] - Restricts channel to either 'guild' or 'dm'.
* @prop {boolean} [ownerOnly=false] - Whether or not to allow client owner(s) only.
* @prop {boolean} [typing=false] - Whether or not to type in channel during execution.
* @prop {boolean} [editable=true] - Whether or not message edits will run this command.
* @prop {number} [cooldown] - The command cooldown in milliseconds.
* @prop {number} [ratelimit=1] - Amount of command uses allowed until cooldown.
* @prop {string|string[]|PrefixSupplier} [prefix] - The prefix(es) to overwrite the global one for this command.
* @prop {PermissionResolvable|PermissionResolvable[]|MissingPermissionSupplier} [userPermissions] - Permissions required by the user to run this command.
* @prop {PermissionResolvable|PermissionResolvable[]|MissingPermissionSupplier} [clientPermissions] - Permissions required by the client to run this command.
* @prop {RegExp|RegexSupplier} [regex] - A regex to match in messages that are not directly commands.
* The args object will have `match` and `matches` properties.
* @prop {ExecutionPredicate} [condition] - Whether or not to run on messages that are not directly commands.
* @prop {BeforeAction} [before] - Function to run before argument parsing and execution.
* @prop {KeySupplier|string} [lock] - The key type or key generator for the locker. If lock is a string, it's expected one of 'guild', 'channel', or 'user'.
* @prop {Snowflake|Snowflake[]|IgnoreCheckPredicate} [ignoreCooldown] - ID of user(s) to ignore cooldown or a function to ignore.
* @prop {Snowflake|Snowflake[]|IgnoreCheckPredicate} [ignorePermissions] - ID of user(s) to ignore `userPermissions` checks or a function to ignore.
* @prop {DefaultArgumentOptions} [argumentDefaults] - The default argument options.
* @prop {string} [description=''] - Description of the command.
*/
/**
* A function to run before argument parsing and execution.
* @typedef {Function} BeforeAction
* @param {Message} message - Message that triggered the command.
* @returns {any}
*/
/**
* A function used to supply the key for the locker.
* @typedef {Function} KeySupplier
* @param {Message} message - Message that triggered the command.
* @param {any} args - Evaluated arguments.
* @returns {string}
*/
/**
* A function used to check if the command should run arbitrarily.
* @typedef {Function} ExecutionPredicate
* @param {Message} message - Message to check.
* @returns {boolean}
*/
/**
* A function used to check if a message has permissions for the command.
* A non-null return value signifies the reason for missing permissions.
* @typedef {Function} MissingPermissionSupplier
* @param {Message} message - Message that triggered the command.
* @returns {any}
*/
/**
* A function used to return a regular expression.
* @typedef {Function} RegexSupplier
* @param {Message} message - Message to get regex for.
* @returns {RegExp}
*/
/**
* Generator for arguments.
* When yielding argument options, that argument is ran and the result of the processing is given.
* The last value when the generator is done is the resulting `args` for the command's `exec`.
* @typedef {GeneratorFunction} ArgumentGenerator
* @param {Message} message - Message that triggered the command.
* @param {ContentParserResult} parsed - Parsed content.
* @param {ArgumentRunnerState} state - Argument processing state.
* @returns {IterableIterator}
*/
================================================
FILE: src/struct/commands/CommandHandler.js
================================================
const AkairoError = require('../../util/AkairoError');
const AkairoHandler = require('../AkairoHandler');
const { BuiltInReasons, CommandHandlerEvents } = require('../../util/Constants');
const { Collection } = require('discord.js');
const Command = require('./Command');
const CommandUtil = require('./CommandUtil');
const Flag = require('./Flag');
const { deepAssign, flatMap, intoArray, intoCallable, isPromise, prefixCompare } = require('../../util/Util');
const TypeResolver = require('./arguments/TypeResolver');
/**
* Loads commands and handles messages.
* @param {AkairoClient} client - The Akairo client.
* @param {CommandHandlerOptions} options - Options.
* @extends {AkairoHandler}
*/
class CommandHandler extends AkairoHandler {
constructor(client, {
directory,
classToHandle = Command,
extensions = ['.js', '.ts'],
automateCategories,
loadFilter,
blockClient = true,
blockBots = true,
fetchMembers = false,
handleEdits = false,
storeMessages = false,
commandUtil,
commandUtilLifetime = 3e5,
commandUtilSweepInterval = 3e5,
defaultCooldown = 0,
ignoreCooldown = client.ownerID,
ignorePermissions = [],
argumentDefaults = {},
prefix = '!',
allowMention = true,
aliasReplacement
} = {}) {
if (!(classToHandle.prototype instanceof Command || classToHandle === Command)) {
throw new AkairoError('INVALID_CLASS_TO_HANDLE', classToHandle.name, Command.name);
}
super(client, {
directory,
classToHandle,
extensions,
automateCategories,
loadFilter
});
/**
* The type resolver.
* @type {TypeResolver}
*/
this.resolver = new TypeResolver(this);
/**
* Collecion of command aliases.
* @type {Collection}
*/
this.aliases = new Collection();
/**
* Regular expression to automatically make command aliases for.
* @type {?RegExp}
*/
this.aliasReplacement = aliasReplacement;
/**
* Collection of prefix overwrites to commands.
* @type {Collection>}
*/
this.prefixes = new Collection();
/**
* Whether or not to block self.
* @type {boolean}
*/
this.blockClient = Boolean(blockClient);
/**
* Whether or not to block bots.
* @type {boolean}
*/
this.blockBots = Boolean(blockBots);
/**
* Whether or not members are fetched on each message author from a guild.
* @type {boolean}
*/
this.fetchMembers = Boolean(fetchMembers);
/**
* Whether or not edits are handled.
* @type {boolean}
*/
this.handleEdits = Boolean(handleEdits);
/**
* Whether or not to store messages in CommandUtil.
* @type {boolean}
*/
this.storeMessages = Boolean(storeMessages);
/**
* Whether or not `message.util` is assigned.
* @type {boolean}
*/
this.commandUtil = Boolean(commandUtil);
if ((this.handleEdits || this.storeMessages) && !this.commandUtil) {
throw new AkairoError('COMMAND_UTIL_EXPLICIT');
}
/**
* Milliseconds a message should exist for before its command util instance is marked for removal.
* @type {number}
*/
this.commandUtilLifetime = commandUtilLifetime;
/**
* Time interval in milliseconds for sweeping command util instances.
* @type {number}
*/
this.commandUtilSweepInterval = commandUtilSweepInterval;
if (this.commandUtilSweepInterval > 0) {
setInterval(() => this.sweepCommandUtil(), this.commandUtilSweepInterval).unref();
}
/**
* Collection of CommandUtils.
* @type {Collection}
*/
this.commandUtils = new Collection();
/**
* Collection of cooldowns.
* The elements in the collection are objects with user IDs as keys
* and {@link CooldownData} objects as values
* @type {Collection}
*/
this.cooldowns = new Collection();
/**
* Default cooldown for commands.
* @type {number}
*/
this.defaultCooldown = defaultCooldown;
/**
* ID of user(s) to ignore cooldown or a function to ignore.
* @type {Snowflake|Snowflake[]|IgnoreCheckPredicate}
*/
this.ignoreCooldown = typeof ignoreCooldown === 'function' ? ignoreCooldown.bind(this) : ignoreCooldown;
/**
* ID of user(s) to ignore `userPermissions` checks or a function to ignore.
* @type {Snowflake|Snowflake[]|IgnoreCheckPredicate}
*/
this.ignorePermissions = typeof ignorePermissions === 'function' ? ignorePermissions.bind(this) : ignorePermissions;
/**
* Collection of sets of ongoing argument prompts.
* @type {Collection>}
*/
this.prompts = new Collection();
/**
* Default argument options.
* @type {DefaultArgumentOptions}
*/
this.argumentDefaults = deepAssign({
prompt: {
start: '',
retry: '',
timeout: '',
ended: '',
cancel: '',
retries: 1,
time: 30000,
cancelWord: 'cancel',
stopWord: 'stop',
optional: false,
infinite: false,
limit: Infinity,
breakout: true
}
}, argumentDefaults);
/**
* The prefix(es) for command parsing.
* @type {string|string[]|PrefixSupplier}
*/
this.prefix = typeof prefix === 'function' ? prefix.bind(this) : prefix;
/**
* Whether or not mentions are allowed for prefixing.
* @type {boolean|MentionPrefixPredicate}
*/
this.allowMention = typeof allowMention === 'function' ? allowMention.bind(this) : Boolean(allowMention);
/**
* Inhibitor handler to use.
* @type {?InhibitorHandler}
*/
this.inhibitorHandler = null;
/**
* Directory to commands.
* @name CommandHandler#directory
* @type {string}
*/
/**
* Commands loaded, mapped by ID to Command.
* @name CommandHandler#modules
* @type {Collection}
*/
this.setup();
}
setup() {
this.client.once('ready', () => {
this.client.on('messageCreate', async m => {
if (m.partial) await m.fetch();
this.handle(m);
});
if (this.handleEdits) {
this.client.on('messageUpdate', async (o, m) => {
if (o.partial) await o.fetch();
if (m.partial) await m.fetch();
if (o.content === m.content) return;
if (this.handleEdits) this.handle(m);
});
}
});
}
/**
* Registers a module.
* @param {Command} command - Module to use.
* @param {string} [filepath] - Filepath of module.
* @returns {void}
*/
register(command, filepath) {
super.register(command, filepath);
for (let alias of command.aliases) {
const conflict = this.aliases.get(alias.toLowerCase());
if (conflict) throw new AkairoError('ALIAS_CONFLICT', alias, command.id, conflict);
alias = alias.toLowerCase();
this.aliases.set(alias, command.id);
if (this.aliasReplacement) {
const replacement = alias.replace(this.aliasReplacement, '');
if (replacement !== alias) {
const replacementConflict = this.aliases.get(replacement);
if (replacementConflict) throw new AkairoError('ALIAS_CONFLICT', replacement, command.id, replacementConflict);
this.aliases.set(replacement, command.id);
}
}
}
if (command.prefix != null) {
let newEntry = false;
if (Array.isArray(command.prefix)) {
for (const prefix of command.prefix) {
const prefixes = this.prefixes.get(prefix);
if (prefixes) {
prefixes.add(command.id);
} else {
this.prefixes.set(prefix, new Set([command.id]));
newEntry = true;
}
}
} else {
const prefixes = this.prefixes.get(command.prefix);
if (prefixes) {
prefixes.add(command.id);
} else {
this.prefixes.set(command.prefix, new Set([command.id]));
newEntry = true;
}
}
if (newEntry) {
this.prefixes = this.prefixes.sort((aVal, bVal, aKey, bKey) => prefixCompare(aKey, bKey));
}
}
}
/**
* Deregisters a module.
* @param {Command} command - Module to use.
* @returns {void}
*/
deregister(command) {
for (let alias of command.aliases) {
alias = alias.toLowerCase();
this.aliases.delete(alias);
if (this.aliasReplacement) {
const replacement = alias.replace(this.aliasReplacement, '');
if (replacement !== alias) this.aliases.delete(replacement);
}
}
if (command.prefix != null) {
if (Array.isArray(command.prefix)) {
for (const prefix of command.prefix) {
const prefixes = this.prefixes.get(prefix);
if (prefixes.size === 1) {
this.prefixes.delete(prefix);
} else {
prefixes.delete(prefix);
}
}
} else {
const prefixes = this.prefixes.get(command.prefix);
if (prefixes.size === 1) {
this.prefixes.delete(command.prefix);
} else {
prefixes.delete(command.prefix);
}
}
}
super.deregister(command);
}
/**
* Handles a message.
* @param {Message} message - Message to handle.
* @returns {Promise}
*/
async handle(message) {
try {
if (this.fetchMembers && message.guild && !message.member && !message.webhookId) {
await message.guild.members.fetch(message.author);
}
if (await this.runAllTypeInhibitors(message)) {
return false;
}
if (this.commandUtil) {
if (this.commandUtils.has(message.id)) {
message.util = this.commandUtils.get(message.id);
} else {
message.util = new CommandUtil(this, message);
this.commandUtils.set(message.id, message.util);
}
}
if (await this.runPreTypeInhibitors(message)) {
return false;
}
let parsed = await this.parseCommand(message);
if (!parsed.command) {
const overParsed = await this.parseCommandOverwrittenPrefixes(message);
if (overParsed.command || (parsed.prefix == null && overParsed.prefix != null)) {
parsed = overParsed;
}
}
if (this.commandUtil) {
message.util.parsed = parsed;
}
let ran;
if (!parsed.command) {
ran = await this.handleRegexAndConditionalCommands(message);
} else {
ran = await this.handleDirectCommand(message, parsed.content, parsed.command);
}
if (ran === false) {
this.emit(CommandHandlerEvents.MESSAGE_INVALID, message);
return false;
}
return ran;
} catch (err) {
this.emitError(err, message);
return null;
}
}
/**
* Handles normal commands.
* @param {Message} message - Message to handle.
* @param {string} content - Content of message without command.
* @param {Command} command - Command instance.
* @param {boolean} [ignore=false] - Ignore inhibitors and other checks.
* @returns {Promise}
*/
async handleDirectCommand(message, content, command, ignore = false) {
let key;
try {
if (!ignore) {
if (message.edited && !command.editable) return false;
if (await this.runPostTypeInhibitors(message, command)) return false;
}
const before = command.before(message);
if (isPromise(before)) await before;
const args = await command.parse(message, content);
if (Flag.is(args, 'cancel')) {
this.emit(CommandHandlerEvents.COMMAND_CANCELLED, message, command);
return true;
} else if (Flag.is(args, 'retry')) {
this.emit(CommandHandlerEvents.COMMAND_BREAKOUT, message, command, args.message);
return this.handle(args.message);
} else if (Flag.is(args, 'continue')) {
const continueCommand = this.modules.get(args.command);
return this.handleDirectCommand(message, args.rest, continueCommand, args.ignore);
}
if (!ignore) {
if (command.lock) key = command.lock(message, args);
if (isPromise(key)) key = await key;
if (key) {
if (command.locker.has(key)) {
key = null;
this.emit(CommandHandlerEvents.COMMAND_LOCKED, message, command);
return true;
}
command.locker.add(key);
}
}
return await this.runCommand(message, command, args);
} catch (err) {
this.emitError(err, message, command);
return null;
} finally {
if (key) command.locker.delete(key);
}
}
/**
* Handles regex and conditional commands.
* @param {Message} message - Message to handle.
* @returns {Promise}
*/
async handleRegexAndConditionalCommands(message) {
const ran1 = await this.handleRegexCommands(message);
const ran2 = await this.handleConditionalCommands(message);
return ran1 || ran2;
}
/**
* Handles regex commands.
* @param {Message} message - Message to handle.
* @returns {Promise}
*/
async handleRegexCommands(message) {
const hasRegexCommands = [];
for (const command of this.modules.values()) {
if (message.edited ? command.editable : true) {
const regex = typeof command.regex === 'function' ? command.regex(message) : command.regex;
if (regex) hasRegexCommands.push({ command, regex });
}
}
const matchedCommands = [];
for (const entry of hasRegexCommands) {
const match = message.content.match(entry.regex);
if (!match) continue;
const matches = [];
if (entry.regex.global) {
let matched;
while ((matched = entry.regex.exec(message.content)) != null) {
matches.push(matched);
}
}
matchedCommands.push({ command: entry.command, match, matches });
}
if (!matchedCommands.length) {
return false;
}
const promises = [];
for (const { command, match, matches } of matchedCommands) {
promises.push((async () => {
try {
if (await this.runPostTypeInhibitors(message, command)) return;
const before = command.before(message);
if (isPromise(before)) await before;
await this.runCommand(message, command, { match, matches });
} catch (err) {
this.emitError(err, message, command);
}
})());
}
await Promise.all(promises);
return true;
}
/**
* Handles conditional commands.
* @param {Message} message - Message to handle.
* @returns {Promise}
*/
async handleConditionalCommands(message) {
const trueCommands = [];
const filterPromises = [];
for (const command of this.modules.values()) {
if (message.edited && !command.editable) continue;
filterPromises.push((async () => {
let cond = command.condition(message);
if (isPromise(cond)) cond = await cond;
if (cond) trueCommands.push(command);
})());
}
await Promise.all(filterPromises);
if (!trueCommands.length) {
return false;
}
const promises = [];
for (const command of trueCommands) {
promises.push((async () => {
try {
if (await this.runPostTypeInhibitors(message, command)) return;
const before = command.before(message);
if (isPromise(before)) await before;
await this.runCommand(message, command, {});
} catch (err) {
this.emitError(err, message, command);
}
})());
}
await Promise.all(promises);
return true;
}
/**
* Runs inhibitors with the all type.
* @param {Message} message - Message to handle.
* @returns {Promise}
*/
async runAllTypeInhibitors(message) {
const reason = this.inhibitorHandler
? await this.inhibitorHandler.test('all', message)
: null;
if (reason != null) {
this.emit(CommandHandlerEvents.MESSAGE_BLOCKED, message, reason);
} else if (this.blockClient && message.author.id === this.client.user.id) {
this.emit(CommandHandlerEvents.MESSAGE_BLOCKED, message, BuiltInReasons.CLIENT);
} else if (this.blockBots && message.author.bot) {
this.emit(CommandHandlerEvents.MESSAGE_BLOCKED, message, BuiltInReasons.BOT);
} else if (this.hasPrompt(message.channel, message.author)) {
this.emit(CommandHandlerEvents.IN_PROMPT, message);
} else {
return false;
}
return true;
}
/**
* Runs inhibitors with the pre type.
* @param {Message} message - Message to handle.
* @returns {Promise}
*/
async runPreTypeInhibitors(message) {
const reason = this.inhibitorHandler
? await this.inhibitorHandler.test('pre', message)
: null;
if (reason != null) {
this.emit(CommandHandlerEvents.MESSAGE_BLOCKED, message, reason);
} else {
return false;
}
return true;
}
/**
* Runs inhibitors with the post type.
* @param {Message} message - Message to handle.
* @param {Command} command - Command to handle.
* @returns {Promise}
*/
async runPostTypeInhibitors(message, command) {
if (command.ownerOnly) {
const isOwner = this.client.isOwner(message.author);
if (!isOwner) {
this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, BuiltInReasons.OWNER);
return true;
}
}
if (command.channel === 'guild' && !message.guild) {
this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, BuiltInReasons.GUILD);
return true;
}
if (command.channel === 'dm' && message.guild) {
this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, BuiltInReasons.DM);
return true;
}
if (await this.runPermissionChecks(message, command)) {
return true;
}
const reason = this.inhibitorHandler
? await this.inhibitorHandler.test('post', message, command)
: null;
if (reason != null) {
this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, reason);
return true;
}
if (this.runCooldowns(message, command)) {
return true;
}
return false;
}
/**
* Runs permission checks.
* @param {Message} message - Message that called the command.
* @param {Command} command - Command to cooldown.
* @returns {Promise}
*/
async runPermissionChecks(message, command) {
if (command.clientPermissions) {
if (typeof command.clientPermissions === 'function') {
let missing = command.clientPermissions(message);
if (isPromise(missing)) missing = await missing;
if (missing != null) {
this.emit(CommandHandlerEvents.MISSING_PERMISSIONS, message, command, 'client', missing);
return true;
}
} else if (message.guild) {
const missing = message.channel.permissionsFor(this.client.user).missing(command.clientPermissions);
if (missing.length) {
this.emit(CommandHandlerEvents.MISSING_PERMISSIONS, message, command, 'client', missing);
return true;
}
}
}
if (command.userPermissions) {
const ignorer = command.ignorePermissions || this.ignorePermissions;
const isIgnored = Array.isArray(ignorer)
? ignorer.includes(message.author.id)
: typeof ignorer === 'function'
? ignorer(message, command)
: message.author.id === ignorer;
if (!isIgnored) {
if (typeof command.userPermissions === 'function') {
let missing = command.userPermissions(message);
if (isPromise(missing)) missing = await missing;
if (missing != null) {
this.emit(CommandHandlerEvents.MISSING_PERMISSIONS, message, command, 'user', missing);
return true;
}
} else if (message.guild) {
const missing = message.channel.permissionsFor(message.author).missing(command.userPermissions);
if (missing.length) {
this.emit(CommandHandlerEvents.MISSING_PERMISSIONS, message, command, 'user', missing);
return true;
}
}
}
}
return false;
}
/**
* Runs cooldowns and checks if a user is under cooldown.
* @param {Message} message - Message that called the command.
* @param {Command} command - Command to cooldown.
* @returns {boolean}
*/
runCooldowns(message, command) {
const ignorer = command.ignoreCooldown || this.ignoreCooldown;
const isIgnored = Array.isArray(ignorer)
? ignorer.includes(message.author.id)
: typeof ignorer === 'function'
? ignorer(message, command)
: message.author.id === ignorer;
if (isIgnored) return false;
const time = command.cooldown != null ? command.cooldown : this.defaultCooldown;
if (!time) return false;
const endTime = message.createdTimestamp + time;
const id = message.author.id;
if (!this.cooldowns.has(id)) this.cooldowns.set(id, {});
if (!this.cooldowns.get(id)[command.id]) {
this.cooldowns.get(id)[command.id] = {
timer: setTimeout(() => {
if (this.cooldowns.get(id)[command.id]) {
clearTimeout(this.cooldowns.get(id)[command.id].timer);
}
this.cooldowns.get(id)[command.id] = null;
if (!Object.keys(this.cooldowns.get(id)).length) {
this.cooldowns.delete(id);
}
}, time).unref(),
end: endTime,
uses: 0
};
}
const entry = this.cooldowns.get(id)[command.id];
if (entry.uses >= command.ratelimit) {
const end = this.cooldowns.get(message.author.id)[command.id].end;
const diff = end - message.createdTimestamp;
this.emit(CommandHandlerEvents.COOLDOWN, message, command, diff);
return true;
}
entry.uses++;
return false;
}
/**
* Runs a command.
* @param {Message} message - Message to handle.
* @param {Command} command - Command to handle.
* @param {any} args - Arguments to use.
* @returns {Promise}
*/
async runCommand(message, command, args) {
if (command.typing) {
message.channel.sendTyping();
}
this.emit(CommandHandlerEvents.COMMAND_STARTED, message, command, args);
const ret = await command.exec(message, args);
this.emit(CommandHandlerEvents.COMMAND_FINISHED, message, command, args, ret);
}
/**
* Parses the command and its argument list.
* @param {Message} message - Message that called the command.
* @returns {Promise}
*/
async parseCommand(message) {
const allowMention = await intoCallable(this.prefix)(message);
let prefixes = intoArray(allowMention);
if (allowMention) {
const mentions = [`<@${this.client.user.id}>`, `<@!${this.client.user.id}>`];
prefixes = [...mentions, ...prefixes];
}
prefixes.sort(prefixCompare);
return this.parseMultiplePrefixes(message, prefixes.map(p => [p, null]));
}
/**
* Parses the command and its argument list using prefix overwrites.
* @param {Message} message - Message that called the command.
* @returns {Promise}
*/
async parseCommandOverwrittenPrefixes(message) {
if (!this.prefixes.size) {
return {};
}
const promises = this.prefixes.map(async (cmds, provider) => {
const prefixes = intoArray(await intoCallable(provider)(message));
return prefixes.map(p => [p, cmds]);
});
const pairs = flatMap(await Promise.all(promises), x => x);
pairs.sort(([a], [b]) => prefixCompare(a, b));
return this.parseMultiplePrefixes(message, pairs);
}
/**
* Runs parseWithPrefix on multiple prefixes and returns the best parse.
* @param {Message} message - Message to parse.
* @param {any[]} pairs - Pairs of prefix to associated commands.
* That is, `[string, Set | null][]`.
* @returns {ParsedComponentData}
*/
parseMultiplePrefixes(message, pairs) {
const parses = pairs.map(([prefix, cmds]) => this.parseWithPrefix(message, prefix, cmds));
const result = parses.find(parsed => parsed.command);
if (result) {
return result;
}
const guess = parses.find(parsed => parsed.prefix != null);
if (guess) {
return guess;
}
return {};
}
/**
* Tries to parse a message with the given prefix and associated commands.
* Associated commands refer to when a prefix is used in prefix overrides.
* @param {Message} message - Message to parse.
* @param {string} prefix - Prefix to use.
* @param {Set} [associatedCommands=null] - Associated commands.
* @returns {ParsedComponentData}
*/
parseWithPrefix(message, prefix, associatedCommands = null) {
const lowerContent = message.content.toLowerCase();
if (!lowerContent.startsWith(prefix.toLowerCase())) {
return {};
}
const endOfPrefix = lowerContent.indexOf(prefix.toLowerCase()) + prefix.length;
const startOfArgs = message.content.slice(endOfPrefix).search(/\S/) + prefix.length;
const alias = message.content.slice(startOfArgs).split(/\s{1,}|\n{1,}/)[0];
const command = this.findCommand(alias);
const content = message.content.slice(startOfArgs + alias.length + 1).trim();
const afterPrefix = message.content.slice(prefix.length).trim();
if (!command) {
return { prefix, alias, content, afterPrefix };
}
if (associatedCommands == null) {
if (command.prefix != null) {
return { prefix, alias, content, afterPrefix };
}
} else if (!associatedCommands.has(command.id)) {
return { prefix, alias, content, afterPrefix };
}
return { command, prefix, alias, content, afterPrefix };
}
/**
* Handles errors from the handling.
* @param {Error} err - The error.
* @param {Message} message - Message that called the command.
* @param {Command} [command] - Command that errored.
* @returns {void}
*/
emitError(err, message, command) {
if (this.listenerCount(CommandHandlerEvents.ERROR)) {
this.emit(CommandHandlerEvents.ERROR, err, message, command);
return;
}
throw err;
}
/**
* Sweep command util instances from cache and returns amount sweeped.
* @param {number} lifetime - Messages older than this will have their command util instance sweeped.
* This is in milliseconds and defaults to the `commandUtilLifetime` option.
* @returns {number}
*/
sweepCommandUtil(lifetime = this.commandUtilLifetime) {
let count = 0;
for (const commandUtil of this.commandUtils.values()) {
const now = Date.now();
const message = commandUtil.message;
if (now - (message.editedTimestamp || message.createdTimestamp) > lifetime) {
count++;
this.commandUtils.delete(message.id);
}
}
return count;
}
/**
* Adds an ongoing prompt in order to prevent command usage in the channel.
* @param {Channel} channel - Channel to add to.
* @param {User} user - User to add.
* @returns {void}
*/
addPrompt(channel, user) {
let users = this.prompts.get(channel.id);
if (!users) this.prompts.set(channel.id, new Set());
users = this.prompts.get(channel.id);
users.add(user.id);
}
/**
* Removes an ongoing prompt.
* @param {Channel} channel - Channel to remove from.
* @param {User} user - User to remove.
* @returns {void}
*/
removePrompt(channel, user) {
const users = this.prompts.get(channel.id);
if (!users) return;
users.delete(user.id);
if (!users.size) this.prompts.delete(user.id);
}
/**
* Checks if there is an ongoing prompt.
* @param {Channel} channel - Channel to check.
* @param {User} user - User to check.
* @returns {boolean}
*/
hasPrompt(channel, user) {
const users = this.prompts.get(channel.id);
if (!users) return false;
return users.has(user.id);
}
/**
* Finds a command by alias.
* @param {string} name - Alias to find with.
* @returns {Command}
*/
findCommand(name) {
return this.modules.get(this.aliases.get(name.toLowerCase()));
}
/**
* Set the inhibitor handler to use.
* @param {InhibitorHandler} inhibitorHandler - The inhibitor handler.
* @returns {CommandHandler}
*/
useInhibitorHandler(inhibitorHandler) {
this.inhibitorHandler = inhibitorHandler;
this.resolver.inhibitorHandler = inhibitorHandler;
return this;
}
/**
* Set the listener handler to use.
* @param {ListenerHandler} listenerHandler - The listener handler.
* @returns {CommandHandler}
*/
useListenerHandler(listenerHandler) {
this.resolver.listenerHandler = listenerHandler;
return this;
}
/**
* Loads a command.
* @method
* @name CommandHandler#load
* @param {string|Command} thing - Module or path to module.
* @returns {Command}
*/
/**
* Reads all commands from the directory and loads them.
* @method
* @name CommandHandler#loadAll
* @param {string} [directory] - Directory to load from.
* Defaults to the directory passed in the constructor.
* @param {LoadPredicate} [filter] - Filter for files, where true means it should be loaded.
* @returns {CommandHandler}
*/
/**
* Removes a command.
* @method
* @name CommandHandler#remove
* @param {string} id - ID of the command.
* @returns {Command}
*/
/**
* Removes all commands.
* @method
* @name CommandHandler#removeAll
* @returns {CommandHandler}
*/
/**
* Reloads a command.
* @method
* @name CommandHandler#reload
* @param {string} id - ID of the command.
* @returns {Command}
*/
/**
* Reloads all commands.
* @method
* @name CommandHandler#reloadAll
* @returns {CommandHandler}
*/
}
module.exports = CommandHandler;
/**
* Emitted when a message is blocked by a pre-message inhibitor.
* The built-in inhibitors are 'client' and 'bot'.
* @event CommandHandler#messageBlocked
* @param {Message} message - Message sent.
* @param {string} reason - Reason for the block.
*/
/**
* Emitted when no command has been run.
* @event CommandHandler#messageInvalid
* @param {Message} message - Message sent.
*/
/**
* Emitted when a command is found disabled.
* @event CommandHandler#commandDisabled
* @param {Message} message - Message sent.
* @param {Command} command - Command found.
*/
/**
* Emitted when a command is blocked by a post-message inhibitor.
* The built-in inhibitors are 'owner', 'guild', and 'dm'.
* @event CommandHandler#commandBlocked
* @param {Message} message - Message sent.
* @param {Command} command - Command blocked.
* @param {string} reason - Reason for the block.
*/
/**
* Emitted when a command starts execution.
* @event CommandHandler#commandStarted
* @param {Message} message - Message sent.
* @param {Command} command - Command executed.
* @param {any} args - The args passed to the command.
*/
/**
* Emitted when a command finishes execution.
* @event CommandHandler#commandFinished
* @param {Message} message - Message sent.
* @param {Command} command - Command executed.
* @param {any} args - The args passed to the command.
* @param {any} returnValue - The command's return value.
*/
/**
* Emitted when a command is cancelled via prompt or argument cancel.
* @event CommandHandler#commandCancelled
* @param {Message} message - Message sent.
* @param {Command} command - Command executed.
* @param {?Message} retryMessage - Message to retry with.
* This is passed when a prompt was broken out of with a message that looks like a command.
*/
/**
* Emitted when a command is found on cooldown.
* @event CommandHandler#cooldown
* @param {Message} message - Message sent.
* @param {Command} command - Command blocked.
* @param {number} remaining - Remaining time in milliseconds for cooldown.
*/
/**
* Emitted when a user is in a command argument prompt.
* Used to prevent usage of commands during a prompt.
* @event CommandHandler#inPrompt
* @param {Message} message - Message sent.
*/
/**
* Emitted when a permissions check is failed.
* @event CommandHandler#missingPermissions
* @param {Message} message - Message sent.
* @param {Command} command - Command blocked.
* @param {string} type - Either 'client' or 'user'.
* @param {any} missing - The missing permissions.
*/
/**
* Emitted when a command or inhibitor errors.
* @event CommandHandler#error
* @param {Error} error - The error.
* @param {Message} message - Message sent.
* @param {?Command} command - Command executed.
*/
/**
* Emitted when a command is loaded.
* @event CommandHandler#load
* @param {Command} command - Module loaded.
* @param {boolean} isReload - Whether or not this was a reload.
*/
/**
* Emitted when a command is removed.
* @event CommandHandler#remove
* @param {Command} command - Command removed.
*/
/**
* Also includes properties from AkairoHandlerOptions.
* @typedef {AkairoHandlerOptions} CommandHandlerOptions
* @prop {boolean} [blockClient=true] - Whether or not to block self.
* @prop {boolean} [blockBots=true] - Whether or not to block bots.
* @prop {string|string[]|PrefixSupplier} [prefix='!'] - Default command prefix(es).
* @prop {boolean|MentionPrefixPredicate} [allowMention=true] - Whether or not to allow mentions to the client user as a prefix.
* @prop {RegExp} [aliasReplacement] - Regular expression to automatically make command aliases.
* For example, using `/-/g` would mean that aliases containing `-` would be valid with and without it.
* So, the alias `command-name` is valid as both `command-name` and `commandname`.
* @prop {boolean} [handleEdits=false] - Whether or not to handle edited messages using CommandUtil.
* @prop {boolean} [storeMessages=false] - Whether or not to have CommandUtil store all prompts and their replies.
* @prop {boolean} [commandUtil=false] - Whether or not to assign `message.util`.
* @prop {number} [commandUtilLifetime=3e5] - Milliseconds a message should exist for before its command util instance is marked for removal.
* If 0, CommandUtil instances will never be removed and will cause memory to increase indefinitely.
* @prop {number} [commandUtilSweepInterval=3e5] - Time interval in milliseconds for sweeping command util instances.
* If 0, CommandUtil instances will never be removed and will cause memory to increase indefinitely.
* @prop {boolean} [fetchMembers=false] - Whether or not to fetch member on each message from a guild.
* @prop {number} [defaultCooldown=0] - The default cooldown for commands.
* @prop {Snowflake|Snowflake[]|IgnoreCheckPredicate} [ignoreCooldown] - ID of user(s) to ignore cooldown or a function to ignore.
* Defaults to the client owner(s).
* @prop {Snowflake|Snowflake[]|IgnoreCheckPredicate} [ignorePermissions=[]] - ID of user(s) to ignore `userPermissions` checks or a function to ignore.
* @prop {DefaultArgumentOptions} [argumentDefaults] - The default argument options.
*/
/**
* Data for managing cooldowns.
* @typedef {Object} CooldownData
* @prop {Timeout} timer - Timeout object.
* @prop {number} end - When the cooldown ends.
* @prop {number} uses - Number of times the command has been used.
*/
/**
* Various parsed components of the message.
* @typedef {Object} ParsedComponentData
* @prop {?Command} command - The command used.
* @prop {?string} prefix - The prefix used.
* @prop {?string} alias - The alias used.
* @prop {?string} content - The content to the right of the alias.
* @prop {?string} afterPrefix - The content to the right of the prefix.
*/
/**
* A function that returns whether this message should be ignored for a certain check.
* @typedef {Function} IgnoreCheckPredicate
* @param {Message} message - Message to check.
* @param {Command} command - Command to check.
* @returns {boolean}
*/
/**
* A function that returns whether mentions can be used as a prefix.
* @typedef {Function} MentionPrefixPredicate
* @param {Message} message - Message to option for.
* @returns {boolean}
*/
/**
* A function that returns the prefix(es) to use.
* @typedef {Function} PrefixSupplier
* @param {Message} message - Message to get prefix for.
* @returns {string|string[]}
*/
================================================
FILE: src/struct/commands/CommandUtil.js
================================================
const { Collection } = require('discord.js');
/**
* Command utilities.
* @param {CommandHandler} handler - The command handler.
* @param {Message} message - Message that triggered the command.
*/
class CommandUtil {
constructor(handler, message) {
/**
* The command handler.
* @type {CommandHandler}
*/
this.handler = handler;
/**
* Message that triggered the command.
* @type {Message}
*/
this.message = message;
/**
* The parsed components.
* @type {?ParsedComponentData}
*/
this.parsed = null;
/**
* Whether or not the last response should be edited.
* @type {boolean}
*/
this.shouldEdit = false;
/**
* The last response sent.
* @type {?Message}
*/
this.lastResponse = null;
if (this.handler.storeMessages) {
/**
* Messages stored from prompts and prompt replies.
* @type {Collection}
*/
this.messages = new Collection();
} else {
this.messages = null;
}
}
/**
* Sets the last repsonse.
* @param {Message|Message[]} message - Message to set.
* @returns {Message}
*/
setLastResponse(message) {
if (Array.isArray(message)) {
this.lastResponse = message.slice(-1)[0];
} else {
this.lastResponse = message;
}
return this.lastResponse;
}
/**
* Adds client prompt or user reply to messages.
* @param {Message|Message[]} message - Message to add.
* @returns {Message|Message[]}
*/
addMessage(message) {
if (this.handler.storeMessages) {
if (Array.isArray(message)) {
for (const msg of message) {
this.messages.set(msg.id, msg);
}
} else {
this.messages.set(message.id, message);
}
}
return message;
}
/**
* Changes if the message should be edited.
* @param {boolean} state - Change to editable or not.
* @returns {CommandUtil}
*/
setEditable(state) {
this.shouldEdit = Boolean(state);
return this;
}
/**
* Sends a response or edits an old response if available.
* @param {string|MessageOptions} [options={}] - Options to use.
* @returns {Promise}
*/
async send(options) {
const transformedOptions = this.constructor.transformOptions(options);
const hasFiles = transformedOptions.files && transformedOptions.files.length > 0;
if (this.shouldEdit && (this.command ? this.command.editable : true) && !hasFiles && !this.lastResponse.deleted && !this.lastResponse.attachments.size) {
return this.lastResponse.edit(transformedOptions);
}
const sent = await this.message.channel.send(transformedOptions);
const lastSent = this.setLastResponse(sent);
this.setEditable(!lastSent.attachments.size);
return sent;
}
/**
* Sends a response, overwriting the last response.
* @param {string|MessageOptions} [options={}] - Options to use.
* @returns {Promise}
*/
async sendNew(options) {
const sent = await this.message.channel.send(this.constructor.transformOptions(options));
const lastSent = this.setLastResponse(sent);
this.setEditable(!lastSent.attachments.size);
return sent;
}
/**
* Sends a response replying to the user's message.
* @param {string|MessageOptions} [options={}] - Options to use.
* @returns {Promise}
*/
reply(options) {
return this.send({
reply: { messageReference: this.message, failIfNotExists: false },
...this.constructor.transformOptions(options)
});
}
/**
* Edits the last response.
* @param {string|MessageEditOptions} [options={}] - Options to use.
* @returns {Promise}
*/
edit(options) {
return this.lastResponse.edit(options);
}
/**
* Transform options for sending.
* @param {string|MessageOptions} [options={}] - Options to use.
* @returns {MessageOptions}
*/
static transformOptions(options) {
const transformedOptions = typeof options === 'string' ? { content: options } : { ...options };
if (!transformedOptions.content) transformedOptions.content = null;
if (!transformedOptions.embeds) transformedOptions.embeds = [];
return transformedOptions;
}
}
module.exports = CommandUtil;
/**
* Extra properties applied to the Discord.js message object.
* @typedef {Object} MessageExtensions
* @prop {?CommandUtil} util - Utilities for command responding.
* Available on all messages after 'all' inhibitors and built-in inhibitors (bot, client).
* Not all properties of the util are available, depending on the input.
*/
================================================
FILE: src/struct/commands/ContentParser.js
================================================
const { ArgumentMatches } = require('../../util/Constants');
/*
* Grammar:
*
* Arguments
* = (Argument (WS? Argument)*)? EOF
*
* Argument
* = Flag
* | Phrase
*
* Flag
* = FlagWord
* | OptionFlagWord WS? Phrase?
*
* Phrase
* = Quote (Word | WS)* Quote?
* | OpenQuote (Word | OpenQuote | Quote | WS)* EndQuote?
* | EndQuote
* | Word
*
* FlagWord = Given
* OptionFlagWord = Given
* Quote = "
* OpenQuote = “
* EndQuote = ”
* Word = /^\S+/ (and not in FlagWord or OptionFlagWord)
* WS = /^\s+/
* EOF = /^$/
*
* With a separator:
*
* Arguments
* = (Argument (WS? Separator WS? Argument)*)? EOF
*
* Argument
* = Flag
* | Phrase
*
* Flag
* = FlagWord
* | OptionFlagWord WS? Phrase?
*
* Phrase
* = Word (WS Word)*
*
* FlagWord = Given
* OptionFlagWord = Given
* Seperator = Given
* Word = /^\S+/ (and not in FlagWord or OptionFlagWord or equal to Seperator)
* WS = /^\s+/
* EOF = /^$/
*/
class Tokenizer {
constructor(content, {
flagWords = [],
optionFlagWords = [],
quoted = true,
separator
} = {}) {
this.content = content;
this.flagWords = flagWords;
this.optionFlagWords = optionFlagWords;
this.quoted = quoted;
this.separator = separator;
this.position = 0;
// 0 -> Default, 1 -> Quotes (""), 2 -> Special Quotes (“”)
this.state = 0;
this.tokens = [];
}
startsWith(str) {
return this.content.slice(this.position, this.position + str.length).toLowerCase() === str.toLowerCase();
}
match(regex) {
return this.content.slice(this.position).match(regex);
}
slice(from, to) {
return this.content.slice(this.position + from, this.position + to);
}
addToken(type, value) {
this.tokens.push({ type, value });
}
advance(n) {
this.position += n;
}
choice(...actions) {
for (const action of actions) {
if (action.call(this)) {
return;
}
}
}
tokenize() {
while (this.position < this.content.length) {
this.runOne();
}
this.addToken('EOF', '');
return this.tokens;
}
runOne() {
this.choice(
this.runWhitespace,
this.runFlags,
this.runOptionFlags,
this.runQuote,
this.runOpenQuote,
this.runEndQuote,
this.runSeparator,
this.runWord
);
}
runFlags() {
if (this.state === 0) {
for (const word of this.flagWords) {
if (this.startsWith(word)) {
this.addToken('FlagWord', this.slice(0, word.length));
this.advance(word.length);
return true;
}
}
}
return false;
}
runOptionFlags() {
if (this.state === 0) {
for (const word of this.optionFlagWords) {
if (this.startsWith(word)) {
this.addToken('OptionFlagWord', this.slice(0, word.length));
this.advance(word.length);
return true;
}
}
}
return false;
}
runQuote() {
if (this.separator == null && this.quoted && this.startsWith('"')) {
if (this.state === 1) {
this.state = 0;
} else if (this.state === 0) {
this.state = 1;
}
this.addToken('Quote', '"');
this.advance(1);
return true;
}
return false;
}
runOpenQuote() {
if (this.separator == null && this.quoted && this.startsWith('"')) {
if (this.state === 0) {
this.state = 2;
}
this.addToken('OpenQuote', '"');
this.advance(1);
return true;
}
return false;
}
runEndQuote() {
if (this.separator == null && this.quoted && this.startsWith('”')) {
if (this.state === 2) {
this.state = 0;
}
this.addToken('EndQuote', '”');
this.advance(1);
return true;
}
return false;
}
runSeparator() {
if (this.separator != null && this.startsWith(this.separator)) {
this.addToken('Separator', this.slice(0, this.separator.length));
this.advance(this.separator.length);
return true;
}
return false;
}
runWord() {
const wordRegex = this.state === 0
? /^\S+/
: this.state === 1
? /^[^\s"]+/
: /^[^\s”]+/;
const wordMatch = this.match(wordRegex);
if (wordMatch) {
if (this.separator) {
if (wordMatch[0].toLowerCase() === this.separator.toLowerCase()) {
return false;
}
const index = wordMatch[0].indexOf(this.separator);
if (index === -1) {
this.addToken('Word', wordMatch[0]);
this.advance(wordMatch[0].length);
return true;
}
const actual = wordMatch[0].slice(0, index);
this.addToken('Word', actual);
this.advance(actual.length);
return true;
}
this.addToken('Word', wordMatch[0]);
this.advance(wordMatch[0].length);
return true;
}
return false;
}
runWhitespace() {
const wsMatch = this.match(/^\s+/);
if (wsMatch) {
this.addToken('WS', wsMatch[0]);
this.advance(wsMatch[0].length);
return true;
}
return false;
}
}
class Parser {
constructor(tokens, { separated }) {
this.tokens = tokens;
this.separated = separated;
this.position = 0;
/*
* Phrases are `{ type: 'Phrase', value, raw }`.
* Flags are `{ type: 'Flag', key, raw }`.
* Option flags are `{ type: 'OptionFlag', key, value, raw }`.
* The `all` property is partitioned into `phrases`, `flags`, and `optionFlags`.
*/
this.results = {
all: [],
phrases: [],
flags: [],
optionFlags: []
};
}
next() {
this.position++;
}
lookaheadN(n, ...types) {
return this.tokens[this.position + n] != null && types.includes(this.tokens[this.position + n].type);
}
lookahead(...types) {
return this.lookaheadN(0, ...types);
}
match(...types) {
if (this.lookahead(...types)) {
this.next();
return this.tokens[this.position - 1];
}
throw new Error(`Unexpected token ${this.tokens[this.position].value} of type ${this.tokens[this.position].type} (this should never happen)`);
}
parse() {
// -1 for EOF.
while (this.position < this.tokens.length - 1) {
this.runArgument();
}
this.match('EOF');
return this.results;
}
runArgument() {
const leading = this.lookahead('WS') ? this.match('WS').value : '';
if (this.lookahead('FlagWord', 'OptionFlagWord')) {
const parsed = this.parseFlag();
const trailing = this.lookahead('WS') ? this.match('WS').value : '';
const separator = this.lookahead('Separator') ? this.match('Separator').value : '';
parsed.raw = `${leading}${parsed.raw}${trailing}${separator}`;
this.results.all.push(parsed);
if (parsed.type === 'Flag') {
this.results.flags.push(parsed);
} else {
this.results.optionFlags.push(parsed);
}
return;
}
const parsed = this.parsePhrase();
const trailing = this.lookahead('WS') ? this.match('WS').value : '';
const separator = this.lookahead('Separator') ? this.match('Separator').value : '';
parsed.raw = `${leading}${parsed.raw}${trailing}${separator}`;
this.results.all.push(parsed);
this.results.phrases.push(parsed);
}
parseFlag() {
if (this.lookahead('FlagWord')) {
const flag = this.match('FlagWord');
const parsed = { type: 'Flag', key: flag.value, raw: flag.value };
return parsed;
}
// Otherwise, `this.lookahead('OptionFlagWord')` should be true.
const flag = this.match('OptionFlagWord');
const parsed = { type: 'OptionFlag', key: flag.value, value: '', raw: flag.value };
const ws = this.lookahead('WS') ? this.match('WS') : null;
if (ws != null) {
parsed.raw += ws.value;
}
const phrase = this.lookahead('Quote', 'OpenQuote', 'EndQuote', 'Word')
? this.parsePhrase()
: null;
if (phrase != null) {
parsed.value = phrase.value;
parsed.raw += phrase.raw;
}
return parsed;
}
parsePhrase() {
if (!this.separated) {
if (this.lookahead('Quote')) {
const parsed = { type: 'Phrase', value: '', raw: '' };
const openQuote = this.match('Quote');
parsed.raw += openQuote.value;
while (this.lookahead('Word', 'WS')) {
const match = this.match('Word', 'WS');
parsed.value += match.value;
parsed.raw += match.value;
}
const endQuote = this.lookahead('Quote') ? this.match('Quote') : null;
if (endQuote != null) {
parsed.raw += endQuote.value;
}
return parsed;
}
if (this.lookahead('OpenQuote')) {
const parsed = { type: 'Phrase', value: '', raw: '' };
const openQuote = this.match('OpenQuote');
parsed.raw += openQuote.value;
while (this.lookahead('Word', 'WS')) {
const match = this.match('Word', 'WS');
if (match.type === 'Word') {
parsed.value += match.value;
parsed.raw += match.value;
} else {
parsed.raw += match.value;
}
}
const endQuote = this.lookahead('EndQuote') ? this.match('EndQuote') : null;
if (endQuote != null) {
parsed.raw += endQuote.value;
}
return parsed;
}
if (this.lookahead('EndQuote')) {
const endQuote = this.match('EndQuote');
const parsed = { type: 'Phrase', value: endQuote.value, raw: endQuote.value };
return parsed;
}
}
if (this.separated) {
const init = this.match('Word');
const parsed = { type: 'Phrase', value: init.value, raw: init.value };
while (this.lookahead('WS') && this.lookaheadN(1, 'Word')) {
const ws = this.match('WS');
const word = this.match('Word');
parsed.value += ws.value + word.value;
}
parsed.raw = parsed.value;
return parsed;
}
const word = this.match('Word');
const parsed = { type: 'Phrase', value: word.value, raw: word.value };
return parsed;
}
}
/**
* Parses content.
* @param {ContentParserOptions} options - Options.
* @private
*/
class ContentParser {
constructor({
flagWords = [],
optionFlagWords = [],
quoted = true,
separator
} = {}) {
this.flagWords = flagWords;
this.flagWords.sort((a, b) => b.length - a.length);
this.optionFlagWords = optionFlagWords;
this.optionFlagWords.sort((a, b) => b.length - a.length);
this.quoted = Boolean(quoted);
this.separator = separator;
}
/**
* Parses content.
* @param {string} content - Content to parse.
* @returns {ContentParserResult}
*/
parse(content) {
const tokens = new Tokenizer(content, {
flagWords: this.flagWords,
optionFlagWords: this.optionFlagWords,
quoted: this.quoted,
separator: this.separator
}).tokenize();
return new Parser(tokens, { separated: this.separator != null }).parse();
}
/**
* Extracts the flags from argument options.
* @param {ArgumentOptions[]} args - Argument options.
* @returns {ExtractedFlags}
*/
static getFlags(args) {
const res = {
flagWords: [],
optionFlagWords: []
};
for (const arg of args) {
const arr = res[arg.match === ArgumentMatches.FLAG ? 'flagWords' : 'optionFlagWords'];
if (arg.match === ArgumentMatches.FLAG || arg.match === ArgumentMatches.OPTION) {
if (Array.isArray(arg.flag)) {
arr.push(...arg.flag);
} else {
arr.push(arg.flag);
}
}
}
return res;
}
}
module.exports = ContentParser;
/**
* Options for the content parser.
* @typedef {Object} ContentParserOptions
* @prop {string[]} [flagWords=[]] - Words considered flags.
* @prop {string[]} [optionFlagWords=[]] - Words considered option flags.
* @prop {boolean} [quoted=true] - Whether to parse quotes.
* @prop {string} [separator] - Whether to parse a separator.
* @private
*/
/**
* Result of parsing.
* @typedef {Object} ContentParserResult
* @prop {StringData[]} all - All phrases and flags.
* @prop {StringData[]} phrases - Phrases.
* @prop {StringData[]} flags - Flags.
* @prop {StringData[]} optionFlags - Option flags.
*/
/**
* Flags extracted from an argument list.
* @typedef {Object} ExtractedFlags
* @prop {string[]} [flagWords=[]] - Words considered flags.
* @prop {string[]} [optionFlagWords=[]] - Words considered option flags.
* @private
*/
/**
* A single phrase or flag.
* @typedef {Object} StringData
* @prop {string} type - One of 'Phrase', 'Flag', 'OptionFlag'.
* @prop {string} raw - The raw string with whitespace and/or separator.
* @prop {?string} key - The key of a 'Flag' or 'OptionFlag'.
* @prop {?string} value - The value of a 'Phrase' or 'OptionFlag'.
*/
================================================
FILE: src/struct/commands/Flag.js
================================================
/**
* Represents a special return value during commmand execution or argument parsing.
* @param {string} type - Type of flag.
* @param {any} [data={}] - Extra data.
*/
class Flag {
constructor(type, data = {}) {
this.type = type;
Object.assign(this, data);
}
/**
* Creates a flag that cancels the command.
* @returns {Flag}
*/
static cancel() {
return new Flag('cancel');
}
/**
* Creates a flag that retries with another input.
* @param {Message} message - Message to handle.
* @returns {Flag}
*/
static retry(message) {
return new Flag('retry', { message });
}
/**
* Creates a flag that acts as argument cast failure with extra data.
* @param {any} value - The extra data for the failure.
* @returns {Flag}
*/
static fail(value) {
return new Flag('fail', { value });
}
/**
* Creates a flag that runs another command with the rest of the arguments.
* @param {string} command - Command ID.
* @param {boolean} [ignore=false] - Whether or not to ignore permission checks.
* @param {string} [rest] - The rest of the arguments.
* If this is not set, the argument handler will automatically use the rest of the content.
* @returns {Flag}
*/
static continue(command, ignore = false, rest = null) {
return new Flag('continue', { command, ignore, rest });
}
/**
* Checks if a value is a flag and of some type.
* @param {any} value - Value to check.
* @param {string} type - Type of flag.
* @returns {boolean}
*/
static is(value, type) {
return value instanceof Flag && value.type === type;
}
}
module.exports = Flag;
================================================
FILE: src/struct/commands/arguments/Argument.js
================================================
const { ArgumentMatches, ArgumentTypes } = require('../../../util/Constants');
const Flag = require('../Flag');
const { choice, intoCallable, isPromise } = require('../../../util/Util');
/**
* Represents an argument for a command.
* @param {Command} command - Command of the argument.
* @param {ArgumentOptions} options - Options for the argument.
*/
class Argument {
constructor(command, {
match = ArgumentMatches.PHRASE,
type = ArgumentTypes.STRING,
flag = null,
multipleFlags = false,
index = null,
unordered = false,
limit = Infinity,
prompt = null,
default: defaultValue = null,
otherwise = null,
modifyOtherwise = null
} = {}) {
/**
* The command this argument belongs to.
* @type {Command}
*/
this.command = command;
/**
* The method to match text.
* @type {ArgumentMatch}
*/
this.match = match;
/**
* The type to cast to or a function to use to cast.
* @type {ArgumentType|ArgumentTypeCaster}
*/
this.type = typeof type === 'function' ? type.bind(this) : type;
/**
* The string(s) to use for flag or option match.
* @type {?string|string[]}
*/
this.flag = flag;
/**
* Whether to process multiple option flags instead of just the first.
* @type {boolean}
*/
this.multipleFlags = multipleFlags;
/**
* The index to start from.
* @type {?number}
*/
this.index = index;
/**
* Whether or not the argument is unordered.
* @type {boolean|number|number[]}
*/
this.unordered = unordered;
/**
* The amount of phrases to match for rest, separate, content, or text match.
* @type {number}
*/
this.limit = limit;
/**
* The prompt options.
* @type {?ArgumentPromptOptions}
*/
this.prompt = prompt;
/**
* The default value of the argument or a function supplying the default value.
* @type {DefaultValueSupplier|any}
*/
this.default = typeof defaultValue === 'function' ? defaultValue.bind(this) : defaultValue;
/**
* The content or function supplying the content sent when argument parsing fails.
* @type {?string|MessageOptions|MessageAdditions|OtherwiseContentSupplier}
*/
this.otherwise = typeof otherwise === 'function' ? otherwise.bind(this) : otherwise;
/**
* Function to modify otherwise content.
* @type {?OtherwiseContentModifier}
*/
this.modifyOtherwise = modifyOtherwise;
}
/**
* The client.
* @type {AkairoClient}
*/
get client() {
return this.command.client;
}
/**
* The command handler.
* @type {CommandHandler}
*/
get handler() {
return this.command.handler;
}
/**
* Processes the type casting and prompting of the argument for a phrase.
* @param {Message} message - The message that called the command.
* @param {string} phrase - The phrase to process.
* @returns {Promise}
*/
async process(message, phrase) {
const commandDefs = this.command.argumentDefaults;
const handlerDefs = this.handler.argumentDefaults;
const optional = choice(
this.prompt && this.prompt.optional,
commandDefs.prompt && commandDefs.prompt.optional,
handlerDefs.prompt && handlerDefs.prompt.optional
);
const doOtherwise = async failure => {
const otherwise = choice(
this.otherwise,
commandDefs.otherwise,
handlerDefs.otherwise
);
const modifyOtherwise = choice(
this.modifyOtherwise,
commandDefs.modifyOtherwise,
handlerDefs.modifyOtherwise
);
let text = await intoCallable(otherwise).call(this, message, { phrase, failure });
if (Array.isArray(text)) {
text = text.join('\n');
}
if (modifyOtherwise) {
text = await modifyOtherwise.call(this, message, text, { phrase, failure });
if (Array.isArray(text)) {
text = text.join('\n');
}
}
if (text) {
const sent = await message.channel.send(text);
if (message.util) message.util.addMessage(sent);
}
return Flag.cancel();
};
if (!phrase && optional) {
if (this.otherwise != null) {
return doOtherwise(null);
}
return intoCallable(this.default)(message, { phrase, failure: null });
}
const res = await this.cast(message, phrase);
if (Argument.isFailure(res)) {
if (this.otherwise != null) {
return doOtherwise(res);
}
if (this.prompt != null) {
return this.collect(message, phrase, res);
}
return this.default == null
? res
: intoCallable(this.default)(message, { phrase, failure: res });
}
return res;
}
/**
* Casts a phrase to this argument's type.
* @param {Message} message - Message that called the command.
* @param {string} phrase - Phrase to process.
* @returns {Promise}
*/
cast(message, phrase) {
return Argument.cast(this.type, this.handler.resolver, message, phrase);
}
/**
* Collects input from the user by prompting.
* @param {Message} message - Message to prompt.
* @param {string} [commandInput] - Previous input from command if there was one.
* @param {any} [parsedInput] - Previous parsed input from command if there was one.
* @returns {Promise}
*/
async collect(message, commandInput = '', parsedInput = null) {
const promptOptions = {};
Object.assign(promptOptions, this.handler.argumentDefaults.prompt);
Object.assign(promptOptions, this.command.argumentDefaults.prompt);
Object.assign(promptOptions, this.prompt || {});
const isInfinite = promptOptions.infinite || (this.match === ArgumentMatches.SEPARATE && !commandInput);
const additionalRetry = Number(Boolean(commandInput));
const values = isInfinite ? [] : null;
const getText = async (promptType, prompter, retryCount, inputMessage, inputPhrase, inputParsed) => {
let text = await intoCallable(prompter).call(this, message, {
retries: retryCount,
infinite: isInfinite,
message: inputMessage,
phrase: inputPhrase,
failure: inputParsed
});
if (Array.isArray(text)) {
text = text.join('\n');
}
const modifier = {
start: promptOptions.modifyStart,
retry: promptOptions.modifyRetry,
timeout: promptOptions.modifyTimeout,
ended: promptOptions.modifyEnded,
cancel: promptOptions.modifyCancel
}[promptType];
if (modifier) {
text = await modifier.call(this, message, text, {
retries: retryCount,
infinite: isInfinite,
message: inputMessage,
phrase: inputPhrase,
failure: inputParsed
});
if (Array.isArray(text)) {
text = text.join('\n');
}
}
return text;
};
// eslint-disable-next-line complexity
const promptOne = async (prevMessage, prevInput, prevParsed, retryCount) => {
let sentStart;
// This is either a retry prompt, the start of a non-infinite, or the start of an infinite.
if (retryCount !== 1 || !isInfinite || !values.length) {
const promptType = retryCount === 1 ? 'start' : 'retry';
const prompter = retryCount === 1 ? promptOptions.start : promptOptions.retry;
const startText = await getText(promptType, prompter, retryCount, prevMessage, prevInput, prevParsed);
if (startText) {
sentStart = await (message.util || message.channel).send(startText);
if (message.util) {
message.util.setEditable(false);
message.util.setLastResponse(sentStart);
message.util.addMessage(sentStart);
}
}
}
let input;
try {
input = (await message.channel.awaitMessages({
filter: m => m.author.id === message.author.id,
max: 1,
time: promptOptions.time,
errors: ['time']
})).first();
if (message.util) message.util.addMessage(input);
} catch (err) {
const timeoutText = await getText('timeout', promptOptions.timeout, retryCount, prevMessage, prevInput, '');
if (timeoutText) {
const sentTimeout = await message.channel.send(timeoutText);
if (message.util) message.util.addMessage(sentTimeout);
}
return Flag.cancel();
}
if (promptOptions.breakout) {
const looksLike = await this.handler.parseCommand(input);
if (looksLike && looksLike.command) return Flag.retry(input);
}
if (input.content.toLowerCase() === promptOptions.cancelWord.toLowerCase()) {
const cancelText = await getText('cancel', promptOptions.cancel, retryCount, input, input.content, 'cancel');
if (cancelText) {
const sentCancel = await message.channel.send(cancelText);
if (message.util) message.util.addMessage(sentCancel);
}
return Flag.cancel();
}
if (isInfinite && input.content.toLowerCase() === promptOptions.stopWord.toLowerCase()) {
if (!values.length) return promptOne(input, input.content, null, retryCount + 1);
return values;
}
const parsedValue = await this.cast(input, input.content);
if (Argument.isFailure(parsedValue)) {
if (retryCount <= promptOptions.retries) {
return promptOne(input, input.content, parsedValue, retryCount + 1);
}
const endedText = await getText('ended', promptOptions.ended, retryCount, input, input.content, 'stop');
if (endedText) {
const sentEnded = await message.channel.send(endedText);
if (message.util) message.util.addMessage(sentEnded);
}
return Flag.cancel();
}
if (isInfinite) {
values.push(parsedValue);
const limit = promptOptions.limit;
if (values.length < limit) return promptOne(message, input.content, parsedValue, 1);
return values;
}
return parsedValue;
};
this.handler.addPrompt(message.channel, message.author);
const returnValue = await promptOne(message, commandInput, parsedInput, 1 + additionalRetry);
if (this.handler.commandUtil) {
message.util.setEditable(false);
}
this.handler.removePrompt(message.channel, message.author);
return returnValue;
}
/**
* Casts a phrase to the specified type.
* @param {ArgumentType|ArgumentTypeCaster} type - Type to use.
* @param {TypeResolver} resolver - Type resolver to use.
* @param {Message} message - Message that called the command.
* @param {string} phrase - Phrase to process.
* @returns {Promise}
*/
static async cast(type, resolver, message, phrase) {
if (Array.isArray(type)) {
for (const entry of type) {
if (Array.isArray(entry)) {
if (entry.some(t => t.toLowerCase() === phrase.toLowerCase())) {
return entry[0];
}
} else if (entry.toLowerCase() === phrase.toLowerCase()) {
return entry;
}
}
return null;
}
if (typeof type === 'function') {
let res = type(message, phrase);
if (isPromise(res)) res = await res;
return res;
}
if (type instanceof RegExp) {
const match = phrase.match(type);
if (!match) return null;
const matches = [];
if (type.global) {
let matched;
while ((matched = type.exec(phrase)) != null) {
matches.push(matched);
}
}
return { match, matches };
}
if (resolver.type(type)) {
let res = resolver.type(type).call(this, message, phrase);
if (isPromise(res)) res = await res;
return res;
}
return phrase || null;
}
/* eslint-disable no-invalid-this */
/**
* Creates a type from multiple types (union type).
* The first type that resolves to a non-void value is used.
* @param {...ArgumentType|ArgumentTypeCaster} types - Types to use.
* @returns {ArgumentTypeCaster}
*/
static union(...types) {
return async function typeFn(message, phrase) {
for (let entry of types) {
if (typeof entry === 'function') entry = entry.bind(this);
const res = await Argument.cast(entry, this.handler.resolver, message, phrase);
if (!Argument.isFailure(res)) return res;
}
return null;
};
}
/**
* Creates a type from multiple types (product type).
* Only inputs where each type resolves with a non-void value are valid.
* @param {...ArgumentType|ArgumentTypeCaster} types - Types to use.
* @returns {ArgumentTypeCaster}
*/
static product(...types) {
return async function typeFn(message, phrase) {
const results = [];
for (let entry of types) {
if (typeof entry === 'function') entry = entry.bind(this);
const res = await Argument.cast(entry, this.handler.resolver, message, phrase);
if (Argument.isFailure(res)) return res;
results.push(res);
}
return results;
};
}
/**
* Creates a type with extra validation.
* If the predicate is not true, the value is considered invalid.
* @param {ArgumentType|ArgumentTypeCaster} type - The type to use.
* @param {ParsedValuePredicate} predicate - The predicate function.
* @returns {ArgumentTypeCaster}
*/
static validate(type, predicate) {
return async function typeFn(message, phrase) {
if (typeof type === 'function') type = type.bind(this);
const res = await Argument.cast(type, this.handler.resolver, message, phrase);
if (Argument.isFailure(res)) return res;
if (!predicate.call(this, message, phrase, res)) return null;
return res;
};
}
/**
* Creates a type where the parsed value must be within a range.
* @param {ArgumentType|ArgumentTypeCaster} type - The type to use.
* @param {number} min - Minimum value.
* @param {number} max - Maximum value.
* @param {boolean} [inclusive=false] - Whether or not to be inclusive on the upper bound.
* @returns {ArgumentTypeCaster}
*/
static range(type, min, max, inclusive = false) {
return Argument.validate(type, (msg, p, x) => {
/* eslint-disable-next-line valid-typeof */
const o = typeof x === 'number' || typeof x === 'bigint'
? x
: x.length != null
? x.length
: x.size != null
? x.size
: x;
return o >= min && (inclusive ? o <= max : o < max);
});
}
/**
* Creates a type that is the left-to-right composition of the given types.
* If any of the types fails, the entire composition fails.
* @param {...ArgumentType|ArgumentTypeCaster} types - Types to use.
* @returns {ArgumentTypeCaster}
*/
static compose(...types) {
return async function typeFn(message, phrase) {
let acc = phrase;
for (let entry of types) {
if (typeof entry === 'function') entry = entry.bind(this);
acc = await Argument.cast(entry, this.handler.resolver, message, acc);
if (Argument.isFailure(acc)) return acc;
}
return acc;
};
}
/**
* Creates a type that is the left-to-right composition of the given types.
* If any of the types fails, the composition still continues with the failure passed on.
* @param {...ArgumentType|ArgumentTypeCaster} types - Types to use.
* @returns {ArgumentTypeCaster}
*/
static composeWithFailure(...types) {
return async function typeFn(message, phrase) {
let acc = phrase;
for (let entry of types) {
if (typeof entry === 'function') entry = entry.bind(this);
acc = await Argument.cast(entry, this.handler.resolver, message, acc);
}
return acc;
};
}
/**
* Creates a type that parses as normal but also carries the original input.
* Result is in an object `{ input, value }` and wrapped in `Flag.fail` when failed.
* @param {ArgumentType|ArgumentTypeCaster} type - The type to use.
* @returns {ArgumentTypeCaster}
*/
static withInput(type) {
return async function typeFn(message, phrase) {
if (typeof type === 'function') type = type.bind(this);
const res = await Argument.cast(type, this.handler.resolver, message, phrase);
if (Argument.isFailure(res)) {
return Flag.fail({ input: phrase, value: res });
}
return { input: phrase, value: res };
};
}
/**
* Creates a type that parses as normal but also tags it with some data.
* Result is in an object `{ tag, value }` and wrapped in `Flag.fail` when failed.
* @param {ArgumentType|ArgumentTypeCaster} type - The type to use.
* @param {any} [tag] - Tag to add.
* Defaults to the `type` argument, so useful if it is a string.
* @returns {ArgumentTypeCaster}
*/
static tagged(type, tag = type) {
async function typeFn(message, phrase) {
if (typeof type === 'function') type = type.bind(this);
const res = await Argument.cast(type, this.handler.resolver, message, phrase);
if (Argument.isFailure(res)) {
return Flag.fail({ tag, value: res });
}
return { tag, value: res };
}
return typeof this === 'undefined' ? typeFn : typeFn.bind(this);
}
/**
* Creates a type that parses as normal but also tags it with some data and carries the original input.
* Result is in an object `{ tag, input, value }` and wrapped in `Flag.fail` when failed.
* @param {ArgumentType|ArgumentTypeCaster} type - The type to use.
* @param {any} [tag] - Tag to add.
* Defaults to the `type` argument, so useful if it is a string.
* @returns {ArgumentTypeCaster}
*/
static taggedWithInput(type, tag = type) {
return async function typeFn(message, phrase) {
if (typeof type === 'function') type = type.bind(this);
const res = await Argument.cast(type, this.handler.resolver, message, phrase);
if (Argument.isFailure(res)) {
return Flag.fail({ tag, input: phrase, value: res });
}
return { tag, input: phrase, value: res };
};
}
/**
* Creates a type from multiple types (union type).
* The first type that resolves to a non-void value is used.
* Each type will also be tagged using `tagged` with themselves.
* @param {...ArgumentType|ArgumentTypeCaster} types - Types to use.
* @returns {ArgumentTypeCaster}
*/
static taggedUnion(...types) {
return async function typeFn(message, phrase) {
for (let entry of types) {
entry = Argument.tagged.bind(this)(entry);
const res = await Argument.cast(entry, this.handler.resolver, message, phrase);
if (!Argument.isFailure(res)) return res;
}
return null;
};
}
/* eslint-enable no-invalid-this */
/**
* Checks if something is null, undefined, or a fail flag.
* @param {any} value - Value to check.
* @returns {boolean}
*/
static isFailure(value) {
return value == null || Flag.is(value, 'fail');
}
}
module.exports = Argument;
/**
* Options for how an argument parses text.
* @typedef {Object} ArgumentOptions
* @prop {string} id - ID of the argument for use in the args object.
* This does nothing inside an ArgumentGenerator.
* @prop {ArgumentMatch} [match='phrase'] - Method to match text.
* @prop {ArgumentType|ArgumentTypeCaster} [type='string'] - Type to cast to.
* @prop {string|string[]} [flag] - The string(s) to use as the flag for flag or option match.
* @prop {boolean} [multipleFlags=false] - Whether or not to have flags process multiple inputs.
* For option flags, this works like the separate match; the limit option will also work here.
* For flags, this will count the number of occurrences.
* @prop {number} [index] - Index of phrase to start from.
* Applicable to phrase, text, content, rest, or separate match only.
* Ignored when used with the unordered option.
* @prop {boolean|number|number[]} [unordered=false] - Marks the argument as unordered.
* Each phrase is evaluated in order until one matches (no input at all means no evaluation).
* Passing in a number forces evaluation from that index onwards.
* Passing in an array of numbers forces evaluation on those indices only.
* If there is a match, that index is considered used and future unordered args will not check that index again.
* If there is no match, then the prompting or default value is used.
* Applicable to phrase match only.
* @prop {number} [limit=Infinity] - Amount of phrases to match when matching more than one.
* Applicable to text, content, rest, or separate match only.
* @prop {DefaultValueSupplier|any} [default=null] - Default value if no input or did not cast correctly.
* If using a flag match, setting the default value to a non-void value inverses the result.
* @prop {string|MessageOptions|MessageAdditions|OtherwiseContentSupplier} [otherwise] - Text sent if argument parsing fails.
* This overrides the `default` option and all prompt options.
* @prop {OtherwiseContentModifier} [modifyOtherwise] - Function to modify otherwise content.
* @prop {ArgumentPromptOptions} [prompt] - Prompt options for when user does not provide input.
*/
/**
* Data passed to argument prompt functions.
* @typedef {Object} ArgumentPromptData
* @prop {number} retries - Amount of retries so far.
* @prop {boolean} infinite - Whether the prompt is infinite or not.
* @prop {Message} message - The message that caused the prompt.
* @prop {string} phrase - The input phrase that caused the prompt if there was one, otherwise an empty string.
* @param {void|Flag} failure - The value that failed if there was one, otherwise null.
*/
/**
* A prompt to run if the user did not input the argument correctly.
* Can only be used if there is not a default value (unless optional is true).
* @typedef {Object} ArgumentPromptOptions
* @prop {number} [retries=1] - Amount of retries allowed.
* @prop {number} [time=30000] - Time to wait for input.
* @prop {string} [cancelWord='cancel'] - Word to use for cancelling the command.
* @prop {string} [stopWord='stop'] - Word to use for ending infinite prompts.
* @prop {boolean} [optional=false] - Prompts only when argument is provided but was not of the right type.
* @prop {boolean} [infinite=false] - Prompts forever until the stop word, cancel word, time limit, or retry limit.
* Note that the retry count resets back to one on each valid entry.
* The final evaluated argument will be an array of the inputs.
* @prop {number} [limit=Infinity] - Amount of inputs allowed for an infinite prompt before finishing.
* @prop {boolean} [breakout=true] - Whenever an input matches the format of a command, this option controls whether or not to cancel this command and run that command.
* The command to be run may be the same command or some other command.
* @prop {string|MessageOptions|MessageAdditions|PromptContentSupplier} [start] - Text sent on start of prompt.
* @prop {string|MessageOptions|MessageAdditions|PromptContentSupplier} [retry] - Text sent on a retry (failure to cast type).
* @prop {string|MessageOptions|MessageAdditions|PromptContentSupplier} [timeout] - Text sent on collector time out.
* @prop {string|MessageOptions|MessageAdditions|PromptContentSupplier} [ended] - Text sent on amount of tries reaching the max.
* @prop {string|MessageOptions|MessageAdditions|PromptContentSupplier} [cancel] - Text sent on cancellation of command.
* @prop {PromptContentModifier} [modifyStart] - Function to modify start prompts.
* @prop {PromptContentModifier} [modifyRetry] - Function to modify retry prompts.
* @prop {PromptContentModifier} [modifyTimeout] - Function to modify timeout messages.
* @prop {PromptContentModifier} [modifyEnded] - Function to modify out of tries messages.
* @prop {PromptContentModifier} [modifyCancel] - Function to modify cancel messages.
*/
/**
* The method to match arguments from text.
* - `phrase` matches by the order of the phrases inputted.
* It ignores phrases that matches a flag.
* - `flag` matches phrases that are the same as its flag.
* The evaluated argument is either true or false.
* - `option` matches phrases that starts with the flag.
* The phrase after the flag is the evaluated argument.
* - `rest` matches the rest of the phrases.
* It ignores phrases that matches a flag.
* It preserves the original whitespace between phrases and the quotes around phrases.
* - `separate` matches the rest of the phrases and processes each individually.
* It ignores phrases that matches a flag.
* - `text` matches the entire text, except for the command.
* It ignores phrases that matches a flag.
* It preserves the original whitespace between phrases and the quotes around phrases.
* - `content` matches the entire text as it was inputted, except for the command.
* It preserves the original whitespace between phrases and the quotes around phrases.
* - `restContent` matches the rest of the text as it was inputted.
* It preserves the original whitespace between phrases and the quotes around phrases.
* - `none` matches nothing at all and an empty string will be used for type operations.
* @typedef {string} ArgumentMatch
*/
/**
* The type that the argument should be cast to.
* - `string` does not cast to any type.
* - `lowercase` makes the input lowercase.
* - `uppercase` makes the input uppercase.
* - `charCodes` transforms the input to an array of char codes.
* - `number` casts to a number.
* - `integer` casts to an integer.
* - `bigint` casts to a big integer.
* - `url` casts to an `URL` object.
* - `date` casts to a `Date` object.
* - `color` casts a hex code to an integer.
* - `commandAlias` tries to resolve to a command from an alias.
* - `command` matches the ID of a command.
* - `inhibitor` matches the ID of an inhibitor.
* - `listener` matches the ID of a listener.
*
* Possible Discord-related types.
* These types can be plural (add an 's' to the end) and a collection of matching objects will be used.
* - `user` tries to resolve to a user.
* - `member` tries to resolve to a member.
* - `relevant` tries to resolve to a relevant user, works in both guilds and DMs.
* - `channel` tries to resolve to a channel.
* - `textChannel` tries to resolve to a text channel.
* - `voiceChannel` tries to resolve to a voice channel.
* - `role` tries to resolve to a role.
* - `emoji` tries to resolve to a custom emoji.
* - `guild` tries to resolve to a guild.
*
* Other Discord-related types:
* - `message` tries to fetch a message from an ID within the channel.
* - `guildMessage` tries to fetch a message from an ID within the guild.
* - `relevantMessage` is a combination of the above, works in both guilds and DMs.
* - `invite` tries to fetch an invite object from a link.
* - `userMention` matches a mention of a user.
* - `memberMention` matches a mention of a guild member.
* - `channelMention` matches a mention of a channel.
* - `roleMention` matches a mention of a role.
* - `emojiMention` matches a mention of an emoji.
*
* An array of strings can be used to restrict input to only those strings, case insensitive.
* The array can also contain an inner array of strings, for aliases.
* If so, the first entry of the array will be used as the final argument.
*
* A regular expression can also be used.
* The evaluated argument will be an object containing the `match` and `matches` if global.
* @typedef {string|string[]} ArgumentType
*/
/**
* A function for processing user input to use as an argument.
* A void return value will use the default value for the argument or start a prompt.
* Any other truthy return value will be used as the evaluated argument.
* If returning a Promise, the resolved value will go through the above steps.
* @typedef {Function} ArgumentTypeCaster
* @param {Message} message - Message that triggered the command.
* @param {string} phrase - The user input.
* @returns {any}
*/
/**
* A function for processing some value to use as an argument.
* This is mainly used in composing argument types.
* @typedef {Function} ArgumentTypeCaster
* @param {Message} message - Message that triggered the command.
* @param {any} value - Some value.
* @returns {any}
*/
/**
* Data passed to functions that run when things failed.
* @typedef {Object} FailureData
* @prop {string} phrase - The input phrase that failed if there was one, otherwise an empty string.
* @param {void|Flag} failure - The value that failed if there was one, otherwise null.
*/
/**
* Defaults for argument options.
* @typedef {Object} DefaultArgumentOptions
* @prop {ArgumentPromptOptions} [prompt] - Default prompt options.
* @prop {string|MessageOptions|MessageAdditions|OtherwiseContentSupplier} [otherwise] - Default text sent if argument parsing fails.
* @prop {OtherwiseContentModifier} [modifyOtherwise] - Function to modify otherwise content.
*/
/**
* Function get the default value of the argument.
* @typedef {Function} DefaultValueSupplier
* @param {Message} message - Message that triggered the command.
* @param {FailureData} data - Miscellaneous data.
* @returns {any}
*/
/**
* A function for validating parsed arguments.
* @typedef {Function} ParsedValuePredicate
* @param {Message} message - Message that triggered the command.
* @param {string} phrase - The user input.
* @param {any} value - The parsed value.
* @returns {boolean}
*/
/**
* A function modifying a prompt text.
* @typedef {Function} OtherwiseContentModifier
* @param {Message} message - Message that triggered the command.
* @param {string|MessageEmbed|MessageAttachment|MessageAttachment[]|MessageOptions} text - Text to modify.
* @param {FailureData} data - Miscellaneous data.
* @returns {string|MessageOptions|MessageAdditions|Promise}
*/
/**
* A function returning the content if argument parsing fails.
* @typedef {Function} OtherwiseContentSupplier
* @param {Message} message - Message that triggered the command.
* @param {FailureData} data - Miscellaneous data.
* @returns {string|MessageOptions|MessageAdditions|Promise}
*/
/**
* A function modifying a prompt text.
* @typedef {Function} PromptContentModifier
* @param {Message} message - Message that triggered the command.
* @param {string|MessageEmbed|MessageAttachment|MessageAttachment[]|MessageOptions} text - Text from the prompt to modify.
* @param {ArgumentPromptData} data - Miscellaneous data.
* @returns {string|MessageOptions|MessageAdditions|Promise}
*/
/**
* A function returning text for the prompt.
* @typedef {Function} PromptContentSupplier
* @param {Message} message - Message that triggered the command.
* @param {ArgumentPromptData} data - Miscellaneous data.
* @returns {string|MessageOptions|MessageAdditions|Promise}
*/
================================================
FILE: src/struct/commands/arguments/ArgumentRunner.js
================================================
const AkairoError = require('../../../util/AkairoError');
const Argument = require('./Argument');
const { ArgumentMatches } = require('../../../util/Constants');
const Flag = require('../Flag');
/**
* Runs arguments.
* @param {Command} command - Command to run for.
* @private
*/
class ArgumentRunner {
constructor(command) {
this.command = command;
}
/**
* The Akairo client.
* @type {AkairoClient}
*/
get client() {
return this.command.client;
}
/**
* The command handler.
* @type {CommandHandler}
*/
get handler() {
return this.command.handler;
}
/**
* Runs the arguments.
* @param {Message} message - Message that triggered the command.
* @param {ContentParserResult} parsed - Parsed data from ContentParser.
* @param {ArgumentGenerator} generator - Argument generator.
* @returns {Promise}
*/
async run(message, parsed, generator) {
const state = {
usedIndices: new Set(),
phraseIndex: 0,
index: 0
};
const augmentRest = val => {
if (Flag.is(val, 'continue')) {
val.rest = parsed.all.slice(state.index).map(x => x.raw).join('');
}
};
const iter = generator(message, parsed, state);
let curr = await iter.next();
while (!curr.done) {
const value = curr.value;
if (ArgumentRunner.isShortCircuit(value)) {
augmentRest(value);
return value;
}
const res = await this.runOne(message, parsed, state, new Argument(this.command, value));
if (ArgumentRunner.isShortCircuit(res)) {
augmentRest(res);
return res;
}
curr = await iter.next(res);
}
augmentRest(curr.value);
return curr.value;
}
/**
* Runs one argument.
* @param {Message} message - Message that triggered the command.
* @param {ContentParserResult} parsed - Parsed data from ContentParser.
* @param {ArgumentRunnerState} state - Argument handling state.
* @param {Argument} arg - Current argument.
* @returns {Promise}
*/
runOne(message, parsed, state, arg) {
const cases = {
[ArgumentMatches.PHRASE]: this.runPhrase,
[ArgumentMatches.FLAG]: this.runFlag,
[ArgumentMatches.OPTION]: this.runOption,
[ArgumentMatches.REST]: this.runRest,
[ArgumentMatches.SEPARATE]: this.runSeparate,
[ArgumentMatches.TEXT]: this.runText,
[ArgumentMatches.CONTENT]: this.runContent,
[ArgumentMatches.REST_CONTENT]: this.runRestContent,
[ArgumentMatches.NONE]: this.runNone
};
const runFn = cases[arg.match];
if (runFn == null) {
throw new AkairoError('UNKNOWN_MATCH_TYPE', arg.match);
}
return runFn.call(this, message, parsed, state, arg);
}
/**
* Runs `phrase` match.
* @param {Message} message - Message that triggered the command.
* @param {ContentParserResult} parsed - Parsed data from ContentParser.
* @param {ArgumentRunnerState} state - Argument handling state.
* @param {Argument} arg - Current argument.
* @returns {Promise}
*/
async runPhrase(message, parsed, state, arg) {
if (arg.unordered || arg.unordered === 0) {
const indices = typeof unordered === 'number'
? Array.from(parsed.phrases.keys()).slice(arg.unordered)
: Array.isArray(arg.unordered)
? arg.unordered
: Array.from(parsed.phrases.keys());
for (const i of indices) {
if (state.usedIndices.has(i)) {
continue;
}
const phrase = parsed.phrases[i] ? parsed.phrases[i].value : '';
// `cast` is used instead of `process` since we do not want prompts.
const res = await arg.cast(message, phrase);
if (res != null) {
state.usedIndices.add(i);
return res;
}
}
// No indices matched.
return arg.process(message, '');
}
const index = arg.index == null ? state.phraseIndex : arg.index;
const ret = arg.process(message, parsed.phrases[index] ? parsed.phrases[index].value : '');
if (arg.index == null) {
ArgumentRunner.increaseIndex(parsed, state);
}
return ret;
}
/**
* Runs `rest` match.
* @param {Message} message - Message that triggered the command.
* @param {ContentParserResult} parsed - Parsed data from ContentParser.
* @param {ArgumentRunnerState} state - Argument handling state.
* @param {Argument} arg - Current argument.
* @returns {Promise}
*/
async runRest(message, parsed, state, arg) {
const index = arg.index == null ? state.phraseIndex : arg.index;
const rest = parsed.phrases.slice(index, index + arg.limit).map(x => x.raw).join('').trim();
const ret = await arg.process(message, rest);
if (arg.index == null) {
ArgumentRunner.increaseIndex(parsed, state);
}
return ret;
}
/**
* Runs `separate` match.
* @param {Message} message - Message that triggered the command.
* @param {ContentParserResult} parsed - Parsed data from ContentParser.
* @param {ArgumentRunnerState} state - Argument handling state.
* @param {Argument} arg - Current argument.
* @returns {Promise}
*/
async runSeparate(message, parsed, state, arg) {
const index = arg.index == null ? state.phraseIndex : arg.index;
const phrases = parsed.phrases.slice(index, index + arg.limit);
if (!phrases.length) {
const ret = await arg.process(message, '');
if (arg.index != null) {
ArgumentRunner.increaseIndex(parsed, state);
}
return ret;
}
const res = [];
for (const phrase of phrases) {
const response = await arg.process(message, phrase.value);
if (Flag.is(response, 'cancel')) {
return response;
}
res.push(response);
}
if (arg.index != null) {
ArgumentRunner.increaseIndex(parsed, state);
}
return res;
}
/**
* Runs `flag` match.
* @param {Message} message - Message that triggered the command.
* @param {ContentParserResult} parsed - Parsed data from ContentParser.
* @param {ArgumentRunnerState} state - Argument handling state.
* @param {Argument} arg - Current argument.
* @returns {Promise}
*/
runFlag(message, parsed, state, arg) {
const names = Array.isArray(arg.flag) ? arg.flag : [arg.flag];
if (arg.multipleFlags) {
const amount = parsed.flags.filter(flag =>
names.some(name =>
name.toLowerCase() === flag.key.toLowerCase()
)
).length;
return amount;
}
const flagFound = parsed.flags.some(flag =>
names.some(name =>
name.toLowerCase() === flag.key.toLowerCase()
)
);
return arg.default == null ? flagFound : !flagFound;
}
/**
* Runs `option` match.
* @param {Message} message - Message that triggered the command.
* @param {ContentParserResult} parsed - Parsed data from ContentParser.
* @param {ArgumentRunnerState} state - Argument handling state.
* @param {Argument} arg - Current argument.
* @returns {Promise}
*/
async runOption(message, parsed, state, arg) {
const names = Array.isArray(arg.flag) ? arg.flag : [arg.flag];
if (arg.multipleFlags) {
const values = parsed.optionFlags.filter(flag =>
names.some(name =>
name.toLowerCase() === flag.key.toLowerCase()
)
).map(x => x.value).slice(0, arg.limit);
const res = [];
for (const value of values) {
res.push(await arg.process(message, value));
}
return res;
}
const foundFlag = parsed.optionFlags.find(flag =>
names.some(name =>
name.toLowerCase() === flag.key.toLowerCase()
)
);
return arg.process(message, foundFlag != null ? foundFlag.value : '');
}
/**
* Runs `text` match.
* @param {Message} message - Message that triggered the command.
* @param {ContentParserResult} parsed - Parsed data from ContentParser.
* @param {ArgumentRunnerState} state - Argument handling state.
* @param {Argument} arg - Current argument.
* @returns {Promise}
*/
runText(message, parsed, state, arg) {
const index = arg.index == null ? 0 : arg.index;
const text = parsed.phrases.slice(index, index + arg.limit).map(x => x.raw).join('').trim();
return arg.process(message, text);
}
/**
* Runs `content` match.
* @param {Message} message - Message that triggered the command.
* @param {ContentParserResult} parsed - Parsed data from ContentParser.
* @param {ArgumentRunnerState} state - Argument handling state.
* @param {Argument} arg - Current argument.
* @returns {Promise}
*/
runContent(message, parsed, state, arg) {
const index = arg.index == null ? 0 : arg.index;
const content = parsed.all.slice(index, index + arg.limit).map(x => x.raw).join('').trim();
return arg.process(message, content);
}
/**
* Runs `restContent` match.
* @param {Message} message - Message that triggered the command.
* @param {ContentParserResult} parsed - Parsed data from ContentParser.
* @param {ArgumentRunnerState} state - Argument handling state.
* @param {Argument} arg - Current argument.
* @returns {Promise}
*/
async runRestContent(message, parsed, state, arg) {
const index = arg.index == null ? state.index : arg.index;
const rest = parsed.all.slice(index, index + arg.limit).map(x => x.raw).join('').trim();
const ret = await arg.process(message, rest);
if (arg.index == null) {
ArgumentRunner.increaseIndex(parsed, state);
}
return ret;
}
/**
* Runs `none` match.
* @param {Message} message - Message that triggered the command.
* @param {ContentParserResult} parsed - Parsed data from ContentParser.
* @param {ArgumentRunnerState} state - Argument handling state.
* @param {Argument} arg - Current argument.
* @returns {Promise}
*/
runNone(message, parsed, state, arg) {
return arg.process(message, '');
}
/**
* Modifies state by incrementing the indices.
* @param {ContentParserResult} parsed - Parsed data from ContentParser.
* @param {ArgumentRunnerState} state - Argument handling state.
* @param {number} n - Number of indices to increase by.
* @returns {Promise}
*/
static increaseIndex(parsed, state, n = 1) {
state.phraseIndex += n;
while (n > 0) {
do {
state.index++;
} while (parsed.all[state.index] && parsed.all[state.index].type !== 'Phrase');
n--;
}
}
/**
* Checks if something is a flag that short circuits.
* @param {any} value - A value.
* @returns {boolean}
*/
static isShortCircuit(value) {
return Flag.is(value, 'cancel') || Flag.is(value, 'retry') || Flag.is(value, 'continue');
}
/**
* Creates an argument generator from argument options.
* @param {ArgumentOptions[]} args - Argument options.
* @returns {GeneratorFunction}
*/
static fromArguments(args) {
return function* generate() {
const res = {};
for (const [id, arg] of args) {
res[id] = yield arg;
}
return res;
};
}
}
module.exports = ArgumentRunner;
/**
* State for the argument runner.
* @typedef {Object} ArgumentRunnerState
* @prop {Set} usedIndices - Indices already used for unordered match.
* @prop {number} phraseIndex - Index in terms of phrases.
* @prop {number} index - Index in terms of the raw strings.
*/
================================================
FILE: src/struct/commands/arguments/TypeResolver.js
================================================
const { ArgumentTypes } = require('../../../util/Constants');
const { Collection } = require('discord.js');
const { URL } = require('url');
/**
* Type resolver for command arguments.
* The types are documented under ArgumentType.
* @param {CommandHandler} handler - The command handler.
*/
class TypeResolver {
constructor(handler) {
/**
* The Akairo client.
* @type {AkairoClient}
*/
this.client = handler.client;
/**
* The command handler.
* @type {CommandHandler}
*/
this.commandHandler = handler;
/**
* The inhibitor handler.
* @type {InhibitorHandler}
*/
this.inhibitorHandler = null;
/**
* The listener handler.
* @type {ListenerHandler}
*/
this.listenerHandler = null;
/**
* Collection of types.
* @type {Collection}
*/
this.types = new Collection();
this.addBuiltInTypes();
}
/**
* Adds built-in types.
* @returns {void}
*/
addBuiltInTypes() {
const builtins = {
[ArgumentTypes.STRING]: (message, phrase) => {
return phrase || null;
},
[ArgumentTypes.LOWERCASE]: (message, phrase) => {
return phrase ? phrase.toLowerCase() : null;
},
[ArgumentTypes.UPPERCASE]: (message, phrase) => {
return phrase ? phrase.toUpperCase() : null;
},
[ArgumentTypes.CHAR_CODES]: (message, phrase) => {
if (!phrase) return null;
const codes = [];
for (const char of phrase) codes.push(char.charCodeAt(0));
return codes;
},
[ArgumentTypes.NUMBER]: (message, phrase) => {
if (!phrase || isNaN(phrase)) return null;
return parseFloat(phrase);
},
[ArgumentTypes.INTEGER]: (message, phrase) => {
if (!phrase || isNaN(phrase)) return null;
return parseInt(phrase);
},
[ArgumentTypes.BIGINT]: (message, phrase) => {
if (!phrase || isNaN(phrase)) return null;
return BigInt(phrase); // eslint-disable-line no-undef, new-cap
},
// Just for fun.
[ArgumentTypes.EMOJINT]: (message, phrase) => {
if (!phrase) return null;
const n = phrase.replace(/0⃣|1⃣|2⃣|3⃣|4⃣|5⃣|6⃣|7⃣|8⃣|9⃣|🔟/g, m => {
return ['0⃣', '1⃣', '2⃣', '3⃣', '4⃣', '5⃣', '6⃣', '7⃣', '8⃣', '9⃣', '🔟'].indexOf(m);
});
if (isNaN(n)) return null;
return parseInt(n);
},
[ArgumentTypes.URL]: (message, phrase) => {
if (!phrase) return null;
if (/^<.+>$/.test(phrase)) phrase = phrase.slice(1, -1);
try {
return new URL(phrase);
} catch (err) {
return null;
}
},
[ArgumentTypes.DATE]: (message, phrase) => {
if (!phrase) return null;
const timestamp = Date.parse(phrase);
if (isNaN(timestamp)) return null;
return new Date(timestamp);
},
[ArgumentTypes.COLOR]: (message, phrase) => {
if (!phrase) return null;
const color = parseInt(phrase.replace('#', ''), 16);
if (color < 0 || color > 0xFFFFFF || isNaN(color)) {
return null;
}
return color;
},
[ArgumentTypes.USER]: (message, phrase) => {
if (!phrase) return null;
return this.client.util.resolveUser(phrase, this.client.users.cache);
},
[ArgumentTypes.USERS]: (message, phrase) => {
if (!phrase) return null;
const users = this.client.util.resolveUsers(phrase, this.client.users.cache);
return users.size ? users : null;
},
[ArgumentTypes.MEMBER]: (message, phrase) => {
if (!phrase) return null;
return this.client.util.resolveMember(phrase, message.guild.members.cache);
},
[ArgumentTypes.MEMBERS]: (message, phrase) => {
if (!phrase) return null;
const members = this.client.util.resolveMembers(phrase, message.guild.members.cache);
return members.size ? members : null;
},
[ArgumentTypes.RELEVANT]: (message, phrase) => {
if (!phrase) return null;
const person = message.guild
? this.client.util.resolveMember(phrase, message.guild.members.cache)
: message.channel.type === 'DM'
? this.client.util.resolveUser(phrase, new Collection([
[message.channel.recipient.id, message.channel.recipient],
[this.client.user.id, this.client.user]
]))
: this.client.util.resolveUser(phrase, new Collection([
[this.client.user.id, this.client.user]
]).concat(message.channel.recipients));
if (!person) return null;
return message.guild ? person.user : person;
},
[ArgumentTypes.RELEVANTS]: (message, phrase) => {
if (!phrase) return null;
const persons = message.guild
? this.client.util.resolveMembers(phrase, message.guild.members.cache)
: message.channel.type === 'DM'
? this.client.util.resolveUsers(phrase, new Collection([
[message.channel.recipient.id, message.channel.recipient],
[this.client.user.id, this.client.user]
]))
: this.client.util.resolveUsers(phrase, new Collection([
[this.client.user.id, this.client.user]
]).concat(message.channel.recipients));
if (!persons.size) return null;
return message.guild ? persons.map(member => member.user) : persons;
},
[ArgumentTypes.CHANNEL]: (message, phrase) => {
if (!phrase) return null;
return this.client.util.resolveChannel(phrase, message.guild.channels.cache);
},
[ArgumentTypes.CHANNELS]: (message, phrase) => {
if (!phrase) return null;
const channels = this.client.util.resolveChannels(phrase, message.guild.channels.cache);
return channels.size ? channels : null;
},
[ArgumentTypes.TEXT_CHANNEL]: (message, phrase) => {
if (!phrase) return null;
const channel = this.client.util.resolveChannel(phrase, message.guild.channels.cache);
if (!channel || channel.type !== 'GUILD_TEXT') return null;
return channel;
},
[ArgumentTypes.TEXT_CHANNELS]: (message, phrase) => {
if (!phrase) return null;
const channels = this.client.util.resolveChannels(phrase, message.guild.channels.cache);
if (!channels.size) return null;
const textChannels = channels.filter(c => c.type === 'GUILD_TEXT');
return textChannels.size ? textChannels : null;
},
[ArgumentTypes.VOICE_CHANNEL]: (message, phrase) => {
if (!phrase) return null;
const channel = this.client.util.resolveChannel(phrase, message.guild.channels.cache);
if (!channel || channel.type !== 'GUILD_VOICE') return null;
return channel;
},
[ArgumentTypes.VOICE_CHANNELS]: (message, phrase) => {
if (!phrase) return null;
const channels = this.client.util.resolveChannels(phrase, message.guild.channels.cache);
if (!channels.size) return null;
const voiceChannels = channels.filter(c => c.type === 'GUILD_VOICE');
return voiceChannels.size ? voiceChannels : null;
},
[ArgumentTypes.CATEGORY_CHANNEL]: (message, phrase) => {
if (!phrase) return null;
const channel = this.client.util.resolveChannel(phrase, message.guild.channels.cache);
if (!channel || channel.type !== 'GUILD_CATEGORY') return null;
return channel;
},
[ArgumentTypes.CATEGORY_CHANNELS]: (message, phrase) => {
if (!phrase) return null;
const channels = this.client.util.resolveChannels(phrase, message.guild.channels.cache);
if (!channels.size) return null;
const categoryChannels = channels.filter(c => c.type === 'GUILD_CATEGORY');
return categoryChannels.size ? categoryChannels : null;
},
[ArgumentTypes.NEWS_CHANNEL]: (message, phrase) => {
if (!phrase) return null;
const channel = this.client.util.resolveChannel(phrase, message.guild.channels.cache);
if (!channel || channel.type !== 'GUILD_NEWS') return null;
return channel;
},
[ArgumentTypes.NEWS_CHANNELS]: (message, phrase) => {
if (!phrase) return null;
const channels = this.client.util.resolveChannels(phrase, message.guild.channels.cache);
if (!channels.size) return null;
const newsChannels = channels.filter(c => c.type === 'GUILD_NEWS');
return newsChannels.size ? newsChannels : null;
},
[ArgumentTypes.STORE_CHANNEL]: (message, phrase) => {
if (!phrase) return null;
const channel = this.client.util.resolveChannel(phrase, message.guild.channels.cache);
if (!channel || channel.type !== 'GUILD_STORE') return null;
return channel;
},
[ArgumentTypes.STORE_CHANNELS]: (message, phrase) => {
if (!phrase) return null;
const channels = this.client.util.resolveChannels(phrase, message.guild.channels.cache);
if (!channels.size) return null;
const storeChannels = channels.filter(c => c.type === 'GUILD_STORE');
return storeChannels.size ? storeChannels : null;
},
[ArgumentTypes.ROLE]: (message, phrase) => {
if (!phrase) return null;
return this.client.util.resolveRole(phrase, message.guild.roles.cache);
},
[ArgumentTypes.ROLES]: (message, phrase) => {
if (!phrase) return null;
const roles = this.client.util.resolveRoles(phrase, message.guild.roles.cache);
return roles.size ? roles : null;
},
[ArgumentTypes.EMOJI]: (message, phrase) => {
if (!phrase) return null;
return this.client.util.resolveEmoji(phrase, message.guild.emojis.cache);
},
[ArgumentTypes.EMOJIS]: (message, phrase) => {
if (!phrase) return null;
const emojis = this.client.util.resolveEmojis(phrase, message.guild.emojis.cache);
return emojis.size ? emojis : null;
},
[ArgumentTypes.GUILD]: (message, phrase) => {
if (!phrase) return null;
return this.client.util.resolveGuild(phrase, this.client.guilds.cache);
},
[ArgumentTypes.GUILDS]: (message, phrase) => {
if (!phrase) return null;
const guilds = this.client.util.resolveGuilds(phrase, this.client.guilds.cache);
return guilds.size ? guilds : null;
},
[ArgumentTypes.MESSAGE]: (message, phrase) => {
if (!phrase) return null;
return message.channel.messages.fetch(phrase).catch(() => null);
},
[ArgumentTypes.GUILD_MESSAGE]: async (message, phrase) => {
if (!phrase) return null;
for (const channel of message.guild.channels.cache.values()) {
if (!channel.isText()) continue;
try {
return await channel.messages.fetch(phrase);
} catch (err) {
if (/^Invalid Form Body/.test(err.message)) return null;
}
}
return null;
},
[ArgumentTypes.RELEVANT_MESSAGE]: async (message, phrase) => {
if (!phrase) return null;
const hereMsg = await message.channel.messages.fetch(phrase).catch(() => null);
if (hereMsg) {
return hereMsg;
}
if (message.guild) {
for (const channel of message.guild.channels.cache.values()) {
if (!channel.isText()) continue;
try {
return await channel.messages.fetch(phrase);
} catch (err) {
if (/^Invalid Form Body/.test(err.message)) return null;
}
}
}
return null;
},
[ArgumentTypes.INVITE]: (message, phrase) => {
if (!phrase) return null;
return this.client.fetchInvite(phrase).catch(() => null);
},
[ArgumentTypes.USER_MENTION]: (message, phrase) => {
if (!phrase) return null;
const id = phrase.match(/<@!?(\d{17,19})>/);
if (!id) return null;
return this.client.users.cache.get(id[1]) || null;
},
[ArgumentTypes.MEMBER_MENTION]: (message, phrase) => {
if (!phrase) return null;
const id = phrase.match(/<@!?(\d{17,19})>/);
if (!id) return null;
return message.guild.members.cache.get(id[1]) || null;
},
[ArgumentTypes.CHANNEL_MENTION]: (message, phrase) => {
if (!phrase) return null;
const id = phrase.match(/<#(\d{17,19})>/);
if (!id) return null;
return message.guild.channels.cache.get(id[1]) || null;
},
[ArgumentTypes.ROLE_MENTION]: (message, phrase) => {
if (!phrase) return null;
const id = phrase.match(/<@&(\d{17,19})>/);
if (!id) return null;
return message.guild.roles.cache.get(id[1]) || null;
},
[ArgumentTypes.EMOJI_MENTION]: (message, phrase) => {
if (!phrase) return null;
const id = phrase.match(//);
if (!id) return null;
return message.guild.emojis.cache.get(id[1]) || null;
},
[ArgumentTypes.COMMAND_ALIAS]: (message, phrase) => {
if (!phrase) return null;
return this.commandHandler.findCommand(phrase) || null;
},
[ArgumentTypes.COMMAND]: (message, phrase) => {
if (!phrase) return null;
return this.commandHandler.modules.get(phrase) || null;
},
[ArgumentTypes.INHIBITOR]: (message, phrase) => {
if (!phrase) return null;
return this.inhibitorHandler.modules.get(phrase) || null;
},
[ArgumentTypes.LISTENER]: (message, phrase) => {
if (!phrase) return null;
return this.listenerHandler.modules.get(phrase) || null;
}
};
for (const [key, value] of Object.entries(builtins)) {
this.types.set(key, value);
}
}
/**
* Gets the resolver function for a type.
* @param {string} name - Name of type.
* @returns {ArgumentTypeCaster}
*/
type(name) {
return this.types.get(name);
}
/**
* Adds a new type.
* @param {string} name - Name of the type.
* @param {ArgumentTypeCaster} fn - Function that casts the type.
* @returns {TypeResolver}
*/
addType(name, fn) {
this.types.set(name, fn);
return this;
}
/**
* Adds multiple new types.
* @param {Object} types - Object with keys as the type name and values as the cast function.
* @returns {TypeResolver}
*/
addTypes(types) {
for (const [key, value] of Object.entries(types)) {
this.addType(key, value);
}
return this;
}
}
module.exports = TypeResolver;
================================================
FILE: src/struct/inhibitors/Inhibitor.js
================================================
const AkairoError = require('../../util/AkairoError');
const AkairoModule = require('../AkairoModule');
/**
* Represents an inhibitor.
* @param {string} id - Inhibitor ID.
* @param {InhibitorOptions} [options={}] - Options for the inhibitor.
* @extends {AkairoModule}
*/
class Inhibitor extends AkairoModule {
constructor(id, {
category,
reason = '',
type = 'post',
priority = 0
} = {}) {
super(id, { category });
/**
* Reason emitted when command is inhibited.
* @type {string}
*/
this.reason = reason;
/**
* The type of the inhibitor for when it should run.
* @type {string}
*/
this.type = type;
/**
* The priority of the inhibitor.
* @type {number}
*/
this.priority = priority;
/**
* The ID of this inhibitor.
* @name Inhibitor#id
* @type {string}
*/
/**
* The inhibitor handler.
* @name Inhibitor#handler
* @type {InhibitorHandler}
*/
}
/**
* Checks if message should be blocked.
* A return value of true will block the message.
* If returning a Promise, a resolved value of true will block the message.
* @abstract
* @param {Message} message - Message being handled.
* @param {Command} [command] - Command to check.
* @returns {boolean|Promise}
*/
exec() {
throw new AkairoError('NOT_IMPLEMENTED', this.constructor.name, 'exec');
}
/**
* Reloads the inhibitor.
* @method
* @name Inhibitor#reload
* @returns {Inhibitor}
*/
/**
* Removes the inhibitor.
* @method
* @name Inhibitor#remove
* @returns {Inhibitor}
*/
}
module.exports = Inhibitor;
/**
* Options to use for inhibitor execution behavior.
* Also includes properties from AkairoModuleOptions.
* @typedef {AkairoModuleOptions} InhibitorOptions
* @prop {string} [reason=''] - Reason emitted when command or message is blocked.
* @prop {string} [type='post'] - Can be 'all' to run on all messages, 'pre' to run on messages not blocked by the built-in inhibitors, or 'post' to run on messages that are commands.
* @prop {number} [priority=0] - Priority for the inhibitor for when more than one inhibitors block a message.
* The inhibitor with the highest priority is the one that is used for the block reason.
*/
================================================
FILE: src/struct/inhibitors/InhibitorHandler.js
================================================
const AkairoError = require('../../util/AkairoError');
const AkairoHandler = require('../AkairoHandler');
const Inhibitor = require('./Inhibitor');
const { isPromise } = require('../../util/Util');
/**
* Loads inhibitors and checks messages.
* @param {AkairoClient} client - The Akairo client.
* @param {AkairoHandlerOptions} options - Options.
* @extends {AkairoHandler}
*/
class InhibitorHandler extends AkairoHandler {
constructor(client, {
directory,
classToHandle = Inhibitor,
extensions = ['.js', '.ts'],
automateCategories,
loadFilter
} = {}) {
if (!(classToHandle.prototype instanceof Inhibitor || classToHandle === Inhibitor)) {
throw new AkairoError('INVALID_CLASS_TO_HANDLE', classToHandle.name, Inhibitor.name);
}
super(client, {
directory,
classToHandle,
extensions,
automateCategories,
loadFilter
});
/**
* Directory to inhibitors.
* @name InhibitorHandler#directory
* @type {string}
*/
/**
* Inhibitors loaded, mapped by ID to Inhibitor.
* @name InhibitorHandler#modules
* @type {Collection}
*/
}
/**
* Tests inhibitors against the message.
* Returns the reason if blocked.
* @param {string} type - Type of inhibitor, 'all', 'pre', or 'post'.
* @param {Message} message - Message to test.
* @param {Command} [command] - Command to use.
* @returns {Promise}
*/
async test(type, message, command) {
if (!this.modules.size) return null;
const inhibitors = this.modules.filter(i => i.type === type);
if (!inhibitors.size) return null;
const promises = [];
for (const inhibitor of inhibitors.values()) {
promises.push((async () => {
let inhibited = inhibitor.exec(message, command);
if (isPromise(inhibited)) inhibited = await inhibited;
if (inhibited) return inhibitor;
return null;
})());
}
const inhibitedInhibitors = (await Promise.all(promises)).filter(r => r);
if (!inhibitedInhibitors.length) return null;
inhibitedInhibitors.sort((a, b) => b.priority - a.priority);
return inhibitedInhibitors[0].reason;
}
/**
* Deregisters a module.
* @method
* @name InhibitorHandler#deregister
* @param {Inhibitor} inhibitor - Module to use.
* @returns {void}
*/
/**
* Registers a module.
* @method
* @name InhibitorHandler#register
* @param {Inhibitor} inhibitor - Module to use.
* @param {string} [filepath] - Filepath of module.
* @returns {void}
*/
/**
* Loads an inhibitor.
* @method
* @param {string|Inhibitor} thing - Module or path to module.
* @name InhibitorHandler#load
* @returns {Inhibitor}
*/
/**
* Reads all inhibitors from the directory and loads them.
* @method
* @name InhibitorHandler#loadAll
* @param {string} [directory] - Directory to load from.
* Defaults to the directory passed in the constructor.
* @param {LoadPredicate} [filter] - Filter for files, where true means it should be loaded.
* @returns {InhibitorHandler}
*/
/**
* Removes an inhibitor.
* @method
* @name InhibitorHandler#remove
* @param {string} id - ID of the inhibitor.
* @returns {Inhibitor}
*/
/**
* Removes all inhibitors.
* @method
* @name InhibitorHandler#removeAll
* @returns {InhibitorHandler}
*/
/**
* Reloads an inhibitor.
* @method
* @name InhibitorHandler#reload
* @param {string} id - ID of the inhibitor.
* @returns {Inhibitor}
*/
/**
* Reloads all inhibitors.
* @method
* @name InhibitorHandler#reloadAll
* @returns {InhibitorHandler}
*/
}
module.exports = InhibitorHandler;
/**
* Emitted when an inhibitor is loaded.
* @event InhibitorHandler#load
* @param {Inhibitor} inhibitor - Inhibitor loaded.
* @param {boolean} isReload - Whether or not this was a reload.
*/
/**
* Emitted when an inhibitor is removed.
* @event InhibitorHandler#remove
* @param {Inhibitor} inhibitor - Inhibitor removed.
*/
================================================
FILE: src/struct/listeners/Listener.js
================================================
const AkairoError = require('../../util/AkairoError');
const AkairoModule = require('../AkairoModule');
/**
* Represents a listener.
* @param {string} id - Listener ID.
* @param {ListenerOptions} [options={}] - Options for the listener.
* @extends {AkairoModule}
*/
class Listener extends AkairoModule {
constructor(id, {
category,
emitter,
event,
type = 'on'
} = {}) {
super(id, { category });
/**
* The event emitter.
* @type {string|EventEmitter}
*/
this.emitter = emitter;
/**
* The event name listened to.
* @type {string}
*/
this.event = event;
/**
* Type of listener.
* @type {string}
*/
this.type = type;
/**
* The ID of this listener.
* @name Listener#id
* @type {string}
*/
/**
* The listener handler.
* @name Listener#handler
* @type {ListenerHandler}
*/
}
/**
* Executes the listener.
* @abstract
* @param {...args} [args] - Arguments.
* @returns {any}
*/
exec() {
throw new AkairoError('NOT_IMPLEMENTED', this.constructor.name, 'exec');
}
/**
* Reloads the listener.
* @method
* @name Listener#reload
* @returns {Listener}
*/
/**
* Removes the listener.
* @method
* @name Listener#remove
* @returns {Listener}
*/
}
module.exports = Listener;
/**
* Options to use for listener execution behavior.
* Also includes properties from AkairoModuleOptions.
* @typedef {AkairoModuleOptions} ListenerOptions
* @prop {string|EventEmitter} emitter - The event emitter, either a key from `ListenerHandler#emitters` or an EventEmitter.
* @prop {string} event - Event name to listen to.
* @prop {string} [type='on'] - Type of listener, either 'on' or 'once'.
*/
================================================
FILE: src/struct/listeners/ListenerHandler.js
================================================
const AkairoError = require('../../util/AkairoError');
const AkairoHandler = require('../AkairoHandler');
const { Collection } = require('discord.js');
const { isEventEmitter } = require('../../util/Util');
const Listener = require('./Listener');
/**
* Loads listeners and registers them with EventEmitters.
* @param {AkairoClient} client - The Akairo client.
* @param {AkairoHandlerOptions} options - Options.
* @extends {AkairoHandler}
*/
class ListenerHandler extends AkairoHandler {
constructor(client, {
directory,
classToHandle = Listener,
extensions = ['.js', '.ts'],
automateCategories,
loadFilter
} = {}) {
if (!(classToHandle.prototype instanceof Listener || classToHandle === Listener)) {
throw new AkairoError('INVALID_CLASS_TO_HANDLE', classToHandle.name, Listener.name);
}
super(client, {
directory,
classToHandle,
extensions,
automateCategories,
loadFilter
});
/**
* EventEmitters for use, mapped by name to EventEmitter.
* By default, 'client' is set to the given client.
* @type {Collection}
*/
this.emitters = new Collection();
this.emitters.set('client', this.client);
/**
* Directory to listeners.
* @name ListenerHandler#directory
* @type {string}
*/
/**
* Listeners loaded, mapped by ID to Listener.
* @name ListenerHandler#modules
* @type {Collection}
*/
}
/**
* Registers a module.
* @param {Listener} listener - Module to use.
* @param {string} [filepath] - Filepath of module.
* @returns {void}
*/
register(listener, filepath) {
super.register(listener, filepath);
listener.exec = listener.exec.bind(listener);
this.addToEmitter(listener.id);
return listener;
}
/**
* Deregisters a module.
* @param {Listener} listener - Module to use.
* @returns {void}
*/
deregister(listener) {
this.removeFromEmitter(listener.id);
super.deregister(listener);
}
/**
* Adds a listener to the EventEmitter.
* @param {string} id - ID of the listener.
* @returns {Listener}
*/
addToEmitter(id) {
const listener = this.modules.get(id.toString());
if (!listener) throw new AkairoError('MODULE_NOT_FOUND', this.classToHandle.name, id);
const emitter = isEventEmitter(listener.emitter) ? listener.emitter : this.emitters.get(listener.emitter);
if (!isEventEmitter(emitter)) throw new AkairoError('INVALID_TYPE', 'emitter', 'EventEmitter', true);
if (listener.type === 'once') {
emitter.once(listener.event, listener.exec);
return listener;
}
emitter.on(listener.event, listener.exec);
return listener;
}
/**
* Removes a listener from the EventEmitter.
* @param {string} id - ID of the listener.
* @returns {Listener}
*/
removeFromEmitter(id) {
const listener = this.modules.get(id.toString());
if (!listener) throw new AkairoError('MODULE_NOT_FOUND', this.classToHandle.name, id);
const emitter = isEventEmitter(listener.emitter) ? listener.emitter : this.emitters.get(listener.emitter);
if (!isEventEmitter(emitter)) throw new AkairoError('INVALID_TYPE', 'emitter', 'EventEmitter', true);
emitter.removeListener(listener.event, listener.exec);
return listener;
}
/**
* Sets custom emitters.
* @param {Object} emitters - Emitters to use.
* The key is the name and value is the emitter.
* @returns {ListenerHandler}
*/
setEmitters(emitters) {
for (const [key, value] of Object.entries(emitters)) {
if (!isEventEmitter(value)) throw new AkairoError('INVALID_TYPE', key, 'EventEmitter', true);
this.emitters.set(key, value);
}
return this;
}
/**
* Loads a listener.
* @method
* @name ListenerHandler#load
* @param {string|Listener} thing - Module or path to module.
* @returns {Listener}
*/
/**
* Reads all listeners from the directory and loads them.
* @method
* @name ListenerHandler#loadAll
* @param {string} [directory] - Directory to load from.
* Defaults to the directory passed in the constructor.
* @param {LoadPredicate} [filter] - Filter for files, where true means it should be loaded.
* @returns {ListenerHandler}
*/
/**
* Removes a listener.
* @method
* @name ListenerHandler#remove
* @param {string} id - ID of the listener.
* @returns {Listener}
*/
/**
* Removes all listeners.
* @method
* @name ListenerHandler#removeAll
* @returns {ListenerHandler}
*/
/**
* Reloads a listener.
* @method
* @name ListenerHandler#reload
* @param {string} id - ID of the listener.
* @returns {Listener}
*/
/**
* Reloads all listeners.
* @method
* @name ListenerHandler#reloadAll
* @returns {ListenerHandler}
*/
}
module.exports = ListenerHandler;
/**
* Emitted when a listener is loaded.
* @event ListenerHandler#load
* @param {Listener} listener - Listener loaded.
* @param {boolean} isReload - Whether or not this was a reload.
*/
/**
* Emitted when a listener is removed.
* @event ListenerHandler#remove
* @param {Listener} listener - Listener removed.
*/
================================================
FILE: src/util/AkairoError.js
================================================
const Messages = {
// Module-related
FILE_NOT_FOUND: filename => `File '${filename}' not found`,
MODULE_NOT_FOUND: (constructor, id) => `${constructor} '${id}' does not exist`,
ALREADY_LOADED: (constructor, id) => `${constructor} '${id}' is already loaded`,
NOT_RELOADABLE: (constructor, id) => `${constructor} '${id}' is not reloadable`,
INVALID_CLASS_TO_HANDLE: (given, expected) => `Class to handle ${given} is not a subclass of ${expected}`,
// Command-related
ALIAS_CONFLICT: (alias, id, conflict) => `Alias '${alias}' of '${id}' already exists on '${conflict}'`,
// Options-related
COMMAND_UTIL_EXPLICIT: 'The command handler options `handleEdits` and `storeMessages` require the `commandUtil` option to be true',
UNKNOWN_MATCH_TYPE: match => `Unknown match type '${match}'`,
// Generic errors
NOT_INSTANTIABLE: constructor => `${constructor} is not instantiable`,
NOT_IMPLEMENTED: (constructor, method) => `${constructor}#${method} has not been implemented`,
INVALID_TYPE: (name, expected, vowel = false) => `Value of '${name}' was not ${vowel ? 'an' : 'a'} ${expected}`
};
/**
* Represents an error for Akairo.
* @param {string} key - Error key.
* @param {...any} args - Arguments.
* @extends {Error}
*/
class AkairoError extends Error {
constructor(key, ...args) {
if (Messages[key] == null) throw new TypeError(`Error key '${key}' does not exist`);
const message = typeof Messages[key] === 'function'
? Messages[key](...args)
: Messages[key];
super(message);
this.code = key;
}
get name() {
return `AkairoError [${this.code}]`;
}
}
module.exports = AkairoError;
================================================
FILE: src/util/Category.js
================================================
const { Collection } = require('discord.js');
/**
* A group of modules.
* @param {string} id - ID of the category.
* @param {Iterable} [iterable] - Entries to set.
* @extends {Collection}
*/
class Category extends Collection {
constructor(id, iterable) {
super(iterable);
/**
* ID of the category.
* @type {string}
*/
this.id = id;
}
/**
* Calls `reload()` on all items in this category.
* @returns {Category}
*/
reloadAll() {
for (const m of Array.from(this.values())) {
if (m.filepath) m.reload();
}
return this;
}
/**
* Calls `remove()` on all items in this category.
* @returns {Category}
*/
removeAll() {
for (const m of Array.from(this.values())) {
if (m.filepath) m.remove();
}
return this;
}
/**
* Returns the ID.
* @returns {string}
*/
toString() {
return this.id;
}
}
module.exports = Category;
================================================
FILE: src/util/Constants.js
================================================
module.exports = {
ArgumentMatches: {
PHRASE: 'phrase',
FLAG: 'flag',
OPTION: 'option',
REST: 'rest',
SEPARATE: 'separate',
TEXT: 'text',
CONTENT: 'content',
REST_CONTENT: 'restContent',
NONE: 'none'
},
ArgumentTypes: {
STRING: 'string',
LOWERCASE: 'lowercase',
UPPERCASE: 'uppercase',
CHAR_CODES: 'charCodes',
NUMBER: 'number',
INTEGER: 'integer',
BIGINT: 'bigint',
EMOJINT: 'emojint',
URL: 'url',
DATE: 'date',
COLOR: 'color',
USER: 'user',
USERS: 'users',
MEMBER: 'member',
MEMBERS: 'members',
RELEVANT: 'relevant',
RELEVANTS: 'relevants',
CHANNEL: 'channel',
CHANNELS: 'channels',
TEXT_CHANNEL: 'textChannel',
TEXT_CHANNELS: 'textChannels',
VOICE_CHANNEL: 'voiceChannel',
VOICE_CHANNELS: 'voiceChannels',
CATEGORY_CHANNEL: 'categoryChannel',
CATEGORY_CHANNELS: 'categoryChannels',
NEWS_CHANNEL: 'newsChannel',
NEWS_CHANNELS: 'newsChannels',
STORE_CHANNEL: 'storeChannel',
STORE_CHANNELS: 'storeChannels',
ROLE: 'role',
ROLES: 'roles',
EMOJI: 'emoji',
EMOJIS: 'emojis',
GUILD: 'guild',
GUILDS: 'guilds',
MESSAGE: 'message',
GUILD_MESSAGE: 'guildMessage',
RELEVANT_MESSAGE: 'relevantMessage',
INVITE: 'invite',
MEMBER_MENTION: 'memberMention',
CHANNEL_MENTION: 'channelMention',
ROLE_MENTION: 'roleMention',
EMOJI_MENTION: 'emojiMention',
COMMAND_ALIAS: 'commandAlias',
COMMAND: 'command',
INHIBITOR: 'inhibitor',
LISTENER: 'listener'
},
AkairoHandlerEvents: {
LOAD: 'load',
REMOVE: 'remove'
},
CommandHandlerEvents: {
MESSAGE_BLOCKED: 'messageBlocked',
MESSAGE_INVALID: 'messageInvalid',
COMMAND_BLOCKED: 'commandBlocked',
COMMAND_STARTED: 'commandStarted',
COMMAND_FINISHED: 'commandFinished',
COMMAND_CANCELLED: 'commandCancelled',
COMMAND_LOCKED: 'commandLocked',
MISSING_PERMISSIONS: 'missingPermissions',
COOLDOWN: 'cooldown',
IN_PROMPT: 'inPrompt',
ERROR: 'error'
},
BuiltInReasons: {
CLIENT: 'client',
BOT: 'bot',
OWNER: 'owner',
GUILD: 'guild',
DM: 'dm'
}
};
================================================
FILE: src/util/Util.js
================================================
class Util {
static isPromise(value) {
return value
&& typeof value.then === 'function'
&& typeof value.catch === 'function';
}
static isEventEmitter(value) {
return value
&& typeof value.on === 'function'
&& typeof value.emit === 'function';
}
static prefixCompare(aKey, bKey) {
if (aKey === '' && bKey === '') return 0;
if (aKey === '') return 1;
if (bKey === '') return -1;
if (typeof aKey === 'function' && typeof bKey === 'function') return 0;
if (typeof aKey === 'function') return 1;
if (typeof bKey === 'function') return -1;
return aKey.length === bKey.length
? aKey.localeCompare(bKey)
: bKey.length - aKey.length;
}
static intoArray(x) {
if (Array.isArray(x)) {
return x;
}
return [x];
}
static intoCallable(thing) {
if (typeof thing === 'function') {
return thing;
}
return () => thing;
}
static flatMap(xs, f) {
const res = [];
for (const x of xs) {
res.push(...f(x));
}
return res;
}
static deepAssign(o1, ...os) {
for (const o of os) {
for (const [k, v] of Object.entries(o)) {
const vIsObject = v && typeof v === 'object';
const o1kIsObject = Object.prototype.hasOwnProperty.call(o1, k) && o1[k] && typeof o1[k] === 'object';
if (vIsObject && o1kIsObject) {
Util.deepAssign(o1[k], v);
} else {
o1[k] = v;
}
}
}
return o1;
}
static choice(...xs) {
for (const x of xs) {
if (x != null) {
return x;
}
}
return null;
}
}
module.exports = Util;
================================================
FILE: test/bot.js
================================================
const TestClient = require('./struct/TestClient');
const client = new TestClient();
const { token } = require('./auth.json');
client.start(token);
process.on('unhandledRejection', err => console.error(err)); // eslint-disable-line no-console
================================================
FILE: test/commands/args.js
================================================
/* eslint-disable no-console */
const { Command } = require('../..');
const util = require('util');
class ArgsCommand extends Command {
constructor() {
super('args', {
aliases: ['args'],
args: [
{
id: 'text',
match: 'text'
},
{
id: 'content',
match: 'content'
},
{
id: 'phrase',
match: 'phrase',
otherwise: () => 'no!'
},
{
id: 'rest',
match: 'rest'
},
{
id: 'restContent',
match: 'restContent'
},
{
id: 'separate',
match: 'separate'
},
{
id: 'flag',
match: 'flag',
flag: ['-f', '--flag']
},
{
id: 'option',
match: 'option',
flag: ['-o', '--option']
}
]
});
}
exec(message, args) {
message.channel.send(util.inspect(args, { depth: 1 }), { code: 'js' });
}
}
module.exports = ArgsCommand;
================================================
FILE: test/commands/ayy.js
================================================
const { Command } = require('../..');
class AyyCommand extends Command {
constructor() {
super('ayy', {
regex: /^ayy+$/i
});
}
exec(message) {
return message.reply('lmao');
}
}
module.exports = AyyCommand;
================================================
FILE: test/commands/condition.js
================================================
const { Command } = require('../../src');
class ConditionalCommand extends Command {
constructor() {
super('condition');
}
condition(message) {
return message.content === 'make me condition';
}
exec(message) {
return message.util.reply('made you condition');
}
}
module.exports = ConditionalCommand;
================================================
FILE: test/commands/condition.promise.js
================================================
const { Command } = require('../../src');
class ConditionalPromiseCommand extends Command {
constructor() {
super('condition.promise');
}
condition(message) {
return Promise.resolve(message.content === 'make me promise condition');
}
exec(message) {
return message.util.reply('made you promise condition');
}
}
module.exports = ConditionalPromiseCommand;
================================================
FILE: test/commands/embed.js
================================================
const { Command } = require('../..');
class EmbedCommand extends Command {
constructor() {
super('embed', {
aliases: ['embed'],
args: [
{
id: 'emptyContent',
match: 'flag',
flag: '-c'
},
{
id: 'emptyEmbed',
match: 'flag',
flag: '-e'
},
{
id: 'phrase',
match: 'phrase'
}
]
});
}
exec(message, args) {
if (args.emptyContent) {
return message.util.send(null, { embed: { description: args.phrase } });
}
if (args.emptyEmbed) {
return message.util.send(args.phrase, { embed: null });
}
return message.util.send(args.phrase, { embed: { description: args.phrase } });
}
}
module.exports = EmbedCommand;
================================================
FILE: test/commands/eval.js
================================================
const { Command } = require('../..');
const util = require('util');
class EvalCommand extends Command {
constructor() {
super('eval', {
aliases: ['eval', 'e'],
category: 'owner',
ownerOnly: true,
quoted: false,
args: [
{
id: 'code',
match: 'content'
}
]
});
}
async exec(message, { code }) {
if (!code) return message.util.reply('No code provided!');
const evaled = {};
const logs = [];
const token = this.client.token.split('').join('[^]{0,2}');
const rev = this.client.token.split('').reverse().join('[^]{0,2}');
const tokenRegex = new RegExp(`${token}|${rev}`, 'g');
const cb = '```';
const print = (...a) => { // eslint-disable-line no-unused-vars
const cleaned = a.map(obj => {
if (typeof o !== 'string') obj = util.inspect(obj, { depth: 1 });
return obj.replace(tokenRegex, '[TOKEN]');
});
if (!evaled.output) {
logs.push(...cleaned);
return;
}
evaled.output += evaled.output.endsWith('\n') ? cleaned.join(' ') : `\n${cleaned.join(' ')}`;
const title = evaled.errored ? '☠\u2000**Error**' : '📤\u2000**Output**';
if (evaled.output.length + code.length > 1900) evaled.output = 'Output too long.';
evaled.message.edit([
`📥\u2000**Input**${cb}js`,
code,
cb,
`${title}${cb}js`,
evaled.output,
cb
]);
};
try {
let output = eval(code);
if (output && typeof output.then === 'function') output = await output;
if (typeof output !== 'string') output = util.inspect(output, { depth: 0 });
output = `${logs.join('\n')}\n${logs.length && output === 'undefined' ? '' : output}`;
output = output.replace(tokenRegex, '[TOKEN]');
if (output.length + code.length > 1900) output = 'Output too long.';
const sent = await message.util.send([
`📥\u2000**Input**${cb}js`,
code,
cb,
`📤\u2000**Output**${cb}js`,
output,
cb
]);
evaled.message = sent;
evaled.errored = false;
evaled.output = output;
return sent;
} catch (err) {
console.error(err); // eslint-disable-line no-console
let error = err;
error = error.toString();
error = `${logs.join('\n')}\n${logs.length && error === 'undefined' ? '' : error}`;
error = error.replace(tokenRegex, '[TOKEN]');
const sent = await message.util.send([
`📥\u2000**Input**${cb}js`,
code,
cb,
`☠\u2000**Error**${cb}js`,
error,
cb
]);
evaled.message = sent;
evaled.errored = true;
evaled.output = error;
return sent;
}
}
}
module.exports = EvalCommand;
================================================
FILE: test/commands/f.js
================================================
/* eslint-disable no-console */
const { Command, Flag } = require('../..');
const util = require('util');
class FCommand extends Command {
constructor() {
super('f', {
aliases: ['f'],
args: [
{
id: 'x',
type: (msg, phrase) => {
if (phrase.length > 10) {
return Flag.fail(phrase);
}
return phrase;
},
default: (msg, value) => {
console.log('failed', value);
return 1;
}
}
]
});
}
exec(message, args) {
message.channel.send(util.inspect(args, { depth: 1 }), { code: 'js' });
}
}
module.exports = FCommand;
================================================
FILE: test/commands/generate.js
================================================
/* eslint-disable no-console */
const { Command, Flag } = require('../..');
const util = require('util');
class GenerateCommand extends Command {
constructor() {
super('generate', {
aliases: ['generate', 'g']
});
}
*args() {
const x = yield {
type: ['1', '2'],
otherwise: 'Type 1 or 2!'
};
if (x === '1') {
return Flag.continue('sub');
}
return { x };
}
exec(message, args) {
message.channel.send(util.inspect(args, { depth: 1 }), { code: 'js' });
}
}
module.exports = GenerateCommand;
================================================
FILE: test/commands/lock.js
================================================
/* eslint-disable no-console */
const { Command } = require('../..');
const sleep = require('util').promisify(setTimeout);
class LockCommand extends Command {
constructor() {
super('lock', {
aliases: ['lock'],
lock: 'guild'
});
}
exec(message) {
return [0, 1, 2, 3, 4].reduce((promise, num) => promise.then(() => sleep(1000)).then(() => message.util.send(num)), Promise.resolve());
}
}
module.exports = LockCommand;
================================================
FILE: test/commands/p.js
================================================
/* eslint-disable no-console */
const { Command } = require('../..');
const util = require('util');
class PCommand extends Command {
constructor() {
super('p', {
aliases: ['p'],
args: [
{
id: 'integer',
type: 'bigint',
prompt: {
start: async () => {
await Promise.resolve(1);
return 'Give me an integer!';
},
retry: 'That\'s not an integer, try again!',
optional: false
}
}
]
});
}
before() {
console.log(1);
}
exec(message, args) {
message.channel.send(util.inspect(args, { depth: 1 }), { code: 'js' });
}
}
module.exports = PCommand;
================================================
FILE: test/commands/q.js
================================================
/* eslint-disable no-console */
const { Command } = require('../..');
class QCommand extends Command {
constructor() {
super('q', {
aliases: ['q']
});
}
exec(message) {
const command = this.handler.modules.get('p');
return this.handler.handleDirectCommand(message, '', command);
}
}
module.exports = QCommand;
================================================
FILE: test/commands/separate.js
================================================
/* eslint-disable no-console */
const { Command } = require('../..');
const util = require('util');
class SeparateCommand extends Command {
constructor() {
super('separate', {
aliases: ['separate', 'sep'],
args: [
{
id: 'integers',
match: 'separate',
type: 'integer',
prompt: {
start: 'Give me some integers!',
retry: (msg, { phrase }) => `"${phrase}" is not an integer, try again!`
}
}
]
});
}
exec(message, args) {
message.channel.send(util.inspect(args, { depth: 1 }), { code: 'js' });
}
}
module.exports = SeparateCommand;
================================================
FILE: test/commands/sub.js
================================================
/* eslint-disable no-console */
const { Command } = require('../../src');
const util = require('util');
class SubCommand extends Command {
constructor() {
super('sub', {
args: [
{
id: 'thing'
}
]
});
}
exec(message, args) {
message.channel.send(util.inspect(args, { depth: 1 }), { code: 'js' });
}
}
module.exports = SubCommand;
================================================
FILE: test/commands/test.js
================================================
/* eslint-disable no-console */
const { Argument: { compose, range, union }, Command } = require('../..');
const util = require('util');
class TestCommand extends Command {
constructor() {
super('test', {
aliases: ['test', 'test-a'],
cooldown: 5000,
prefix: ['$', '%'],
args: [
{
id: 'x',
match: 'rest',
type: compose((m, s) => s.replace(/\s/g, ''), range(union('integer', 'emojint'), 0, 50))
}
]
});
}
exec(message, args) {
message.channel.send(util.inspect(args, { depth: 1 }), { code: 'js' });
}
}
module.exports = TestCommand;
================================================
FILE: test/commands/test2.js
================================================
/* eslint-disable no-console */
const { Argument: { compose, range, union }, Command } = require('../..');
const util = require('util');
class Test2Command extends Command {
constructor() {
super('test2', {
aliases: ['test2'],
cooldown: 5000,
prefix: () => ['/', '>'],
args: [
{
id: 'y',
match: 'rest',
type: compose((m, s) => s.replace(/\s/g, ''), range(union('integer', 'emojint'), 0, 50))
}
]
});
}
exec(message, args) {
message.channel.send(util.inspect(args, { depth: 1 }), { code: 'js' });
}
}
module.exports = Test2Command;
================================================
FILE: test/commands/unordered.js
================================================
/* eslint-disable no-console */
const { Command } = require('../..');
const util = require('util');
class UnorderedCommand extends Command {
constructor() {
super('unordered', {
aliases: ['unordered', 'un'],
args: [
{
id: 'integer1',
unordered: true,
type: 'integer'
},
{
id: 'integer2',
unordered: true,
type: 'integer'
}
]
});
}
exec(message, args) {
message.channel.send(util.inspect(args, { depth: 1 }), { code: 'js' });
}
}
module.exports = UnorderedCommand;
================================================
FILE: test/listeners/invalidMessage.js
================================================
/* eslint-disable no-console */
const { Listener } = require('../..');
class InvalidMessageListener extends Listener {
constructor() {
super('messageInvalid', {
emitter: 'commandHandler',
event: 'messageInvalid',
category: 'commandHandler'
});
}
exec(msg) {
console.log(msg.util.parsed);
}
}
module.exports = InvalidMessageListener;
================================================
FILE: test/listeners/message.js
================================================
/* eslint-disable no-console */
const { Listener } = require('../..');
class MessageListener extends Listener {
constructor() {
super('message', {
emitter: 'client',
event: 'message',
category: 'client'
});
}
exec(msg) {
console.log(msg.content);
}
}
module.exports = MessageListener;
================================================
FILE: test/struct/TestClient.js
================================================
const { AkairoClient, CommandHandler, InhibitorHandler, ListenerHandler, SQLiteProvider } = require('../../src/index');
const sqlite = require('sqlite');
class TestClient extends AkairoClient {
constructor() {
super({
ownerID: '123992700587343872'
});
this.commandHandler = new CommandHandler(this, {
directory: './test/commands/',
ignoreCooldownID: ['132266422679240704'],
aliasReplacement: /-/g,
prefix: '!!',
allowMention: true,
commandUtil: true,
commandUtilLifetime: 10000,
commandUtilSweepInterval: 10000,
storeMessages: true,
handleEdits: true,
argumentDefaults: {
prompt: {
start: 'What is thing?',
modifyStart: (msg, text) => `${msg.author}, ${text}\nType \`cancel\` to cancel this command.`,
retry: 'What is thing, again?',
modifyRetry: (msg, text) => `${msg.author}, ${text}\nType \`cancel\` to cancel this command.`,
timeout: 'Out of time.',
ended: 'No more tries.',
cancel: 'Cancelled.',
retries: 5
},
modifyOtherwise: (msg, text) => `${msg.author}, ${text}`
}
});
this.inhibitorHandler = new InhibitorHandler(this, {
directory: './test/inhibitors/'
});
this.listenerHandler = new ListenerHandler(this, {
directory: './test/listeners/'
});
const db = sqlite.open('./test/db.sqlite')
.then(d => d.run('CREATE TABLE IF NOT EXISTS guilds (id TEXT NOT NULL UNIQUE, settings TEXT)').then(() => d));
this.settings = new SQLiteProvider(db, 'guilds', { dataColumn: 'settings' });
this.setup();
}
setup() {
this.commandHandler.useInhibitorHandler(this.inhibitorHandler);
this.commandHandler.useListenerHandler(this.listenerHandler);
this.listenerHandler.setEmitters({
commandHandler: this.commandHandler,
inhibitorHandler: this.inhibitorHandler,
listenerHandler: this.listenerHandler
});
this.commandHandler.loadAll();
this.inhibitorHandler.loadAll();
this.listenerHandler.loadAll();
const resolver = this.commandHandler.resolver;
resolver.addType('1-10', (message, phrase) => {
const num = resolver.type('integer')(phrase);
if (num == null) return null;
if (num < 1 || num > 10) return null;
return num;
});
}
async start(token) {
await this.settings.init();
await this.login(token);
console.log('Ready!'); // eslint-disable-line no-console
}
}
module.exports = TestClient;
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"noImplicitAny": true,
"strictNullChecks": true,
"noEmit": true,
"forceConsistentCasingInFileNames": true
},
"files": [
"./src/index.d.ts"
]
}
================================================
FILE: tslint.json
================================================
{
"extends": [
"tslint-config-typings"
],
"rules": {
"class-name": true,
"comment-format": [
true,
"check-space"
],
"indent": [
true,
"spaces",
4
],
"member-ordering": [
true,
{
"order": [
"constructor",
"instance-field",
"instance-method",
"static-field",
"static-method"
]
}
],
"no-duplicate-variable": true,
"no-unused-variable": [false],
"no-eval": true,
"no-internal-module": true,
"no-trailing-whitespace": true,
"no-unsafe-finally": true,
"no-var-keyword": true,
"one-line": [
true,
"check-open-brace",
"check-whitespace"
],
"quotemark": [
true,
"single"
],
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": [
true,
"check-format",
"allow-pascal-case",
"allow-leading-underscore",
"allow-trailing-underscore"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}