@joogl/disco.js v1.0.2
Disco
Wrapper around the Discord.JS package to easily create command-based bots.
Installation
npm i @joogl/disco.js
Usage
Creating a bot
The Bot
class exported by the package extends the Client
class from Discord.JS, so all methods you're used to from that package will also work here. Mind you that this package wraps around the Discord.JS library, so you can work as you're used to with no hassle.
This means you can still do this to create a new client instance.
const { Bot } = require("disco.js");
let client = new Bot();
await client.login("token");
Creating and adding commands
The package exports a Command
class which can be extended to create a command implementation. Commands must inherit two functions, otherwise the bot will throw an error when trying to execute the command. These functions are getName
and execute
. A help command to send a list of available commands to the channel a command was executed in would look like this.
const { Command } = require("disco.js");
class HelpCommand extends Command {
getName() {
return "help";
}
async execute(message, args) {
await message.channel.send("To be implemented.");
}
}
The constructor from the
Command
class takes a bot instance as an argument. So unless you override the constructor and give a bot as an argument to the super call, it is best to not implement any logic in the command constructor itself.
To register the command, the Bot
class provides the function registerCommand
which takes a command class constructor as an argument. When registered, the bot will listen to incoming messages, evaluate them and see if a command can be executed. Using the example from before, you'd register the help command like this.
client.registerCommand(HelpCommand);
Command names are case-insensitive. This means that a command with the name
help
will also be called if someone in chat uses it asHelp
orHELP
or whatever else you can think of.
To trigger a command execution, the command has to be prefixed. That's usually just one character. By default, the bot reacts to the .
prefix. You can always change the command prefix using the setCommandPrefix
method exposed by the Bot
class. So the above command implementation would only be executed if someone in chat sent a message containing .help
.
Creating storage implementations
Storage implementations can be defined using the Storage
class. All classes that extend the storage class must infer a type variable representing the storage created when instantiated. They must also override
the instantiate
method. A class to implement ioredis
for instance could look like this.
const Redis = require("ioredis");
const { Storage } = require("disco.js");
class RedisStorage extends Storage<Redis.Redis> {
async instantiate() {
return new Redis();
}
}
The idea behind having a seperate storage implementation is to add convenience functions to the class, like reading and writing data in a database for a set of server properties, and have them all running in the same spot.
A storage implementation can be bound to a bot instance. Bots expose the setStorage
function which takes a storage implementation as an argument. The bot will then try to instantiate the storage. If it's successful, the storage instance will be able to be accessed from any part of code that has access to the bot using getStorage
.
await client.setStorage(new RedisStorage());
This is especially useful within command classes since they have access to the bot instance they're assigned to by default. So commands can easily query storage solutions.
async execute(message, args) {
this.bot.getStorage().getSomeServerVariable(message.guild);
}
Resolving arguments
When you have a predictable command syntax, you sometimes want mentions to resolve to their instances at runtime. Assuming you want a command that pings a server member, the message that is sent over the wire looks something like .ping <@302551113424678982>
. You want the first argument to be automatically resolved into a user instance.
The package provides an ArgumentResolver
. It provides functions to resolve channels, members, users and roles from a message given a mention like in the example above. Mind you it can only resolve objects that have been mentioned in the message. Otherwise any of its methods will throw an error. For the above command example, it'd look something like this.
async execute(message, args) {
let resolver = new ArgumentResolver(message);
let user = resolver.resolveMember(args[0]);
}
Guessing channels, users and roles from text
In some cases, you can't mention certain things on your server like voice channels, roles or members in case you just don't want to annoy them. Hence it's better to not mention but rather provide the name for the thing you want to mention.
For this exact purpose, this package provides the Guesser
class. Given user input, the class returns a list of items that are most similar to the given input in descending order. It uses the Levenshtein distance as a metric to determine the similarity between user input and the given role, user, member and channel names in a server.
Say you have the example of the ping
command and you want to mention a user who has whitespaces in their nickname, then you can implement it a little something like this.
async execute(message, args) {
let name = args[0];
let guesser = new Guesser(message.guild);
let user = guesser.guessMembersByNickname(name)[0];
await message.channel.send(`Hey ${user}, wake up!`);
}
Assuming someone executed the command like .ping "Big Mean Admin"
, the above code would then mention the user whose nickname is the closest to "Big Mean Admin"
.
By default, the
Guesser
ignores result with a Levenshtein distance higher than three to avoid far-fetched results. The threshold distance can always be changed using an additional constructor parameter.
Documentation
Bot => DiscordJS.Client
Constructor
new Bot()
Creates a new bot instance.
Methods
getCommandPrefix() => string
Gets the current command prefix. (.
by default)
setCommandPrefix(newPrefix: string) => string
Sets the command prefix.
getStorage() => ? extends Storage
Gets the storage instance bound to this bot.
async setStorage(newStorage: ? extends Storage)
Sets the storage instance to be bound to this bot.
registerCommand(cmdClass: typeof Command)
Registers a command.
Command
Constructor
new Command(bot: Bot)
Creates a new command instance. Takes the bot instance to bind to as a parameter.
Methods
getName() => string
Name of the command.
async execute(message: Message, args: string[])
Executes the command. Takes the message instance that triggered the command and a string array containing all the supplied arguments as parameters.
Properties
bot => Bot
Bot instance the command is registered to.
Storage\<T>
T
defines the type of storage that will be instantiated.
Constructor
new Storage()
Creates a new storage instance.
Methods
async instantiate() => Promise<T>
Creates an instance of the storage implementation.
async create() => Promise<Storage<T>>
Creates the storage implementation instance and returns the storage wrapper.
isInstantiated() => boolean
Returns true if the storage was instantiated, false otherwise.
get() => T
Gets the storage implementation instance. Throws an error if it wasn't instantiated yet.
ArgumentResolver
Constructor
new ArgumentResolver(message: Message)
Creates a new argument resolver. Takes the message to resolve instances from as a parameter.
Methods
isTextChannel(str: string) => boolean
Checks if a mention can be resolved to a text channel.
isRole(str: string) => boolean
Checks if a mention can be resolved to a role.
isUser(str: string) => boolean
Checks if a mention can be resolved to a user.
isMember(str: string) => boolean
Checks if a mention can be resolved to a member.
resolveTextChannel(str: string) => TextChannel
Resolves a Discord text channel mention representation into its instance.
resolveRole(str: string) => Role
Resolves a Discord role mention representation into its instance.
resolveUser(str: string) => User
Resolves a Discord user mention representation into its instance.
resolveMember(str: string) => GuildMember
Resolves a Discord member mention representation into its instance.
Guesser
Constructor
new Guesser(guild: Guild, maxDistance?: number)
Creates a new guesser. Takes the guild to perform the guesses on as the first argument. Optionally allows a threshold Levenshtein distance as a second argument. All results with a distance higher than that will be discarded.
Methods
guessRoles(name: string) => Role[]
Considers all roles in a guild and returns a list of roles that are most similar to the name given as the argument in descending order. Takes a role name as an argument.
guessMembersByUsername(name: string) => GuildMember[]
Considers all members in a guild and returns a list of members that are most similar to the name given as the argument in descending order. Only their usernames will be considered. Takes a member name as an argument.
guessMembersByNickname(name: string) => GuildMember[]
Considers all members in a guild and returns a list of members that are most similar to the name given as the argument in descending order. Only their display names will be considered, or their user names if the former isn't available. Takes a member name as an argument.
guessTextChannels(name: string) => TextChannel[]
Considers all text channels in a guild and returns a list of text channels that are most similar to the name given as the argument in descending order. Takes a text channel name as an argument.
guessVoiceChannels(name: string) => VoiceChannel[]
Considers all voice channels in a guild and returns a list of voice channels that are most similar to the name given as the argument in descending order. Takes a voice channel name as an argument.
Contributing
This project is built on top of Typescript. It comes with a compiler configuration file. To work on the code, clone this repo, make and commit your changes and create a pull request with your suggested changes. All improvements and new helper ideas are welcome.
License
MIT.