Skip to content

Commit 87bafd3

Browse files
committed
Merge branch 'baseline'
2 parents 02dc3c5 + 338b9a7 commit 87bafd3

File tree

20 files changed

+428
-10
lines changed

20 files changed

+428
-10
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ fabric.properties
108108
### Gradle template
109109
.gradle
110110
**/build/
111-
!src/**/build/
111+
!koncorda/src/**/build/
112112

113113
# Ignore Gradle GUI config
114114
gradle-app.setting

README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,40 @@
11
# koncorda
22

3-
A small DSL for quickly bot creation with JDA.
3+
A small DSL for quickly creating a command bot with [JDA](https://github.com/DV8FromTheWorld/JDA).
4+
Inspired by [Ktor](https://github.com/ktorio/ktor).
5+
6+
# Getting started
7+
8+
Using Koncorda is easy, especially with the `koncorda` entrypoint.
9+
10+
```kotlin
11+
fun main() {
12+
koncorda {
13+
commmands {
14+
tail("hello") {
15+
event.respond("Hello world!")
16+
}
17+
}
18+
}.start()
19+
}
20+
```
21+
22+
DSLs like `command` make adding new functionality as easy as possible.
23+
The above example creates a bot that responds "Hello world!" to `!hello`.
24+
25+
Extending the configuration is possible by implementing KoncordaConfig. By default, it defines basic needs for getting
26+
a bot running like `DISCORD_TOKEN` and `COMMAND_PREFIX` which by default come from the environment. For more info, check
27+
out the [konfy](https://github.com/TanVD/konfy) project.
28+
29+
To see what a more complex usage looks like, check out the TestBot under the tests source.
30+
31+
# Logging
32+
33+
While Koncorda is intended to be somewhat opinionated, you are free to chose your SLF4J implementation.
34+
Logback is recommended for the sole reason that it's what the Ktor project generator suggests.
35+
36+
```kotlin
37+
dependecies {
38+
implementation("ch.qos.logback:logback-classic:1.2.3")
39+
}
40+
```

build.gradle.kts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
11
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
22

3+
group = "org.yttr"
4+
version = "0.0.1"
5+
36
plugins {
4-
kotlin("jvm") version "1.4.21"
7+
id("tanvd.kosogor") version "1.0.10" apply true
8+
id("io.gitlab.arturbosch.detekt") version "1.15.0" apply true
9+
kotlin("jvm") version "1.4.21" apply false
510
}
611

7-
group = "org.yttr"
8-
version = "0.1.0"
12+
subprojects {
13+
apply {
14+
plugin("kotlin")
15+
plugin("tanvd.kosogor")
16+
plugin("io.gitlab.arturbosch.detekt")
17+
plugin("maven-publish")
18+
}
919

10-
repositories {
11-
mavenCentral()
12-
}
20+
repositories {
21+
jcenter()
22+
}
1323

14-
tasks.withType<KotlinCompile>() {
15-
kotlinOptions.jvmTarget = "1.8"
24+
tasks.withType<KotlinCompile>() {
25+
kotlinOptions {
26+
jvmTarget = "1.8"
27+
languageVersion = "1.4"
28+
apiVersion = "1.4"
29+
}
30+
}
1631
}

koncorda/build.gradle.kts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import tanvd.kosogor.proxy.publishJar
2+
3+
group = rootProject.group
4+
version = rootProject.version
5+
6+
dependencies {
7+
implementation("net.dv8tion:JDA:4.2.0_227") {
8+
exclude(module = "opus-java")
9+
}
10+
implementation("tanvd.konfy:konfy:0.1.18")
11+
testImplementation("ch.qos.logback:logback-classic:1.2.3")
12+
}
13+
14+
publishJar {
15+
enablePublication = true
16+
}
17+
18+
publishing {
19+
repositories {
20+
maven {
21+
name = "GitHubPackages"
22+
url = uri("https://maven.pkg.github.com/yttrian/koncorda")
23+
credentials {
24+
username = System.getenv("GITHUB_ACTOR")
25+
password = System.getenv("GITHUB_TOKEN")
26+
}
27+
}
28+
}
29+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.yttr.koncorda
2+
3+
@DslMarker
4+
@Target(AnnotationTarget.FUNCTION)
5+
annotation class CreationDsl
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package org.yttr.koncorda
2+
3+
import net.dv8tion.jda.api.JDABuilder
4+
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
5+
import net.dv8tion.jda.api.hooks.ListenerAdapter
6+
import org.yttr.koncorda.command.Command
7+
import org.yttr.koncorda.config.DefaultKoncordaConfig
8+
import org.yttr.koncorda.config.KoncordaConfig
9+
import tanvd.konfy.ConfigView
10+
11+
/**
12+
* The Discord bot application, with useful DSLs for setting up different features.
13+
*/
14+
class Koncorda(val config: KoncordaConfig) : ConfigView, ListenerAdapter() {
15+
private val baseCommands = mutableListOf<Command.Branch>()
16+
17+
/**
18+
* Complete the JDA builder and start the bot
19+
* @param lowMemory Whether or not to use JDA's low memory profile
20+
*/
21+
fun start(lowMemory: Boolean = false) = if (lowMemory) {
22+
JDABuilder.createLight(config.discordToken)
23+
} else {
24+
JDABuilder.createDefault(config.discordToken)
25+
}.addEventListeners(this).setEnabledIntents(config.gatewayIntents).build()
26+
27+
internal fun addBaseCommand(command: Command.Branch) = baseCommands.add(command)
28+
29+
override fun onMessageReceived(event: MessageReceivedEvent) {
30+
// ignore improper messages
31+
if (event.isIgnorable) return
32+
33+
// split the args
34+
val args = event.message.contentStripped.trim().split(" ")
35+
36+
// find the command handler, if it exists
37+
when (val baseCommand = allBaseCommands()[args.first()]) {
38+
is Command.Leaf -> baseCommand
39+
is Command.Branch -> baseCommand[args.drop(1)]
40+
else -> null
41+
}?.let {
42+
if (it.check(event)) it(event, args) else {
43+
event.channel.sendMessage("You are not allowed to use that command.").queue()
44+
}
45+
}
46+
}
47+
48+
private fun allBaseCommands(): Map<String, Command> = baseCommands
49+
.map { it.routes }
50+
.fold(emptyMap()) { acc, baseRoute ->
51+
acc + baseRoute
52+
}
53+
54+
private val MessageReceivedEvent.isIgnorable
55+
get() = author.isBot || isWebhookMessage
56+
}
57+
58+
/**
59+
* Commands DSL, an easy way of registering command handlers and the path to them with Koncorda.
60+
*/
61+
fun Koncorda.commands(prefix: String = config.commandPrefix, build: Command.Branch.() -> Unit) =
62+
addBaseCommand(Command.Branch(prefix = prefix).apply(build))
63+
64+
/**
65+
* The simplest way to start building a bot with Koncorda. Make sure to add .start() to the end!
66+
*/
67+
fun koncorda(config: KoncordaConfig = DefaultKoncordaConfig, init: Koncorda.() -> Unit) =
68+
Koncorda(config).apply(init)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package org.yttr.koncorda.command
2+
3+
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
4+
import org.yttr.koncorda.CreationDsl
5+
import org.yttr.koncorda.command.check.CommandCheck
6+
7+
/**
8+
* A callable bot command.
9+
*/
10+
sealed class Command(
11+
protected val depth: Int,
12+
private val prefix: String = ""
13+
) {
14+
protected val checks: MutableSet<CommandCheck> = mutableSetOf()
15+
protected val nextDepth = depth + 1
16+
17+
/**
18+
* Represents a command branch.
19+
* @param handler action to perform when branch is called a terminal, defaults to autogenerated help
20+
* @param prefix the command prefix, only used by the base route!
21+
*/
22+
class Branch(depth: Int = 0, handler: CommandHandler? = null, prefix: String = "") : Command(depth, prefix) {
23+
val routes = mutableMapOf<String, Command>()
24+
private val fallback by lazy {
25+
if (handler != null) Leaf(depth, handler) else {
26+
val autoHelpCommandHandler = object : CommandHandler {
27+
override fun CommandCall.handle() {
28+
val path = args.joinToString(" ")
29+
val validSubcommands = routes.keys.joinToString { "`$it`" }
30+
event.respond("Valid subcommands for `$path` are $validSubcommands.")
31+
}
32+
}
33+
Leaf(depth, autoHelpCommandHandler, true).also { it.checks.addAll(checks) }
34+
}
35+
}
36+
37+
/**
38+
* Try to follow a path to a terminal.
39+
* Cannot tailrec optimize: https://stackoverflow.com/a/44626117
40+
*/
41+
operator fun get(path: List<String>): Leaf {
42+
val arg = path.firstOrNull() ?: return fallback
43+
44+
return when (val foundCommand = routes[arg]) {
45+
is Branch -> foundCommand[path.drop(1)]
46+
is Leaf -> foundCommand
47+
else -> fallback
48+
}
49+
}
50+
51+
override fun addCheck(addedCheck: CommandCheck) {
52+
this.checks += addedCheck
53+
this.routes.values.forEach { it.addCheck(addedCheck) }
54+
}
55+
}
56+
57+
/**
58+
* Represents the end of a branch where a command will be handled.
59+
*/
60+
class Leaf(
61+
depth: Int,
62+
private val handler: CommandHandler,
63+
private val help: Boolean = false
64+
) : Command(depth) {
65+
operator fun invoke(event: MessageReceivedEvent, args: List<String>) = with(handler) {
66+
CommandCall(event, if (help) args.take(depth) else args.drop(depth)).handle()
67+
}
68+
69+
override fun addCheck(addedCheck: CommandCheck) {
70+
this.checks += addedCheck
71+
}
72+
}
73+
74+
/**
75+
* Build a branch.
76+
*/
77+
@CreationDsl
78+
fun Branch.branch(arg: String, handler: CommandHandler? = null, build: Branch.() -> Unit) {
79+
routes["$prefix$arg"] = Branch(nextDepth, handler).apply(build)
80+
}
81+
82+
/**
83+
* Set the handler.
84+
*/
85+
@CreationDsl
86+
fun Branch.leaf(arg: String, handler: CommandHandler) {
87+
routes["$prefix$arg"] = Leaf(nextDepth, handler)
88+
}
89+
90+
/**
91+
* Set the handler.
92+
*/
93+
@CreationDsl
94+
fun Branch.leaf(arg: String, handler: CommandCall.() -> Unit) {
95+
routes["$prefix$arg"] = Leaf(nextDepth, object : CommandHandler {
96+
override fun CommandCall.handle() = handler()
97+
})
98+
}
99+
100+
/**
101+
* Create a checked branch.
102+
*/
103+
fun Branch.check(check: CommandCheck, build: Branch.() -> Unit) {
104+
val checkedBranch = Branch().also(build)
105+
checkedBranch.addCheck(check)
106+
this.routes += checkedBranch.routes
107+
}
108+
109+
abstract fun addCheck(addedCheck: CommandCheck)
110+
111+
fun check(event: MessageReceivedEvent) = checks.all { it.check(event) }
112+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.yttr.koncorda.command
2+
3+
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
4+
5+
data class CommandCall(val event: MessageReceivedEvent, val args: List<String>) {
6+
/**
7+
* Respond to the event.
8+
*/
9+
fun MessageReceivedEvent.respond(content: String) {
10+
if (content.isNotBlank()) channel.sendMessage(content).queue()
11+
}
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.yttr.koncorda.command
2+
3+
/**
4+
* A command handler.
5+
*/
6+
interface CommandHandler {
7+
/**
8+
* Handle the message event for a command invocation.
9+
*/
10+
fun CommandCall.handle()
11+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.yttr.koncorda.command.check
2+
3+
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
4+
5+
interface CommandCheck {
6+
fun check(event: MessageReceivedEvent): Boolean
7+
}

0 commit comments

Comments
 (0)