Kord Discord bot - kordlib/kordx.commands GitHub Wiki
This guide will go over the features of kordx.commands.Kord and tries to emulate the steps of building a bot. It does not expect you to have read any other parts of the wiki but won't provide anything but a surface level explanation.
-
context:kordx.commandsworks with different contexts, which describe what type of objects your code will be dealing with. We'll be using theKordContext, allowing us to interact with a Kord-based implementation of the library.
repositories {
mavenCentral()
maven { url "https://dl.bintray.com/kordlib/Kord" }
jcenter()
}dependencies {
implementation "com.gitlab.kordlib.kordx:kordx-commands-runtime-kord:$commandsVersion"
}This guide will assume you're using kapt to automatically wire up all your commands/modules/dependencies/etc. While you can do this manually, it's not recommended and not explained here.
plugins {
id "org.jetbrains.kotlin.kapt" version "$kotlinVersion"
}
dependencies {
kapt "com.gitlab.kordlib.kordx:kordx-commands-processor:$commandsVersion"
}The first thing our bot needs is an entry point, we'll use the bot DSL for this:
suspend fun main() = bot(System.getenv("BOT_TOKEN")) {
}The bot function will suspend so long as Kord's gateway doesn't close. In practice that means until you shut it down or the gateway gives up on reconnecting.
For this example, we'll read our token from the system's variables. There are many altnatives such as taken it from the main's arguments or reading it from a file.
DO NOT paste your token directly into your code if you're thinking of hosting your source code online (gitlab/github). Other people will be able to use your token for nefarious purposes and you'll be held responsible.
You can pass an instance of
Kordinstead of your token if you want to setup Kord outside of it's default configuration.
No discord bot is a real bot unless it has some commands, and no bot tutorial would be complete without a ping command!
kordx.commands.kordprovides an override for thecommandDSL that'll assume you're working with theKordContextby default. In practice this means you don't have to specify theProcessorContexteach time.
@AutoWired
@ModuleName("test-commands")
fun pingCommand() = command("ping") {
invoke {
respond("pong")
}
}There's no real reason to make pingCommand a function, you can just as well make it a property.
-
@AutoWired: Will make the command automatically included in the generatedconfigureblock. (If you are autowiring things, make sure they are public and top-level declarations, the annotation processor won't be able to interact with your function/property otherwise!) -
@ModuleName: Inkordx.commandsall commands belong to a (single) module, this annotation tells our processor to which module this command belongs. Omitting this annotation will lead to a compile time error. -
command("ping"): That's our command, it's namedpingand will thusly be invoked by typingping. Talking about invoking... -
invoke: You might have guessed it, that's where we defined the behavior of our command once it gets invoked. Our ping will simply respond with apong.
Unlike real conversations, commands don't have to
respondat all, neither are they limiting to responding only once.
When you've written down your ping command, build your program. The annotation processor will get to work and generate a nice configure function for you, which you can now call in your main:
suspend fun main() = bot(System.getenv("BOT_TOKEN")) {
configure()
}No command library would be complete without some arguments, and no argument tutorial would be complete without an echo command:
@AutoWired
@ModuleName("test-commands")
fun echoCommand() = command("echo") {
invoke(StringArgument) {
respond(it)
}
}If you're tired of writing
@AutoWiredon everything inside a file (we sure are), consider annotating your file with@file:AutoWiredinstead, it'll pick up on all functions and properties that return autowirable stuff automatically. Yeah, that's right, it autowires the autowiring.😎
There's two new things here:
-
invoke(StringArgument): Our invoke functions has gained an argument! TheStringArgumenttakes anything that's after the command name and returns it as a String. Perfect for our echo command. -
respond(it): That's right, the argument is supplied as a typed argument inside the invoke function. Since it's aStringwe can just respond it directly.
naturally, adding more arguments works in a similar way:
@file:AutoWired //don't forget, we're using this now in every file instead!
@ModuleName("test-commands")
fun addCommand() = command("add") {
invoke(IntArgument, IntArgument) { a, b ->
respond("${a + b}")
}
}If you're wondering, we provide implementations for up to 20 arguments, if you need anything more we strongly consider you to reevaluate your design choices.
You might have already tried running your bot. In which case, good on you for being so eager to test things out, but you'll probably have notice a warning popping up your console:
You currently don't have a prefix registered for Kord, allowing users to accidentally invoke a command when they don't intend to.
Consider setting a prefix for the KordContext or CommonContext.
That's right, we haven't supplied a prefix for the bot yet. While it's valid to run a bot without a prefix, Discord strongly recommends you to create one to make sure no unintentional commands are triggered. We'll defined our prefix for our bot to be +:
val prefix = prefix {
kord { literal("+") }
}If you've worked with prefixes before, this might seem a bit different than what you're used to. Since kordx.commands is able to run multiple contexts at the same time, you're allowed to define a prefix per context. kordx.commands.kord comes with an extension function named kord that'll set the prefix for the KordContext.
If you want to use the same prefix for all contexts, you can use
add(CommonContext) { literal("your-prefix") }and not set any prefix for specific contexts.
Prefixes itself are also autowirable, so these will be picked up on your next compile.
Note that the prefix is in actuallity a function that takes a
MessageCreateEventand returns aString. This allows you to do fancy things like have a per- user/channel/guild prefix if you'd like.
are you getting annoyed at typing those @ModuleName("test-commands") yet? Same! We'll try putting those commands in a module directly instead:
fun testCommands() = module("test-commands") {
command("ping") {
invoke {
respond("pong")
}
}
command("echo") {
invoke(StringArgument) {
respond(it)
}
}
command("add") {
invoke(IntArgument, IntArgument) { a, b ->
respond("${a + b}")
}
}
}There we go, we've traded a level of indentation for the luxury of not having to write @ModuleName anymore! As with all other things, modules will also be autowired on compile, provided there's a @AutoWired or @file:AutoWired.
There's not much to say about modules, for most use cases they'll just be a container to put commands in. If you do want to learn more there's the Modules page.
kordx.commands uses Koin for its Dependency injection, you can learn more about Koin on their official site.
Having people interact with your bot is cool, but maybe we want some people not use our bots. Maybe they've abused our poor toaster, we'll make a command to ignore a certain user so that our bot won't reply to them anymore.
First things first, we'll need a place to put our ignored users, a simple set of ignored users should do the trick:
class Ignores(val ignored: MutableSet<Snowflake> = mutableSetOf())You'll probably want something tied to a database for a real bot, but that's outside the scope of this tutorial.
now we'll create a module for our Koin depdency, we only need one instance across our program so we'll make it a single:
val ignoredDependencies = org.koin.dsl.module {
single { Ignores() }
}Now let's set up our commands to ignore and un-ignore a user, this should feel rather familiar by now:
fun ignoreCommands() = module("ignore-commands") {
command("ignore") {
invoke(MemberArgument) {
TODO("ignore the user")
}
}
command("un-ignore") {
invoke(MemberArgument) {
TODO("un-ignore the user")
}
}
}We'll want to get access to our Ignores from this module. There's two ways we can do this:
- on the creation of the module
- on the invocation of a command
The first way would involve us adding the dependency as a function argument, we could add the Ignores like this:
fun ignoreCommands(ignores: Ignores) = //...kordx.commands will automatically delegate arguments of autowirable functions to Koin, allowing us to use anything from the Koin modules that got autowired.
This pattern has a big upside in that we'll know immediately on startup if we misconfigured a dependency. If we didn't have the ignoredDependencies autowired we'd find a crash at startup, which is better than after hours of uptime.
The other method allows for a more elegant approach if you're into extension functions, we'll be using this one for the tutorial.
Every CommandContext is its own KoinComponent, which means we can get dependencies during the invocation of a command, we'll introduce some fancy extension functions to toggle a user's ignore:
fun KordCommandContext.ignore(user: UserBehavior) =
get<Ignores>().ignored.add(user.id)
fun KordCommandContext.unIgnore(user: UserBehavior) =
get<Ignores>().ignored.remove(user.id)
getis aKoinfunction on theKoinComponentthat allows us to fetch a dependency.
Now we can complete our ignore/unignore commands:
fun ignoreCommands() = module("ignore-commands") {
command("ignore") {
invoke(MemberArgument) {
ignore(it)
}
}
command("un-ignore") {
invoke(MemberArgument) {
unIgnore(it)
}
}
}
kordx.commandsUses its ownKoinApplicationinstead of the default global one. If you need dependencies resolved post bot configuration, you can do so from the command events.
We've created our ignore commands, but we aren't actually ignoring ayone. We'll need to filter out events before they get to the command processor. For that, we can use an event filter:
fun ignoreIgnoredUsers(ignores: Ignores) = eventFilter {
message.author?.id in ignores.ignored
}A masterful naming scheme, the word
ignorehas lost all meaning to me.
We combined some knowledge from the previous topics here, but the general idea of how an EventFilter works is pretty straightforward. Like any other filter function, you get a MessageCreateEvent and return a Boolean.
You're free to tell the user they're ignored with
message.channel.createMessage, but then we wouldn't be ignoring them anymore.
You might have noticed that anyone can ignore/unignore anyone, that's probably a bad idea. We should limit the ignore commands to some higher up people, like people who are able to ban members from the server. Let's write one up in our ignore-commands module:
fun ignoreCommands() = module("ignore-commands") {
precondition {
val guildId = guild?.id ?: return@precondition run {
respond("can only ignore users in a server")
false
}
val permissions = author.asMember(guildId)!!.getPermissions()
(Permission.BanMembers in permissions).also {
if (!it) respond("only users with ban permission can ignore users")
}
}
//commands...
}Preconditions work similarily to EventFilters, but they only happen after we've figured out a command exists but before a command's arguments are parsed. The can be declared at:
- top level: applies to all commands
- module level: applies to all commands in that module
- command level: applies to that specific command only
Getting information from a command's invocation is nice, but sometimes we need something that more closely resembles a conversation between the bot and the user. Maybe your command has branching paths that require more input or you'd like the insertion of arguments to be better spaces for UX reasons. Kord allows you to read in Arguments after a command's invocation using read:
fun conversations() = module("conversation-commands") {
command("conversation") {
invoke {
respond("What's your name?")
val name = read(StringArgument)
respond("Hello $name!")
}
}
}You can add input control by passing a suspend (T -> Boolean) at the end that works
similar to a filter:
fun conversations() = module("conversation-commands") {
command("conversation") {
invoke {
respond("What's your age?")
val age = read(IntArgument) {
if(it > 0) return@read true
respond("Try to give somewhat of a valid age")
false
}
respond("Really? You don't look a day older than ${age + 5}!")
}
}
}You might have noticed that read doesn't ever stop until it gets valid input, which might get annoying if your user no longer wants to continue the conversation. For this you can use the escape parameter that allows you to pass a filter to decide when to quit asking for input. The return value of read will become nullable instead, returning null when reading was escaped:
val number: Int? = read(IntArgument, escape = { it.message.content == "stop" })Note that you can't use escapes with arguments that produce nullable results (try
IntArgument.optional()) because this would lead to a null value with a confusing meaning.
We discussed most features that kordx.commands has to offer at a surface level, this should allow you to build a complex bot without running into too much spaghetti code.
If you'd like a more deepdive into the individual entities, you can do so under the common section in the sidebar.