Skip to content
This repository has been archived by the owner on Apr 16, 2023. It is now read-only.

Kord Discord bot

Bart Arys edited this page Nov 19, 2020 · 4 revisions

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.

Glossary

  • context: kordx.commands works with different contexts, which describe what type of objects your code will be dealing with. We'll be using the KordContext, allowing us to interact with a Kord-based implementation of the library.

Dependencies

latest version: Download

repositories

repositories {
    mavenCentral()
    maven { url "https://dl.bintray.com/kordlib/Kord" }
    jcenter()
}

kordx.commands.kord

dependencies {
    implementation "com.gitlab.kordlib.kordx:kordx-commands-runtime-kord:$commandsVersion"
}

kapt (optional)

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"
}

Setting up main

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.

Defining a command

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 the command DSL that'll assume you're working with the KordContext by default. In practice this means you don't have to specify the ProcessorContext 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 generated configure 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: In kordx.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 named ping and will thusly be invoked by typing ping. 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 a pong.

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() 
}

Arguments

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! The StringArgument 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 a String 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.

Adding a Prefix

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 a String. This allows you to do fancy things like have a per- user/channel/guild prefix if you'd like.

Defining a module

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.

Dependency injection

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 a Koin function on the KoinComponent 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 own KoinApplication instead of the default global one. If you need dependencies resolved post bot configuration, you can do so from the command events.

Create an event filter

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.

Creating a precondition

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 user input

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!")
        }
    }
}

Input control

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}!")
        }
    }
}

Escape reading

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.

Conclusion

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.

Clone this wiki locally