-
Notifications
You must be signed in to change notification settings - Fork 3
Kord Discord bot
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.commands
works 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
Kord
instead 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.kord
provides an override for thecommand
DSL that'll assume you're working with theKordContext
by default. In practice this means you don't have to specify theProcessorContext
each 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 generatedconfigure
block. (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.commands
all 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 namedping
and 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
respond
at 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
@AutoWired
on everything inside a file (we sure are), consider annotating your file with@file:AutoWired
instead, 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! TheStringArgument
takes 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 aString
we 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
MessageCreateEvent
and 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)
get
is aKoin
function on theKoinComponent
that 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.commands
Uses its ownKoinApplication
instead 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
ignore
has 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.