This guide is intended to take you through the structure of a Kord Extensions bot, by walking you through creating your bot and setting up a basic extension. In this guide, we'll be creating a bot that does the following:
?
!
for the test server.env
fileThis bot is what's included in our template repository. If you'd rather just read the code there, feel free to click through to it.
This guide assumes that you have a basic Gradle project created, as outlined in Guide: Project Setup. A project set up this way will be using Kotlin build scripts, Gradle 7 (or later) and version catalogs.
Since you'll need to provide a logging framework for logging to actually function, you'll need to add some dependencies. You'll also want to add the Shadow plugin - and in this instance, we'll add Detekt as well. Your Gradle build files should look like this:
pluginManagement {
plugins {
// Update this in libs.version.toml when you change it here
kotlin("jvm") version "1.7.10"
kotlin("plugin.serialization") version "1.7.10"
// Update this in libs.version.toml when you change it here
id("io.gitlab.arturbosch.detekt") version "1.21.0-RC2"
id("com.github.jakemarsden.git-hooks") version "0.0.1"
id("com.github.johnrengelman.shadow") version "5.2.0"
}
}
rootProject.name = "template"
enableFeaturePreview("VERSION_CATALOGS")
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("libs.versions.toml"))
}
}
}
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
application
kotlin("jvm")
kotlin("plugin.serialization")
id("com.github.jakemarsden.git-hooks")
id("com.github.johnrengelman.shadow")
id("io.gitlab.arturbosch.detekt")
}
group = "template"
version = "1.0-SNAPSHOT"
repositories {
google()
mavenCentral()
maven {
name = "Sonatype Snapshots (Legacy)"
url = uri("https://oss.sonatype.org/content/repositories/snapshots")
}
maven {
name = "Sonatype Snapshots"
url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots")
}
}
dependencies {
detektPlugins(libs.detekt)
implementation(libs.kord.extensions)
implementation(libs.kotlin.stdlib)
implementation(libs.kx.ser)
// Logging dependencies
implementation(libs.groovy)
implementation(libs.jansi)
implementation(libs.logback)
implementation(libs.logging)
}
application {
// This is deprecated, but the Shadow plugin requires it
mainClassName = "template.AppKt"
}
gitHooks {
setHooks(
mapOf("pre-commit" to "detekt")
)
}
tasks.withType<KotlinCompile> {
// Current LTS version of Java
kotlinOptions.jvmTarget = "11"
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
}
tasks.jar {
manifest {
attributes(
"Main-Class" to "template.AppKt"
)
}
}
java {
// Current LTS version of Java
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
detekt {
buildUponDefaultConfig = true
config = rootProject.files("detekt.yml")
}
[versions]
detekt = "1.21.0-RC2" # Note: Plugin versions must be updated in the settings.gradle.kts too
kotlin = "1.7.10" # Note: Plugin versions must be updated in the settings.gradle.kts too
groovy = "3.0.9"
jansi = "2.4.0"
kord-extensions = "1.5.6-SNAPSHOT"
kx-ser = "1.3.3"
logging = "2.1.23"
logback = "1.2.5"
[libraries]
detekt = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
groovy = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" }
jansi = { module = "org.fusesource.jansi:jansi", version.ref = "jansi" }
kord-extensions = { module = "com.kotlindiscord.kord.extensions:kord-extensions", version.ref = "kord-extensions" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8" }
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
logging = { module = "io.github.microutils:kotlin-logging", version.ref = "logging" }
kx-ser = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kx-ser" }
In src/main/resources
, you'll also want to create a file named logback.groovy
:
import ch.qos.logback.core.joran.spi.ConsoleTarget
def environment = System.getenv().getOrDefault("ENVIRONMENT", "production")
def defaultLevel = INFO
def defaultTarget = ConsoleTarget.SystemErr
if (environment == "dev") {
defaultLevel = DEBUG
defaultTarget = ConsoleTarget.SystemOut
// Silence warning about missing native PRNG
logger("io.ktor.util.random", ERROR)
}
appender("CONSOLE", ConsoleAppender) {
encoder(PatternLayoutEncoder) {
pattern = "%boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n"
withJansi = true
}
target = defaultTarget
}
root(defaultLevel, ["CONSOLE"])
This file sets up console logging for you, with the given format and a default logging level of INFO
. If you'd like debug logging, simply set the ENVIRONMENT
environment variable (or variable in .env
) to debug
.
With the busywork out of the way, it's time for the moment you've been waiting for. The first thing you'll want to do here is to create a file named App.kt
in src/main/kotlin
, creating package directories as needed.
Before creating the bot, let's load up the settings we need. We can do this using the env(name)
function, which attempts to load a variable from a file named .env
, before attempting the same from your environment variables.
We need two settings here: the bot token, and the test server ID. We'll create two variables for this, using the TOKEN
and TEST_SERVER
environment variables, respectively.
val TEST_SERVER_ID = Snowflake( // Store this as a Discord snowflake, aka an ID
env("TEST_SERVER") // An exception will be thrown if it can't be found
)
private val TOKEN = env("TOKEN")
The easiest way to provide these is to create a file named .env
in your project's root folder, like so:
TOKEN=abcde
TEST_SERVER=12345
Make sure you don't commit this or make it public!
Now, create a suspending main
function, create the bot object within it, and start the bot.
suspend fun main() {
val bot = ExtensibleBot(TOKEN) {
}
bot.start()
}
This will create and run a basic bot - but the bot will do nothing! Let's fix that.
Kord Extensions is built around the concept of Extensions, which are the building blocks that make up your bot's functionality. Essentially, each piece of functionality that makes up your bot should be represented by an extension.
For now, let's create a basic extension. In the folder containing your App.kt
file, create extensions/TestExtension.kt
. This should contain a single class that extends the Extension
abstract class.
class TextExtension: Extension() {
// Used throughout KordEx to refer to your extension
override val name = "test"
override suspend fun setup() {
// We'll do all our setup tasks here
}
}
Now we can get to work - to start with, let's create a basic slap
command. This will slap whoever uses it with the classic IRC warfare tool. Inside the setup
function:
// ...
override suspend fun setup() {
chatCommand {
name = "slap"
description = "Get slapped!"
action {
message.respond(
"*slaps you with a large, smelly trout!*"
)
}
}
}
// ...
Now, head back to App.kt
and load your extension:
// ...
suspend fun main() {
val bot = ExtensibleBot(TOKEN) {
chatCommands {
// Enable chat command handling
enabled = true
}
extensions {
add(::TestExtension)
}
}
}
// ...
If you run your bot now, you'll be able to test it out!
Nice work, you have a functional bot - but it'd be nice if this command could do more, right?
Kord Extensions contains a comprehensive command arguments system, with a fully-typed argument parsing system that does all the work for you, out of the box. To start, you'll need to create a class that extends Arguments
, like the following:
// ...
inner class SlapArgs : Arguments() {
// A single user argument, required for the command to be able to run
val target by user("target", description = "Person you want to slap")
// A single string argument that consumes the rest of the command's arguments,
// with a default value if the user doesn't provide anything for it
val weapon by coalescingDefaultingString {
name = "weapon"
description = "What you want to slap with"
defaultValue = "large, smelly trout"
}
}
// ...
Now, let's head back to our slap command and modify it to add these arguments. We'll also need to update the action
block to use them.
// ...
chatCommand(::SlapArgs) { // Pass the arguments class constructor in here
// ...
// Make sure the command didn't come from a webhook - the command will only run or be
// shown in the help command when this returns `true`, and it'll be ignored otherwise
check { failIf(event.message.author == null) }
action { // Now `arguments` here will contain an instance of our arguments class
// Because of the DslMarker annotation KordEx uses, we need to grab Kord explicitly
val kord = [email protected]
// Don't slap ourselves, slap the person that ran the command!
val realTarget = if (arguments.target.id == kord.selfId) {
message.author!!
} else {
arguments.target
}
message.respond("*slaps ${realTarget.mention} with their ${arguments.weapon}*")
}
}
// ...
That's it! Let's start the bot again and try running the command.
Oh yeah, that's the stuff.
We should return to App.kt
and change some of the bot's options. For starters, let's update the settings for message commands:
// ...
suspend fun main() {
val bot = ExtensibleBot(TOKEN) {
chatCommands {
// Set the prefix used on most servers
defaultPrefix = "?"
// Enable chat command handling
enabled = true
prefix { default ->
if (guildId == TEST_SERVER_ID) {
// For the test server, we use ! as the command prefix
"!"
} else {
// For other servers, we use the configured default prefix
default
}
}
}
// ...
}
// ...
}
Now, let's return to our extension and add a slash command. Like before, we'll need an arguments class and a slash command definition in the setup function.
// ...
override suspend fun setup() {
// ...
publicSlashCommand(::SlapSlashArgs) { // Public slash commands have public responses
name = "slap"
description = "Ask the bot to slap another user"
// Use guild commands for testing, global ones take up to an hour to update
guild(TEST_SERVER_ID)
action {
val kord = [email protected]
val realTarget = if (arguments.target.id == kord.selfId) {
member
} else {
arguments.target
}
respond {
content = "*slaps ${realTarget?.mention} with their ${arguments.weapon}*"
}
}
}
}
// ...
inner class SlapSlashArgs : Arguments() {
val target by user {
name = "target"
description = "Person you want to slap"
}
// Coalesced strings are not currently supported by slash commands
val weapon by defaultingString {
name = "weapon"
description = "What you want to slap with"
defaultValue = "large, smelly trout"
}
}
// ...
Once again, let's start up the bot - this time, we'll use /slap
, on our test server.
Excellent, that works well too!
Congratulations - you've made it to the end of this guide! Great work!
As you can see, writing commands with Kord Extensions is fairly simple. This guide only scratches the surface of what you can do with KordEx, but you're now ready to explore the rest of the docs and start writing your own bots from scratch!
Remember, if you need any help, feel free to ask in #kordex-support
on Discord - you'll find a link to the Discord server we use in the left sidebar.