diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 60c02b50cc..ce319e3adc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,14 +26,16 @@ jobs:
echo "org.gradle.jvmargs=-XX:MaxMetaspaceSize=5G" >> ~/.gradle/gradle.properties
- name: Gradle (Build)
- run: sh gradlew build
+ uses: gradle/gradle-build-action@v2
+ with:
+ arguments: build
- name: Upload artifact (Extra Module JARs)
uses: actions/upload-artifact@v2
with:
name: JARs (Extra Modules)
- path: extra/*/build/libs/*.jar
+ path: extra-modules/*/build/libs/*.jar
- name: Upload artifact (Main JARs)
uses: actions/upload-artifact@v2
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 393e8294d2..14b94feeae 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -18,17 +18,30 @@ jobs:
with:
java-version: 1.14
+ - name: Import GPG key
+ uses: crazy-max/ghaction-import-gpg@v3
+
+ with:
+ gpg-private-key: ${{ secrets.GPG_KEY }}
+ passphrase: ${{ secrets.GPG_PASSWORD }}
+
- name: Set up Gradle properties
run: |
mkdir -p ~/.gradle
echo "githubToken=${{ secrets.GITHUB_TOKEN }}" >> ~/.gradle/gradle.properties
echo -e "\norg.gradle.jvmargs=-XX:MaxMetaspaceSize=5G" >> ~/.gradle/gradle.properties
+ echo -e "\nsigning.gnupg.keyName=BFAAD5D6093EF5E62BC9A16A10DB8C6B4AE61C2F" >> ~/.gradle/gradle.properties
+ echo -e "\nsigning.gnupg.passphrase=${{ secrets.GPG_PASSWORD }}" >> ~/.gradle/gradle.properties
- name: Gradle (Build)
- run: sh gradlew build
+ uses: gradle/gradle-build-action@v2
+ with:
+ arguments: build
- name: Gradle (Publish)
- run: sh gradlew -Pkotdis.user=${{ secrets.MAVEN_USER }} -Pkotdis.password=${{ secrets.MAVEN_PASSWORD }} publish
+ uses: gradle/gradle-build-action@v2
+ with:
+ arguments: publish -Pkotdis.user=${{ secrets.MAVEN_USER }} -Pkotdis.password=${{ secrets.MAVEN_PASSWORD }}
- name: Upload artifact (Extra Module JARs)
uses: actions/upload-artifact@v2
diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml
index 0964254988..eb3e582b79 100644
--- a/.github/workflows/tag.yml
+++ b/.github/workflows/tag.yml
@@ -16,7 +16,14 @@ jobs:
uses: actions/setup-java@v1
with:
- java-version: 1.11
+ java-version: 1.14
+
+ - name: Import GPG key
+ uses: crazy-max/ghaction-import-gpg@v3
+
+ with:
+ gpg-private-key: ${{ secrets.GPG_KEY }}
+ passphrase: ${{ secrets.GPG_PASSWORD }}
- name: Set up Kotlin
uses: fwilhe2/setup-kotlin@main
@@ -25,6 +32,8 @@ jobs:
run: |
mkdir -p ~/.gradle
echo "org.gradle.jvmargs=-XX:MaxMetaspaceSize=5G" >> ~/.gradle/gradle.properties
+ echo -e "\nsigning.gnupg.keyName=BFAAD5D6093EF5E62BC9A16A10DB8C6B4AE61C2F" >> ~/.gradle/gradle.properties
+ echo -e "\nsigning.gnupg.passphrase=${{ secrets.GPG_PASSWORD }}" >> ~/.gradle/gradle.properties
- name: Set up git credentials
uses: oleksiyrudenko/gha-git-credentials@v2-latest
@@ -34,10 +43,14 @@ jobs:
token: '${{ secrets.GITHUB_TOKEN }}'
- name: Gradle (Build)
- run: sh gradlew build
+ uses: gradle/gradle-build-action@v2
+ with:
+ arguments: build
- name: Gradle (Publish)
- run: sh gradlew -Pkotdis.user=${{ secrets.MAVEN_USER }} -Pkotdis.password=${{ secrets.MAVEN_PASSWORD }} publish
+ uses: gradle/gradle-build-action@v2
+ with:
+ arguments: publish -Pkotdis.user=${{ secrets.MAVEN_USER }} -Pkotdis.password=${{ secrets.MAVEN_PASSWORD }}
- name: Create release description
run: kotlin .github/tag.main.kts
diff --git a/.idea/GradleUpdaterPlugin.xml b/.idea/GradleUpdaterPlugin.xml
new file mode 100644
index 0000000000..6ed5855a3a
--- /dev/null
+++ b/.idea/GradleUpdaterPlugin.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 6f6540bdc8..dfaf95a0b3 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -2,9 +2,6 @@
-
-
-
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index 3305aa1d69..8fc19501fa 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -61,5 +61,15 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000000..797acea53e
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 97bb578388..6b28a199ef 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,11 @@
# Kord Extensions
-[![Docs: Click here](https://img.shields.io/static/v1?label=Docs&message=Click%20here&color=7289DA&style=for-the-badge&logo=read-the-docs)](https://kordex.kotlindiscord.com/) [![Discord: Click here](https://img.shields.io/static/v1?label=Discord&message=Click%20here&color=7289DA&style=for-the-badge&logo=discord)](https://discord.gg/gjXqqCS) [![Build Status](https://img.shields.io/github/workflow/status/Kotlin-Discord/kord-extensions/CI/root?logo=github&style=for-the-badge)](https://github.com/Kotlin-Discord/kord-extensions/actions?query=workflow%3ACI+branch%3Aroot)
+[![Docs: Click here](https://img.shields.io/static/v1?label=Docs&message=Click%20here&color=7289DA&style=for-the-badge&logo=read-the-docs)](https://kordex.kotlindiscord.com/) [![Discord: Click here](https://img.shields.io/static/v1?label=Discord&message=Click%20here&color=7289DA&style=for-the-badge&logo=discord)](https://discord.gg/gjXqqCS)
+[![Build Status](https://img.shields.io/github/workflow/status/Kotlin-Discord/kord-extensions/CI/root?logo=github&style=for-the-badge)](https://github.com/Kotlin-Discord/kord-extensions/actions?query=workflow%3ACI+branch%3Aroot) [![Weblate project translated](https://img.shields.io/weblate/progress/kord-extensions?style=for-the-badge)]((https://hosted.weblate.org/engage/kord-extensions/))
[![Release](https://img.shields.io/nexus/r/com.kotlindiscord.kord.extensions/kord-extensions?nexusVersion=3&logo=gradle&color=blue&label=Release&server=https%3A%2F%2Fmaven.kotlindiscord.com&style=for-the-badge)](https://maven.kotlindiscord.com/#browse/browse:maven-releases:com%2Fkotlindiscord%2Fkord%2Fextensions%2Fkord-extensions) [![Snapshot](https://img.shields.io/nexus/s/com.kotlindiscord.kord.extensions/kord-extensions?logo=gradle&color=orange&label=Snapshot&server=https%3A%2F%2Fmaven.kotlindiscord.com&style=for-the-badge)](https://maven.kotlindiscord.com/#browse/browse:maven-snapshots:com%2Fkotlindiscord%2Fkord%2Fextensions%2Fkord-extensions)
+[![Translation status](https://hosted.weblate.org/widgets/kord-extensions/-/main/287x66-grey.png)](https://hosted.weblate.org/engage/kord-extensions/)
+
Kord Extensions is an addon for the excellent [Kord library](https://github.com/kordlib/kord). It intends to provide a
framework for larger bot projects, with easy-to-use commands, rich argument parsing and event handling, wrapped up
into individual extension classes.
@@ -14,22 +17,3 @@ for our fairly object-oriented design, especially where it comes to its extensio
Discord.py). Despite this, we still strive to provide an idiomatic API that makes full use of Kotlin's niceties.
If you're ready to get started, please [take a look at the documentation](https://kordex.kotlindiscord.com/).
-
-# Why not kordx.commands?
-
-Kord has released their own command framework, [kordx.commands](https://github.com/kordlib/kordx.commands). It's
-a competent library, but it takes some very different approaches to solving the same problems Kord Extensions does.
-Most notably, it requires the use of [kapt](https://kotlinlang.org/docs/reference/kapt.html) and makes use of an
-annotation-based autowire system for getting things registered.
-
-In contrast, Kord Extensions provides a less magical approach that is more closely tied to object-oriented
-programming, and may be more suitable for embedding into other applications. In addition, it provides many useful
-utilities and niceties that make working with Kord a breeze. At the end of the day, though, the
-choice is yours - both approaches have pros and cons, and it's worth checking both out to see what you like
-better!
-
-# Under Development
-
-This file is in an early state, and we're working on bringing over our framework from our bot project. Once we're
-happy with what we've done, and we've written up some documentation, we'll update this file and make a proper
-release.
diff --git a/annotation-processor/build.gradle.kts b/annotation-processor/build.gradle.kts
index d9c6f0a929..e24dc4d2ba 100644
--- a/annotation-processor/build.gradle.kts
+++ b/annotation-processor/build.gradle.kts
@@ -1,13 +1,7 @@
-import java.io.ByteArrayOutputStream
-import java.net.URL
-
plugins {
- `maven-publish`
-
- kotlin("jvm")
-
- id("io.gitlab.arturbosch.detekt")
- id("org.jetbrains.dokka")
+ `kordex-module`
+ `published-module`
+ `dokka-module`
}
dependencies {
@@ -22,124 +16,10 @@ dependencies {
detektPlugins(libs.detekt)
}
-val sourceJar = task("sourceJar", Jar::class) {
- dependsOn(tasks["classes"])
- archiveClassifier.set("sources")
- from(sourceSets.main.get().allSource)
-}
-
-val javadocJar = task("javadocJar", Jar::class) {
- dependsOn("dokkaJavadoc")
- archiveClassifier.set("javadoc")
- from(tasks.javadoc)
-}
-
-java {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
-}
-
-kotlin {
- explicitApi()
-}
-
-detekt {
- buildUponDefaultConfig = true
- config = files("$rootDir/detekt.yml")
-
- autoCorrect = true
-}
-
-publishing {
- repositories {
- maven {
- name = "KotDis"
-
- url = if (project.version.toString().contains("SNAPSHOT")) {
- uri("https://maven.kotlindiscord.com/repository/maven-snapshots/")
- } else {
- uri("https://maven.kotlindiscord.com/repository/maven-releases/")
- }
-
- credentials {
- username = project.findProperty("kotdis.user") as String? ?: System.getenv("KOTLIN_DISCORD_USER")
- password = project.findProperty("kotdis.password") as String?
- ?: System.getenv("KOTLIN_DISCORD_PASSWORD")
- }
-
- version = project.version
- }
- }
-
- publications {
- create("maven") {
- from(components.getByName("java"))
-
- artifact(sourceJar)
- artifact(javadocJar)
- }
- }
-}
-
-fun runCommand(command: String): String {
- val output = ByteArrayOutputStream()
-
- project.exec {
- commandLine(command.split(" "))
- standardOutput = output
- }
-
- return output.toString().trim()
-}
-
-fun getCurrentGitBranch(): String { // https://gist.github.com/lordcodes/15b2a4aecbeff7c3238a70bfd20f0931
- var gitBranch = "Unknown branch"
-
- try {
- gitBranch = runCommand("git rev-parse --abbrev-ref HEAD")
- } catch (t: Throwable) {
- println(t)
- }
-
- return gitBranch
-}
-
-tasks.dokkaHtml.configure {
+dokkaModule {
moduleName.set("Kord Extensions: Annotation Processor")
-
- dokkaSourceSets {
- configureEach {
- includeNonPublic.set(false)
- skipDeprecated.set(false)
-
- displayName.set("Kord Extensions: Java Time")
- includes.from("packages.md")
- jdkVersion.set(8)
-
- sourceLink {
- localDirectory.set(file("${project.projectDir}/src/main/kotlin"))
-
- remoteUrl.set(
- URL(
- "https://github.com/Kotlin-Discord/kord-extensions/" +
- "tree/${getCurrentGitBranch()}/annotation-processor/src/main/kotlin"
- )
- )
-
- remoteLineSuffix.set("#L")
- }
-
- externalDocumentationLink {
- url.set(URL("http://kordlib.github.io/kord/common/common/"))
- }
-
- externalDocumentationLink {
- url.set(URL("http://kordlib.github.io/kord/core/core/"))
- }
- }
- }
}
-tasks.build {
- this.finalizedBy(sourceJar, javadocJar)
+kordex {
+ jvmTarget.set("1.8")
}
diff --git a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterProcessor.kt b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterProcessor.kt
index de6ca2eaae..9abb8d280e 100644
--- a/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterProcessor.kt
+++ b/annotation-processor/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/annotations/converters/ConverterProcessor.kt
@@ -228,7 +228,7 @@ public class ConverterProcessor(
// Imports that all converters need
import com.kotlindiscord.kord.extensions.commands.converters.*
- import com.kotlindiscord.kord.extensions.commands.parser.Arguments
+ import com.kotlindiscord.kord.extensions.commands.Arguments
import dev.kord.common.annotation.KordPreview
diff --git a/annotations/build.gradle.kts b/annotations/build.gradle.kts
index 6f6205391c..6e28ddcbe2 100644
--- a/annotations/build.gradle.kts
+++ b/annotations/build.gradle.kts
@@ -1,13 +1,7 @@
-import java.io.ByteArrayOutputStream
-import java.net.URL
-
plugins {
- `maven-publish`
-
- kotlin("jvm")
-
- id("io.gitlab.arturbosch.detekt")
- id("org.jetbrains.dokka")
+ `kordex-module`
+ `published-module`
+ `dokka-module`
}
dependencies {
@@ -16,124 +10,10 @@ dependencies {
detektPlugins(libs.detekt)
}
-val sourceJar = task("sourceJar", Jar::class) {
- dependsOn(tasks["classes"])
- archiveClassifier.set("sources")
- from(sourceSets.main.get().allSource)
-}
-
-val javadocJar = task("javadocJar", Jar::class) {
- dependsOn("dokkaJavadoc")
- archiveClassifier.set("javadoc")
- from(tasks.javadoc)
-}
-
-java {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
-}
-
-kotlin {
- explicitApi()
-}
-
-detekt {
- buildUponDefaultConfig = true
- config = files("$rootDir/detekt.yml")
-
- autoCorrect = true
-}
-
-publishing {
- repositories {
- maven {
- name = "KotDis"
-
- url = if (project.version.toString().contains("SNAPSHOT")) {
- uri("https://maven.kotlindiscord.com/repository/maven-snapshots/")
- } else {
- uri("https://maven.kotlindiscord.com/repository/maven-releases/")
- }
-
- credentials {
- username = project.findProperty("kotdis.user") as String? ?: System.getenv("KOTLIN_DISCORD_USER")
- password = project.findProperty("kotdis.password") as String?
- ?: System.getenv("KOTLIN_DISCORD_PASSWORD")
- }
-
- version = project.version
- }
- }
-
- publications {
- create("maven") {
- from(components.getByName("java"))
-
- artifact(sourceJar)
- artifact(javadocJar)
- }
- }
-}
-
-fun runCommand(command: String): String {
- val output = ByteArrayOutputStream()
-
- project.exec {
- commandLine(command.split(" "))
- standardOutput = output
- }
-
- return output.toString().trim()
-}
-
-fun getCurrentGitBranch(): String { // https://gist.github.com/lordcodes/15b2a4aecbeff7c3238a70bfd20f0931
- var gitBranch = "Unknown branch"
-
- try {
- gitBranch = runCommand("git rev-parse --abbrev-ref HEAD")
- } catch (t: Throwable) {
- println(t)
- }
-
- return gitBranch
-}
-
-tasks.dokkaHtml.configure {
+dokkaModule {
moduleName.set("Kord Extensions: Annotation Processor")
-
- dokkaSourceSets {
- configureEach {
- includeNonPublic.set(false)
- skipDeprecated.set(false)
-
- displayName.set("Kord Extensions: Java Time")
- includes.from("packages.md")
- jdkVersion.set(8)
-
- sourceLink {
- localDirectory.set(file("${project.projectDir}/src/main/kotlin"))
-
- remoteUrl.set(
- URL(
- "https://github.com/Kotlin-Discord/kord-extensions/" +
- "tree/${getCurrentGitBranch()}/annotation-processor/src/main/kotlin"
- )
- )
-
- remoteLineSuffix.set("#L")
- }
-
- externalDocumentationLink {
- url.set(URL("http://kordlib.github.io/kord/common/common/"))
- }
-
- externalDocumentationLink {
- url.set(URL("http://kordlib.github.io/kord/core/core/"))
- }
- }
- }
}
-tasks.build {
- this.finalizedBy(sourceJar, javadocJar)
+kordex {
+ jvmTarget.set("1.8")
}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 0000000000..9aabad9254
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,18 @@
+plugins {
+ `kotlin-dsl`
+}
+
+repositories {
+ google()
+ gradlePluginPortal()
+}
+
+dependencies {
+ implementation(kotlin("gradle-plugin", version = "1.5.30"))
+ implementation(kotlin("serialization", version = "1.5.30"))
+ implementation("org.jetbrains.dokka", "dokka-gradle-plugin", "1.5.0")
+ implementation("com.google.devtools.ksp", "com.google.devtools.ksp.gradle.plugin", "1.5.30-1.0.0-beta08")
+ implementation("io.gitlab.arturbosch.detekt", "detekt-gradle-plugin", "1.17.1")
+ implementation(gradleApi())
+ implementation(localGroovy())
+}
diff --git a/buildSrc/src/main/kotlin/GitTools.kt b/buildSrc/src/main/kotlin/GitTools.kt
new file mode 100644
index 0000000000..cf59b32922
--- /dev/null
+++ b/buildSrc/src/main/kotlin/GitTools.kt
@@ -0,0 +1,25 @@
+import org.gradle.api.Project
+import java.io.ByteArrayOutputStream
+
+fun Project.runCommand(command: String): String {
+ val output = ByteArrayOutputStream()
+
+ exec {
+ commandLine(command.split(" "))
+ standardOutput = output
+ }
+
+ return output.toString().trim()
+}
+
+fun Project.getCurrentGitBranch(): String { // https://gist.github.com/lordcodes/15b2a4aecbeff7c3238a70bfd20f0931
+ var gitBranch = "Unknown branch"
+
+ try {
+ gitBranch = runCommand("git rev-parse --abbrev-ref HEAD")
+ } catch (t: Throwable) {
+ println(t)
+ }
+
+ return gitBranch
+}
diff --git a/buildSrc/src/main/kotlin/disable-explicit-api-mode.gradle.kts b/buildSrc/src/main/kotlin/disable-explicit-api-mode.gradle.kts
new file mode 100644
index 0000000000..be996c1466
--- /dev/null
+++ b/buildSrc/src/main/kotlin/disable-explicit-api-mode.gradle.kts
@@ -0,0 +1,23 @@
+import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
+
+plugins {
+ kotlin("jvm")
+}
+
+kotlin {
+ // https://github.com/JetBrains/kotlin/pull/4598
+ fixExplicitApiModeArg()
+ // We still need to set this, because the IntelliJ Kotlin plugin Inspections
+ // look for this option instead of the CLI arg
+ explicitApi = ExplicitApiMode.Disabled
+}
+
+fun fixExplicitApiModeArg() {
+ val clazz = ExplicitApiMode.Disabled.javaClass
+ val field = clazz.getDeclaredField("cliOption")
+
+ with(field) {
+ isAccessible = true
+ set(ExplicitApiMode.Disabled, "disable")
+ }
+}
diff --git a/buildSrc/src/main/kotlin/dokka-module.gradle.kts b/buildSrc/src/main/kotlin/dokka-module.gradle.kts
new file mode 100644
index 0000000000..5fbe4a0c16
--- /dev/null
+++ b/buildSrc/src/main/kotlin/dokka-module.gradle.kts
@@ -0,0 +1,61 @@
+import java.net.URL
+
+plugins {
+ id("org.jetbrains.dokka")
+}
+
+val dokkaModuleExtensionName = "dokkaModule"
+
+abstract class DokkaModuleExtension {
+ abstract val moduleName: Property
+ abstract val includes: ListProperty
+}
+
+extensions.create(dokkaModuleExtensionName)
+
+tasks {
+ afterEvaluate {
+ val projectDir = project.projectDir.relativeTo(rootProject.rootDir).toString()
+ dokkaHtml {
+ val extension = project.extensions.getByName(dokkaModuleExtensionName)
+ extension.moduleName.orNull?.let {
+ moduleName.set(it)
+ }
+
+ dokkaSourceSets {
+ configureEach {
+ includeNonPublic.set(false)
+ skipDeprecated.set(false)
+ extension.moduleName.orNull?.let {
+ displayName.set(it)
+ }
+ extension.includes.orNull?.let {
+ includes.from(*it.toTypedArray())
+ }
+ jdkVersion.set(8)
+
+ sourceLink {
+ localDirectory.set(file("${project.projectDir}/src/main/kotlin"))
+
+ remoteUrl.set(
+ URL(
+ "https://github.com/Kotlin-Discord/kord-extensions/" +
+ "tree/${getCurrentGitBranch()}/${projectDir}/src/main/kotlin"
+ )
+ )
+
+ remoteLineSuffix.set("#L")
+ }
+
+ externalDocumentationLink {
+ url.set(URL("http://kordlib.github.io/kord/common/common/"))
+ }
+
+ externalDocumentationLink {
+ url.set(URL("http://kordlib.github.io/kord/core/core/"))
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/kordex-module.gradle.kts b/buildSrc/src/main/kotlin/kordex-module.gradle.kts
new file mode 100644
index 0000000000..7ed01f4177
--- /dev/null
+++ b/buildSrc/src/main/kotlin/kordex-module.gradle.kts
@@ -0,0 +1,58 @@
+import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ kotlin("jvm")
+
+ id("io.gitlab.arturbosch.detekt")
+}
+
+abstract class KordexExtension {
+ abstract val jvmTarget: Property
+ abstract val javaVersion: Property
+}
+
+val kordexExtensionName = "kordex"
+
+extensions.create(kordexExtensionName)
+
+val sourceJar = task("sourceJar", Jar::class) {
+ dependsOn(tasks["classes"])
+ archiveClassifier.set("sources")
+ from(sourceSets.main.get().allSource)
+}
+
+val javadocJar = task("javadocJar", Jar::class) {
+ dependsOn("dokkaJavadoc")
+ archiveClassifier.set("javadoc")
+ from(tasks.javadoc)
+}
+
+tasks {
+ build {
+ finalizedBy(sourceJar, javadocJar)
+ }
+ kotlin {
+ explicitApi()
+ }
+
+ afterEvaluate {
+ val extension = project.extensions.getByName(kordexExtensionName)
+ java {
+ sourceCompatibility = extension.javaVersion.getOrElse(JavaVersion.VERSION_1_8)
+ }
+ withType().configureEach {
+ kotlinOptions {
+ jvmTarget = extension.jvmTarget.getOrElse("1.8")
+ }
+ }
+ }
+
+}
+
+detekt {
+ buildUponDefaultConfig = true
+ config = files("$rootDir/detekt.yml")
+
+ autoCorrect = true
+}
diff --git a/buildSrc/src/main/kotlin/ksp-module.gradle.kts b/buildSrc/src/main/kotlin/ksp-module.gradle.kts
new file mode 100644
index 0000000000..e1c3aa0a8b
--- /dev/null
+++ b/buildSrc/src/main/kotlin/ksp-module.gradle.kts
@@ -0,0 +1,18 @@
+plugins {
+ java
+ id("com.google.devtools.ksp")
+}
+
+sourceSets {
+ main {
+ java {
+ srcDir(file("$buildDir/generated/ksp/main/kotlin/"))
+ }
+ }
+
+ test {
+ java {
+ srcDir(file("$buildDir/generated/ksp/test/kotlin/"))
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/published-module.gradle.kts b/buildSrc/src/main/kotlin/published-module.gradle.kts
new file mode 100644
index 0000000000..30419fe9d5
--- /dev/null
+++ b/buildSrc/src/main/kotlin/published-module.gradle.kts
@@ -0,0 +1,45 @@
+import org.gradle.api.publish.maven.MavenPublication
+
+plugins {
+ `maven-publish`
+ signing
+}
+
+val sourceJar: Task by tasks.getting
+val javadocJar: Task by tasks.getting
+
+publishing {
+ repositories {
+ maven {
+ name = "KotDis"
+
+ url = if (project.version.toString().contains("SNAPSHOT")) {
+ uri("https://maven.kotlindiscord.com/repository/maven-snapshots/")
+ } else {
+ uri("https://maven.kotlindiscord.com/repository/maven-releases/")
+ }
+
+ credentials {
+ username = project.findProperty("kotdis.user") as String? ?: System.getenv("KOTLIN_DISCORD_USER")
+ password = project.findProperty("kotdis.password") as String?
+ ?: System.getenv("KOTLIN_DISCORD_PASSWORD")
+ }
+
+ version = project.version
+ }
+ }
+
+ publications {
+ create("maven") {
+ from(components.getByName("java"))
+
+ artifact(sourceJar)
+ artifact(javadocJar)
+ }
+ }
+}
+
+signing {
+ useGpgCmd()
+ sign(publishing.publications["maven"])
+}
diff --git a/buildSrc/src/main/kotlin/tested-module.gradle.kts b/buildSrc/src/main/kotlin/tested-module.gradle.kts
new file mode 100644
index 0000000000..0530c19976
--- /dev/null
+++ b/buildSrc/src/main/kotlin/tested-module.gradle.kts
@@ -0,0 +1,17 @@
+plugins {
+ java
+}
+
+tasks {
+ test {
+ useJUnitPlatform()
+
+ testLogging.showStandardStreams = true
+
+ testLogging {
+ events("PASSED", "FAILED", "SKIPPED", "STANDARD_OUT", "STANDARD_ERROR")
+ }
+
+ systemProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug")
+ }
+}
diff --git a/changes/1.5.1-RC1.md b/changes/1.5.1-RC1.md
new file mode 100644
index 0000000000..2b96176add
--- /dev/null
+++ b/changes/1.5.1-RC1.md
@@ -0,0 +1,18 @@
+# KordEx 1.5.1-RC1
+
+This release is our first "stable" release in quite a long time, targeting Kord `0.8.0-M7`. The reason for this is largely due to Kord's extremely long snapshot cycle, which itself was caused by many changes to Discord's APIs. In turn, this means that this release contains a mind-boggling number of internal changes.
+
+We've done our best to keep things as compatible as possible, API-wise. Despite this, though, we've had no choice but to break a few things.
+
+Highlights of this release:
+
+* With a lot of help from ByteAlex, we've been able to eliminate many, many unnecessary cache hits, making use of behaviors rather than entities wherever possible. This makes KordEx far more suitable for large bots with different caching requirements.
+* Full support for message and user commands have been added, which come with a full rewrite of the application commands system. Application commands now always require a `public` or `ephemeral` type, to help keep things safe. Additionally, our old message commands are now named chat commands, and have their functions prefixed with `chat`.
+* The components system has been fully rewritten, including a similar typing requirement to application commands. It comes with a `ComponentContainer` type which makes it easier to re-use components, as well as a callback registry for advanced use-cases (such as components that need to work after a restart).
+* The Sentry integration has been rewritten, and you'll find a `SentryContext` provided everywhere you'd expect to be able to make use of Sentry, instead of a plain list of breadcrumbs. This, along with several other improvements, should make Sentry much more pleasant to work with.
+* Our translations platform [has been switched to Weblate](https://hosted.weblate.org/engage/kord-extensions/). If you're a translator (or would like to help with translations), please head over there!
+* Lots of deprecated things have now been removed. If you were still using them, well, you were warned!
+
+There are far too many changes to list here. The [existing pages on the wiki](https://kordex.kotlindiscord.com/) have been rewritten for this release, and we'd suggest taking a look at them to refresh your knowledge. There's still documentation work that needs doing, but we'll get there!
+
+As always, if you run into any problems, please let us know!
diff --git a/crowdin.yml b/crowdin.yml
deleted file mode 100644
index f0f1984840..0000000000
--- a/crowdin.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-files:
- - source: /kord-extensions/src/main/resources/translations/kordex/strings.properties
- translation: >-
- /kord-extensions/src/main/resources/translations/kordex/%file_name%_%locale_with_underscore%.%file_extension%
-
-"commit_message": "[ci skip]"
diff --git a/detekt.yml b/detekt.yml
index 03268694e5..90bc7c785a 100644
--- a/detekt.yml
+++ b/detekt.yml
@@ -170,7 +170,7 @@ exceptions:
active: true
methodNames: [toString, hashCode, equals, finalize]
InstanceOfCheckForException:
- active: true
+ active: false # It does often make things more readable
excludes: ['**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt']
NotImplementedDeclaration:
active: false
diff --git a/extra-modules/extra-common/build.gradle.kts b/extra-modules/extra-common/build.gradle.kts
index f973315098..2d2eb52dd8 100644
--- a/extra-modules/extra-common/build.gradle.kts
+++ b/extra-modules/extra-common/build.gradle.kts
@@ -1,9 +1,9 @@
plugins {
- `maven-publish`
+ `kordex-module`
+ `published-module`
+ `dokka-module`
+ `disable-explicit-api-mode`
- id("io.gitlab.arturbosch.detekt")
-
- kotlin("jvm")
kotlin("plugin.serialization")
}
@@ -25,68 +25,7 @@ dependencies {
implementation(project(":kord-extensions"))
}
-/**
- * You probably don't want to touch anything below this line. It contains mostly boilerplate, and expands variables
- * from the gradle.properties file.
- */
-
-val sourceJar = task("sourceJar", Jar::class) {
- dependsOn(tasks["classes"])
- archiveClassifier.set("sources")
- from(sourceSets.main.get().allSource)
-}
-
-detekt {
- buildUponDefaultConfig = true
- config = rootProject.files("detekt.yml")
-
- autoCorrect = true
-}
-
-sourceSets {
- main {
- java {
- srcDir(file("$buildDir/generated/ksp/main/kotlin/"))
- }
- }
-
- test {
- java {
- srcDir(file("$buildDir/generated/ksp/test/kotlin/"))
- }
- }
-}
-
-tasks.build {
- this.finalizedBy(sourceJar)
-}
-
-publishing {
- repositories {
- maven {
- name = "KotDis"
-
- url = if (project.version.toString().contains("SNAPSHOT")) {
- uri("https://maven.kotlindiscord.com/repository/maven-snapshots/")
- } else {
- uri("https://maven.kotlindiscord.com/repository/maven-releases/")
- }
-
- credentials {
- username = project.findProperty("kotdis.user") as String? ?: System.getenv("KOTLIN_DISCORD_USER")
- password = project.findProperty("kotdis.password") as String?
- ?: System.getenv("KOTLIN_DISCORD_PASSWORD")
- }
-
- version = project.version
- }
- }
-
- publications {
- create("maven") {
- from(components.getByName("java"))
-
- artifact(sourceJar)
- }
- }
+kordex {
+ jvmTarget.set("9")
+ javaVersion.set(JavaVersion.VERSION_1_9)
}
diff --git a/extra-modules/extra-common/src/main/kotlin/com/kotlindiscord/kordex/ext/common/extensions/EmojiExtension.kt b/extra-modules/extra-common/src/main/kotlin/com/kotlindiscord/kordex/ext/common/extensions/EmojiExtension.kt
index d095d6d5bc..b671c9b2fa 100644
--- a/extra-modules/extra-common/src/main/kotlin/com/kotlindiscord/kordex/ext/common/extensions/EmojiExtension.kt
+++ b/extra-modules/extra-common/src/main/kotlin/com/kotlindiscord/kordex/ext/common/extensions/EmojiExtension.kt
@@ -1,8 +1,8 @@
package com.kotlindiscord.kordex.ext.common.extensions
import com.kotlindiscord.kord.extensions.checks.inGuild
-import com.kotlindiscord.kord.extensions.checks.or
import com.kotlindiscord.kord.extensions.extensions.Extension
+import com.kotlindiscord.kord.extensions.extensions.event
import com.kotlindiscord.kordex.ext.common.builders.ExtCommonBuilder
import com.kotlindiscord.kordex.ext.common.configuration.emoji.EmojiConfig
import com.kotlindiscord.kordex.ext.common.emoji.NamedEmoji
@@ -24,19 +24,15 @@ class EmojiExtension : Extension() {
}
event {
- check(
- or(
- // No configured guilds? Do them all.
- { config.getGuilds().isEmpty() },
-
- {
- config.getGuilds()
- .mapNotNull { kord.getGuild(it) }
- .map { inGuild { it } }
- .any()
- }
- )
- )
+ check {
+ failIfNot {
+ config.getGuilds().isEmpty() ||
+ config.getGuilds()
+ .mapNotNull { kord.getGuild(it) }
+ .map { guild -> inGuild { guild } }
+ .any()
+ }
+ }
action { populateEmojis(event.guildId) }
}
diff --git a/extra-modules/extra-mappings/README.md b/extra-modules/extra-mappings/README.md
index 3f7001e804..ec10bcf155 100644
--- a/extra-modules/extra-mappings/README.md
+++ b/extra-modules/extra-mappings/README.md
@@ -14,15 +14,19 @@ If you're looking for older versions (and older tags), you can find them
* **Maven repo:** `https://maven.kotlindiscord.com/repository/maven-public/`
* **Maven coordinates:** `com.kotlindiscord.kord.extensions:extra-mappings:VERSION`
-* If Fabric dependencies fail to resolve, you may also need to add their Maven repo: `https://maven.fabricmc.net`
+
+* Some extra Maven repos are required:
+ * **FabricMC**: `https://maven.fabricmc.net/`
+ * **QuiltMC (Releases)**: `https://maven.quiltmc.org/repository/release/`
+ * **QuiltMC (Snapshots)**: `https://maven.quiltmc.org/repository/snapshot/`
At its simplest, you can add this extension directly to your bot with no further configuration. For example:
```kotlin
suspend fun main() {
val bot = ExtensibleBot(System.getenv("TOKEN")) {
- commands {
- defaultPrefix = "!"
+ chatCommands {
+ enabled = true
}
extensions {
@@ -42,8 +46,9 @@ they're detailed below.
This extension provides a number of commands for use on Discord.
-* Commands for retrieving information about mappings namespaces: `legacy-yarn`, `mcp`, `mojang`, `plasma`, `yarn`
+* Commands for retrieving information about mappings namespaces: `hashed`, `legacy-yarn`, `mcp`, `mojang`, `plasma`, `yarn`
and `yarrn`
+* Hashed Mojang-specific lookup commands: `hc`, `hf` and `hm`
* Legacy Yarn-specific lookup commands: `lyc`, `lyf` and `lym`
* MCP-specific lookup commands: `mcpc`, `mcpf` and `mcpm`
* Mojang-specific lookup commands: `mmc`, `mmf` and `mmm`
@@ -80,7 +85,7 @@ following configuration keys are available:
other guilds. This setting takes priority over `guilds.banned`.
* `guilds.banned`: List of guilds mappings commands may **not** be run within. When set, mappings commands may not be
run within the given guilds.
-* `settings.namespaces`: List of enabled namespaces. Currently, `legacy-yarn`, `mcp`, `mojang`, `plasma`, `yarn`
+* `settings.namespaces`: List of enabled namespaces. Currently, `hashed-mojang`, `legacy-yarn`, `mcp`, `mojang`, `plasma`, `yarn`
and `yarrn` are supported, and they will all be enabled by default.
* `settings.timeout`: Time (in seconds) to wait before destroying mappings paginators, defaulting to 5 minutes (300
seconds). Be careful when setting this value to something high - a busy bot may end up running out of memory if
diff --git a/extra-modules/extra-mappings/build.gradle.kts b/extra-modules/extra-mappings/build.gradle.kts
index fa90d90bab..09670d3828 100644
--- a/extra-modules/extra-mappings/build.gradle.kts
+++ b/extra-modules/extra-mappings/build.gradle.kts
@@ -1,9 +1,8 @@
plugins {
- `maven-publish`
-
- id("io.gitlab.arturbosch.detekt")
-
- kotlin("jvm")
+ `kordex-module`
+ `published-module`
+ `dokka-module`
+ `disable-explicit-api-mode`
}
repositories {
@@ -17,6 +16,16 @@ repositories {
url = uri("https://maven.fabricmc.net/")
}
+ maven {
+ name = "QuiltMC (Releases)"
+ url = uri("https://maven.quiltmc.org/repository/release/")
+ }
+
+ maven {
+ name = "QuiltMC (Snapshots)"
+ url = uri("https://maven.quiltmc.org/repository/snapshot/")
+ }
+
maven {
name = "Shedaniel"
url = uri("https://maven.shedaniel.me")
@@ -46,63 +55,7 @@ dependencies {
group = "com.kotlindiscord.kord.extensions"
-val sourceJar = task("sourceJar", Jar::class) {
- dependsOn(tasks["classes"])
- archiveClassifier.set("sources")
- from(sourceSets.main.get().allSource)
-}
-
-detekt {
- buildUponDefaultConfig = true
- config = rootProject.files("detekt.yml")
-
- autoCorrect = true
-}
-
-sourceSets {
- main {
- java {
- srcDir(file("$buildDir/generated/ksp/main/kotlin/"))
- }
- }
-
- test {
- java {
- srcDir(file("$buildDir/generated/ksp/test/kotlin/"))
- }
- }
-}
-
-tasks.build {
- this.finalizedBy(sourceJar)
-}
-
-publishing {
- repositories {
- maven {
- name = "KotDis"
-
- url = if (project.version.toString().contains("SNAPSHOT")) {
- uri("https://maven.kotlindiscord.com/repository/maven-snapshots/")
- } else {
- uri("https://maven.kotlindiscord.com/repository/maven-releases/")
- }
-
- credentials {
- username = project.findProperty("kotdis.user") as String? ?: System.getenv("KOTLIN_DISCORD_USER")
- password = project.findProperty("kotdis.password") as String?
- ?: System.getenv("KOTLIN_DISCORD_PASSWORD")
- }
-
- version = project.version
- }
- }
-
- publications {
- create("maven") {
- from(components.getByName("java"))
-
- artifact(sourceJar)
- }
- }
+kordex {
+ jvmTarget.set("9")
+ javaVersion.set(JavaVersion.VERSION_1_9)
}
diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/Checks.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/Checks.kt
index 1dbcadfec4..1f3683470d 100644
--- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/Checks.kt
+++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/Checks.kt
@@ -2,7 +2,7 @@ package com.kotlindiscord.kord.extensions.modules.extra.mappings
import com.kotlindiscord.kord.extensions.checks.channelFor
import com.kotlindiscord.kord.extensions.checks.guildFor
-import com.kotlindiscord.kord.extensions.checks.types.Check
+import com.kotlindiscord.kord.extensions.checks.types.CheckContext
import dev.kord.common.entity.Snowflake
import dev.kord.core.entity.channel.CategorizableChannel
import dev.kord.core.entity.channel.GuildChannel
@@ -26,19 +26,19 @@ import mu.KotlinLogging
* @param allowed List of allowed category IDs
* @param banned List of banned category IDs
*/
-fun allowedCategory(
+suspend fun CheckContext.allowedCategory(
allowed: List,
banned: List
-): Check = {
+) {
val logger = KotlinLogging.logger { }
val channel = channelFor(event)
if (channel == null) {
- logger.debug { "Passing: Event is not channel-related" }
+ logger.trace { "Passing: Event is not channel-related" }
pass()
} else if (channel !is CategorizableChannel) {
- logger.debug { "Passing: Channel is not categorizable (eg, it's a DM)" }
+ logger.trace { "Passing: Channel is not categorizable (eg, it's a DM)" }
pass()
} else {
@@ -50,7 +50,7 @@ fun allowedCategory(
fail()
} else if (allowed.contains(parent.id)) {
- logger.debug { "Passing: Event happened in an allowed category" }
+ logger.trace { "Passing: Event happened in an allowed category" }
pass()
} else {
@@ -60,14 +60,14 @@ fun allowedCategory(
}
} else {
if (parent == null) {
- logger.debug {
+ logger.trace {
"Passing: We have no allowed categories, and the message was sent outside of a category"
}
pass()
} else if (banned.isNotEmpty()) {
if (!banned.contains(parent.id)) {
- logger.debug { "Passing: Event did not happen in a banned category" }
+ logger.trace { "Passing: Event did not happen in a banned category" }
pass()
} else {
@@ -76,7 +76,7 @@ fun allowedCategory(
fail()
}
} else {
- logger.debug { "Passing: No allowed or banned categories configured" }
+ logger.trace { "Passing: No allowed or banned categories configured" }
pass()
}
@@ -100,24 +100,24 @@ fun allowedCategory(
* @param allowed List of allowed channel IDs
* @param banned List of banned channel IDs
*/
-fun allowedChannel(
+suspend fun CheckContext.allowedChannel(
allowed: List,
banned: List
-): Check = {
+) {
val logger = KotlinLogging.logger { }
val channel = channelFor(event)
if (channel == null) {
- logger.debug { "Passing: Event is not channel-related" }
+ logger.trace { "Passing: Event is not channel-related" }
pass()
} else if (channel !is GuildChannel) {
- logger.debug { "Passing: Message was sent privately" }
+ logger.trace { "Passing: Message was sent privately" }
pass() // It's a DM
} else if (allowed.isNotEmpty()) {
if (allowed.contains(channel.id)) {
- logger.debug { "Passing: Event happened in an allowed channel" }
+ logger.trace { "Passing: Event happened in an allowed channel" }
pass()
} else {
@@ -127,7 +127,7 @@ fun allowedChannel(
}
} else if (banned.isNotEmpty()) {
if (!banned.contains(channel.id)) {
- logger.debug { "Passing: Event did not happen in a banned channel" }
+ logger.trace { "Passing: Event did not happen in a banned channel" }
pass()
} else {
@@ -136,7 +136,7 @@ fun allowedChannel(
fail()
}
} else {
- logger.debug { "Passing: No allowed or banned channels configured" }
+ logger.trace { "Passing: No allowed or banned channels configured" }
pass()
}
@@ -157,20 +157,20 @@ fun allowedChannel(
* @param allowed List of allowed guild IDs
* @param banned List of banned guild IDs
*/
-fun allowedGuild(
+suspend fun CheckContext.allowedGuild(
allowed: List,
banned: List
-): Check = {
+) {
val logger = KotlinLogging.logger { }
val guild = guildFor(event)
if (guild == null) {
- logger.debug { "Passing: Event is not guild-related" }
+ logger.trace { "Passing: Event is not guild-related" }
pass()
} else if (allowed.isNotEmpty()) {
if (allowed.contains(guild.id)) {
- logger.debug { "Passing: Event happened in an allowed guild" }
+ logger.trace { "Passing: Event happened in an allowed guild" }
pass()
} else {
@@ -180,7 +180,7 @@ fun allowedGuild(
}
} else if (banned.isNotEmpty()) {
if (!banned.contains(guild.id)) {
- logger.debug { "Passing: Event did not happen in a banned guild" }
+ logger.trace { "Passing: Event did not happen in a banned guild" }
pass()
} else {
@@ -189,7 +189,7 @@ fun allowedGuild(
fail()
}
} else {
- logger.debug { "Passing: No allowed or banned guilds configured" }
+ logger.trace { "Passing: No allowed or banned guilds configured" }
pass()
}
diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/MappingsExtension.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/MappingsExtension.kt
index f742f2375c..b59483b403 100644
--- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/MappingsExtension.kt
+++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/MappingsExtension.kt
@@ -2,11 +2,12 @@
package com.kotlindiscord.kord.extensions.modules.extra.mappings
-import com.kotlindiscord.kord.extensions.checks.and
import com.kotlindiscord.kord.extensions.checks.types.Check
-import com.kotlindiscord.kord.extensions.commands.MessageCommandContext
-import com.kotlindiscord.kord.extensions.commands.parser.Arguments
+import com.kotlindiscord.kord.extensions.checks.types.CheckContext
+import com.kotlindiscord.kord.extensions.commands.Arguments
+import com.kotlindiscord.kord.extensions.commands.chat.ChatCommandContext
import com.kotlindiscord.kord.extensions.extensions.Extension
+import com.kotlindiscord.kord.extensions.extensions.chatCommand
import com.kotlindiscord.kord.extensions.modules.extra.mappings.arguments.*
import com.kotlindiscord.kord.extensions.modules.extra.mappings.builders.ExtMappingsBuilder
import com.kotlindiscord.kord.extensions.modules.extra.mappings.enums.Channels
@@ -19,8 +20,12 @@ import com.kotlindiscord.kord.extensions.pagination.EXPAND_EMOJI
import com.kotlindiscord.kord.extensions.pagination.MessageButtonPaginator
import com.kotlindiscord.kord.extensions.pagination.pages.Page
import com.kotlindiscord.kord.extensions.pagination.pages.Pages
+import com.kotlindiscord.kord.extensions.sentry.BreadcrumbType
import com.kotlindiscord.kord.extensions.utils.respond
import dev.kord.core.behavior.channel.withTyping
+import dev.kord.core.event.message.MessageCreateEvent
+import kotlinx.coroutines.newSingleThreadContext
+import kotlinx.coroutines.withContext
import me.shedaniel.linkie.*
import me.shedaniel.linkie.namespaces.*
import me.shedaniel.linkie.utils.MappingsQuery
@@ -49,6 +54,7 @@ class MappingsExtension : Extension() {
"legacy-yarn" -> namespaces.add(LegacyYarnNamespace)
"mcp" -> namespaces.add(MCPNamespace)
"mojang" -> namespaces.add(MojangNamespace)
+ "hashed-mojang" -> namespaces.add(MojangHashedNamespace)
"plasma" -> namespaces.add(PlasmaNamespace)
"yarn" -> namespaces.add(YarnNamespace)
"yarrn" -> namespaces.add(YarrnNamespace)
@@ -67,15 +73,24 @@ class MappingsExtension : Extension() {
val legacyYarnEnabled = enabledNamespaces.contains("legacy-yarn")
val mcpEnabled = enabledNamespaces.contains("mcp")
val mojangEnabled = enabledNamespaces.contains("mojang")
+ val hashedMojangEnabled = enabledNamespaces.contains("hashed-mojang")
val plasmaEnabled = enabledNamespaces.contains("plasma")
val yarnEnabled = enabledNamespaces.contains("yarn")
val yarrnEnabled = enabledNamespaces.contains("yarrn")
val patchworkEnabled = builder.config.yarnChannelEnabled(YarnChannels.PATCHWORK)
- val categoryCheck = allowedCategory(builder.config.getAllowedCategories(), builder.config.getBannedCategories())
- val channelCheck = allowedGuild(builder.config.getAllowedChannels(), builder.config.getBannedChannels())
- val guildCheck = allowedGuild(builder.config.getAllowedGuilds(), builder.config.getBannedGuilds())
+ val categoryCheck: Check = {
+ allowedCategory(builder.config.getAllowedCategories(), builder.config.getBannedCategories())
+ }
+
+ val channelCheck: Check = {
+ allowedGuild(builder.config.getAllowedChannels(), builder.config.getBannedChannels())
+ }
+
+ val guildCheck: Check = {
+ allowedGuild(builder.config.getAllowedGuilds(), builder.config.getBannedGuilds())
+ }
val yarnChannels = YarnChannels.values().filter {
it != YarnChannels.PATCHWORK || patchworkEnabled
@@ -85,7 +100,7 @@ class MappingsExtension : Extension() {
if (legacyYarnEnabled) {
// Class
- command(::LegacyYarnArguments) {
+ chatCommand(::LegacyYarnArguments) {
name = "lyc"
aliases = arrayOf("lyarnc", "legacy-yarnc", "legacyyarnc", "legacyarnc")
@@ -94,7 +109,7 @@ class MappingsExtension : Extension() {
"For more information or a list of versions for Legacy Yarn mappings, you can use the " +
"`lyarn` command."
- check(customChecks(name, LegacyYarnNamespace))
+ check { customChecks(name, LegacyYarnNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -103,7 +118,7 @@ class MappingsExtension : Extension() {
}
// Field
- command(::LegacyYarnArguments) {
+ chatCommand(::LegacyYarnArguments) {
name = "lyf"
aliases = arrayOf("lyarnf", "legacy-yarnf", "legacyyarnf", "legacyarnf")
@@ -112,7 +127,7 @@ class MappingsExtension : Extension() {
"For more information or a list of versions for Legacy Yarn mappings, you can use the " +
"`lyarn` command."
- check(customChecks(name, LegacyYarnNamespace))
+ check { customChecks(name, LegacyYarnNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -121,7 +136,7 @@ class MappingsExtension : Extension() {
}
// Method
- command(::LegacyYarnArguments) {
+ chatCommand(::LegacyYarnArguments) {
name = "lym"
aliases = arrayOf("lyarnm", "legacy-yarnm", "legacyyarnm", "legacyarnm")
@@ -130,7 +145,7 @@ class MappingsExtension : Extension() {
"For more information or a list of versions for Legacy Yarn mappings, you can use the " +
"`lyarn` command."
- check(customChecks(name, LegacyYarnNamespace))
+ check { customChecks(name, LegacyYarnNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -145,14 +160,14 @@ class MappingsExtension : Extension() {
if (mcpEnabled) {
// Class
- command(::MCPArguments) {
+ chatCommand(::MCPArguments) {
name = "mcpc"
description = "Look up MCP mappings info for a class.\n\n" +
"For more information or a list of versions for MCP mappings, you can use the `mcp` command."
- check(customChecks(name, MCPNamespace))
+ check { customChecks(name, MCPNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -161,14 +176,14 @@ class MappingsExtension : Extension() {
}
// Field
- command(::MCPArguments) {
+ chatCommand(::MCPArguments) {
name = "mcpf"
description = "Look up MCP mappings info for a field.\n\n" +
"For more information or a list of versions for MCP mappings, you can use the `mcp` command."
- check(customChecks(name, MCPNamespace))
+ check { customChecks(name, MCPNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -177,14 +192,14 @@ class MappingsExtension : Extension() {
}
// Method
- command(::MCPArguments) {
+ chatCommand(::MCPArguments) {
name = "mcpm"
description = "Look up MCP mappings info for a method.\n\n" +
"For more information or a list of versions for MCP mappings, you can use the `mcp` command."
- check(customChecks(name, MCPNamespace))
+ check { customChecks(name, MCPNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -199,7 +214,7 @@ class MappingsExtension : Extension() {
if (mojangEnabled) {
// Class
- command(::MojangArguments) {
+ chatCommand(::MojangArguments) {
name = "mmc"
aliases = arrayOf("mojc", "mojmapc")
@@ -211,7 +226,7 @@ class MappingsExtension : Extension() {
"For more information or a list of versions for Mojang mappings, you can use the `mojang` " +
"command."
- check(customChecks(name, MojangNamespace))
+ check { customChecks(name, MojangNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -220,7 +235,7 @@ class MappingsExtension : Extension() {
}
// Field
- command(::MojangArguments) {
+ chatCommand(::MojangArguments) {
name = "mmf"
aliases = arrayOf("mojf", "mojmapf")
@@ -232,7 +247,7 @@ class MappingsExtension : Extension() {
"For more information or a list of versions for Mojang mappings, you can use the `mojang` " +
"command."
- check(customChecks(name, MojangNamespace))
+ check { customChecks(name, MojangNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -241,7 +256,7 @@ class MappingsExtension : Extension() {
}
// Method
- command(::MojangArguments) {
+ chatCommand(::MojangArguments) {
name = "mmm"
aliases = arrayOf("mojm", "mojmapm")
@@ -253,7 +268,7 @@ class MappingsExtension : Extension() {
"For more information or a list of versions for Mojang mappings, you can use the `mojang` " +
"command."
- check(customChecks(name, MojangNamespace))
+ check { customChecks(name, MojangNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -264,11 +279,80 @@ class MappingsExtension : Extension() {
// endregion
+ // region: Hashed Mojang mappings lookups
+
+ if (hashedMojangEnabled) {
+ // Class
+ chatCommand(::HashedMojangArguments) {
+ name = "hc"
+ aliases = arrayOf("hmojc", "hmojmapc", "hmc", "qhc")
+
+ description = "Look up Hashed Mojang mappings info for a class.\n\n" +
+
+ "**Channels:** " + Channels.values().joinToString(", ") { "`${it.str}`" } +
+ "\n\n" +
+
+ "For more information or a list of versions for Mojang mappings, you can use the `mojang` " +
+ "command."
+
+ check { customChecks(name, MojangHashedNamespace) }
+ check(categoryCheck, channelCheck, guildCheck) // Default checks
+
+ action {
+ queryClasses(MojangHashedNamespace, arguments.query, arguments.version, arguments.channel?.str)
+ }
+ }
+
+ // Field
+ chatCommand(::HashedMojangArguments) {
+ name = "hf"
+ aliases = arrayOf("hmojf", "hmojmapf", "hmf", "qhf")
+
+ description = "Look up Hashed Mojang mappings info for a field.\n\n" +
+
+ "**Channels:** " + Channels.values().joinToString(", ") { "`${it.str}`" } +
+ "\n\n" +
+
+ "For more information or a list of versions for Mojang mappings, you can use the `mojang` " +
+ "command."
+
+ check { customChecks(name, MojangHashedNamespace) }
+ check(categoryCheck, channelCheck, guildCheck) // Default checks
+
+ action {
+ queryFields(MojangHashedNamespace, arguments.query, arguments.version, arguments.channel?.str)
+ }
+ }
+
+ // Method
+ chatCommand(::HashedMojangArguments) {
+ name = "hm"
+ aliases = arrayOf("hmojm", "hmojmapm", "hmm", "qhm")
+
+ description = "Look up Hashed Mojang mappings info for a method.\n\n" +
+
+ "**Channels:** " + Channels.values().joinToString(", ") { "`${it.str}`" } +
+ "\n\n" +
+
+ "For more information or a list of versions for Mojang mappings, you can use the `mojang` " +
+ "command."
+
+ check { customChecks(name, MojangHashedNamespace) }
+ check(categoryCheck, channelCheck, guildCheck) // Default checks
+
+ action {
+ queryMethods(MojangHashedNamespace, arguments.query, arguments.version, arguments.channel?.str)
+ }
+ }
+ }
+
+ // endregion
+
// region: Plasma mappings lookups
if (plasmaEnabled) {
// Class
- command(::PlasmaArguments) {
+ chatCommand(::PlasmaArguments) {
name = "pc"
description = "Look up Plasma mappings info for a class.\n\n" +
@@ -276,7 +360,7 @@ class MappingsExtension : Extension() {
"For more information or a list of versions for Plasma mappings, you can use the " +
"`plasma` command."
- check(customChecks(name, PlasmaNamespace))
+ check { customChecks(name, PlasmaNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -285,7 +369,7 @@ class MappingsExtension : Extension() {
}
// Field
- command(::PlasmaArguments) {
+ chatCommand(::PlasmaArguments) {
name = "pf"
description = "Look up Plasma mappings info for a field.\n\n" +
@@ -293,7 +377,7 @@ class MappingsExtension : Extension() {
"For more information or a list of versions for Plasma mappings, you can use the " +
"`plasma` command."
- check(customChecks(name, PlasmaNamespace))
+ check { customChecks(name, PlasmaNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -302,7 +386,7 @@ class MappingsExtension : Extension() {
}
// Method
- command(::PlasmaArguments) {
+ chatCommand(::PlasmaArguments) {
name = "pm"
description = "Look up Plasma mappings info for a method.\n\n" +
@@ -310,7 +394,7 @@ class MappingsExtension : Extension() {
"For more information or a list of versions for Plasma mappings, you can use the " +
"`plasma` command."
- check(customChecks(name, PlasmaNamespace))
+ check { customChecks(name, PlasmaNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -325,7 +409,7 @@ class MappingsExtension : Extension() {
if (yarnEnabled) {
// Class
- command({ YarnArguments(patchworkEnabled) }) {
+ chatCommand({ YarnArguments(patchworkEnabled) }) {
name = "yc"
aliases = arrayOf("yarnc")
@@ -337,7 +421,7 @@ class MappingsExtension : Extension() {
"For more information or a list of versions for Yarn mappings, you can use the `yarn` " +
"command."
- check(customChecks(name, YarnNamespace))
+ check { customChecks(name, YarnNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -351,7 +435,7 @@ class MappingsExtension : Extension() {
}
// Field
- command({ YarnArguments(patchworkEnabled) }) {
+ chatCommand({ YarnArguments(patchworkEnabled) }) {
name = "yf"
aliases = arrayOf("yarnf")
@@ -363,7 +447,7 @@ class MappingsExtension : Extension() {
"For more information or a list of versions for Yarn mappings, you can use the `yarn` " +
"command."
- check(customChecks(name, YarnNamespace))
+ check { customChecks(name, YarnNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -377,7 +461,7 @@ class MappingsExtension : Extension() {
}
// Method
- command({ YarnArguments(patchworkEnabled) }) {
+ chatCommand({ YarnArguments(patchworkEnabled) }) {
name = "ym"
aliases = arrayOf("yarnm")
@@ -389,7 +473,7 @@ class MappingsExtension : Extension() {
"For more information or a list of versions for Yarn mappings, you can use the `yarn` " +
"command."
- check(customChecks(name, YarnNamespace))
+ check { customChecks(name, YarnNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -409,7 +493,7 @@ class MappingsExtension : Extension() {
if (yarrnEnabled) {
// Class
- command(::YarrnArguments) {
+ chatCommand(::YarrnArguments) {
name = "yrc"
description = "Look up Yarrn mappings info for a class.\n\n" +
@@ -417,7 +501,7 @@ class MappingsExtension : Extension() {
"For more information or a list of versions for Yarrn mappings, you can use the " +
"`yarrn` command."
- check(customChecks(name, YarrnNamespace))
+ check { customChecks(name, YarrnNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -426,7 +510,7 @@ class MappingsExtension : Extension() {
}
// Field
- command(::YarrnArguments) {
+ chatCommand(::YarrnArguments) {
name = "yrf"
description = "Look up Yarrn mappings info for a field.\n\n" +
@@ -434,7 +518,7 @@ class MappingsExtension : Extension() {
"For more information or a list of versions for Yarrn mappings, you can use the " +
"`yarrn` command."
- check(customChecks(name, YarrnNamespace))
+ check { customChecks(name, YarrnNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -443,7 +527,7 @@ class MappingsExtension : Extension() {
}
// Method
- command(::YarrnArguments) {
+ chatCommand(::YarrnArguments) {
name = "yrm"
description = "Look up Yarrn mappings info for a method.\n\n" +
@@ -451,7 +535,7 @@ class MappingsExtension : Extension() {
"For more information or a list of versions for Yarrn mappings, you can use the " +
"`yarrn` command."
- check(customChecks(name, YarrnNamespace))
+ check { customChecks(name, YarrnNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -465,13 +549,13 @@ class MappingsExtension : Extension() {
// region: Mappings info commands
if (legacyYarnEnabled) {
- command {
+ chatCommand {
name = "lyarn"
aliases = arrayOf("legacy-yarn", "legacyyarn", "legacyarn")
description = "Get information and a list of supported versions for Legacy Yarn mappings."
- check(customChecks(name, LegacyYarnNamespace))
+ check { customChecks(name, LegacyYarnNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -517,7 +601,6 @@ class MappingsExtension : Extension() {
}
val paginator = MessageButtonPaginator(
- extension = this@MappingsExtension,
targetMessage = event.message,
pages = pagesObj,
keepEmbed = true,
@@ -532,12 +615,12 @@ class MappingsExtension : Extension() {
}
if (mcpEnabled) {
- command {
+ chatCommand {
name = "mcp"
description = "Get information and a list of supported versions for MCP mappings."
- check(customChecks(name, MCPNamespace))
+ check { customChecks(name, MCPNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -582,7 +665,6 @@ class MappingsExtension : Extension() {
}
val paginator = MessageButtonPaginator(
- extension = this@MappingsExtension,
targetMessage = event.message,
pages = pagesObj,
keepEmbed = true,
@@ -597,13 +679,13 @@ class MappingsExtension : Extension() {
}
if (mojangEnabled) {
- command {
+ chatCommand {
name = "mojang"
aliases = arrayOf("mojmap")
description = "Get information and a list of supported versions for Mojang mappings."
- check(customChecks(name, MojangNamespace))
+ check { customChecks(name, MojangNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -651,7 +733,75 @@ class MappingsExtension : Extension() {
}
val paginator = MessageButtonPaginator(
- extension = this@MappingsExtension,
+ targetMessage = event.message,
+ pages = pagesObj,
+ keepEmbed = true,
+ owner = message.author,
+ timeoutSeconds = getTimeout(),
+ locale = getLocale(),
+ )
+
+ paginator.send()
+ }
+ }
+ }
+
+ if (hashedMojangEnabled) {
+ chatCommand {
+ name = "hashed"
+ aliases = arrayOf("hashed-mojmap", "hashed-mojang", "quilt-hashed", "qh", "hm")
+
+ description = "Get information and a list of supported versions for hashed Mojang mappings."
+
+ check { customChecks(name, MojangHashedNamespace) }
+ check(categoryCheck, channelCheck, guildCheck) // Default checks
+
+ action {
+ val defaultVersion = MojangHashedNamespace.getDefaultVersion()
+ val allVersions = MojangHashedNamespace.getAllSortedVersions()
+
+ val pages = allVersions.chunked(VERSION_CHUNK_SIZE).map {
+ it.joinToString("\n") { version ->
+ if (version == defaultVersion) {
+ "**» $version** (Default)"
+ } else {
+ "**»** $version"
+ }
+ }
+ }.toMutableList()
+
+ pages.add(
+ 0,
+ "Hashed Mojang mappings are available for queries across **${allVersions.size}** " +
+ "versions.\n\n" +
+
+ "**Default version:** $defaultVersion\n\n" +
+
+ "**Channels:** " + Channels.values().joinToString(", ") { "`${it.str}`" } +
+ "\n" +
+ "**Commands:** `hc`, `hf`, `hm`\n\n" +
+
+ "For a full list of supported hashed Mojang versions, please view the rest of the pages."
+ )
+
+ val pagesObj = Pages()
+ val pageTitle = "Mappings info: Hashed Mojang"
+
+ pages.forEach {
+ pagesObj.addPage(
+ Page {
+ description = it
+ title = pageTitle
+
+ footer {
+ text = PAGE_FOOTER
+ icon = PAGE_FOOTER_ICON
+ }
+ }
+ )
+ }
+
+ val paginator = MessageButtonPaginator(
targetMessage = event.message,
pages = pagesObj,
keepEmbed = true,
@@ -666,12 +816,12 @@ class MappingsExtension : Extension() {
}
if (plasmaEnabled) {
- command {
+ chatCommand {
name = "plasma"
description = "Get information and a list of supported versions for Plasma mappings."
- check(customChecks(name, PlasmaNamespace))
+ check { customChecks(name, PlasmaNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -717,7 +867,6 @@ class MappingsExtension : Extension() {
}
val paginator = MessageButtonPaginator(
- extension = this@MappingsExtension,
targetMessage = event.message,
pages = pagesObj,
keepEmbed = true,
@@ -732,12 +881,12 @@ class MappingsExtension : Extension() {
}
if (yarnEnabled) {
- command {
+ chatCommand {
name = "yarn"
description = "Get information and a list of supported versions for Yarn mappings."
- check(customChecks(name, YarnNamespace))
+ check { customChecks(name, YarnNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -811,7 +960,6 @@ class MappingsExtension : Extension() {
}
val paginator = MessageButtonPaginator(
- extension = this@MappingsExtension,
targetMessage = event.message,
pages = pagesObj,
keepEmbed = true,
@@ -826,12 +974,12 @@ class MappingsExtension : Extension() {
}
if (yarrnEnabled) {
- command {
+ chatCommand {
name = "yarrn"
description = "Get information and a list of supported versions for Yarrn mappings."
- check(customChecks(name, YarrnNamespace))
+ check { customChecks(name, YarrnNamespace) }
check(categoryCheck, channelCheck, guildCheck) // Default checks
action {
@@ -877,7 +1025,6 @@ class MappingsExtension : Extension() {
}
val paginator = MessageButtonPaginator(
- extension = this@MappingsExtension,
targetMessage = event.message,
pages = pagesObj,
keepEmbed = true,
@@ -896,347 +1043,465 @@ class MappingsExtension : Extension() {
logger.info { "Mappings extension set up - namespaces: " + enabledNamespaces.joinToString(", ") }
}
- private suspend fun MessageCommandContext.queryClasses(
+ private suspend fun ChatCommandContext.queryClasses(
namespace: Namespace,
givenQuery: String,
version: MappingsContainer?,
channel: String? = null
) {
- val provider = if (version == null) {
- if (channel != null) {
- namespace.getProvider(
- namespace.getDefaultVersion { channel }
- )
- } else {
- MappingsProvider.empty(namespace)
- }
- } else {
- namespace.getProvider(version.version)
+ sentry.breadcrumb(BreadcrumbType.Query) {
+ message = "Beginning class lookup"
+
+ data["channel"] = channel ?: "N/A"
+ data["namespace"] = namespace.id
+ data["query"] = givenQuery
+ data["version"] = version?.version ?: "N/A"
}
- provider.injectDefaultVersion(
- namespace.getDefaultProvider {
- channel ?: namespace.getDefaultMappingChannel()
- }
- )
-
- val query = givenQuery.replace(".", "/")
- var pages: List>
-
- message.channel.withTyping {
- @Suppress("TooGenericExceptionCaught")
- val result = try {
- MappingsQuery.queryClasses(
- QueryContext(
- provider = provider,
- searchKey = query
- )
- )
- } catch (e: NullPointerException) {
- message.respond(e.localizedMessage)
- return@queryClasses
- }
+ val context = newSingleThreadContext("c: $givenQuery")
- pages = classesToPages(namespace, result)
- }
+ try {
+ withContext(context) {
+ val provider = if (version == null) {
+ if (channel != null) {
+ namespace.getProvider(
+ namespace.getDefaultVersion { channel }
+ )
+ } else {
+ MappingsProvider.empty(namespace)
+ }
+ } else {
+ namespace.getProvider(version.version)
+ }
- if (pages.isEmpty()) {
- message.respond("No results found")
- return
- }
+ provider.injectDefaultVersion(
+ namespace.getDefaultProvider {
+ channel ?: namespace.getDefaultMappingChannel()
+ }
+ )
- val meta = provider.get()
+ sentry.breadcrumb(BreadcrumbType.Info) {
+ message = "Provider resolved, with injected default version"
- val pagesObj = Pages("${EXPAND_EMOJI.mention} for more")
- val pageTitle = "List of ${meta.name} classes: ${meta.version}"
+ data["version"] = provider.version ?: "Unknown"
+ }
- val shortPages = mutableListOf()
- val longPages = mutableListOf()
+ val query = givenQuery.replace(".", "/")
+ var pages: List>
- pages.forEach { (short, long) ->
- shortPages.add(short)
- longPages.add(long)
- }
+ sentry.breadcrumb(BreadcrumbType.Info) {
+ message = "Attempting to run sanitized query"
- shortPages.forEach {
- pagesObj.addPage(
- "${EXPAND_EMOJI.mention} for more",
+ data["query"] = query
+ }
+
+ message.channel.withTyping {
+ @Suppress("TooGenericExceptionCaught")
+ val result = try {
+ MappingsQuery.queryClasses(
+ QueryContext(
+ provider = provider,
+ searchKey = query
+ )
+ )
+ } catch (e: NullPointerException) {
+ message.respond(e.localizedMessage)
+ return@withContext
+ }
- Page {
- description = it
- title = pageTitle
+ sentry.breadcrumb(BreadcrumbType.Info) {
+ message = "Generating pages for results"
- footer {
- text = PAGE_FOOTER
- icon = PAGE_FOOTER_ICON
+ data["resultCount"] = result.value.size
}
+
+ pages = classesToPages(namespace, result)
+ }
+
+ if (pages.isEmpty()) {
+ message.respond("No results found")
+ return@withContext
}
- )
- }
- if (shortPages != longPages) {
- longPages.forEach {
- pagesObj.addPage(
- "${EXPAND_EMOJI.mention} for less",
+ val meta = provider.get()
- Page {
- description = it
- title = pageTitle
+ val pagesObj = Pages("${EXPAND_EMOJI.mention} for more")
+ val pageTitle = "List of ${meta.name} classes: ${meta.version}"
- footer {
- text = PAGE_FOOTER
- icon = PAGE_FOOTER_ICON
+ val shortPages = mutableListOf()
+ val longPages = mutableListOf()
+
+ pages.forEach { (short, long) ->
+ shortPages.add(short)
+ longPages.add(long)
+ }
+
+ shortPages.forEach {
+ pagesObj.addPage(
+ "${EXPAND_EMOJI.mention} for more",
+
+ Page {
+ description = it
+ title = pageTitle
+
+ footer {
+ text = PAGE_FOOTER
+ icon = PAGE_FOOTER_ICON
+ }
}
+ )
+ }
+
+ if (shortPages != longPages) {
+ longPages.forEach {
+ pagesObj.addPage(
+ "${EXPAND_EMOJI.mention} for less",
+
+ Page {
+ description = it
+ title = pageTitle
+
+ footer {
+ text = PAGE_FOOTER
+ icon = PAGE_FOOTER_ICON
+ }
+ }
+ )
}
+ }
+
+ sentry.breadcrumb(BreadcrumbType.Info) {
+ message = "Creating and sending paginator to Discord"
+ }
+
+ val paginator = MessageButtonPaginator(
+ targetMessage = event.message,
+ pages = pagesObj,
+ keepEmbed = true,
+ owner = message.author,
+ timeoutSeconds = getTimeout(),
+ locale = getLocale(),
)
+
+ paginator.send()
}
+ } finally {
+ context.close()
}
-
- val paginator = MessageButtonPaginator(
- extension = this@MappingsExtension,
- targetMessage = event.message,
- pages = pagesObj,
- keepEmbed = true,
- owner = message.author,
- timeoutSeconds = getTimeout(),
- locale = getLocale(),
- )
-
- paginator.send()
}
- private suspend fun MessageCommandContext.queryFields(
+ private suspend fun ChatCommandContext.queryFields(
namespace: Namespace,
givenQuery: String,
version: MappingsContainer?,
channel: String? = null
) {
- val provider = if (version == null) {
- if (channel != null) {
- namespace.getProvider(
- namespace.getDefaultVersion { channel }
- )
- } else {
- MappingsProvider.empty(namespace)
- }
- } else {
- namespace.getProvider(version.version)
+ sentry.breadcrumb(BreadcrumbType.Query) {
+ message = "Beginning field lookup"
+
+ data["channel"] = channel ?: "N/A"
+ data["namespace"] = namespace.id
+ data["query"] = givenQuery
+ data["version"] = version?.version ?: "N/A"
}
- provider.injectDefaultVersion(
- namespace.getDefaultProvider {
- channel ?: namespace.getDefaultMappingChannel()
- }
- )
-
- val query = givenQuery.replace(".", "/")
- var pages: List>
-
- message.channel.withTyping {
- @Suppress("TooGenericExceptionCaught")
- val result = try {
- MappingsQuery.queryFields(
- QueryContext(
- provider = provider,
- searchKey = query
- )
- )
- } catch (e: NullPointerException) {
- message.respond(e.localizedMessage)
- return@queryFields
- }
+ val context = newSingleThreadContext("f: $givenQuery")
- pages = fieldsToPages(namespace, provider.get(), result)
- }
+ try {
+ withContext(context) {
+ val provider = if (version == null) {
+ if (channel != null) {
+ namespace.getProvider(
+ namespace.getDefaultVersion { channel }
+ )
+ } else {
+ MappingsProvider.empty(namespace)
+ }
+ } else {
+ namespace.getProvider(version.version)
+ }
- if (pages.isEmpty()) {
- message.respond("No results found")
- return
- }
+ provider.injectDefaultVersion(
+ namespace.getDefaultProvider {
+ channel ?: namespace.getDefaultMappingChannel()
+ }
+ )
- val meta = provider.get()
+ sentry.breadcrumb(BreadcrumbType.Info) {
+ message = "Provider resolved, with injected default version"
- val pagesObj = Pages("${EXPAND_EMOJI.mention} for more")
- val pageTitle = "List of ${meta.name} fields: ${meta.version}"
+ data["version"] = provider.version ?: "Unknown"
+ }
- val shortPages = mutableListOf()
- val longPages = mutableListOf()
+ val query = givenQuery.replace(".", "/")
+ var pages: List>
- pages.forEach { (short, long) ->
- shortPages.add(short)
- longPages.add(long)
- }
+ sentry.breadcrumb(BreadcrumbType.Info) {
+ message = "Attempting to run sanitized query"
+
+ data["query"] = query
+ }
- shortPages.forEach {
- pagesObj.addPage(
- "${EXPAND_EMOJI.mention} for more",
+ message.channel.withTyping {
+ @Suppress("TooGenericExceptionCaught")
+ val result = try {
+ MappingsQuery.queryFields(
+ QueryContext(
+ provider = provider,
+ searchKey = query
+ )
+ )
+ } catch (e: NullPointerException) {
+ message.respond(e.localizedMessage)
+ return@withContext
+ }
- Page {
- description = it
- title = pageTitle
+ sentry.breadcrumb(BreadcrumbType.Info) {
+ message = "Generating pages for results"
- footer {
- text = PAGE_FOOTER
- icon = PAGE_FOOTER_ICON
+ data["resultCount"] = result.value.size
}
+
+ pages = fieldsToPages(namespace, provider.get(), result)
}
- )
- }
- if (shortPages != longPages) {
- longPages.forEach {
- pagesObj.addPage(
- "${EXPAND_EMOJI.mention} for less",
+ if (pages.isEmpty()) {
+ message.respond("No results found")
+ return@withContext
+ }
+
+ val meta = provider.get()
+
+ val pagesObj = Pages("${EXPAND_EMOJI.mention} for more")
+ val pageTitle = "List of ${meta.name} fields: ${meta.version}"
+
+ val shortPages = mutableListOf()
+ val longPages = mutableListOf()
+
+ pages.forEach { (short, long) ->
+ shortPages.add(short)
+ longPages.add(long)
+ }
- Page {
- description = it
- title = pageTitle
+ shortPages.forEach {
+ pagesObj.addPage(
+ "${EXPAND_EMOJI.mention} for more",
- footer {
- text = PAGE_FOOTER
- icon = PAGE_FOOTER_ICON
+ Page {
+ description = it
+ title = pageTitle
+
+ footer {
+ text = PAGE_FOOTER
+ icon = PAGE_FOOTER_ICON
+ }
}
+ )
+ }
+
+ if (shortPages != longPages) {
+ longPages.forEach {
+ pagesObj.addPage(
+ "${EXPAND_EMOJI.mention} for less",
+
+ Page {
+ description = it
+ title = pageTitle
+
+ footer {
+ text = PAGE_FOOTER
+ icon = PAGE_FOOTER_ICON
+ }
+ }
+ )
}
+ }
+
+ sentry.breadcrumb(BreadcrumbType.Info) {
+ message = "Creating and sending paginator to Discord"
+ }
+
+ val paginator = MessageButtonPaginator(
+ targetMessage = event.message,
+ pages = pagesObj,
+ keepEmbed = true,
+ owner = message.author,
+ timeoutSeconds = getTimeout(),
+ locale = getLocale(),
)
+
+ paginator.send()
}
+ } finally {
+ context.close()
}
-
- val paginator = MessageButtonPaginator(
- extension = this@MappingsExtension,
- targetMessage = event.message,
- pages = pagesObj,
- keepEmbed = true,
- owner = message.author,
- timeoutSeconds = getTimeout(),
- locale = getLocale(),
- )
-
- paginator.send()
}
- private suspend fun MessageCommandContext.queryMethods(
+ private suspend fun ChatCommandContext.queryMethods(
namespace: Namespace,
givenQuery: String,
version: MappingsContainer?,
channel: String? = null
) {
- val provider = if (version == null) {
- if (channel != null) {
- namespace.getProvider(
- namespace.getDefaultVersion { channel }
- )
- } else {
- MappingsProvider.empty(namespace)
- }
- } else {
- namespace.getProvider(version.version)
+ sentry.breadcrumb(BreadcrumbType.Query) {
+ message = "Beginning method lookup"
+
+ data["channel"] = channel ?: "N/A"
+ data["namespace"] = namespace.id
+ data["query"] = givenQuery
+ data["version"] = version?.version ?: "N/A"
}
- provider.injectDefaultVersion(
- namespace.getDefaultProvider {
- channel ?: namespace.getDefaultMappingChannel()
- }
- )
-
- val query = givenQuery.replace(".", "/")
- var pages: List>
-
- message.channel.withTyping {
- @Suppress("TooGenericExceptionCaught")
- val result = try {
- MappingsQuery.queryMethods(
- QueryContext(
- provider = provider,
- searchKey = query
- )
- )
- } catch (e: NullPointerException) {
- message.respond(e.localizedMessage)
- return@queryMethods
- }
+ val context = newSingleThreadContext("m: $givenQuery")
- pages = methodsToPages(namespace, provider.get(), result)
- }
+ try {
+ withContext(context) {
+ val provider = if (version == null) {
+ if (channel != null) {
+ namespace.getProvider(
+ namespace.getDefaultVersion { channel }
+ )
+ } else {
+ MappingsProvider.empty(namespace)
+ }
+ } else {
+ namespace.getProvider(version.version)
+ }
- if (pages.isEmpty()) {
- message.respond("No results found")
- return
- }
+ provider.injectDefaultVersion(
+ namespace.getDefaultProvider {
+ channel ?: namespace.getDefaultMappingChannel()
+ }
+ )
+
+ sentry.breadcrumb(BreadcrumbType.Info) {
+ message = "Provider resolved, with injected default version"
- val meta = provider.get()
+ data["version"] = provider.version ?: "Unknown"
+ }
- val pagesObj = Pages("${EXPAND_EMOJI.mention} for more")
- val pageTitle = "List of ${meta.name} methods: ${meta.version}"
+ val query = givenQuery.replace(".", "/")
+ var pages: List>
- val shortPages = mutableListOf()
- val longPages = mutableListOf()
+ sentry.breadcrumb(BreadcrumbType.Info) {
+ message = "Attempting to run sanitized query"
- pages.forEach { (short, long) ->
- shortPages.add(short)
- longPages.add(long)
- }
+ data["query"] = query
+ }
- shortPages.forEach {
- pagesObj.addPage(
- "${EXPAND_EMOJI.mention} for more",
+ message.channel.withTyping {
+ @Suppress("TooGenericExceptionCaught")
+ val result = try {
+ MappingsQuery.queryMethods(
+ QueryContext(
+ provider = provider,
+ searchKey = query
+ )
+ )
+ } catch (e: NullPointerException) {
+ message.respond(e.localizedMessage)
+ return@withContext
+ }
- Page {
- description = it
- title = pageTitle
+ sentry.breadcrumb(BreadcrumbType.Info) {
+ message = "Generating pages for results"
- footer {
- text = PAGE_FOOTER
- icon = PAGE_FOOTER_ICON
+ data["resultCount"] = result.value.size
}
+
+ pages = methodsToPages(namespace, provider.get(), result)
+ }
+
+ if (pages.isEmpty()) {
+ message.respond("No results found")
+ return@withContext
}
- )
- }
- if (shortPages != longPages) {
- longPages.forEach {
- pagesObj.addPage(
- "${EXPAND_EMOJI.mention} for less",
+ val meta = provider.get()
- Page {
- description = it
- title = pageTitle
+ val pagesObj = Pages("${EXPAND_EMOJI.mention} for more")
+ val pageTitle = "List of ${meta.name} methods: ${meta.version}"
- footer {
- text = PAGE_FOOTER
- icon = PAGE_FOOTER_ICON
+ val shortPages = mutableListOf()
+ val longPages = mutableListOf()
+
+ pages.forEach { (short, long) ->
+ shortPages.add(short)
+ longPages.add(long)
+ }
+
+ shortPages.forEach {
+ pagesObj.addPage(
+ "${EXPAND_EMOJI.mention} for more",
+
+ Page {
+ description = it
+ title = pageTitle
+
+ footer {
+ text = PAGE_FOOTER
+ icon = PAGE_FOOTER_ICON
+ }
}
+ )
+ }
+
+ if (shortPages != longPages) {
+ longPages.forEach {
+ pagesObj.addPage(
+ "${EXPAND_EMOJI.mention} for less",
+
+ Page {
+ description = it
+ title = pageTitle
+
+ footer {
+ text = PAGE_FOOTER
+ icon = PAGE_FOOTER_ICON
+ }
+ }
+ )
}
+ }
+
+ sentry.breadcrumb(BreadcrumbType.Info) {
+ message = "Creating and sending paginator to Discord"
+ }
+
+ val paginator = MessageButtonPaginator(
+ targetMessage = event.message,
+ pages = pagesObj,
+ keepEmbed = true,
+ owner = message.author,
+ timeoutSeconds = getTimeout(),
+ locale = getLocale(),
)
+
+ paginator.send()
}
+ } finally {
+ context.close()
}
-
- val paginator = MessageButtonPaginator(
- extension = this@MappingsExtension,
- targetMessage = event.message,
- pages = pagesObj,
- keepEmbed = true,
- owner = message.author,
- timeoutSeconds = getTimeout(),
- locale = getLocale(),
- )
-
- paginator.send()
}
private suspend fun getTimeout() = builder.config.getTimeout()
- private suspend fun customChecks(command: String, namespace: Namespace): Check<*> {
- val allChecks = builder.commandChecks.map { it.invoke(command) }.toMutableList()
- val allNamespaceChecks = builder.namespaceChecks.map { it.invoke(namespace) }.toMutableList()
+ private suspend fun CheckContext.customChecks(command: String, namespace: Namespace) {
+ builder.commandChecks.forEach {
+ it(command)()
- var rootCheck = allChecks.removeFirstOrNull()
- ?: allNamespaceChecks.removeFirstOrNull()
- ?: return { pass() }
+ if (!passed) {
+ return
+ }
+ }
- allChecks.forEach { rootCheck = rootCheck and it }
- allNamespaceChecks.forEach { rootCheck = rootCheck and it }
+ builder.namespaceChecks.forEach {
+ it(namespace)()
- return rootCheck
+ if (!passed) {
+ return
+ }
+ }
}
companion object {
diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/HashedMojangArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/HashedMojangArguments.kt
new file mode 100644
index 0000000000..08e39f493a
--- /dev/null
+++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/HashedMojangArguments.kt
@@ -0,0 +1,27 @@
+package com.kotlindiscord.kord.extensions.modules.extra.mappings.arguments
+
+import com.kotlindiscord.kord.extensions.commands.Arguments
+import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalEnum
+import com.kotlindiscord.kord.extensions.commands.converters.impl.string
+import com.kotlindiscord.kord.extensions.modules.extra.mappings.converters.optionalMappingsVersion
+import com.kotlindiscord.kord.extensions.modules.extra.mappings.enums.Channels
+import me.shedaniel.linkie.namespaces.MojangHashedNamespace
+
+/** Arguments for hashed Mojang mappings lookup commands. **/
+@Suppress("UndocumentedPublicProperty")
+class HashedMojangArguments : Arguments() {
+ val query by string("query", "Name to query mappings for")
+
+ val channel by optionalEnum(
+ displayName = "channel",
+ description = "Mappings channel to use for this query",
+ typeName = "official/snapshot"
+ )
+
+ val version by optionalMappingsVersion(
+ "version",
+ "Minecraft version to use for this query",
+ true,
+ MojangHashedNamespace
+ )
+}
diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/LegacyYarnArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/LegacyYarnArguments.kt
index 438099c667..c73ffd8114 100644
--- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/LegacyYarnArguments.kt
+++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/LegacyYarnArguments.kt
@@ -1,7 +1,7 @@
package com.kotlindiscord.kord.extensions.modules.extra.mappings.arguments
+import com.kotlindiscord.kord.extensions.commands.Arguments
import com.kotlindiscord.kord.extensions.commands.converters.impl.string
-import com.kotlindiscord.kord.extensions.commands.parser.Arguments
import com.kotlindiscord.kord.extensions.modules.extra.mappings.converters.optionalMappingsVersion
import me.shedaniel.linkie.namespaces.LegacyYarnNamespace
diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MCPArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MCPArguments.kt
index 3b5370c40b..66027a3722 100644
--- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MCPArguments.kt
+++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MCPArguments.kt
@@ -1,7 +1,7 @@
package com.kotlindiscord.kord.extensions.modules.extra.mappings.arguments
+import com.kotlindiscord.kord.extensions.commands.Arguments
import com.kotlindiscord.kord.extensions.commands.converters.impl.string
-import com.kotlindiscord.kord.extensions.commands.parser.Arguments
import com.kotlindiscord.kord.extensions.modules.extra.mappings.converters.optionalMappingsVersion
import me.shedaniel.linkie.namespaces.MCPNamespace
diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MojangArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MojangArguments.kt
index b0edd449aa..5c6dbb7f4c 100644
--- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MojangArguments.kt
+++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/MojangArguments.kt
@@ -1,8 +1,8 @@
package com.kotlindiscord.kord.extensions.modules.extra.mappings.arguments
+import com.kotlindiscord.kord.extensions.commands.Arguments
import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalEnum
import com.kotlindiscord.kord.extensions.commands.converters.impl.string
-import com.kotlindiscord.kord.extensions.commands.parser.Arguments
import com.kotlindiscord.kord.extensions.modules.extra.mappings.converters.optionalMappingsVersion
import com.kotlindiscord.kord.extensions.modules.extra.mappings.enums.Channels
import me.shedaniel.linkie.namespaces.MojangNamespace
diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/PlasmaArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/PlasmaArguments.kt
index cbbbc9706d..92da012a79 100644
--- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/PlasmaArguments.kt
+++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/PlasmaArguments.kt
@@ -1,7 +1,7 @@
package com.kotlindiscord.kord.extensions.modules.extra.mappings.arguments
+import com.kotlindiscord.kord.extensions.commands.Arguments
import com.kotlindiscord.kord.extensions.commands.converters.impl.string
-import com.kotlindiscord.kord.extensions.commands.parser.Arguments
import com.kotlindiscord.kord.extensions.modules.extra.mappings.converters.optionalMappingsVersion
import me.shedaniel.linkie.namespaces.PlasmaNamespace
diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarnArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarnArguments.kt
index 6138b3e317..3ca311dcfb 100644
--- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarnArguments.kt
+++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarnArguments.kt
@@ -1,8 +1,8 @@
package com.kotlindiscord.kord.extensions.modules.extra.mappings.arguments
+import com.kotlindiscord.kord.extensions.commands.Arguments
import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalEnum
import com.kotlindiscord.kord.extensions.commands.converters.impl.string
-import com.kotlindiscord.kord.extensions.commands.parser.Arguments
import com.kotlindiscord.kord.extensions.modules.extra.mappings.converters.optionalMappingsVersion
import com.kotlindiscord.kord.extensions.modules.extra.mappings.enums.YarnChannels
import me.shedaniel.linkie.namespaces.YarnNamespace
diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarrnArguments.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarrnArguments.kt
index d9981830af..1d1aa889a9 100644
--- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarrnArguments.kt
+++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/arguments/YarrnArguments.kt
@@ -1,7 +1,7 @@
package com.kotlindiscord.kord.extensions.modules.extra.mappings.arguments
+import com.kotlindiscord.kord.extensions.commands.Arguments
import com.kotlindiscord.kord.extensions.commands.converters.impl.string
-import com.kotlindiscord.kord.extensions.commands.parser.Arguments
import com.kotlindiscord.kord.extensions.modules.extra.mappings.converters.optionalMappingsVersion
import me.shedaniel.linkie.namespaces.YarrnNamespace
diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/builders/ExtMappingsBuilder.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/builders/ExtMappingsBuilder.kt
index 25f9694cca..9e01be2484 100644
--- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/builders/ExtMappingsBuilder.kt
+++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/builders/ExtMappingsBuilder.kt
@@ -12,18 +12,18 @@ class ExtMappingsBuilder {
var config: MappingsConfigAdapter = TomlMappingsConfig()
/** List of checks to apply against the name of the command. **/
- val commandChecks: MutableList Check<*>> = mutableListOf()
+ val commandChecks: MutableList Check> = mutableListOf()
/** List of checks to apply against the namespace corresponding with the command. **/
- val namespaceChecks: MutableList Check<*>> = mutableListOf()
+ val namespaceChecks: MutableList Check> = mutableListOf()
/** Register a check that applies against the name of a command, and its message creation event. **/
fun commandCheck(check: suspend (String) -> Check) {
- commandChecks.add(check as (suspend (String) -> Check<*>))
+ commandChecks.add(check)
}
/** Register a check that applies against the mappings namespace for a command, and its message creation event. **/
fun namespaceCheck(check: suspend (Namespace) -> Check) {
- namespaceChecks.add(check as (suspend (Namespace) -> Check<*>))
+ namespaceChecks.add(check)
}
}
diff --git a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/converters/MappingsVersionConverter.kt b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/converters/MappingsVersionConverter.kt
index 0b6308014f..83a6afcba6 100644
--- a/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/converters/MappingsVersionConverter.kt
+++ b/extra-modules/extra-mappings/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/mappings/converters/MappingsVersionConverter.kt
@@ -2,15 +2,16 @@
package com.kotlindiscord.kord.extensions.modules.extra.mappings.converters
-import com.kotlindiscord.kord.extensions.CommandException
+import com.kotlindiscord.kord.extensions.DiscordRelayedException
+import com.kotlindiscord.kord.extensions.commands.Argument
+import com.kotlindiscord.kord.extensions.commands.Arguments
import com.kotlindiscord.kord.extensions.commands.CommandContext
import com.kotlindiscord.kord.extensions.commands.converters.ConverterToOptional
import com.kotlindiscord.kord.extensions.commands.converters.SingleConverter
import com.kotlindiscord.kord.extensions.commands.converters.Validator
-import com.kotlindiscord.kord.extensions.commands.parser.Argument
-import com.kotlindiscord.kord.extensions.commands.parser.Arguments
import com.kotlindiscord.kord.extensions.parser.StringParser
import dev.kord.common.annotation.KordPreview
+import dev.kord.core.entity.interaction.OptionValue
import dev.kord.rest.builder.interaction.OptionsBuilder
import dev.kord.rest.builder.interaction.StringChoiceBuilder
import me.shedaniel.linkie.MappingsContainer
@@ -34,20 +35,6 @@ class MappingsVersionConverter(
if (arg in namespace.getAllVersions()) {
val version = namespace.getProvider(arg).getOrNull()
-// if (version == null) {
-// throw CommandException("Invalid ${namespace.id} version: `$arg`")
-//
-// val created = namespace.createAndAdd(arg)
-//
-// if (created != null) {
-// this.parsed = created
-// } else {
-// throw CommandException("Invalid ${namespace.id} version: `$arg`")
-// }
-// } else {
-// this.parsed = version
-// }
-
if (version != null) {
this.parsed = version
@@ -55,11 +42,28 @@ class MappingsVersionConverter(
}
}
- throw CommandException("Invalid ${namespace.id} version: `$arg`")
+ throw DiscordRelayedException("Invalid ${namespace.id} version: `$arg`")
}
override suspend fun toSlashOption(arg: Argument<*>): OptionsBuilder =
StringChoiceBuilder(arg.displayName, arg.description).apply { required = true }
+
+ override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean {
+ val optionValue = (option as? OptionValue.StringOptionValue)?.value ?: return false
+ val namespace = namespaceGetter.invoke()
+
+ if (optionValue in namespace.getAllVersions()) {
+ val version = namespace.getProvider(optionValue).getOrNull()
+
+ if (version != null) {
+ this.parsed = version
+
+ return true
+ }
+ }
+
+ throw DiscordRelayedException("Invalid ${namespace.id} version: `$optionValue`")
+ }
}
/** Optional mappings version converter; see KordEx bundled functions for more info. **/
diff --git a/extra-modules/extra-mappings/src/main/resources/kordex/mappings/default.toml b/extra-modules/extra-mappings/src/main/resources/kordex/mappings/default.toml
index 787f875c64..d68f93eb03 100644
--- a/extra-modules/extra-mappings/src/main/resources/kordex/mappings/default.toml
+++ b/extra-modules/extra-mappings/src/main/resources/kordex/mappings/default.toml
@@ -20,8 +20,8 @@ allowed = []
banned = []
[settings]
-# Which namespaces to allow lookups for - "legacy-yarn", "plasma", "mcp", "mojang", "yarn" or "yarrn"
-namespaces = ["legacy-yarn", "plasma", "mcp", "mojang", "yarn", "yarrn"]
+# Which namespaces to allow lookups for - "hashed-mojang", "legacy-yarn", "plasma", "mcp", "mojang", "yarn" or "yarrn"
+namespaces = ["hashed-mojang", "legacy-yarn", "plasma", "mcp", "mojang", "yarn", "yarrn"]
# How long to wait before closing mappings paginators (in seconds), defaults to 5 minutes
# timeout = 300
diff --git a/extra-modules/extra-mappings/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt b/extra-modules/extra-mappings/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt
index 6795038fbe..1ed5021c91 100644
--- a/extra-modules/extra-mappings/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt
+++ b/extra-modules/extra-mappings/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt
@@ -1,35 +1,35 @@
package com.kotlindiscord.kord.extensions.test.bot
import com.kotlindiscord.kord.extensions.ExtensibleBot
-import com.kotlindiscord.kord.extensions.checks.isNotbot
+import com.kotlindiscord.kord.extensions.checks.isNotBot
import com.kotlindiscord.kord.extensions.modules.extra.mappings.extMappings
import com.kotlindiscord.kord.extensions.utils.env
-import me.shedaniel.linkie.namespaces.YarnNamespace
import org.koin.core.logger.Level
suspend fun main() {
- val bot = ExtensibleBot(env("TOKEN")!!) {
+ val bot = ExtensibleBot(env("TOKEN")) {
koinLogLevel = Level.DEBUG
- messageCommands {
- check(isNotbot)
+ chatCommands {
+ check { isNotBot() }
+ enabled = true
}
- slashCommands {
+ applicationCommands {
enabled = true
}
extensions {
extMappings {
- namespaceCheck { namespace ->
- {
- if (namespace == YarnNamespace) {
- pass()
- } else {
- fail("Yarn only, ya dummy.")
- }
- }
- }
+// namespaceCheck { namespace ->
+// {
+// if (namespace == YarnNamespace) {
+// pass()
+// } else {
+// fail("Yarn only, ya dummy.")
+// }
+// }
+// }
}
}
}
diff --git a/extra-modules/extra-phishing/README.md b/extra-modules/extra-phishing/README.md
new file mode 100644
index 0000000000..0a3a2e0c70
--- /dev/null
+++ b/extra-modules/extra-phishing/README.md
@@ -0,0 +1,56 @@
+# Phishing Extension
+
+[![Discord: Click here](https://img.shields.io/static/v1?label=Discord&message=Click%20here&color=7289DA&style=for-the-badge&logo=discord)](https://discord.gg/gjXqqCS) [![Release](https://img.shields.io/nexus/r/com.kotlindiscord.kord.extensions/extra-phishing?nexusVersion=3&logo=gradle&color=blue&label=Release&server=https%3A%2F%2Fmaven.kotlindiscord.com&style=for-the-badge)](https://maven.kotlindiscord.com/#browse/browse:maven-releases:com%2Fkotlindiscord%2Fkord%2Fextensions%2Fextra-phishing) [![Snapshot](https://img.shields.io/nexus/s/com.kotlindiscord.kord.extensions/extra-phishing?logo=gradle&color=orange&label=Snapshot&server=https%3A%2F%2Fmaven.kotlindiscord.com&style=for-the-badge)](https://maven.kotlindiscord.com/#browse/browse:maven-snapshots:com%2Fkotlindiscord%2Fkord%2Fextensions%2Fextra-phishing)
+
+This module contains an extension written to provide some anti-phishing protection, based on the crowdsourced [Sinking Yachts API](https://phish.sinking.yachts/docs).
+
+# Getting Started
+
+* **Maven repo:** `https://maven.kotlindiscord.com/repository/maven-public/`
+* **Maven coordinates:** `com.kotlindiscord.kord.extensions:extra-phishing:VERSION`
+
+At its simplest, you can add this extension directly to your bot with a minimum configuration. For example:
+
+```kotlin
+suspend fun main() {
+ val bot = ExtensibleBot(System.getenv("TOKEN")) {
+
+ extensions {
+ extPhishing {
+ appName = "My Bot"
+ }
+ }
+ }
+
+ bot.start()
+}
+```
+
+This will install the extension using its default configuration. However, the extension may be configured in several ways - as is detailed below.
+
+# Commands
+
+This extension provides a number of commands for use on Discord.
+
+* Slash command: `/phishing-check`, for checking whether the given argument is a phishing domain
+* Message command: `Phishing Check`, for manually checking a specific message via the right-click menu
+
+Access to both commands can be limited to a specific Discord permission. This can be configured below, but defaults to "Manage Messages".
+
+# Configuration
+
+To configure this module, values can be provided within the `extPhishing` builder.
+
+* **Required:** `appName` - Give your application a name so that Sinking Yachts can identify it for statistics purposes
+
+* `detectionAction` (default: Delete) - What to do when a message containing a phishing domain is detected
+* `logChannelName` (default: "logs") - The name of the channel to use for logging; the extension will search the channels present on the current server and use the last one with an exactly-matching name
+* `notifyUser` (default: True) - Whether to DM the user, letting them know they posted a phishing domain and what action was taken
+* `requiredCommandPermission` (default: Manage Server) - The permission a user must have in order to run the bundled message and slash commands
+* `updateDelay` (default: 15 minutes) - How often to check for new phishing domains, five minutes at minimum
+* `urlRegex` (default: [The Perfect URL Regex](https://urlregex.com/)) - A regular expression used to extract domains from messages, with **exactly one capturing group containing the entire domain** (and optionally the URL path)
+
+Additionally, the following configuration functions are available:
+
+* `check` - Used to define checks that must pass for event handlers to be run, and thus for messages to be checked (you could use this to exempt your staff, for example)
+* `regex` - A convenience function for registering a `String` as URL regex, with the case-insensitive flag
diff --git a/extra-modules/extra-phishing/build.gradle.kts b/extra-modules/extra-phishing/build.gradle.kts
new file mode 100644
index 0000000000..d9eb8ff125
--- /dev/null
+++ b/extra-modules/extra-phishing/build.gradle.kts
@@ -0,0 +1,35 @@
+plugins {
+ `kordex-module`
+ `published-module`
+ `dokka-module`
+ `disable-explicit-api-mode`
+
+ kotlin("plugin.serialization")
+}
+
+repositories {
+ maven {
+ name = "KotDis"
+ url = uri("https://maven.kotlindiscord.com/repository/maven-public/")
+ }
+}
+
+dependencies {
+ detektPlugins(libs.detekt)
+
+ implementation(libs.logging)
+ implementation(libs.kotlin.stdlib)
+ implementation(libs.ktor.logging)
+
+ testImplementation(libs.groovy) // For logback config
+ testImplementation(libs.logback)
+
+ implementation(project(":kord-extensions"))
+}
+
+group = "com.kotlindiscord.kord.extensions"
+
+kordex {
+ jvmTarget.set("9")
+ javaVersion.set(JavaVersion.VERSION_1_9)
+}
diff --git a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/DetectionAction.kt b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/DetectionAction.kt
new file mode 100644
index 0000000000..2122d61075
--- /dev/null
+++ b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/DetectionAction.kt
@@ -0,0 +1,22 @@
+package com.kotlindiscord.kord.extensions.modules.extra.phishing
+
+/**
+ * Sealed class representing what should happen when a phishing link is detected.
+ *
+ * The extension will always try to log the message, but you can specify [LogOnly] if that's _all_ you want.
+ *
+ * @property message Message to return to the user.
+ */
+sealed class DetectionAction(val message: String) {
+ /** Ban 'em and delete the message. **/
+ object Ban : DetectionAction("you have been banned from the server")
+
+ /** Delete the message. **/
+ object Delete : DetectionAction("it has been deleted")
+
+ /** Kick 'em and delete the message. **/
+ object Kick : DetectionAction("you have been kicked from the server")
+
+ /** Don't do anything, just log it in the logs channel. **/
+ object LogOnly : DetectionAction("it has been logged for the server staff to review")
+}
diff --git a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/DomainChange.kt b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/DomainChange.kt
new file mode 100644
index 0000000000..622c9fe2d2
--- /dev/null
+++ b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/DomainChange.kt
@@ -0,0 +1,26 @@
+package com.kotlindiscord.kord.extensions.modules.extra.phishing
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * Data class representing a Sinking Yachts domain change object.
+ *
+ * @property type Domain change type - add or delete
+ * @property domains Set of domains that this change concerns
+ */
+@Serializable
+data class DomainChange(
+ val type: DomainChangeType,
+ val domains: Set
+)
+
+/** Enum representing domain change types. **/
+@Serializable
+enum class DomainChangeType {
+ @SerialName("add")
+ Add,
+
+ @SerialName("delete")
+ Delete
+}
diff --git a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/ExtPhishingBuilder.kt b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/ExtPhishingBuilder.kt
new file mode 100644
index 0000000000..2ff7b4e4cd
--- /dev/null
+++ b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/ExtPhishingBuilder.kt
@@ -0,0 +1,85 @@
+@file:OptIn(ExperimentalTime::class)
+@file:Suppress("MagicNumber")
+
+package com.kotlindiscord.kord.extensions.modules.extra.phishing
+
+import com.kotlindiscord.kord.extensions.checks.types.Check
+import dev.kord.common.entity.Permission
+import dev.kord.core.event.Event
+import kotlin.time.Duration
+import kotlin.time.ExperimentalTime
+
+/** Builder used to configure the phishing extension. **/
+class ExtPhishingBuilder {
+ /** The name of your application, which allows the Sinking Yachts maintainers to identify what it is. **/
+ lateinit var appName: String
+
+ /** Delay between domain update checks, 5 minutes at minimum. **/
+ var updateDelay = Duration.minutes(15)
+
+ /**
+ * Regular expression used to extract domains from messages.
+ *
+ * If you customize this, it must contain exactly one capturing group, containing the full domain name, and
+ * optionally the path, if this is an actual URL. You can mark a group as non-capturing by prefixing it with
+ * `?:`.
+ *
+ * The provided regex comes from https://urlregex.com/ - but you can provide a different regex if you need
+ * detection to be more sensitive than just clickable links.
+ */
+ var urlRegex = "(?:https?|ftp|file|discord)://([-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|])"
+ .toRegex(RegexOption.IGNORE_CASE)
+
+ /** @suppress List of checks to apply to event handlers. **/
+ val checks: MutableList> = mutableListOf()
+
+ /**
+ * If you want to require a permission for the phishing check commands, supply it here. Alternatively, supply
+ * `null` and everyone will be given access to them.
+ */
+ var requiredCommandPermission: Permission? = Permission.ManageMessages
+
+ /**
+ * What to do when a message creation/edit contains a phishing domain.
+ *
+ * @see DetectionAction
+ */
+ var detectionAction: DetectionAction = DetectionAction.Delete
+
+ /** Whether to DM users when their messages contain phishing domains, with the action taken. **/
+ var notifyUser = true
+
+ /**
+ * The name of the logs channel to use for detection messages, if not "logs".
+ *
+ * The extension will try to find the last channel in the channel list with a name exactly matching the
+ * given name here, "logs" by default.
+ */
+ var logChannelName = "logs"
+
+ /** Register a check that must pass in order for an event handler to run, and for messages to be processed. **/
+ fun check(check: Check) {
+ checks.add(check)
+ }
+
+ /** Register checks that must pass in order for an event handler to run, and for messages to be processed. **/
+ fun check(vararg checkList: Check) {
+ checks.addAll(checkList)
+ }
+
+ /** Convenience function for supplying a case-insensitive [urlRegex]. **/
+ fun regex(pattern: String) {
+ urlRegex = pattern.toRegex(RegexOption.IGNORE_CASE)
+ }
+
+ /** @suppress **/
+ fun validate() {
+ if (!this::appName.isInitialized) {
+ error("Application name must be provided")
+ }
+
+ if (updateDelay < Duration.minutes(5)) {
+ error("The update delay must be at least five minutes - don't spam the API!")
+ }
+ }
+}
diff --git a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/Functions.kt b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/Functions.kt
new file mode 100644
index 0000000000..deb9008f49
--- /dev/null
+++ b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/Functions.kt
@@ -0,0 +1,16 @@
+package com.kotlindiscord.kord.extensions.modules.extra.phishing
+
+import com.kotlindiscord.kord.extensions.builders.ExtensibleBotBuilder
+
+/**
+ * Configure the phishing extension and add it to the bot.
+ */
+inline fun ExtensibleBotBuilder.ExtensionsBuilder.extPhishing(builder: ExtPhishingBuilder.() -> Unit) {
+ val settings = ExtPhishingBuilder()
+
+ builder(settings)
+
+ settings.validate()
+
+ add { PhishingExtension(settings) }
+}
diff --git a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/PhishingApi.kt b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/PhishingApi.kt
new file mode 100644
index 0000000000..27e654210b
--- /dev/null
+++ b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/PhishingApi.kt
@@ -0,0 +1,42 @@
+package com.kotlindiscord.kord.extensions.modules.extra.phishing
+
+import io.ktor.client.*
+import io.ktor.client.features.json.*
+import io.ktor.client.features.logging.*
+import io.ktor.client.request.*
+
+internal const val ALL_PATH = "https://phish.sinking.yachts/v2/all"
+internal const val CHECK_PATH = "https://phish.sinking.yachts/v2/check/%"
+internal const val RECENT_PATH = "https://phish.sinking.yachts/v2/recent/%"
+internal const val SIZE_PATH = "https://phish.sinking.yachts/v2/dbsize"
+
+/** Implementation of the Sinking Yachts phishing domain API. **/
+class PhishingApi(internal val appName: String) {
+ internal val client = HttpClient {
+ install(JsonFeature)
+
+ install(Logging) {
+ level = LogLevel.INFO
+ }
+ }
+
+ internal suspend inline fun get(url: String): T = client.get(url) {
+ header("X-Identity", "$appName (via Kord Extensions)")
+ }
+
+ /** Get all known phishing domains from the API. **/
+ suspend fun getAllDomains(): Set =
+ get(ALL_PATH)
+
+ /** Query the API directly to check a specific domain. **/
+ suspend fun checkDomain(domain: String): Boolean =
+ get(CHECK_PATH.replace("%", domain))
+
+ /** Get all new phishing domains added in the previous [seconds] seconds. **/
+ suspend fun getRecentDomains(seconds: Long): List =
+ get(RECENT_PATH.replace("%", seconds.toString()))
+
+ /** Get the total number of phishing domains that the API knows about. **/
+ suspend fun getTotalDomains(): Long =
+ get(SIZE_PATH)
+}
diff --git a/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/PhishingExtension.kt b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/PhishingExtension.kt
new file mode 100644
index 0000000000..4d95810a0e
--- /dev/null
+++ b/extra-modules/extra-phishing/src/main/kotlin/com/kotlindiscord/kord/extensions/modules/extra/phishing/PhishingExtension.kt
@@ -0,0 +1,306 @@
+@file:OptIn(ExperimentalTime::class)
+@file:Suppress("StringLiteralDuplication")
+
+package com.kotlindiscord.kord.extensions.modules.extra.phishing
+
+import com.kotlindiscord.kord.extensions.DISCORD_RED
+import com.kotlindiscord.kord.extensions.DiscordRelayedException
+import com.kotlindiscord.kord.extensions.checks.hasPermission
+import com.kotlindiscord.kord.extensions.checks.isNotBot
+import com.kotlindiscord.kord.extensions.commands.Arguments
+import com.kotlindiscord.kord.extensions.commands.converters.impl.string
+import com.kotlindiscord.kord.extensions.extensions.Extension
+import com.kotlindiscord.kord.extensions.extensions.ephemeralMessageCommand
+import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand
+import com.kotlindiscord.kord.extensions.extensions.event
+import com.kotlindiscord.kord.extensions.types.respond
+import com.kotlindiscord.kord.extensions.utils.dm
+import com.kotlindiscord.kord.extensions.utils.getJumpUrl
+import com.kotlindiscord.kord.extensions.utils.scheduling.Scheduler
+import com.kotlindiscord.kord.extensions.utils.scheduling.Task
+import dev.kord.core.behavior.ban
+import dev.kord.core.behavior.channel.createMessage
+import dev.kord.core.entity.Message
+import dev.kord.core.entity.channel.GuildMessageChannel
+import dev.kord.core.event.message.MessageCreateEvent
+import dev.kord.core.event.message.MessageUpdateEvent
+import dev.kord.rest.builder.message.create.embed
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.lastOrNull
+import kotlinx.coroutines.launch
+import mu.KotlinLogging
+import kotlin.time.ExperimentalTime
+
+/** Phishing extension, responsible for checking for phishing domains in messages. **/
+class PhishingExtension(private val settings: ExtPhishingBuilder) : Extension() {
+ override val name = "phishing"
+
+ private val api = PhishingApi(settings.appName)
+ private val domainCache: MutableSet = mutableSetOf()
+ private val logger = KotlinLogging.logger { }
+
+ private val scheduler = Scheduler()
+ private var checkTask: Task? = null
+
+ override suspend fun setup() {
+ domainCache.addAll(api.getAllDomains())
+
+ checkTask = scheduler.schedule(settings.updateDelay, pollingSeconds = 30, callback = ::updateDomains)
+
+ event {
+ check { isNotBot() }
+ check { event.message.author != null }
+ check { event.guildId != null }
+
+ check {
+ settings.checks.forEach {
+ if (passed) it()
+ }
+ }
+
+ action {
+ handleMessage(event.message)
+ }
+ }
+
+ event {
+ check { isNotBot() }
+ check { event.new.author.value != null }
+ check { event.new.guildId.value != null }
+
+ check {
+ settings.checks.forEach {
+ if (passed) it()
+ }
+ }
+
+ action {
+ handleMessage(event.message.asMessage())
+ }
+ }
+
+ ephemeralMessageCommand {
+ name = "Phishing Check"
+
+ if (this@PhishingExtension.settings.requiredCommandPermission != null) {
+ check { hasPermission(this@PhishingExtension.settings.requiredCommandPermission!!) }
+ }
+
+ action {
+ for (message in targetMessages) {
+ val domains = parseDomains(message.content.lowercase())
+ val matches = domains intersect domainCache
+
+ respond {
+ content = if (matches.isNotEmpty()) {
+ "⚠️ [Message ${message.id.value}](${message.getJumpUrl()}) " +
+ "**contains ${matches.size} phishing link/s**."
+ } else {
+ "✅ [Message ${message.id.value}](${message.getJumpUrl()}) " +
+ "**does not contain any phishing links**."
+ }
+ }
+ }
+ }
+ }
+
+ ephemeralSlashCommand(::DomainArgs) {
+ name = "phishing-check"
+ description = "Check whether a given domain is a known phishing domain."
+
+ if (this@PhishingExtension.settings.requiredCommandPermission != null) {
+ check { hasPermission(this@PhishingExtension.settings.requiredCommandPermission!!) }
+ }
+
+ action {
+ respond {
+ content = if (domainCache.contains(arguments.domain.lowercase())) {
+ "⚠️ `${arguments.domain}` is a known phishing domain."
+ } else {
+ "✅ `${arguments.domain}` is not a known phishing domain."
+ }
+ }
+ }
+ }
+ }
+
+ internal suspend fun handleMessage(message: Message) {
+ val domains = parseDomains(message.content.lowercase())
+ val matches = domains intersect domainCache
+
+ if (matches.isNotEmpty()) {
+ logger.debug { "Found a message with ${matches.size} phishing domains." }
+
+ if (settings.notifyUser) {
+ message.kord.launch {
+ message.author!!.dm {
+ content = "We've detected that the following message contains a phishing domain. For this " +
+ "reason, **${settings.detectionAction.message}**."
+
+ embed {
+ title = "Phishing domain detected"
+ description = message.content
+ color = DISCORD_RED
+
+ field {
+ inline = true
+
+ name = "Channel"
+ value = message.channel.mention
+ }
+
+ field {
+ inline = true
+
+ name = "Message ID"
+ value = "`${message.id.value}`"
+ }
+
+ field {
+ inline = true
+
+ name = "Server"
+ value = message.getGuild().name
+ }
+ }
+ }
+ }
+ }
+
+ when (settings.detectionAction) {
+ DetectionAction.Ban -> {
+ message.getAuthorAsMember()!!.ban {
+ reason = "Message contained a phishing domain"
+ }
+
+ message.delete("Message contained a phishing domain")
+ }
+
+ DetectionAction.Delete -> message.delete("Message contained a phishing domain")
+
+ DetectionAction.Kick -> {
+ message.getAuthorAsMember()!!.kick("Message contained a phishing domain")
+ message.delete("Message contained a phishing domain")
+ }
+
+ DetectionAction.LogOnly -> {
+ // Do nothing, we always log
+ }
+ }
+
+ logDeletion(message, matches)
+ }
+ }
+
+ internal suspend fun logDeletion(message: Message, matches: Set) {
+ val guild = message.getGuild()
+
+ val channel = message
+ .getGuild()
+ .channels
+ .filter { it.name == settings.logChannelName }
+ .lastOrNull()
+ ?.asChannelOrNull() as? GuildMessageChannel
+
+ if (channel == null) {
+ logger.warn {
+ "Unable to find a channel named ${settings.logChannelName} on ${guild.name} (${guild.id.value})"
+ }
+
+ return
+ }
+
+ val matchList = "# Phishing Domain Matches\n\n" +
+ "**Total:** ${matches.size}\n\n" +
+ matches.joinToString("\n") { "* `$it`" }
+
+ channel.createMessage {
+ addFile("matches.md", matchList.byteInputStream())
+
+ embed {
+ title = "Phishing domain detected"
+ description = message.content
+ color = DISCORD_RED
+
+ field {
+ inline = true
+
+ name = "Author"
+ value = "${message.author!!.mention} (" +
+ "`${message.author!!.tag}` / " +
+ "`${message.author!!.id.value}`" +
+ ")"
+ }
+
+ field {
+ inline = true
+
+ name = "Channel"
+ value = "${message.channel.mention} (`${message.channelId.value}`)"
+ }
+
+ field {
+ inline = true
+
+ name = "Message"
+ value = "[`${message.id.value}`](${message.getJumpUrl()})"
+ }
+
+ field {
+ inline = true
+
+ name = "Total Matches"
+ value = matches.size.toString()
+ }
+ }
+ }
+ }
+
+ internal fun parseDomains(content: String): MutableSet {
+ val domains: MutableSet = mutableSetOf()
+
+ for (match in settings.urlRegex.findAll(content)) {
+ var found = match.groups[1]!!.value.trim('/')
+
+ if ("/" in found) {
+ found = found.split("/", limit = 2).first()
+ }
+
+ domains.add(found)
+ }
+
+ logger.debug { "Found ${domains.size} domains: ${domains.joinToString()}" }
+
+ return domains
+ }
+
+ override suspend fun unload() {
+ checkTask?.cancel()
+ checkTask = null
+ }
+
+ @Suppress("MagicNumber")
+ internal suspend fun updateDomains() {
+ logger.trace { "Updating domains..." }
+
+ // An extra 30 seconds for safety
+ api.getRecentDomains(settings.updateDelay.inWholeSeconds + 30).forEach {
+ when (it.type) {
+ DomainChangeType.Add -> domainCache.addAll(it.domains)
+ DomainChangeType.Delete -> domainCache.removeAll(it.domains)
+ }
+ }
+
+ checkTask?.restart() // Off we go again
+ }
+
+ /** Arguments class for domain-relevant commands. **/
+ inner class DomainArgs : Arguments() {
+ /** Targeted domain string. **/
+ val domain by string("domain", "Domain to check") { _, value ->
+ if ("/" in value) {
+ throw DiscordRelayedException("Please provide the domain name only, without the protocol or a path.")
+ }
+ }
+ }
+}
diff --git a/extra-modules/extra-phishing/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt b/extra-modules/extra-phishing/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt
new file mode 100644
index 0000000000..417f949f93
--- /dev/null
+++ b/extra-modules/extra-phishing/src/test/kotlin/com/kotlindiscord/kord/extensions/test/bot/Bot.kt
@@ -0,0 +1,31 @@
+package com.kotlindiscord.kord.extensions.test.bot
+
+import com.kotlindiscord.kord.extensions.ExtensibleBot
+import com.kotlindiscord.kord.extensions.checks.isNotBot
+import com.kotlindiscord.kord.extensions.modules.extra.phishing.extPhishing
+import com.kotlindiscord.kord.extensions.utils.env
+import org.koin.core.logger.Level
+
+suspend fun main() {
+ val bot = ExtensibleBot(env("TOKEN")) {
+ koinLogLevel = Level.DEBUG
+
+ chatCommands {
+ check { isNotBot() }
+ enabled = true
+ }
+
+ applicationCommands {
+ enabled = true
+ }
+
+ extensions {
+ extPhishing {
+ appName = "Integration test bot"
+ logChannelName = "alerts"
+ }
+ }
+ }
+
+ bot.start()
+}
diff --git a/extra-modules/extra-phishing/src/test/resources/junit-platform.properties b/extra-modules/extra-phishing/src/test/resources/junit-platform.properties
new file mode 100644
index 0000000000..1d27b78fbb
--- /dev/null
+++ b/extra-modules/extra-phishing/src/test/resources/junit-platform.properties
@@ -0,0 +1 @@
+junit.jupiter.execution.parallel.enabled=true
diff --git a/extra-modules/extra-phishing/src/test/resources/logback.groovy b/extra-modules/extra-phishing/src/test/resources/logback.groovy
new file mode 100644
index 0000000000..1bab8d7d2d
--- /dev/null
+++ b/extra-modules/extra-phishing/src/test/resources/logback.groovy
@@ -0,0 +1,30 @@
+import ch.qos.logback.core.joran.spi.ConsoleTarget
+
+def environment = System.getenv().getOrDefault("ENVIRONMENT", "production")
+
+def defaultLevel = DEBUG
+
+if (environment == "spam") {
+ logger("dev.kord.rest.DefaultGateway", TRACE)
+} else {
+ // Silence warning about missing native PRNG
+ logger("io.ktor.util.random", ERROR)
+}
+
+appender("CONSOLE", ConsoleAppender) {
+ encoder(PatternLayoutEncoder) {
+ pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n"
+ }
+
+ target = ConsoleTarget.SystemErr
+}
+
+appender("FILE", FileAppender) {
+ file = "output.log"
+
+ encoder(PatternLayoutEncoder) {
+ pattern = "%d{yyyy-MM-dd HH:mm:ss:SSS Z} | %5level | %40.40logger{40} | %msg%n"
+ }
+}
+
+root(defaultLevel, ["CONSOLE", "FILE"])
diff --git a/gradle.properties b/gradle.properties
index 9e583586fa..ebb2d0ed50 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,7 @@ kotlin.incremental = true
ksp.incremental = false
-projectVersion = 1.4.4-RC4
+projectVersion = 1.5.1-RC1
+
+#dokka will run out of memory with the default meta space
+org.gradle.jvmargs=-XX:MaxMetaspaceSize=1024m
diff --git a/kord-extensions/build.gradle.kts b/kord-extensions/build.gradle.kts
index 9235379a9b..7801faa26f 100644
--- a/kord-extensions/build.gradle.kts
+++ b/kord-extensions/build.gradle.kts
@@ -1,5 +1,4 @@
-import java.io.ByteArrayOutputStream
-import java.net.URL
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
buildscript {
repositories {
@@ -11,13 +10,11 @@ buildscript {
}
plugins {
- `maven-publish`
-
- kotlin("jvm")
-
- id("com.google.devtools.ksp")
- id("io.gitlab.arturbosch.detekt")
- id("org.jetbrains.dokka")
+ `kordex-module`
+ `published-module`
+ `dokka-module`
+ `tested-module`
+ `ksp-module`
}
dependencies {
@@ -46,152 +43,12 @@ dependencies {
ksp(project(":annotation-processor"))
}
-val sourceJar = task("sourceJar", Jar::class) {
- dependsOn(tasks["classes"])
- archiveClassifier.set("sources")
- from(sourceSets.main.get().allSource)
-}
-
-val javadocJar = task("javadocJar", Jar::class) {
- dependsOn("dokkaJavadoc")
- archiveClassifier.set("javadoc")
- from(tasks.javadoc)
-}
-
-java {
- sourceCompatibility = JavaVersion.VERSION_1_9
- targetCompatibility = JavaVersion.VERSION_1_9
-}
-
-kotlin {
- explicitApi()
-}
-
-sourceSets {
- main {
- java {
- srcDir(file("$buildDir/generated/ksp/main/kotlin/"))
- }
- }
-
- test {
- java {
- srcDir(file("$buildDir/generated/ksp/test/kotlin/"))
- }
- }
-}
-
-detekt {
- buildUponDefaultConfig = true
- config = files("../detekt.yml")
-
- autoCorrect = true
-}
-
-publishing {
- repositories {
- maven {
- name = "KotDis"
-
- url = if (project.version.toString().contains("SNAPSHOT")) {
- uri("https://maven.kotlindiscord.com/repository/maven-snapshots/")
- } else {
- uri("https://maven.kotlindiscord.com/repository/maven-releases/")
- }
-
- credentials {
- username = project.findProperty("kotdis.user") as String?
- ?: System.getenv("KOTLIN_DISCORD_USER")
-
- password = project.findProperty("kotdis.password") as String?
- ?: System.getenv("KOTLIN_DISCORD_PASSWORD")
- }
-
- version = project.version
- }
- }
-
- publications {
- create("maven") {
- from(components.getByName("java"))
-
- artifact(sourceJar)
- artifact(javadocJar)
- }
- }
-}
-
-fun runCommand(command: String): String {
- val output = ByteArrayOutputStream()
-
- project.exec {
- commandLine(command.split(" "))
- standardOutput = output
- }
-
- return output.toString().trim()
-}
-
-fun getCurrentGitBranch(): String { // https://gist.github.com/lordcodes/15b2a4aecbeff7c3238a70bfd20f0931
- var gitBranch = "Unknown branch"
-
- try {
- gitBranch = runCommand("git rev-parse --abbrev-ref HEAD")
- } catch (t: Throwable) {
- println(t)
- }
-
- return gitBranch
-}
-
-tasks.dokkaHtml.configure {
- moduleName.set("Kord Extensions")
-
- dokkaSourceSets {
- configureEach {
- includeNonPublic.set(false)
- skipDeprecated.set(false)
-
- displayName.set("Kord Extensions")
- includes.from("packages.md")
- jdkVersion.set(8)
-
- sourceLink {
- localDirectory.set(file("${project.projectDir}/src/main/kotlin"))
-
- remoteUrl.set(
- URL(
- "https://github.com/Kotlin-Discord/kord-extensions/" +
- "tree/${getCurrentGitBranch()}/kord-extensions/src/main/kotlin"
- )
- )
-
- remoteLineSuffix.set("#L")
- }
-
- externalDocumentationLink {
- url.set(URL("http://kordlib.github.io/kord/common/common/"))
- }
-
- externalDocumentationLink {
- url.set(URL("http://kordlib.github.io/kord/core/core/"))
- }
- }
- }
-}
-
-tasks.test {
- useJUnitPlatform()
-
- testLogging.showStandardStreams = true
-
- testLogging {
- events("PASSED", "FAILED", "SKIPPED", "STANDARD_OUT", "STANDARD_ERROR")
- }
+val compileKotlin: KotlinCompile by tasks
- systemProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug")
+compileKotlin.kotlinOptions {
+ languageVersion = "1.5"
}
-tasks.build {
- this.finalizedBy(sourceJar, javadocJar)
+dokkaModule {
+ includes.add("packages.md")
}
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/Exceptions.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/Exceptions.kt
index f4c25bb7e8..af6b8d68e6 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/Exceptions.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/Exceptions.kt
@@ -1,8 +1,11 @@
package com.kotlindiscord.kord.extensions
-import com.kotlindiscord.kord.extensions.commands.MessageCommand
+import com.kotlindiscord.kord.extensions.commands.Argument
+import com.kotlindiscord.kord.extensions.commands.Arguments
+import com.kotlindiscord.kord.extensions.commands.chat.ChatCommand
import com.kotlindiscord.kord.extensions.events.EventHandler
import com.kotlindiscord.kord.extensions.extensions.Extension
+import com.kotlindiscord.kord.extensions.parser.StringParser
import kotlin.reflect.KClass
/**
@@ -50,10 +53,10 @@ public class EventHandlerRegistrationException(public val reason: String) : Exte
}
/**
- * Thrown when a [MessageCommand] could not be validated.
+ * Thrown when a [ChatCommand] could not be validated.
*
- * @param name The [MessageCommand] name
- * @param reason Why this [MessageCommand] is considered invalid.
+ * @param name The [ChatCommand] name
+ * @param reason Why this [ChatCommand] is considered invalid.
*/
public class InvalidCommandException(public val name: String?, public val reason: String) : ExtensionsException() {
override fun toString(): String {
@@ -66,10 +69,10 @@ public class InvalidCommandException(public val name: String?, public val reason
}
/**
- * Thrown when an attempt to register a [MessageCommand] fails.
+ * Thrown when an attempt to register a [ChatCommand] fails.
*
- * @param name The [MessageCommand] name
- * @param reason Why this [MessageCommand] could not be registered.
+ * @param name The [ChatCommand] name
+ * @param reason Why this [ChatCommand] could not be registered.
*/
public class CommandRegistrationException(public val name: String?, public val reason: String) : ExtensionsException() {
override fun toString(): String {
@@ -82,14 +85,40 @@ public class CommandRegistrationException(public val name: String?, public val r
}
/**
- * Thrown when something bad happens during command processing.
+ * Thrown when something exceptional happens that the actioning user on Discord needs to be aware of.
*
* Provided [reason] will be returned to the user verbatim.
*
- * @param reason Human-readable reason for the failure.
+ * @param reason Human-readable reason for the failure. May be translated.
+ * @param translationKey Translation key used to create the [reason] string, if any.
*/
-public open class CommandException(public var reason: String) : ExtensionsException() {
- public constructor(other: CommandException) : this(other.reason)
+public open class DiscordRelayedException(
+ public open val reason: String,
+ public open val translationKey: String? = null
+) : ExtensionsException() {
+ public constructor(other: DiscordRelayedException) : this(other.reason)
+
+ override fun toString(): String = reason
+}
+
+/**
+ * Thrown when something happens during argument parsing.
+ *
+ * @param reason Human-readable reason for the failure. May be translated.
+ * @param translationKey Translation key used to create the [reason] string, if any.
+ * @param argument Current Argument object, if any.
+ * @param arguments Arguments object for the command.
+ * @param parser Tokenizing string parser used for this parse attempt, if this was a chat command.
+ */
+public open class ArgumentParsingException(
+ public override val reason: String,
+ public override val translationKey: String?,
+ public val argument: Argument<*>?,
+ public val arguments: Arguments,
+ public val parser: StringParser?
+) : DiscordRelayedException(reason, translationKey) {
+ public constructor(other: ArgumentParsingException) :
+ this(other.reason, other.translationKey, other.argument, other.arguments, other.parser)
override fun toString(): String = reason
}
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/ExtensibleBot.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/ExtensibleBot.kt
index 10c3014c64..7ad781da63 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/ExtensibleBot.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/ExtensibleBot.kt
@@ -3,12 +3,11 @@
package com.kotlindiscord.kord.extensions
import com.kotlindiscord.kord.extensions.builders.ExtensibleBotBuilder
-import com.kotlindiscord.kord.extensions.commands.MessageCommand
-import com.kotlindiscord.kord.extensions.commands.MessageCommandRegistry
-import com.kotlindiscord.kord.extensions.commands.parser.Arguments
-import com.kotlindiscord.kord.extensions.commands.slash.SlashCommandRegistry
+import com.kotlindiscord.kord.extensions.commands.application.ApplicationCommandRegistry
+import com.kotlindiscord.kord.extensions.commands.chat.ChatCommandRegistry
+import com.kotlindiscord.kord.extensions.components.ComponentRegistry
import com.kotlindiscord.kord.extensions.events.EventHandler
-import com.kotlindiscord.kord.extensions.events.ExtensionEvent
+import com.kotlindiscord.kord.extensions.events.KordExEvent
import com.kotlindiscord.kord.extensions.extensions.Extension
import com.kotlindiscord.kord.extensions.extensions.impl.HelpExtension
import com.kotlindiscord.kord.extensions.extensions.impl.SentryExtension
@@ -20,7 +19,7 @@ import dev.kord.core.event.Event
import dev.kord.core.event.gateway.DisconnectEvent
import dev.kord.core.event.gateway.ReadyEvent
import dev.kord.core.event.guild.GuildCreateEvent
-import dev.kord.core.event.interaction.InteractionCreateEvent
+import dev.kord.core.event.interaction.*
import dev.kord.core.event.message.MessageCreateEvent
import dev.kord.core.on
import dev.kord.gateway.Intents
@@ -33,7 +32,6 @@ import kotlinx.coroutines.launch
import mu.KLogger
import mu.KotlinLogging
import org.koin.core.component.KoinComponent
-import org.koin.core.component.inject
import org.koin.dsl.bind
/**
@@ -49,50 +47,6 @@ import org.koin.dsl.bind
* @param token Token for connecting to Discord.
*/
public open class ExtensibleBot(public val settings: ExtensibleBotBuilder, private val token: String) : KoinComponent {
- /**
- * @suppress
- */
- @Deprecated(
- "Use Koin to get this instead. This will be private in future.",
-
- ReplaceWith(
- "getKoin().get()",
-
- "com.kotlindiscord.kord.extensions.utils.getKoin",
- "dev.kord.core.Kord"
- ),
- level = DeprecationLevel.ERROR
- )
- public val kord: Kord by inject()
-
- /** Message command registry, keeps track of and executes message commands. **/
- @Deprecated(
- "Use Koin to get this instead. This will be made private in future.",
-
- ReplaceWith(
- "getKoin().get()",
-
- "com.kotlindiscord.kord.extensions.utils.getKoin",
- "com.kotlindiscord.kord.extensions.commands.MessageCommandRegistry"
- ),
- level = DeprecationLevel.ERROR
- )
- public open val messageCommands: MessageCommandRegistry by inject()
-
- /** Slash command registry, keeps track of and executes slash commands. **/
- @Deprecated(
- "Use Koin to get this instead. This will be made private in future.",
-
- ReplaceWith(
- "getKoin().get()",
-
- "com.kotlindiscord.kord.extensions.utils.getKoin",
- "com.kotlindiscord.kord.extensions.commands.slash.SlashCommandRegistry"
- ),
- level = DeprecationLevel.ERROR
- )
- public open val slashCommands: SlashCommandRegistry by inject()
-
/**
* A list of all registered event handlers.
*/
@@ -117,45 +71,44 @@ public open class ExtensibleBot(public val settings: ExtensibleBotBuilder, priva
/** @suppress Function that sets up the bot early on, called by the builder. **/
public open suspend fun setup() {
- val kord = Kord(token) {
+ val kord = settings.kordBuilder(token) {
cache {
settings.cacheBuilder.builder.invoke(this, it)
}
defaultStrategy = settings.cacheBuilder.defaultStrategy
- if (settings.intentsBuilder != null) {
- this.intents = Intents(settings.intentsBuilder!!)
- }
-
if (settings.shardingBuilder != null) {
sharding(settings.shardingBuilder!!)
}
enableShutdownHook = settings.hooksBuilder.kordShutdownHook
- settings.kordBuilders.forEach { it() }
+ settings.kordHooks.forEach { it() }
}
loadModule { single { kord } bind Kord::class }
settings.cacheBuilder.dataCacheBuilder.invoke(kord, kord.cache)
- registerListeners()
- addDefaultExtensions()
-
kord.on {
- kord.launch {
+ this.launch {
send(this@on)
}
}
+
+ addDefaultExtensions()
}
/** Start up the bot and log into Discord. **/
public open suspend fun start() {
settings.hooksBuilder.runBeforeStart(this)
+ registerListeners()
- getKoin().get().login(settings.presenceBuilder)
+ getKoin().get().login {
+ this.presence(settings.presenceBuilder)
+ this.intents = Intents(settings.intentsBuilder!!)
+ }
}
/** This function sets up all of the bot's default event listeners. **/
@@ -178,39 +131,61 @@ public open class ExtensibleBot(public val settings: ExtensibleBotBuilder, priva
logger.warn { "Disconnected: $closeCode" }
}
- on {
- if (!initialized) { // We do this because a reconnect will cause this event to happen again.
- initialized = true
-
- if (settings.slashCommandsBuilder.enabled) {
- getKoin().get().syncAll()
- } else {
- logger.info {
- "Slash command support is disabled - set `enabled` to `true` in the `slashCommands` builder" +
- " if you want to use them."
- }
- }
- }
+ on {
+ getKoin().get().handle(this)
+ }
- logger.info { "Ready!" }
+ on {
+ getKoin().get().handle(this)
}
- if (settings.messageCommandsBuilder.enabled) {
+ if (settings.chatCommandsBuilder.enabled) {
on {
- getKoin().get().handleEvent(this)
+ getKoin().get().handleEvent(this)
+ }
+ } else {
+ logger.debug {
+ "Chat command support is disabled - set `enabled` to `true` in the `chatCommands` builder" +
+ " if you want to use them."
}
}
- if (settings.slashCommandsBuilder.enabled) {
- on {
- getKoin().get().handle(this)
+ if (settings.applicationCommandsBuilder.enabled) {
+ on {
+ getKoin().get().handle(this)
+ }
+
+ on {
+ getKoin().get().handle(this)
+ }
+
+ on {
+ getKoin().get().handle(this)
}
+
+ getKoin().get().initialRegistration()
+ } else {
+ logger.debug {
+ "Application command support is disabled - set `enabled` to `true` in the " +
+ "`applicationCommands` builder if you want to use them."
+ }
+ }
+
+ if (!initialized) {
+ eventHandlers.forEach { handler ->
+ handler.listenerRegistrationCallable?.invoke() ?: logger.error {
+ "Event handler $handler does not have a listener registration callback. This should never happen!"
+ }
+ }
+
+ initialized = true
}
}
/** This function adds all of the default extensions when the bot is being set up. **/
public open suspend fun addDefaultExtensions() {
val extBuilder = settings.extensionsBuilder
+
if (extBuilder.helpExtensionBuilder.enableBundledExtension) {
this.addExtension(::HelpExtension)
}
@@ -224,13 +199,13 @@ public open class ExtensibleBot(public val settings: ExtensibleBotBuilder, priva
* Subscribe to an event. You shouldn't need to use this directly, but it's here just in case.
*
* You can subscribe to any type, realistically - but this is intended to be used only with Kord
- * [Event] subclasses, and our own [ExtensionEvent]s.
+ * [Event] subclasses, and our own [KordExEvent]s.
*
* @param T Types of event to subscribe to.
* @param scope Coroutine scope to run the body of your callback under.
* @param consumer The callback to run when the event is fired.
*/
- public inline fun on(
+ public inline fun on(
launch: Boolean = true,
scope: CoroutineScope = this.getKoin().get(),
noinline consumer: suspend T.() -> Unit
@@ -239,7 +214,7 @@ public open class ExtensibleBot(public val settings: ExtensibleBotBuilder, priva
.filterIsInstance()
.onEach {
runCatching {
- if (launch) scope.launch { consumer(it) } else consumer(it)
+ if (launch) it.launch { consumer(it) } else consumer(it)
}.onFailure { logger.catching(it) }
}.catch { logger.catching(it) }
.launchIn(scope)
@@ -336,55 +311,32 @@ public open class ExtensibleBot(public val settings: ExtensibleBotBuilder, priva
}
/**
- * Directly register a [MessageCommand] to this bot.
+ * Directly register an [EventHandler] to this bot.
*
* Generally speaking, you shouldn't call this directly - instead, create an [Extension] and
- * call the [Extension.command] function in your [Extension.setup] function.
- *
- * This function will throw a [CommandRegistrationException] if the command has already been registered, if
- * a command with the same name exists, or if a command with one of the same aliases exists.
- *
- * @param command The command to be registered.
- * @throws CommandRegistrationException Thrown if the command could not be registered.
- */
- @Deprecated(
- "Use the equivalent function within `MessageCommandRegistry` instead.",
-
- ReplaceWith(
- "getKoin().get().add(command)",
-
- "org.koin.core.component.KoinComponent.getKoin",
- "com.kotlindiscord.kord.extensions.commands.MessageCommand"
- ),
- level = DeprecationLevel.ERROR
- )
- @Throws(CommandRegistrationException::class)
- public open fun addCommand(command: MessageCommand): Unit = getKoin()
- .get()
- .add(command)
-
- /**
- * Directly remove a registered [MessageCommand] from this bot.
+ * call the [Extension.event] function in your [Extension.setup] function.
*
- * This function is used when extensions are unloaded, in order to clear out their commands.
- * No exception is thrown if the command wasn't registered.
+ * This function will throw an [EventHandlerRegistrationException] if the event handler has already been registered.
*
- * @param command The command to be removed.
+ * @param handler The event handler to be registered.
+ * @throws EventHandlerRegistrationException Thrown if the event handler could not be registered.
*/
- @Deprecated(
- "Use the equivalent function within `MessageCommandRegistry` instead.",
+ @Throws(EventHandlerRegistrationException::class)
+ public inline fun addEventHandler(handler: EventHandler) {
+ if (eventHandlers.contains(handler)) {
+ throw EventHandlerRegistrationException(
+ "Event handler already registered in '${handler.extension.name}' extension."
+ )
+ }
- ReplaceWith(
- "getKoin().get().remove(command)",
+ if (initialized) {
+ handler.listenerRegistrationCallable?.invoke() ?: error(
+ "Event handler $handler does not have a listener registration callback. This should never happen!"
+ )
+ }
- "org.koin.core.component.KoinComponent.getKoin",
- "com.kotlindiscord.kord.extensions.commands.MessageCommand"
- ),
- level = DeprecationLevel.ERROR
- )
- public open fun removeCommand(command: MessageCommand): Boolean = getKoin()
- .get()
- .remove(command)
+ eventHandlers.add(handler)
+ }
/**
* Directly register an [EventHandler] to this bot.
@@ -398,18 +350,10 @@ public open class ExtensibleBot(public val settings: ExtensibleBotBuilder, priva
* @throws EventHandlerRegistrationException Thrown if the event handler could not be registered.
*/
@Throws(EventHandlerRegistrationException::class)
- public inline fun addEventHandler(handler: EventHandler): Job {
- if (eventHandlers.contains(handler)) {
- throw EventHandlerRegistrationException(
- "Event handler already registered in '${handler.extension.name}' extension."
- )
+ public inline fun registerListenerForHandler(handler: EventHandler): Job {
+ return on {
+ handler.call(this)
}
-
- val job = on { handler.call(this) }
-
- eventHandlers.add(handler)
-
- return job
}
/**
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/builders/ExtensibleBotBuilder.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/builders/ExtensibleBotBuilder.kt
index 661862b1df..e528b581ec 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/builders/ExtensibleBotBuilder.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/builders/ExtensibleBotBuilder.kt
@@ -6,13 +6,21 @@ import com.kotlindiscord.kord.extensions.DISCORD_BLURPLE
import com.kotlindiscord.kord.extensions.ExtensibleBot
import com.kotlindiscord.kord.extensions.annotations.BotBuilderDSL
import com.kotlindiscord.kord.extensions.checks.types.Check
-import com.kotlindiscord.kord.extensions.commands.MessageCommandRegistry
-import com.kotlindiscord.kord.extensions.commands.slash.SlashCommandRegistry
+import com.kotlindiscord.kord.extensions.checks.types.MessageCommandCheck
+import com.kotlindiscord.kord.extensions.checks.types.SlashCommandCheck
+import com.kotlindiscord.kord.extensions.checks.types.UserCommandCheck
+import com.kotlindiscord.kord.extensions.commands.application.ApplicationCommandRegistry
+import com.kotlindiscord.kord.extensions.commands.application.DefaultApplicationCommandRegistry
+import com.kotlindiscord.kord.extensions.commands.chat.ChatCommandRegistry
+import com.kotlindiscord.kord.extensions.components.ComponentRegistry
+import com.kotlindiscord.kord.extensions.components.callbacks.ComponentCallbackRegistry
import com.kotlindiscord.kord.extensions.extensions.Extension
import com.kotlindiscord.kord.extensions.i18n.ResourceBundleTranslations
import com.kotlindiscord.kord.extensions.i18n.SupportedLocales
import com.kotlindiscord.kord.extensions.i18n.TranslationsProvider
import com.kotlindiscord.kord.extensions.sentry.SentryAdapter
+import com.kotlindiscord.kord.extensions.types.FailureReason
+import com.kotlindiscord.kord.extensions.utils.getKoin
import com.kotlindiscord.kord.extensions.utils.loadModule
import dev.kord.cache.api.DataCache
import dev.kord.common.Color
@@ -25,14 +33,14 @@ import dev.kord.core.behavior.GuildBehavior
import dev.kord.core.behavior.UserBehavior
import dev.kord.core.behavior.channel.ChannelBehavior
import dev.kord.core.builder.kord.KordBuilder
-import dev.kord.core.builder.kord.Shards
import dev.kord.core.cache.KordCacheBuilder
-import dev.kord.core.event.interaction.InteractionCreateEvent
import dev.kord.core.event.message.MessageCreateEvent
import dev.kord.core.supplier.EntitySupplier
import dev.kord.core.supplier.EntitySupplyStrategy
import dev.kord.gateway.Intents
import dev.kord.gateway.builder.PresenceBuilder
+import dev.kord.gateway.builder.Shards
+import dev.kord.rest.builder.message.create.MessageCreateBuilder
import mu.KLogger
import mu.KotlinLogging
import org.koin.core.context.startKoin
@@ -49,6 +57,9 @@ internal typealias LocaleResolver = suspend (
user: UserBehavior?
) -> Locale?
+internal typealias FailureResponseBuilder =
+ suspend (MessageCreateBuilder).(message: String, type: FailureReason<*>) -> Unit
+
/**
* Builder class used for configuring and creating an [ExtensibleBot].
*
@@ -60,6 +71,14 @@ public open class ExtensibleBotBuilder {
/** @suppress Builder that shouldn't be set directly by the user. **/
public val cacheBuilder: CacheBuilder = CacheBuilder()
+ /** @suppress Builder that shouldn't be set directly by the user. **/
+ public val componentsBuilder: ComponentsBuilder = ComponentsBuilder()
+
+ /**
+ * @suppress Builder that shouldn't be set directly by the user.
+ */
+ public var failureResponseBuilder: FailureResponseBuilder = { message, _ -> content = message }
+
/** @suppress Builder that shouldn't be set directly by the user. **/
public open val extensionsBuilder: ExtensionsBuilder = ExtensionsBuilder()
@@ -70,13 +89,21 @@ public open class ExtensibleBotBuilder {
public val i18nBuilder: I18nBuilder = I18nBuilder()
/** @suppress Builder that shouldn't be set directly by the user. **/
- public var intentsBuilder: (Intents.IntentsBuilder.() -> Unit)? = null
+ public var intentsBuilder: (Intents.IntentsBuilder.() -> Unit)? = {
+ +Intents.nonPrivileged
+
+ getKoin().get().extensions.values.forEach { extension ->
+ extension.intents.forEach {
+ +it
+ }
+ }
+ }
/** @suppress Builder that shouldn't be set directly by the user. **/
public val membersBuilder: MembersBuilder = MembersBuilder()
/** @suppress Builder that shouldn't be set directly by the user. **/
- public val messageCommandsBuilder: MessageCommandsBuilder = MessageCommandsBuilder()
+ public val chatCommandsBuilder: ChatCommandsBuilder = ChatCommandsBuilder()
/** @suppress Builder that shouldn't be set directly by the user. **/
public var presenceBuilder: PresenceBuilder.() -> Unit = { status = PresenceStatus.Online }
@@ -85,10 +112,15 @@ public open class ExtensibleBotBuilder {
public var shardingBuilder: ((recommended: Int) -> Shards)? = null
/** @suppress Builder that shouldn't be set directly by the user. **/
- public val slashCommandsBuilder: SlashCommandsBuilder = SlashCommandsBuilder()
+ public val applicationCommandsBuilder: ApplicationCommandsBuilder = ApplicationCommandsBuilder()
/** @suppress List of Kord builders, shouldn't be set directly by the user. **/
- public val kordBuilders: MutableList Unit> = mutableListOf()
+ public val kordHooks: MutableList Unit> = mutableListOf()
+
+ /** @suppress Kord builder, creates a Kord instance. **/
+ public var kordBuilder: suspend (String, suspend KordBuilder.() -> Unit) -> Kord = { token, builder ->
+ Kord(token) { builder() }
+ }
/** Logging level Koin should use, defaulting to ERROR. **/
public var koinLogLevel: Level = Level.ERROR
@@ -103,6 +135,25 @@ public open class ExtensibleBotBuilder {
builder(cacheBuilder)
}
+ /**
+ * DSL function used to configure the bot's components system.
+ *
+ * @see ComponentsBuilder
+ */
+ @BotBuilderDSL
+ public suspend fun components(builder: suspend ComponentsBuilder.() -> Unit) {
+ builder(componentsBuilder)
+ }
+
+ /**
+ * Register the message builder responsible for formatting error responses, which are sent to users during command
+ * and component body execution.
+ */
+ @BotBuilderDSL
+ public fun errorResponse(builder: FailureResponseBuilder) {
+ failureResponseBuilder = builder
+ }
+
/**
* DSL function used to insert code at various points in the bot's lifecycle.
*
@@ -114,8 +165,8 @@ public open class ExtensibleBotBuilder {
}
/**
- * DSL function allowing for additional Kord builders to be specified, allowing for direct customisation of the
- * Kord object.
+ * DSL function allowing for additional Kord configuration builders to be specified, allowing for direct
+ * customisation of the Kord object.
*
* Multiple builders may be registered, and they'll be called in the order they were registered here. Builders are
* called after Kord Extensions has applied its own builder actions - so you can override the changes it makes here
@@ -125,27 +176,39 @@ public open class ExtensibleBotBuilder {
*/
@BotBuilderDSL
public fun kord(builder: suspend KordBuilder.() -> Unit) {
- kordBuilders.add(builder)
+ kordHooks.add(builder)
}
/**
- * DSL function used to configure the bot's message command options.
+ * Function allowing you to specify a callable that constructs and returns a Kord instance. This can be used
+ * to specify your own Kord subclass, if you need to - but shouldn't be a replacement for registering a [kord]
+ * configuration builder.
*
- * @see MessageCommandsBuilder
+ * @see Kord
*/
@BotBuilderDSL
- public suspend fun messageCommands(builder: suspend MessageCommandsBuilder.() -> Unit) {
- builder(messageCommandsBuilder)
+ public fun customKordBuilder(builder: suspend (String, suspend KordBuilder.() -> Unit) -> Kord) {
+ kordBuilder = builder
}
/**
- * DSL function used to configure the bot's slash command options.
+ * DSL function used to configure the bot's chat command options.
*
- * @see SlashCommandsBuilder
+ * @see ChatCommandsBuilder
*/
@BotBuilderDSL
- public suspend fun slashCommands(builder: suspend SlashCommandsBuilder.() -> Unit) {
- builder(slashCommandsBuilder)
+ public suspend fun chatCommands(builder: suspend ChatCommandsBuilder.() -> Unit) {
+ builder(chatCommandsBuilder)
+ }
+
+ /**
+ * DSL function used to configure the bot's application command options.
+ *
+ * @see ApplicationCommandsBuilder
+ */
+ @BotBuilderDSL
+ public suspend fun applicationCommands(builder: suspend ApplicationCommandsBuilder.() -> Unit) {
+ builder(applicationCommandsBuilder)
}
/**
@@ -161,11 +224,33 @@ public open class ExtensibleBotBuilder {
/**
* DSL function used to configure the bot's intents.
*
+ * @param addDefaultIntents Whether to automatically add all non-privileged intents to the builder before running
+ * the given lambda.
+ * @param addDefaultIntents Whether to automatically add the required intents defined within each loaded extension
+ *
* @see Intents.IntentsBuilder
*/
@BotBuilderDSL
- public fun intents(builder: Intents.IntentsBuilder.() -> Unit) {
- this.intentsBuilder = builder
+ public fun intents(
+ addDefaultIntents: Boolean = true,
+ addExtensionIntents: Boolean = true,
+ builder: Intents.IntentsBuilder.() -> Unit
+ ) {
+ this.intentsBuilder = {
+ if (addDefaultIntents) {
+ +Intents.nonPrivileged
+ }
+
+ if (addExtensionIntents) {
+ getKoin().get().extensions.values.forEach { extension ->
+ extension.intents.forEach {
+ +it
+ }
+ }
+ }
+
+ builder()
+ }
}
/**
@@ -225,8 +310,15 @@ public open class ExtensibleBotBuilder {
loadModule { single { this@ExtensibleBotBuilder } bind ExtensibleBotBuilder::class }
loadModule { single { i18nBuilder.translationsProvider } bind TranslationsProvider::class }
- loadModule { single { messageCommandsBuilder.registryBuilder() } bind MessageCommandRegistry::class }
- loadModule { single { slashCommandsBuilder.slashRegistryBuilder() } bind SlashCommandRegistry::class }
+ loadModule { single { chatCommandsBuilder.registryBuilder() } bind ChatCommandRegistry::class }
+ loadModule { single { componentsBuilder.registryBuilder() } bind ComponentRegistry::class }
+ loadModule { single { componentsBuilder.callbackRegistryBuilder() } bind ComponentCallbackRegistry::class }
+
+ loadModule {
+ single {
+ applicationCommandsBuilder.applicationCommandRegistryBuilder()
+ } bind ApplicationCommandRegistry::class
+ }
loadModule {
single {
@@ -252,11 +344,14 @@ public open class ExtensibleBotBuilder {
loadModule { single { bot } bind ExtensibleBot::class }
hooksBuilder.runCreated(bot)
+
bot.setup()
- hooksBuilder.runSetup(bot)
+ hooksBuilder.runSetup(bot)
hooksBuilder.runBeforeExtensionsAdded(bot)
+
extensionsBuilder.extensions.forEach { bot.addExtension(it) }
+
hooksBuilder.runAfterExtensionsAdded(bot)
return bot
@@ -269,7 +364,7 @@ public open class ExtensibleBotBuilder {
* Number of messages to keep in the cache. Defaults to 10,000.
*
* To disable automatic configuration of the message cache, set this to `null` or `0`. You can configure the
- * cache yourself using the [kord] function, and interact with the resulting [DataCache] object using the
+ * cache yourself using the [kordHook] function, and interact with the resulting [DataCache] object using the
* [transformCache] function.
*/
@Suppress("MagicNumber")
@@ -306,6 +401,32 @@ public open class ExtensibleBotBuilder {
}
}
+ /** Builder used to configure the bot's components settings. **/
+ @BotBuilderDSL
+ public class ComponentsBuilder {
+ /** @suppress Component callback registry builder. **/
+ public var callbackRegistryBuilder: () -> ComponentCallbackRegistry = ::ComponentCallbackRegistry
+
+ /** @suppress Component registry builder. **/
+ public var registryBuilder: () -> ComponentRegistry = ::ComponentRegistry
+
+ /**
+ * Register a builder (usually a constructor) returning a [ComponentCallbackRegistry] instance, which may
+ * be useful if you need to register a custom subclass.
+ */
+ public fun callbackRegistry(builder: () -> ComponentCallbackRegistry) {
+ callbackRegistryBuilder = builder
+ }
+
+ /**
+ * Register a builder (usually a constructor) returning a [ComponentRegistry] instance, which may be useful
+ * if you need to register a custom subclass.
+ */
+ public fun registry(builder: () -> ComponentRegistry) {
+ registryBuilder = builder
+ }
+ }
+
/** Builder used for configuring the bot's extension options, and registering custom extensions. **/
@BotBuilderDSL
public open class ExtensionsBuilder {
@@ -318,14 +439,6 @@ public open class ExtensibleBotBuilder {
/** @suppress Sentry extension builder. **/
public open val sentryExtensionBuilder: SentryExtensionBuilder = SentryExtensionBuilder()
- /** Whether to enable the bundled Sentry extension. Defaults to `true`. **/
- @Deprecated("Use the sentry { } builder instead.", level = DeprecationLevel.ERROR)
- public var sentry: Boolean
- get() = sentryExtensionBuilder.debug
- set(value) {
- sentryExtensionBuilder.debug = value
- }
-
/** Add a custom extension to the bot via a builder - probably the extension constructor. **/
public open fun add(builder: () -> Extension) {
extensions.add(builder)
@@ -517,6 +630,7 @@ public open class ExtensibleBotBuilder {
* Register a lambda to be called after all the extensions in the [ExtensionsBuilder] have been added. This
* will be called regardless of how many were successfully set up.
*/
+ @BotBuilderDSL
public fun afterExtensionsAdded(body: suspend ExtensibleBot.() -> Unit): Boolean =
afterExtensionsAddedList.add(body)
@@ -524,6 +638,7 @@ public open class ExtensibleBotBuilder {
* Register a lambda to be called after Koin has been set up. You can use this to register overriding modules
* via `loadModule` before the modules are actually accessed.
*/
+ @BotBuilderDSL
public fun afterKoinSetup(body: () -> Unit): Boolean =
afterKoinSetupList.add(body)
@@ -531,18 +646,21 @@ public open class ExtensibleBotBuilder {
* Register a lambda to be called before Koin has been set up. You can use this to register Koin modules
* early, if needed.
*/
+ @BotBuilderDSL
public fun beforeKoinSetup(body: () -> Unit): Boolean =
beforeKoinSetupList.add(body)
/**
* Register a lambda to be called before all the extensions in the [ExtensionsBuilder] have been added.
*/
+ @BotBuilderDSL
public fun beforeExtensionsAdded(body: suspend ExtensibleBot.() -> Unit): Boolean =
beforeExtensionsAddedList.add(body)
/**
* Register a lambda to be called just before the bot tries to connect to Discord.
*/
+ @BotBuilderDSL
public fun beforeStart(body: suspend ExtensibleBot.() -> Unit): Boolean =
beforeStartList.add(body)
@@ -550,18 +668,21 @@ public open class ExtensibleBotBuilder {
* Register a lambda to be called right after the [ExtensibleBot] object has been created, before it gets set
* up.
*/
+ @BotBuilderDSL
public fun created(body: suspend ExtensibleBot.() -> Unit): Boolean =
createdList.add(body)
/**
* Register a lambda to be called after any extension is successfully added to the bot.
*/
+ @BotBuilderDSL
public fun extensionAdded(body: suspend ExtensibleBot.(extension: Extension) -> Unit): Boolean =
extensionAddedList.add(body)
/**
* Register a lambda to be called after the [ExtensibleBot] object has been created and set up.
*/
+ @BotBuilderDSL
public fun setup(body: suspend ExtensibleBot.() -> Unit): Boolean =
setupList.add(body)
@@ -730,7 +851,7 @@ public open class ExtensibleBotBuilder {
* Requires the `GUILD_MEMBERS` privileged intent. Make sure you've enabled it for your bot!
*/
@JvmName("fillLongs") // These are the same for the JVM
- public fun fill(ids: Collection): Boolean? =
+ public fun fill(ids: Collection): Boolean? =
guildsToFill?.addAll(ids.map { Snowflake(it) })
/**
@@ -755,7 +876,7 @@ public open class ExtensibleBotBuilder {
*
* Requires the `GUILD_MEMBERS` privileged intent. Make sure you've enabled it for your bot!
*/
- public fun fill(id: Long): Boolean? =
+ public fun fill(id: ULong): Boolean? =
guildsToFill?.add(Snowflake(id))
/**
@@ -783,26 +904,23 @@ public open class ExtensibleBotBuilder {
}
}
- /** Builder used for configuring the bot's message command options. **/
+ /** Builder used for configuring the bot's chat command options. **/
@BotBuilderDSL
- public class MessageCommandsBuilder {
- /** Whether to invoke commands on bot mentions, in addition to using message prefixes. Defaults to `true`. **/
+ public class ChatCommandsBuilder {
+ /** Whether to invoke commands on bot mentions, in addition to using chat prefixes. Defaults to `true`. **/
public var invokeOnMention: Boolean = true
/** Prefix to require for command invocations on Discord. Defaults to `"!"`. **/
public var defaultPrefix: String = "!"
- /** Whether to register and process message commands. Defaults to `true`. **/
- public var enabled: Boolean = true
-
- /** Number of threads to use for command execution. Defaults to twice the number of CPU threads. **/
- public var threads: Int = Runtime.getRuntime().availableProcessors() * 2
+ /** Whether to register and process chat commands. Defaults to `false`. **/
+ public var enabled: Boolean = false
/** @suppress Builder that shouldn't be set directly by the user. **/
public var prefixCallback: suspend (MessageCreateEvent).(String) -> String = { defaultPrefix }
/** @suppress Builder that shouldn't be set directly by the user. **/
- public var registryBuilder: () -> MessageCommandRegistry = { MessageCommandRegistry() }
+ public var registryBuilder: () -> ChatCommandRegistry = { ChatCommandRegistry() }
/**
* List of command checks.
@@ -815,7 +933,7 @@ public open class ExtensibleBotBuilder {
* Register a lambda that takes a [MessageCreateEvent] object and the default prefix, and returns the
* command prefix to be made use of for that message event.
*
- * This is intended to allow for different message command prefixes in different contexts - for example,
+ * This is intended to allow for different chat command prefixes in different contexts - for example,
* guild-specific prefixes.
*/
public fun prefix(builder: suspend (MessageCreateEvent).(String) -> String) {
@@ -823,10 +941,10 @@ public open class ExtensibleBotBuilder {
}
/**
- * Register the builder used to create the [MessageCommandRegistry]. You can change this if you need to make
- * use of a subclass.
+ * Register the builder used to create the [ChatCommandRegistry]. You can change this if you need to
+ * make use of a subclass.
*/
- public fun registry(builder: () -> MessageCommandRegistry) {
+ public fun registry(builder: () -> ChatCommandRegistry) {
registryBuilder = builder
}
@@ -855,39 +973,54 @@ public open class ExtensibleBotBuilder {
}
}
- /** Builder used for configuring the bot's slash command options. **/
+ /** Builder used for configuring the bot's application command options. **/
@BotBuilderDSL
- public class SlashCommandsBuilder {
- /** Whether to register and process slash commands. Defaults to `false`. **/
- public var enabled: Boolean = false
+ public class ApplicationCommandsBuilder {
+ /** Whether to register and process application commands. Defaults to `true`. **/
+ public var enabled: Boolean = true
- /** The guild ID to use for all global slash commands. Intended for testing. **/
+ /** The guild ID to use for all global application commands. Intended for testing. **/
public var defaultGuild: Snowflake? = null
- /** Whether to attempt to register the bot's slash commands. Intended for multi-instance sharded bots. **/
+ /** Whether to attempt to register the bot's application commands. Intended for multi-instance sharded bots. **/
public var register: Boolean = true
/** @suppress Builder that shouldn't be set directly by the user. **/
- public var slashRegistryBuilder: () -> SlashCommandRegistry = { SlashCommandRegistry() }
+ public var applicationCommandRegistryBuilder: () -> ApplicationCommandRegistry =
+ { DefaultApplicationCommandRegistry() }
+
+ /**
+ * List of message command checks.
+ *
+ * These checks will be checked against all message commands.
+ */
+ public val messageCommandChecks: MutableList = mutableListOf()
/**
* List of slash command checks.
*
* These checks will be checked against all slash commands.
*/
- public val checkList: MutableList> = mutableListOf()
+ public val slashCommandChecks: MutableList = mutableListOf()
+
+ /**
+ * List of user command checks.
+ *
+ * These checks will be checked against all user commands.
+ */
+ public val userCommandChecks: MutableList = mutableListOf()
- /** Set a guild ID to use for all global slash commands. Intended for testing. **/
+ /** Set a guild ID to use for all global application commands. Intended for testing. **/
public fun defaultGuild(id: Snowflake) {
defaultGuild = id
}
- /** Set a guild ID to use for all global slash commands. Intended for testing. **/
- public fun defaultGuild(id: Long) {
+ /** Set a guild ID to use for all global application commands. Intended for testing. **/
+ public fun defaultGuild(id: ULong) {
defaultGuild = Snowflake(id)
}
- /** Set a guild ID to use for all global slash commands. Intended for testing. **/
+ /** Set a guild ID to use for all global application commands. Intended for testing. **/
public fun defaultGuild(id: String) {
defaultGuild = Snowflake(id)
}
@@ -896,15 +1029,40 @@ public open class ExtensibleBotBuilder {
* Register the builder used to create the [SlashCommandRegistry]. You can change this if you need to make
* use of a subclass.
*/
- public fun slashRegistry(builder: () -> SlashCommandRegistry) {
- slashRegistryBuilder = builder
+ public fun applicationCommandRegistry(builder: () -> ApplicationCommandRegistry) {
+ applicationCommandRegistryBuilder = builder
+ }
+
+ /**
+ * Define a check which must pass for a message command to be executed. This check will be applied to all
+ * message commands.
+ *
+ * A message command may have multiple checks - all checks must pass for the command to be executed.
+ * Checks will be run in the order that they're defined.
+ *
+ * This function can be used DSL-style with a given body, or it can be passed one or more
+ * predefined functions. See the samples for more information.
+ *
+ * @param checks Checks to apply to all slash commands.
+ */
+ public fun messageCommandCheck(vararg checks: MessageCommandCheck) {
+ checks.forEach { messageCommandChecks.add(it) }
}
/**
- * Define a check which must pass for a command to be executed. This check will be applied to all
+ * Overloaded message command check function to allow for DSL syntax.
+ *
+ * @param check Check to apply to all slash commands.
+ */
+ public fun messageCommandCheck(check: MessageCommandCheck) {
+ messageCommandChecks.add(check)
+ }
+
+ /**
+ * Define a check which must pass for a slash command to be executed. This check will be applied to all
* slash commands.
*
- * A command may have multiple checks - all checks must pass for the command to be executed.
+ * A slash command may have multiple checks - all checks must pass for the command to be executed.
* Checks will be run in the order that they're defined.
*
* This function can be used DSL-style with a given body, or it can be passed one or more
@@ -912,17 +1070,42 @@ public open class ExtensibleBotBuilder {
*
* @param checks Checks to apply to all slash commands.
*/
- public fun check(vararg checks: Check) {
- checks.forEach { checkList.add(it) }
+ public fun slashCommandCheck(vararg checks: SlashCommandCheck) {
+ checks.forEach { slashCommandChecks.add(it) }
}
/**
- * Overloaded check function to allow for DSL syntax.
+ * Overloaded slash command check function to allow for DSL syntax.
*
* @param check Check to apply to all slash commands.
*/
- public fun check(check: Check) {
- checkList.add(check)
+ public fun slashCommandCheck(check: SlashCommandCheck) {
+ slashCommandChecks.add(check)
+ }
+
+ /**
+ * Define a check which must pass for a user command to be executed. This check will be applied to all
+ * user commands.
+ *
+ * A user command may have multiple checks - all checks must pass for the command to be executed.
+ * Checks will be run in the order that they're defined.
+ *
+ * This function can be used DSL-style with a given body, or it can be passed one or more
+ * predefined functions. See the samples for more information.
+ *
+ * @param checks Checks to apply to all slash commands.
+ */
+ public fun userCommandCheck(vararg checks: UserCommandCheck) {
+ checks.forEach { userCommandChecks.add(it) }
+ }
+
+ /**
+ * Overloaded user command check function to allow for DSL syntax.
+ *
+ * @param check Check to apply to all slash commands.
+ */
+ public fun userCommandCheck(check: UserCommandCheck) {
+ userCommandChecks.add(check)
}
}
}
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/ChannelChecks.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/ChannelChecks.kt
index c6c4cf7cca..19791fbacd 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/ChannelChecks.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/ChannelChecks.kt
@@ -2,7 +2,7 @@
package com.kotlindiscord.kord.extensions.checks
-import com.kotlindiscord.kord.extensions.checks.types.Check
+import com.kotlindiscord.kord.extensions.checks.types.CheckContext
import dev.kord.common.entity.Snowflake
import dev.kord.core.behavior.channel.CategoryBehavior
import dev.kord.core.behavior.channel.ChannelBehavior
@@ -21,7 +21,11 @@ import mu.KotlinLogging
*
* @param builder Lambda returning the channel to compare to.
*/
-public fun inChannel(builder: suspend () -> ChannelBehavior): Check<*> = {
+public suspend fun CheckContext.inChannel(builder: suspend (T) -> ChannelBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.inChannel")
val eventChannel = channelFor(event)
@@ -30,7 +34,7 @@ public fun inChannel(builder: suspend () -> ChannelBehavior): Check<*> = {
fail()
} else {
- val channel = builder()
+ val channel = builder(event)
if (eventChannel.id == channel.id) {
logger.passed()
@@ -57,7 +61,11 @@ public fun inChannel(builder: suspend () -> ChannelBehavior): Check<*> = {
*
* @param builder Lambda returning the channel to compare to.
*/
-public fun notInChannel(builder: suspend () -> ChannelBehavior): Check<*> = {
+public suspend fun CheckContext.notInChannel(builder: suspend (T) -> ChannelBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.notInChannel")
val eventChannel = channelFor(event)
@@ -66,7 +74,7 @@ public fun notInChannel(builder: suspend () -> ChannelBehavior): Check<*> = {
pass()
} else {
- val channel = builder()
+ val channel = builder(event)
if (eventChannel.id != channel.id) {
logger.passed()
@@ -93,7 +101,11 @@ public fun notInChannel(builder: suspend () -> ChannelBehavior): Check<*> = {
*
* @param builder Lambda returning the category to compare to.
*/
-public fun inCategory(builder: suspend () -> CategoryBehavior): Check<*> = {
+public suspend fun CheckContext.inCategory(builder: suspend (T) -> CategoryBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.inCategory")
val eventChannel = topChannelFor(event)
@@ -102,7 +114,7 @@ public fun inCategory(builder: suspend () -> CategoryBehavior): Check<*> = {
fail()
} else {
- val category = builder()
+ val category = builder(event)
val channels = category.channels.toList().map { it.id }
if (channels.contains(eventChannel.id)) {
@@ -130,7 +142,11 @@ public fun inCategory(builder: suspend () -> CategoryBehavior): Check<*> = {
*
* @param builder Lambda returning the category to compare to.
*/
-public fun notInCategory(builder: suspend () -> CategoryBehavior): Check<*> = {
+public suspend fun CheckContext.notInCategory(builder: suspend (T) -> CategoryBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.notInCategory")
val eventChannel = topChannelFor(event)
@@ -139,7 +155,7 @@ public fun notInCategory(builder: suspend () -> CategoryBehavior): Check<*> = {
pass()
} else {
- val category = builder()
+ val category = builder(event)
val channels = category.channels.toList().map { it.id }
if (channels.contains(eventChannel.id)) {
@@ -167,7 +183,11 @@ public fun notInCategory(builder: suspend () -> CategoryBehavior): Check<*> = {
*
* @param builder Lambda returning the channel to compare to.
*/
-public fun channelHigher(builder: suspend () -> ChannelBehavior): Check<*> = {
+public suspend fun CheckContext.channelHigher(builder: suspend (T) -> ChannelBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.channelHigher")
val eventChannel = channelFor(event)
@@ -176,7 +196,7 @@ public fun channelHigher(builder: suspend () -> ChannelBehavior): Check<*> = {
fail()
} else {
- val channel = builder()
+ val channel = builder(event)
if (eventChannel > channel) {
logger.passed()
@@ -203,7 +223,11 @@ public fun channelHigher(builder: suspend () -> ChannelBehavior): Check<*> = {
*
* @param builder Lambda returning the channel to compare to.
*/
-public fun channelLower(builder: suspend () -> ChannelBehavior): Check<*> = {
+public suspend fun CheckContext.channelLower(builder: suspend (T) -> ChannelBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.channelLower")
val eventChannel = channelFor(event)
@@ -212,7 +236,7 @@ public fun channelLower(builder: suspend () -> ChannelBehavior): Check<*> = {
fail()
} else {
- val channel = builder()
+ val channel = builder(event)
if (eventChannel < channel) {
logger.passed()
@@ -239,7 +263,11 @@ public fun channelLower(builder: suspend () -> ChannelBehavior): Check<*> = {
*
* @param builder Lambda returning the channel to compare to.
*/
-public fun channelHigherOrEqual(builder: suspend () -> ChannelBehavior): Check<*> = {
+public suspend fun CheckContext.channelHigherOrEqual(builder: suspend (T) -> ChannelBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.channelHigherOrEqual")
val eventChannel = channelFor(event)
@@ -248,7 +276,7 @@ public fun channelHigherOrEqual(builder: suspend () -> ChannelBehavior): Check<*
fail()
} else {
- val channel = builder()
+ val channel = builder(event)
if (eventChannel >= channel) {
logger.passed()
@@ -275,7 +303,11 @@ public fun channelHigherOrEqual(builder: suspend () -> ChannelBehavior): Check<*
*
* @param builder Lambda returning the channel to compare to.
*/
-public fun channelLowerOrEqual(builder: suspend () -> ChannelBehavior): Check<*> = {
+public suspend fun CheckContext.channelLowerOrEqual(builder: suspend (T) -> ChannelBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.channelLowerOrEqual")
val eventChannel = channelFor(event)
@@ -284,7 +316,7 @@ public fun channelLowerOrEqual(builder: suspend () -> ChannelBehavior): Check<*>
fail()
} else {
- val channel = builder()
+ val channel = builder(event)
if (eventChannel <= channel) {
logger.passed()
@@ -316,7 +348,11 @@ public fun channelLowerOrEqual(builder: suspend () -> ChannelBehavior): Check<*>
*
* @param id Channel snowflake to compare to.
*/
-public fun inChannel(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.inChannel(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.inChannel")
val channel = event.kord.getChannel(id)
@@ -325,7 +361,7 @@ public fun inChannel(id: Snowflake): Check<*> = {
fail()
} else {
- inChannel { channel }()
+ inChannel { channel }
}
}
@@ -337,7 +373,11 @@ public fun inChannel(id: Snowflake): Check<*> = {
*
* @param id Channel snowflake to compare to.
*/
-public fun notInChannel(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.notInChannel(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.notInChannel")
val channel = event.kord.getChannel(id)
@@ -346,7 +386,7 @@ public fun notInChannel(id: Snowflake): Check<*> = {
pass()
} else {
- notInChannel { channel }()
+ notInChannel { channel }
}
}
@@ -358,7 +398,11 @@ public fun notInChannel(id: Snowflake): Check<*> = {
*
* @param id Category snowflake to compare to.
*/
-public fun inCategory(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.inCategory(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.inCategory")
val category = event.kord.getChannelOf(id)
@@ -367,7 +411,7 @@ public fun inCategory(id: Snowflake): Check<*> = {
fail()
} else {
- inCategory { category }()
+ inCategory { category }
}
}
@@ -379,7 +423,11 @@ public fun inCategory(id: Snowflake): Check<*> = {
*
* @param id Category snowflake to compare to.
*/
-public fun notInCategory(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.notInCategory(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.notInCategory")
val category = event.kord.getChannelOf(id)
@@ -388,7 +436,7 @@ public fun notInCategory(id: Snowflake): Check<*> = {
pass()
} else {
- notInCategory { category }()
+ notInCategory { category }
}
}
@@ -400,7 +448,11 @@ public fun notInCategory(id: Snowflake): Check<*> = {
*
* @param id Channel snowflake to compare to.
*/
-public fun channelHigher(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.channelHigher(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.channelHigher")
val channel = event.kord.getChannel(id)
@@ -409,7 +461,7 @@ public fun channelHigher(id: Snowflake): Check<*> = {
fail()
} else {
- channelHigher { channel }()
+ channelHigher { channel }
}
}
@@ -421,7 +473,11 @@ public fun channelHigher(id: Snowflake): Check<*> = {
*
* @param id Channel snowflake to compare to.
*/
-public fun channelLower(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.channelLower(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.channelLower")
val channel = event.kord.getChannel(id)
@@ -430,7 +486,7 @@ public fun channelLower(id: Snowflake): Check<*> = {
fail()
} else {
- channelLower { channel }()
+ channelLower { channel }
}
}
@@ -442,7 +498,11 @@ public fun channelLower(id: Snowflake): Check<*> = {
*
* @param id Channel snowflake to compare to.
*/
-public fun channelHigherOrEqual(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.channelHigherOrEqual(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.channelHigherOrEqual")
val channel = event.kord.getChannel(id)
@@ -451,7 +511,7 @@ public fun channelHigherOrEqual(id: Snowflake): Check<*> = {
fail()
} else {
- channelHigherOrEqual { channel }()
+ channelHigherOrEqual { channel }
}
}
@@ -463,7 +523,11 @@ public fun channelHigherOrEqual(id: Snowflake): Check<*> = {
*
* @param id Channel snowflake to compare to.
*/
-public fun channelLowerOrEqual(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.channelLowerOrEqual(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.channelLowerOrEqual")
val channel = event.kord.getChannel(id)
@@ -472,7 +536,7 @@ public fun channelLowerOrEqual(id: Snowflake): Check<*> = {
fail()
} else {
- channelLowerOrEqual { channel }()
+ channelLowerOrEqual { channel }
}
}
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/ChannelTypeChecks.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/ChannelTypeChecks.kt
index a6cc233a07..20134f7c2d 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/ChannelTypeChecks.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/ChannelTypeChecks.kt
@@ -2,18 +2,11 @@
package com.kotlindiscord.kord.extensions.checks
-import com.kotlindiscord.kord.extensions.builders.ExtensibleBotBuilder
-import com.kotlindiscord.kord.extensions.checks.types.Check
-import com.kotlindiscord.kord.extensions.utils.getKoin
+import com.kotlindiscord.kord.extensions.checks.types.CheckContext
import com.kotlindiscord.kord.extensions.utils.translate
import dev.kord.common.entity.ChannelType
import dev.kord.core.event.Event
import mu.KotlinLogging
-import java.util.*
-
-private val defaultLocale: Locale
- get() =
- getKoin().get().i18nBuilder.defaultLocale
/**
* Check asserting that the channel an [Event] fired in is of a given set of types.
@@ -23,7 +16,11 @@ private val defaultLocale: Locale
*
* @param channelTypes The channel types to compare to.
*/
-public fun channelType(vararg channelTypes: ChannelType): Check<*> = {
+public suspend fun CheckContext<*>.channelType(vararg channelTypes: ChannelType) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.channelType")
val eventChannel = channelFor(event)
@@ -59,7 +56,11 @@ public fun channelType(vararg channelTypes: ChannelType): Check<*> = {
*
* @param channelTypes The channel types to compare to.
*/
-public fun notChannelType(vararg channelTypes: ChannelType): Check<*> = {
+public suspend fun CheckContext<*>.notChannelType(vararg channelTypes: ChannelType) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.notChannelType")
val eventChannel = channelFor(event)
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/CheckCombinators.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/CheckCombinators.kt
deleted file mode 100644
index 5d3d56150a..0000000000
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/CheckCombinators.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-package com.kotlindiscord.kord.extensions.checks
-
-import com.kotlindiscord.kord.extensions.checks.types.Check
-import com.kotlindiscord.kord.extensions.checks.types.CheckContext
-import mu.KotlinLogging
-
-/**
- * Special check that passes if any of the given checks pass.
- *
- * You can think of this as an `or` operation - pass it a bunch of checks, and
- * this one will return `true` if any of them pass.
- *
- * @param checks Two or more checks to combine.
- * @return Whether any of the checks passed.
- */
-public fun or(vararg checks: Check<*>): Check<*> = {
- val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.or")
-
- val contexts = checks.map {
- val context = CheckContext(event, locale)
-
- it(context)
- context
- }
-
- if (contexts.any { it.passed }) {
- logger.passed()
-
- pass()
- } else {
- logger.failed("None of the given checks passed")
-
- val failedContext = contexts.firstOrNull { !it.passed }
-
- if (failedContext != null) {
- fail(failedContext.message)
- } else {
- fail()
- }
- }
-}
-
-/** Infix-function version of [or]. **/
-public infix fun (Check<*>).or(other: Check<*>): Check<*> = or(this, other)
-
-/**
- * Special check that passes if all of the given checks pass.
- *
- * You can think of this as an `and` operation - pass it a bunch of checks, and
- * this one will return `true` if they all pass.
- *
- * Don't use this unless you're already using combinators. The `check` functions
- * can simply be passed multiple checks.
- *
- * @param checks Two or more checks to combine.
- * @return Whether all of the checks passed.
- */
-public fun and(vararg checks: Check<*>): Check<*> = {
- val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.and")
-
- val contexts = checks.map {
- val context = CheckContext(event, locale)
-
- it(context)
- context
- }
-
- if (contexts.all { it.passed }) {
- logger.passed()
-
- pass()
- } else {
- logger.failed("At least one of the given checks failed")
-
- val failedContext = contexts.firstOrNull { !it.passed }
-
- if (failedContext != null) {
- fail(failedContext.message)
- } else {
- fail()
- }
- }
-}
-
-/** Infix-function version of [and]. **/
-public infix fun (Check<*>).and(other: Check<*>): Check<*> = and(this, other)
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/CheckUtils.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/CheckUtils.kt
index 6cc7e9450a..ff028fccd1 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/CheckUtils.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/CheckUtils.kt
@@ -1,15 +1,19 @@
-@file:OptIn(KordPreview::class)
+@file:OptIn(KordPreview::class, KordUnsafe::class, KordExperimental::class)
package com.kotlindiscord.kord.extensions.checks
-import com.kotlindiscord.kord.extensions.checks.types.Check
+import com.kotlindiscord.kord.extensions.checks.types.CheckContext
+import com.kotlindiscord.kord.extensions.utils.authorId
+import dev.kord.common.annotation.KordExperimental
import dev.kord.common.annotation.KordPreview
+import dev.kord.common.annotation.KordUnsafe
import dev.kord.common.entity.Snowflake
import dev.kord.core.behavior.*
import dev.kord.core.behavior.channel.ChannelBehavior
import dev.kord.core.behavior.channel.threads.ThreadChannelBehavior
-import dev.kord.core.entity.channel.thread.ThreadChannel
-import dev.kord.core.entity.interaction.GuildInteraction
+import dev.kord.core.cache.data.toData
+import dev.kord.core.entity.Member
+import dev.kord.core.entity.interaction.GuildApplicationCommandInteraction
import dev.kord.core.event.Event
import dev.kord.core.event.channel.*
import dev.kord.core.event.channel.thread.*
@@ -78,9 +82,9 @@ public suspend fun channelFor(event: Event): ChannelBehavior? {
* @return A [ChannelBehavior] representing the channel, or null if there isn't one.
*/
public suspend fun topChannelFor(event: Event): ChannelBehavior? {
- val channel = channelFor(event) ?: return null
+ val channel = channelFor(event)?.asChannelOrNull() ?: return null
- return if (channel is ThreadChannel) {
+ return if (channel is ThreadChannelBehavior) {
channel.parent
} else {
channel
@@ -98,7 +102,7 @@ public suspend fun topChannelFor(event: Event): ChannelBehavior? {
* @param event The event concerning to the channel to retrieve.
* @return A [Long] representing the channel ID, or null if there isn't one.
*/
-public suspend fun channelIdFor(event: Event): Long? {
+public suspend fun channelIdFor(event: Event): ULong? {
return when (event) {
is ChannelCreateEvent -> event.channel.id.value
is ChannelDeleteEvent -> event.channel.id.value
@@ -202,7 +206,7 @@ public suspend fun guildFor(event: Event): GuildBehavior? {
if (guildId == null) {
null
} else {
- event.kord.getGuild(guildId)
+ event.kord.unsafe.guild(guildId)
}
}
@@ -213,9 +217,29 @@ public suspend fun guildFor(event: Event): GuildBehavior? {
is MemberLeaveEvent -> event.guild
is MemberUpdateEvent -> event.guild
is MessageBulkDeleteEvent -> event.guild
- is MessageCreateEvent -> event.message.getGuildOrNull()
+
+ is MessageCreateEvent -> {
+ val guildId = event.message.data.guildId.value
+
+ if (guildId == null) {
+ guildId
+ } else {
+ event.kord.unsafe.guild(guildId)
+ }
+ }
+
is MessageDeleteEvent -> event.guild
- is MessageUpdateEvent -> event.getMessage().getGuildOrNull()
+
+ is MessageUpdateEvent -> {
+ val guildId = event.new.guildId.value
+
+ if (guildId == null) {
+ guildId
+ } else {
+ event.kord.unsafe.guild(guildId)
+ }
+ }
+
is NewsChannelCreateEvent -> event.channel.guild
is NewsChannelDeleteEvent -> event.channel.guild
is NewsChannelUpdateEvent -> event.channel.guild
@@ -258,25 +282,25 @@ public suspend fun guildFor(event: Event): GuildBehavior? {
*/
public suspend fun memberFor(event: Event): MemberBehavior? {
return when {
- event is InteractionCreateEvent -> (event.interaction as? GuildInteraction)?.member
+ event is InteractionCreateEvent -> (event.interaction as? GuildApplicationCommandInteraction)?.member
event is MemberJoinEvent -> event.member
event is MemberUpdateEvent -> event.member
-
- event is MessageCreateEvent && event.message.getGuildOrNull() != null ->
- event.message.getAuthorAsMember()
-
- event is MessageDeleteEvent && event.message?.getGuildOrNull() != null ->
- event.message?.getAuthorAsMember()
-
- event is MessageUpdateEvent && event.message.asMessageOrNull()?.getGuildOrNull() != null ->
- event.getMessage().getAuthorAsMember()
-
- event is ReactionAddEvent && event.message.asMessageOrNull()?.getGuildOrNull() != null ->
- event.getUserAsMember()
-
- event is ReactionRemoveEvent && event.message.asMessageOrNull()?.getGuildOrNull() != null ->
- event.getUserAsMember()
+ event is MessageCreateEvent -> event.member
+ event is MessageDeleteEvent -> event.message?.data?.guildId?.value
+ ?.let { event.kord.unsafe.member(it, event.message!!.data.authorId) }
+
+ event is MessageUpdateEvent -> {
+ val message = event.new
+ if (message.author.value != null && message.member.value != null) {
+ val userData = message.author.value!!.toData()
+ val memberData = message.member.value!!.toData(userData.id, event.new.guildId.value!!)
+ return Member(memberData, userData, event.kord)
+ }
+ return null
+ }
+ event is ReactionAddEvent -> event.userAsMember
+ event is ReactionRemoveEvent -> event.userAsMember
event is TypingStartEvent -> if (event.guildId != null) {
event.getGuild()!!.getMemberOrNull(event.userId)
@@ -399,6 +423,7 @@ public suspend fun userFor(event: Event): UserBehavior? {
is DMChannelCreateEvent -> event.channel.recipients.first { it.id != event.kord.selfId }
is DMChannelDeleteEvent -> event.channel.recipients.first { it.id != event.kord.selfId }
is DMChannelUpdateEvent -> event.channel.recipients.first { it.id != event.kord.selfId }
+
is InteractionCreateEvent -> event.interaction.user
is MemberJoinEvent -> event.member
is MemberLeaveEvent -> event.user
@@ -423,9 +448,7 @@ public suspend fun userFor(event: Event): UserBehavior? {
}
}
-/** Wrap an existing check, calling it but ensuring that no message is produced. **/
-public suspend fun Check<*>.silenced(): Check<*> = {
- this@silenced()
-
+/** Silence the current check by removing any message it may have set. **/
+public fun CheckContext<*>.silence() {
message = null
}
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/GuildChecks.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/GuildChecks.kt
index 68075b758b..0880d44021 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/GuildChecks.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/GuildChecks.kt
@@ -2,12 +2,11 @@
package com.kotlindiscord.kord.extensions.checks
-import com.kotlindiscord.kord.extensions.checks.types.Check
+import com.kotlindiscord.kord.extensions.checks.types.CheckContext
import dev.kord.common.entity.Snowflake
import dev.kord.core.behavior.GuildBehavior
import dev.kord.core.event.Event
import mu.KotlinLogging
-import java.util.*
/**
* Check asserting an [Event] was fired within a guild.
@@ -16,7 +15,11 @@ import java.util.*
* that fired within a guild the bot doesn't have access to, or that it can't get the GuildBehavior for (for
* example, due to a niche Kord configuration).
*/
-public val anyGuild: Check<*> = {
+public suspend fun CheckContext<*>.anyGuild() {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.anyGuild")
if (guildFor(event) != null) {
@@ -39,7 +42,11 @@ public val anyGuild: Check<*> = {
* that fired within a guild the bot doesn't have access to, or that it can't get the GuildBehavior for (for
* example, due to a niche Kord configuration).
*/
-public val noGuild: Check<*> = {
+public suspend fun CheckContext<*>.noGuild() {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.noGuild")
if (guildFor(event) == null) {
@@ -65,7 +72,11 @@ public val noGuild: Check<*> = {
*
* @param builder Lambda returning the guild to compare to.
*/
-public fun inGuild(builder: suspend () -> GuildBehavior): Check<*> = {
+public suspend fun CheckContext.inGuild(builder: suspend (T) -> GuildBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.inGuild")
val eventGuild = guildFor(event)?.asGuildOrNull()
@@ -74,7 +85,7 @@ public fun inGuild(builder: suspend () -> GuildBehavior): Check<*> = {
fail()
} else {
- val guild = builder()
+ val guild = builder(event)
if (eventGuild.id == guild.id) {
logger.passed()
@@ -101,7 +112,11 @@ public fun inGuild(builder: suspend () -> GuildBehavior): Check<*> = {
*
* @param builder Lambda returning the guild to compare to.
*/
-public fun notInGuild(builder: suspend () -> GuildBehavior): Check<*> = {
+public suspend fun CheckContext.notInGuild(builder: suspend (T) -> GuildBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.notInGuild")
val eventGuild = guildFor(event)?.asGuild()
@@ -110,7 +125,7 @@ public fun notInGuild(builder: suspend () -> GuildBehavior): Check<*> = {
pass()
} else {
- val guild = builder()
+ val guild = builder(event)
if (eventGuild.id != guild.id) {
logger.passed()
@@ -141,7 +156,11 @@ public fun notInGuild(builder: suspend () -> GuildBehavior): Check<*> = {
*
* @param id Guild snowflake to compare to.
*/
-public fun inGuild(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.inGuild(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.inGuild")
val guild = event.kord.getGuild(id)
@@ -150,7 +169,7 @@ public fun inGuild(id: Snowflake): Check<*> = {
fail()
} else {
- inGuild { guild }()
+ inGuild { guild }
}
}
@@ -162,7 +181,11 @@ public fun inGuild(id: Snowflake): Check<*> = {
*
* @param id Guild snowflake to compare to.
*/
-public fun notInGuild(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.notInGuild(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.notInGuild")
val guild = event.kord.getGuild(id)
@@ -171,7 +194,7 @@ public fun notInGuild(id: Snowflake): Check<*> = {
pass()
} else {
- notInGuild { guild }()
+ notInGuild { guild }
}
}
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/MemberChecks.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/MemberChecks.kt
index 7827e68365..9b0c7a7b34 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/MemberChecks.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/MemberChecks.kt
@@ -2,9 +2,7 @@
package com.kotlindiscord.kord.extensions.checks
-import com.kotlindiscord.kord.extensions.builders.ExtensibleBotBuilder
-import com.kotlindiscord.kord.extensions.checks.types.Check
-import com.kotlindiscord.kord.extensions.utils.getKoin
+import com.kotlindiscord.kord.extensions.checks.types.CheckContext
import com.kotlindiscord.kord.extensions.utils.hasPermission
import com.kotlindiscord.kord.extensions.utils.permissionsForMember
import com.kotlindiscord.kord.extensions.utils.translate
@@ -12,10 +10,6 @@ import dev.kord.common.entity.Permission
import dev.kord.core.entity.channel.GuildChannel
import dev.kord.core.event.Event
import mu.KotlinLogging
-import java.util.*
-
-private val defaultLocale: Locale
- get() = getKoin().get().i18nBuilder.defaultLocale
/**
* Check asserting that the user an [Event] fired for has a given permission, or the Administrator permission.
@@ -25,7 +19,11 @@ private val defaultLocale: Locale
*
* @param perm The permission to check for.
*/
-public fun hasPermission(perm: Permission): Check<*> = {
+public suspend fun CheckContext<*>.hasPermission(perm: Permission) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.hasPermission")
val channel = channelFor(event) as? GuildChannel
val member = memberFor(event)
@@ -70,7 +68,11 @@ public fun hasPermission(perm: Permission): Check<*> = {
*
* @param perm The permission to check for.
*/
-public fun notHasPermission(perm: Permission): Check<*> = {
+public suspend fun CheckContext<*>.notHasPermission(perm: Permission) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.notHasPermission")
val channel = channelFor(event) as? GuildChannel
val member = memberFor(event)
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/MiscChecks.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/MiscChecks.kt
index fd30fd79c0..88fe60849c 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/MiscChecks.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/MiscChecks.kt
@@ -1,17 +1,18 @@
package com.kotlindiscord.kord.extensions.checks
-import com.kotlindiscord.kord.extensions.checks.types.Check
+import com.kotlindiscord.kord.extensions.checks.types.CheckContext
import dev.kord.core.behavior.channel.threads.ThreadChannelBehavior
import dev.kord.core.event.Event
import mu.KotlinLogging
-import java.util.*
/**
* Check asserting the user for an [Event] is a bot. Will fail if the event doesn't concern a user.
- *
- * @param event Event object to check.
*/
-public val isBot: Check<*> = {
+public suspend fun CheckContext<*>.isBot() {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.isBot")
val user = userFor(event)?.asUserOrNull()
@@ -34,10 +35,12 @@ public val isBot: Check<*> = {
/**
* Check asserting the user for an [Event] is **not** a bot. Will fail if the event doesn't concern a user.
- *
- * @param event Event object to check.
*/
-public val isNotbot: Check<*> = {
+public suspend fun CheckContext<*>.isNotBot() {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.isNotBot")
val user = userFor(event)?.asUserOrNull()
@@ -61,7 +64,11 @@ public val isNotbot: Check<*> = {
/**
* Check asserting that the event was triggered within a thread.
*/
-public val isInThread: Check<*> = {
+public suspend fun CheckContext<*>.isInThread() {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.isInThread")
val channel = channelFor(event)?.asChannelOrNull()
@@ -86,7 +93,11 @@ public val isInThread: Check<*> = {
* Check asserting that the event was **not** triggered within a thread, including events that don't concern any
* specific channel.
*/
-public val isNotInThread: Check<*> = {
+public suspend fun CheckContext<*>.isNotInThread() {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.isNotInThread")
val channel = channelFor(event)?.asChannelOrNull()
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/RoleChecks.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/RoleChecks.kt
index bd1a943f4e..bb316559a8 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/RoleChecks.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/RoleChecks.kt
@@ -2,7 +2,7 @@
package com.kotlindiscord.kord.extensions.checks
-import com.kotlindiscord.kord.extensions.checks.types.Check
+import com.kotlindiscord.kord.extensions.checks.types.CheckContext
import com.kotlindiscord.kord.extensions.utils.getTopRole
import dev.kord.common.entity.Snowflake
import dev.kord.core.behavior.RoleBehavior
@@ -20,7 +20,11 @@ import mu.KotlinLogging
*
* @param builder Lambda returning the role to compare to.
*/
-public fun hasRole(builder: suspend () -> RoleBehavior): Check<*> = {
+public suspend fun CheckContext.hasRole(builder: suspend (T) -> RoleBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.hasRole")
val member = memberFor(event)
@@ -29,7 +33,7 @@ public fun hasRole(builder: suspend () -> RoleBehavior): Check<*> = {
fail()
} else {
- val role = builder()
+ val role = builder(event)
if (member.asMember().roles.toList().contains(role)) {
logger.passed()
@@ -56,7 +60,11 @@ public fun hasRole(builder: suspend () -> RoleBehavior): Check<*> = {
*
* @param builder Lambda returning the role to compare to.
*/
-public fun notHasRole(builder: suspend () -> RoleBehavior): Check<*> = {
+public suspend fun CheckContext.notHasRole(builder: suspend (T) -> RoleBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.notHasRole")
val member = memberFor(event)
@@ -65,7 +73,7 @@ public fun notHasRole(builder: suspend () -> RoleBehavior): Check<*> = {
pass()
} else {
- val role = builder()
+ val role = builder(event)
if (member.asMember().roles.toList().contains(role)) {
logger.failed("Member $member has role $role")
@@ -92,7 +100,11 @@ public fun notHasRole(builder: suspend () -> RoleBehavior): Check<*> = {
*
* @param builder Lambda returning the role to compare to.
*/
-public fun topRoleEqual(builder: suspend () -> RoleBehavior): Check<*> = {
+public suspend fun CheckContext.topRoleEqual(builder: suspend (T) -> RoleBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.topRoleEqual")
val member = memberFor(event)
@@ -101,7 +113,7 @@ public fun topRoleEqual(builder: suspend () -> RoleBehavior): Check<*> = {
fail()
} else {
- val role = builder()
+ val role = builder(event)
val topRole = member.asMember().getTopRole()
when {
@@ -144,7 +156,11 @@ public fun topRoleEqual(builder: suspend () -> RoleBehavior): Check<*> = {
*
* @param builder Lambda returning the role to compare to.
*/
-public fun topRoleNotEqual(builder: suspend () -> RoleBehavior): Check<*> = {
+public suspend fun CheckContext.topRoleNotEqual(builder: suspend (T) -> RoleBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.topRoleNotEqual")
val member = memberFor(event)
@@ -153,7 +169,7 @@ public fun topRoleNotEqual(builder: suspend () -> RoleBehavior): Check<*> = {
pass()
} else {
- val role = builder()
+ val role = builder(event)
when (member.asMember().getTopRole()) {
null -> {
@@ -190,7 +206,11 @@ public fun topRoleNotEqual(builder: suspend () -> RoleBehavior): Check<*> = {
*
* @param builder Lambda returning the role to compare to.
*/
-public fun topRoleHigher(builder: suspend () -> RoleBehavior): Check<*> = {
+public suspend fun CheckContext.topRoleHigher(builder: suspend (T) -> RoleBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.topRoleHigher")
val member = memberFor(event)
@@ -199,7 +219,7 @@ public fun topRoleHigher(builder: suspend () -> RoleBehavior): Check<*> = {
fail()
} else {
- val role = builder()
+ val role = builder(event)
val topRole = member.asMember().getTopRole()
when {
@@ -244,7 +264,11 @@ public fun topRoleHigher(builder: suspend () -> RoleBehavior): Check<*> = {
*
* @param builder Lambda returning the role to compare to.
*/
-public fun topRoleLower(builder: suspend () -> RoleBehavior): Check<*> = {
+public suspend fun CheckContext.topRoleLower(builder: suspend (T) -> RoleBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.topRoleLower")
val member = memberFor(event)
@@ -253,7 +277,7 @@ public fun topRoleLower(builder: suspend () -> RoleBehavior): Check<*> = {
fail()
} else {
- val role = builder()
+ val role = builder(event)
val topRole = member.asMember().getTopRole()
when {
@@ -292,7 +316,11 @@ public fun topRoleLower(builder: suspend () -> RoleBehavior): Check<*> = {
*
* @param builder Lambda returning the role to compare to.
*/
-public fun topRoleHigherOrEqual(builder: suspend () -> RoleBehavior): Check<*> = {
+public suspend fun CheckContext.topRoleHigherOrEqual(builder: suspend (T) -> RoleBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.topRoleHigherOrEqual")
val member = memberFor(event)
@@ -301,7 +329,7 @@ public fun topRoleHigherOrEqual(builder: suspend () -> RoleBehavior): Check<*> =
fail()
} else {
- val role = builder()
+ val role = builder(event)
val topRole = member.asMember().getTopRole()
when {
@@ -347,7 +375,11 @@ public fun topRoleHigherOrEqual(builder: suspend () -> RoleBehavior): Check<*> =
*
* @param builder Lambda returning the role to compare to.
*/
-public fun topRoleLowerOrEqual(builder: suspend () -> RoleBehavior): Check<*> = {
+public suspend fun CheckContext.topRoleLowerOrEqual(builder: suspend (T) -> RoleBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.topRoleLowerOrEqual")
val member = memberFor(event)
@@ -356,7 +388,7 @@ public fun topRoleLowerOrEqual(builder: suspend () -> RoleBehavior): Check<*> =
fail()
} else {
- val role = builder()
+ val role = builder(event)
val topRole = member.asMember().getTopRole()
when {
@@ -398,7 +430,11 @@ public fun topRoleLowerOrEqual(builder: suspend () -> RoleBehavior): Check<*> =
*
* @param id Role snowflake to compare to.
*/
-public fun hasRole(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.hasRole(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.hasRole")
val role = guildFor(event)?.getRoleOrNull(id)
@@ -407,7 +443,7 @@ public fun hasRole(id: Snowflake): Check<*> = {
fail()
} else {
- hasRole { role }()
+ hasRole { role }
}
}
@@ -419,7 +455,11 @@ public fun hasRole(id: Snowflake): Check<*> = {
*
* @param id Role snowflake to compare to.
*/
-public fun notHasRole(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.notHasRole(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.notHasRole")
val role = guildFor(event)?.getRoleOrNull(id)
@@ -428,7 +468,7 @@ public fun notHasRole(id: Snowflake): Check<*> = {
pass()
} else {
- notHasRole { role }()
+ notHasRole { role }
}
}
@@ -440,7 +480,11 @@ public fun notHasRole(id: Snowflake): Check<*> = {
*
* @param id Role snowflake to compare to.
*/
-public fun topRoleEqual(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.topRoleEqual(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.topRoleEqual")
val role = guildFor(event)?.getRoleOrNull(id)
@@ -449,7 +493,7 @@ public fun topRoleEqual(id: Snowflake): Check<*> = {
fail()
} else {
- topRoleEqual { role }()
+ topRoleEqual { role }
}
}
@@ -461,7 +505,11 @@ public fun topRoleEqual(id: Snowflake): Check<*> = {
*
* @param id Role snowflake to compare to.
*/
-public fun topRoleNotEqual(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.topRoleNotEqual(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.topRoleNotEqual")
val role = guildFor(event)?.getRoleOrNull(id)
@@ -470,7 +518,7 @@ public fun topRoleNotEqual(id: Snowflake): Check<*> = {
pass()
} else {
- topRoleNotEqual { role }()
+ topRoleNotEqual { role }
}
}
@@ -482,7 +530,11 @@ public fun topRoleNotEqual(id: Snowflake): Check<*> = {
*
* @param id Role snowflake to compare to.
*/
-public fun topRoleHigher(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.topRoleHigher(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.topRoleHigher")
val role = guildFor(event)?.getRoleOrNull(id)
@@ -491,7 +543,7 @@ public fun topRoleHigher(id: Snowflake): Check<*> = {
fail()
} else {
- topRoleHigher { role }()
+ topRoleHigher { role }
}
}
@@ -505,7 +557,11 @@ public fun topRoleHigher(id: Snowflake): Check<*> = {
*
* @param id Role snowflake to compare to.
*/
-public fun topRoleLower(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.topRoleLower(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.topRoleLower")
val role = guildFor(event)?.getRoleOrNull(id)
@@ -514,7 +570,7 @@ public fun topRoleLower(id: Snowflake): Check<*> = {
fail()
} else {
- topRoleLower { role }()
+ topRoleLower { role }
}
}
@@ -527,7 +583,11 @@ public fun topRoleLower(id: Snowflake): Check<*> = {
*
* @param id Role snowflake to compare to.
*/
-public fun topRoleHigherOrEqual(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.topRoleHigherOrEqual(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.topRoleHigherOrEqual")
val role = guildFor(event)?.getRoleOrNull(id)
@@ -536,7 +596,7 @@ public fun topRoleHigherOrEqual(id: Snowflake): Check<*> = {
fail()
} else {
- topRoleHigherOrEqual { role }()
+ topRoleHigherOrEqual { role }
}
}
@@ -551,7 +611,11 @@ public fun topRoleHigherOrEqual(id: Snowflake): Check<*> = {
*
* @param id Role snowflake to compare to.
*/
-public fun topRoleLowerOrEqual(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.topRoleLowerOrEqual(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.topRoleLowerOrEqual")
val role = guildFor(event)?.getRoleOrNull(id)
@@ -560,7 +624,7 @@ public fun topRoleLowerOrEqual(id: Snowflake): Check<*> = {
fail()
} else {
- topRoleLowerOrEqual { role }()
+ topRoleLowerOrEqual { role }
}
}
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/TopChannelChecks.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/TopChannelChecks.kt
index 5891d5a4b4..4d4c88848d 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/TopChannelChecks.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/TopChannelChecks.kt
@@ -2,7 +2,7 @@
package com.kotlindiscord.kord.extensions.checks
-import com.kotlindiscord.kord.extensions.checks.types.Check
+import com.kotlindiscord.kord.extensions.checks.types.CheckContext
import dev.kord.common.entity.Snowflake
import dev.kord.core.behavior.channel.ChannelBehavior
import dev.kord.core.entity.channel.thread.ThreadChannel
@@ -20,7 +20,11 @@ import mu.KotlinLogging
*
* @param builder Lambda returning the channel to compare to.
*/
-public fun inTopChannel(builder: suspend () -> ChannelBehavior): Check<*> = {
+public suspend fun CheckContext.inTopChannel(builder: suspend (T) -> ChannelBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.inChannel")
val eventChannel = topChannelFor(event)
@@ -29,7 +33,7 @@ public fun inTopChannel(builder: suspend () -> ChannelBehavior): Check<*> = {
fail()
} else {
- val channel = builder()
+ val channel = builder(event)
if (eventChannel.id == channel.id) {
logger.passed()
@@ -57,7 +61,11 @@ public fun inTopChannel(builder: suspend () -> ChannelBehavior): Check<*> = {
*
* @param builder Lambda returning the channel to compare to.
*/
-public fun notInTopChannel(builder: suspend () -> ChannelBehavior): Check<*> = {
+public suspend fun CheckContext.notInTopChannel(builder: suspend (T) -> ChannelBehavior) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.notInChannel")
val eventChannel = topChannelFor(event)
@@ -66,7 +74,7 @@ public fun notInTopChannel(builder: suspend () -> ChannelBehavior): Check<*> = {
pass()
} else {
- val channel = builder()
+ val channel = builder(event)
if (eventChannel.id != channel.id) {
logger.passed()
@@ -98,7 +106,11 @@ public fun notInTopChannel(builder: suspend () -> ChannelBehavior): Check<*> = {
*
* @param id Channel snowflake to compare to.
*/
-public fun inTopChannel(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.inTopChannel(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.inChannel")
var channel = event.kord.getChannel(id)
@@ -111,7 +123,7 @@ public fun inTopChannel(id: Snowflake): Check<*> = {
fail()
} else {
- inChannel { channel }()
+ inTopChannel { channel }
}
}
@@ -124,7 +136,11 @@ public fun inTopChannel(id: Snowflake): Check<*> = {
*
* @param id Channel snowflake to compare to.
*/
-public fun notInTopChannel(id: Snowflake): Check<*> = {
+public suspend fun CheckContext.notInTopChannel(id: Snowflake) {
+ if (!passed) {
+ return
+ }
+
val logger = KotlinLogging.logger("com.kotlindiscord.kord.extensions.checks.notInChannel")
var channel = event.kord.getChannel(id)
@@ -137,7 +153,7 @@ public fun notInTopChannel(id: Snowflake): Check<*> = {
pass()
} else {
- notInChannel { channel }()
+ notInTopChannel { channel }
}
}
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/types/CheckContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/types/CheckContext.kt
index 954ce02326..e25bb886b6 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/types/CheckContext.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/types/CheckContext.kt
@@ -1,5 +1,6 @@
package com.kotlindiscord.kord.extensions.checks.types
+import com.kotlindiscord.kord.extensions.DiscordRelayedException
import com.kotlindiscord.kord.extensions.i18n.TranslationsProvider
import dev.kord.core.event.Event
import org.koin.core.component.KoinComponent
@@ -17,6 +18,19 @@ public class CheckContext(public val event: T, public val locale:
/** Translations provider. **/
public val translations: TranslationsProvider by inject()
+ /**
+ * Translation key to use for the error response message, if not the default.
+ *
+ * The string pointed to by this variable must accept one replacement value, which is the error message itself.
+ *
+ * **Note:** This *must* be a translation key. A bare string may not work, as the error response function uses
+ * the replacement functionality of the translations system.
+ */
+ public var errorResponseKey: String = "checks.responseTemplate"
+
+ /** Translation bundle used by [translate] by default and the error response translation, if not the default. **/
+ public var defaultBundle: String? = null
+
/** Human-readable message for the user, if any. **/
public var message: String? = null
@@ -73,6 +87,45 @@ public class CheckContext(public val event: T, public val locale:
public suspend fun failIfNot(message: String? = null, callback: suspend () -> Boolean): Boolean =
failIfNot(callback(), message)
+ /**
+ * If [value] is `true`, mark this check as having passed.
+ *
+ * Returns `true` if the check was marked as having passed, `false` otherwise.
+ */
+ public fun passIf(value: Boolean): Boolean {
+ if (value) {
+ pass()
+
+ return true
+ }
+
+ return false
+ }
+
+ /**
+ * If [callback] returns `true`, mark this check as having passed.
+ *
+ * Returns `true` if the check was marked as having passed, `false` otherwise.
+ */
+ public suspend fun passIf(callback: suspend () -> Boolean): Boolean =
+ passIf(callback())
+
+ /**
+ * If [value] is `true`, mark this check as having passed.
+ *
+ * Returns `true` if the check was marked as having passed, `false` otherwise.
+ */
+ public fun passIfNot(value: Boolean): Boolean =
+ passIf(!value)
+
+ /**
+ * If [callback] returns `true`, mark this check as having passed.
+ *
+ * Returns `true` if the check was marked as having passed, `false` otherwise.
+ */
+ public suspend fun passIfNot(callback: suspend () -> Boolean): Boolean =
+ passIfNot(callback())
+
/** Call the given block if the Boolean receiver is `true`. **/
public inline fun Boolean.whenTrue(body: () -> T?): T? {
if (this) {
@@ -92,6 +145,30 @@ public class CheckContext(public val event: T, public val locale:
}
/** Quick access to translate strings using this check context's [locale]. **/
- public fun translate(key: String, bundle: String? = null, replacements: Array = arrayOf()): String =
+ public fun translate(
+ key: String,
+ bundle: String? = defaultBundle,
+ replacements: Array = arrayOf()
+ ): String =
translations.translate(key, locale, bundleName = bundle, replacements = replacements)
+
+ /**
+ * If this check has failed and a message is set, throw a [DiscordRelayedException] with the translated message.
+ */
+ @Throws(DiscordRelayedException::class)
+ public fun throwIfFailedWithMessage() {
+ if (passed.not() && message != null) {
+ throw DiscordRelayedException(
+ getTranslatedMessage()!!
+ )
+ }
+ }
+
+ /** Get the translated check failure message, if the check has failed and a message was set. **/
+ public fun getTranslatedMessage(): String? =
+ if (passed.not() && message != null) {
+ translate(errorResponseKey, defaultBundle, replacements = arrayOf(message))
+ } else {
+ null
+ }
}
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/types/_Types.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/types/_Types.kt
index 8cde40f2a2..320dc60fdf 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/types/_Types.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/checks/types/_Types.kt
@@ -2,5 +2,22 @@
package com.kotlindiscord.kord.extensions.checks.types
+import dev.kord.core.event.interaction.ChatInputCommandInteractionCreateEvent
+import dev.kord.core.event.interaction.MessageCommandInteractionCreateEvent
+import dev.kord.core.event.interaction.UserCommandInteractionCreateEvent
+import dev.kord.core.event.message.MessageCreateEvent
+
/** Types alias representing a check function for a specific event type. **/
public typealias Check = suspend CheckContext.() -> Unit
+
+/** Check type for chat commands. **/
+public typealias ChatCommandCheck = Check
+
+/** Check type for message commands. **/
+public typealias MessageCommandCheck = Check
+
+/** Check type for slash commands. **/
+public typealias SlashCommandCheck = Check
+
+/** Check type for user commands. **/
+public typealias UserCommandCheck = Check
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/parser/Argument.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Argument.kt
similarity index 90%
rename from kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/parser/Argument.kt
rename to kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Argument.kt
index 032dd7018f..338b6777d6 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/parser/Argument.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Argument.kt
@@ -1,4 +1,4 @@
-package com.kotlindiscord.kord.extensions.commands.parser
+package com.kotlindiscord.kord.extensions.commands
import com.kotlindiscord.kord.extensions.commands.converters.Converter
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/parser/Arguments.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Arguments.kt
similarity index 92%
rename from kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/parser/Arguments.kt
rename to kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Arguments.kt
index 0741bc168f..e581502b5f 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/parser/Arguments.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Arguments.kt
@@ -1,9 +1,6 @@
-@file:OptIn(KordPreview::class)
-
-package com.kotlindiscord.kord.extensions.commands.parser
+package com.kotlindiscord.kord.extensions.commands
import com.kotlindiscord.kord.extensions.commands.converters.*
-import dev.kord.common.annotation.KordPreview
/**
* Abstract base class for a class containing a set of command arguments.
@@ -162,4 +159,19 @@ public open class Arguments {
return converter
}
+
+ /** Validation function that will throw an error if there's a problem with this Arguments class/subclass. **/
+ public open fun validate() {
+ val names: MutableSet = mutableSetOf()
+
+ args.forEach {
+ val name = it.displayName.lowercase()
+
+ if (name in names) {
+ error("Duplicate argument name: ${it.displayName}")
+ }
+
+ names.add(name)
+ }
+ }
}
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Command.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Command.kt
index cbe9cead88..3de1601ab8 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Command.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/Command.kt
@@ -1,9 +1,15 @@
+@file:Suppress("UnnecessaryAbstractClass") // No idea why we're getting this
+
package com.kotlindiscord.kord.extensions.commands
import com.kotlindiscord.kord.extensions.InvalidCommandException
import com.kotlindiscord.kord.extensions.annotations.ExtensionDSL
-import com.kotlindiscord.kord.extensions.commands.parser.ArgumentParser
+import com.kotlindiscord.kord.extensions.commands.events.CommandEvent
import com.kotlindiscord.kord.extensions.extensions.Extension
+import com.kotlindiscord.kord.extensions.types.Lockable
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
/**
* Abstract base class representing the few things that command objects can have in common.
@@ -13,26 +19,36 @@ import com.kotlindiscord.kord.extensions.extensions.Extension
* @property extension The extension object this command belongs to.
*/
@ExtensionDSL
-public abstract class Command(public val extension: Extension) {
+public abstract class Command(public val extension: Extension) : Lockable {
/**
* The name of this command, for invocation and help commands.
*/
public open lateinit var name: String
- /**
- * Argument parser object responsible for transforming arguments into Kotlin types.
- */
- public abstract val parser: ArgumentParser
+ /** Set this to `true` to lock command execution with a Mutex. **/
+ public override var locking: Boolean = false
+
+ override var mutex: Mutex? = null
/**
- * An internal function used to ensure that all of a command's required arguments are present.
+ * An internal function used to ensure that all of a command's required arguments are present and correct.
*
- * @throws InvalidCommandException Thrown when a required argument hasn't been set.
+ * @throws InvalidCommandException Thrown when a required argument hasn't been set or is invalid.
*/
@Throws(InvalidCommandException::class)
public open fun validate() {
- if (!::name.isInitialized) {
+ if (!::name.isInitialized || name.isEmpty()) {
throw InvalidCommandException(null, "No command name given.")
}
+
+ if (locking && mutex == null) {
+ mutex = Mutex()
+ }
}
+
+ /** Quick shortcut for emitting a command event without blocking. **/
+ public open suspend fun emitEventAsync(event: CommandEvent<*, *>): Job =
+ event.launch {
+ extension.bot.send(event)
+ }
}
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/CommandContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/CommandContext.kt
index d25f290942..3bfe3e9de5 100644
--- a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/CommandContext.kt
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/CommandContext.kt
@@ -5,16 +5,12 @@ import com.kotlindiscord.kord.extensions.checks.channelFor
import com.kotlindiscord.kord.extensions.checks.guildFor
import com.kotlindiscord.kord.extensions.checks.userFor
import com.kotlindiscord.kord.extensions.i18n.TranslationsProvider
-import com.kotlindiscord.kord.extensions.parser.StringParser
-import com.kotlindiscord.kord.extensions.sentry.SentryAdapter
+import com.kotlindiscord.kord.extensions.sentry.SentryContext
import dev.kord.core.behavior.GuildBehavior
import dev.kord.core.behavior.MemberBehavior
-import dev.kord.core.behavior.MessageBehavior
import dev.kord.core.behavior.UserBehavior
import dev.kord.core.behavior.channel.ChannelBehavior
import dev.kord.core.event.Event
-import io.sentry.Breadcrumb
-import io.sentry.SentryLevel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.*
@@ -28,23 +24,18 @@ import java.util.*
* @param command Respective command for this context object.
* @param eventObj Event that triggered this command.
* @param commandName Command name given by the user to invoke the command - lower-cased.
- * @param parser String parser instance, if any - will be `null` if this isn't a message command.
*/
@ExtensionDSL
public abstract class CommandContext(
public open val command: Command,
public open val eventObj: Event,
public open val commandName: String,
- public open val parser: StringParser?,
) : KoinComponent {
/** Translations provider, for retrieving translations. **/
public val translationsProvider: TranslationsProvider by inject()
- /** Sentry adapter, for easy access to Sentry functions. **/
- public val sentry: SentryAdapter by inject()
-
- /** A list of Sentry breadcrumbs created during command execution. **/
- public open val breadcrumbs: MutableList = mutableListOf()
+ /** Current Sentry context, containing breadcrumbs and other goodies. **/
+ public val sentry: SentryContext = SentryContext()
/** Cached locale variable, stored and retrieved by [getLocale]. **/
public open var resolvedLocale: Locale? = null
@@ -61,34 +52,9 @@ public abstract class CommandContext(
/** Extract member information from event data, if that context is available. **/
public abstract suspend fun getMember(): MemberBehavior?
- /** Extract message information from event data, if that context is available. **/
- public abstract suspend fun getMessage(): MessageBehavior?
-
/** Extract user information from event data, if that context is available. **/
public abstract suspend fun getUser(): UserBehavior?
- /**
- * Add a Sentry breadcrumb to this command context.
- *
- * This should be used for the purposes of tracing what exactly is happening during your
- * command processing. If the bot administrator decides to enable Sentry integration, the
- * breadcrumbs will be sent to Sentry when there's a command processing error.
- */
- public fun breadcrumb(
- category: String? = null,
- level: SentryLevel? = null,
- message: String? = null,
- type: String? = null,
-
- data: Map = mapOf()
- ): Breadcrumb {
- val crumb = sentry.createBreadcrumb(category, level, message, type, data)
-
- breadcrumbs.add(crumb)
-
- return crumb
- }
-
/** Resolve the locale for this command context. **/
public suspend fun getLocale(): Locale {
var locale: Locale? = resolvedLocale
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommand.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommand.kt
new file mode 100644
index 0000000000..1e870fb6e8
--- /dev/null
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommand.kt
@@ -0,0 +1,251 @@
+package com.kotlindiscord.kord.extensions.commands.application
+
+import com.kotlindiscord.kord.extensions.DiscordRelayedException
+import com.kotlindiscord.kord.extensions.builders.ExtensibleBotBuilder
+import com.kotlindiscord.kord.extensions.checks.types.Check
+import com.kotlindiscord.kord.extensions.checks.types.CheckContext
+import com.kotlindiscord.kord.extensions.commands.Command
+import com.kotlindiscord.kord.extensions.extensions.Extension
+import com.kotlindiscord.kord.extensions.i18n.TranslationsProvider
+import com.kotlindiscord.kord.extensions.sentry.SentryAdapter
+import com.kotlindiscord.kord.extensions.utils.getLocale
+import dev.kord.common.entity.ApplicationCommandType
+import dev.kord.common.entity.Permission
+import dev.kord.common.entity.Snowflake
+import dev.kord.core.Kord
+import dev.kord.core.any
+import dev.kord.core.behavior.GuildBehavior
+import dev.kord.core.behavior.RoleBehavior
+import dev.kord.core.behavior.UserBehavior
+import dev.kord.core.event.interaction.InteractionCreateEvent
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import java.util.*
+
+/**
+ * Abstract class representing an application command - extend this for actual implementations.
+ *
+ * @param extension Extension this application command belongs to.
+ */
+public abstract class ApplicationCommand(
+ extension: Extension
+) : Command(extension), KoinComponent {
+ /** Translations provider, for retrieving translations. **/
+ public val translationsProvider: TranslationsProvider by inject()
+
+ /** Quick access to the command registry. **/
+ public val registry: ApplicationCommandRegistry by inject()
+
+ /** Bot settings object. **/
+ public val settings: ExtensibleBotBuilder by inject()
+
+ /** Kord instance, backing the ExtensibleBot. **/
+ public val kord: Kord by inject()
+
+ /** Sentry adapter, for easy access to Sentry functions. **/
+ public val sentry: SentryAdapter by inject()
+
+ /** Discord-side command type, for matching up. **/
+ public abstract val type: ApplicationCommandType
+
+ /** @suppress **/
+ public open val checkList: MutableList> = mutableListOf()
+
+ /** @suppress **/
+ public open var guildId: Snowflake? = settings.applicationCommandsBuilder.defaultGuild
+
+ /**
+ * Whether to allow everyone to use this command by default.
+ *
+ * Leaving this at `true` means that your allowed roles/users sets will effectively be ignored, but your denied
+ * roles/users won't be.
+ *
+ * This will be set to `false` automatically by the `allowX` functions, to ensure that they're applied by Discord.
+ */
+ public open var allowByDefault: Boolean = true
+
+ /**
+ * List of allowed role IDs. Allows take priority over disallows.
+ */
+ public open val allowedRoles: MutableSet = mutableSetOf()
+
+ /**
+ * List of allowed invoker IDs. Allows take priority over disallows.
+ */
+ public open val allowedUsers: MutableSet = mutableSetOf()
+
+ /**
+ * List of disallowed role IDs. Allows take priority over disallows.
+ */
+ public open val disallowedRoles: MutableSet = mutableSetOf()
+
+ /**
+ * List of disallowed invoker IDs. Allows take priority over disallows.
+ */
+ public open val disallowedUsers: MutableSet = mutableSetOf()
+
+ /** Permissions required to be able to run this command. **/
+ public open val requiredPerms: MutableSet = mutableSetOf()
+
+ /** Translation cache, so we don't have to look up translations every time. **/
+ public open val nameTranslationCache: MutableMap = mutableMapOf()
+
+ /** Translation cache, so we don't have to look up translations every time. **/
+ public open val descriptionTranslationCache: MutableMap = mutableMapOf()
+
+ /** Return this command's name translated for the given locale, cached as required. **/
+ public open fun getTranslatedName(locale: Locale): String {
+ if (!nameTranslationCache.containsKey(locale)) {
+ nameTranslationCache[locale] = translationsProvider.translate(
+ this.name,
+ this.extension.bundle,
+ locale
+ )
+ }
+
+ return nameTranslationCache[locale]!!
+ }
+
+ /** If your bot requires permissions to be able to execute the command, add them using this function. **/
+ public fun requireBotPermissions(vararg perms: Permission) {
+ perms.forEach { requiredPerms.add(it) }
+ }
+
+ /** Specify a specific guild for this application command to be locked to. **/
+ public open fun guild(guild: Snowflake) {
+ this.guildId = guild
+ }
+
+ /** Specify a specific guild for this application command to be locked to. **/
+ public open fun guild(guild: Long) {
+ this.guildId = Snowflake(guild)
+ }
+
+ /** Specify a specific guild for this application command to be locked to. **/
+ public open fun guild(guild: GuildBehavior) {
+ this.guildId = guild.id
+ }
+
+ /** Register an allowed role, and set [allowByDefault] to `false`. **/
+ public open fun allowRole(role: Snowflake): Boolean {
+ allowByDefault = false
+
+ return allowedRoles.add(role)
+ }
+
+ /** Register an allowed role, and set [allowByDefault] to `false`. **/
+ public open fun allowRole(role: RoleBehavior): Boolean =
+ allowRole(role.id)
+
+ /** Register a disallowed role, and set [allowByDefault] to `false`. **/
+ public open fun disallowRole(role: Snowflake): Boolean =
+ disallowedRoles.add(role)
+
+ /** Register a disallowed role, and set [allowByDefault] to `false`. **/
+ public open fun disallowRole(role: RoleBehavior): Boolean =
+ disallowRole(role.id)
+
+ /** Register an allowed user, and set [allowByDefault] to `false`. **/
+ public open fun allowUser(user: Snowflake): Boolean {
+ allowByDefault = false
+
+ return allowedUsers.add(user)
+ }
+
+ /** Register an allowed user, and set [allowByDefault] to `false`. **/
+ public open fun allowUser(user: UserBehavior): Boolean =
+ allowUser(user.id)
+
+ /** Register a disallowed user. **/
+ public open fun disallowUser(user: Snowflake): Boolean =
+ disallowedUsers.add(user)
+
+ /** Register a disallowed user. **/
+ public open fun disallowUser(user: UserBehavior): Boolean =
+ disallowUser(user.id)
+
+ /**
+ * Define a check which must pass for the command to be executed.
+ *
+ * A command may have multiple checks - all checks must pass for the command to be executed.
+ * Checks will be run in the order that they're defined.
+ *
+ * This function can be used DSL-style with a given body, or it can be passed one or more
+ * predefined functions. See the samples for more information.
+ *
+ * @param checks Checks to apply to this command.
+ */
+ public open fun check(vararg checks: Check) {
+ checkList.addAll(checks)
+ }
+
+ /**
+ * Overloaded check function to allow for DSL syntax.
+ *
+ * @param check Check to apply to this command.
+ */
+ public open fun check(check: Check) {
+ checkList.add(check)
+ }
+
+ /** Called in order to execute the command. **/
+ public open suspend fun doCall(event: E): Unit = withLock {
+ if (!runChecks(event)) {
+ return@withLock
+ }
+
+ call(event)
+ }
+
+ /** Runs standard checks that can be handled in a generic way, without worrying about subclass-specific checks. **/
+ @Throws(DiscordRelayedException::class)
+ public open suspend fun runStandardChecks(event: E): Boolean {
+ val locale = event.getLocale()
+
+ checkList.forEach { check ->
+ val context = CheckContext(event, locale)
+
+ check(context)
+
+ if (!context.passed) {
+ context.throwIfFailedWithMessage()
+
+ return false
+ }
+ }
+
+ // Handle discord-side perms checks, as they can't be relied on to enforce them
+
+ val guildId = event.interaction.data.guildId.value ?: return allowByDefault
+ val memberId = event.interaction.user.id
+
+ var isAllowed = memberId in allowedUsers
+ var isDenied = memberId in disallowedUsers
+
+ if (allowedRoles.isNotEmpty()) {
+ val member = GuildBehavior(guildId, kord).getMember(memberId)
+
+ isAllowed = isAllowed || member.roles.any { it.id in allowedRoles }
+ }
+
+ if (disallowedRoles.isNotEmpty()) {
+ val member = GuildBehavior(guildId, kord).getMember(memberId)
+
+ isDenied = isDenied || member.roles.any { it.id in disallowedRoles }
+ }
+
+ return if (allowByDefault) {
+ !isDenied
+ } else {
+ isAllowed && !isDenied
+ }
+ }
+
+ /** Override this in order to implement any subclass-specific checks. **/
+ @Throws(DiscordRelayedException::class)
+ public open suspend fun runChecks(event: E): Boolean =
+ runStandardChecks(event)
+
+ /** Override this to implement the calling logic for your subclass. **/
+ public abstract suspend fun call(event: E)
+}
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommandContext.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommandContext.kt
new file mode 100644
index 0000000000..b023c43abe
--- /dev/null
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommandContext.kt
@@ -0,0 +1,67 @@
+@file:OptIn(KordUnsafe::class, KordExperimental::class)
+
+package com.kotlindiscord.kord.extensions.commands.application
+
+import com.kotlindiscord.kord.extensions.builders.ExtensibleBotBuilder
+import com.kotlindiscord.kord.extensions.commands.CommandContext
+import dev.kord.common.annotation.KordExperimental
+import dev.kord.common.annotation.KordUnsafe
+import dev.kord.core.behavior.GuildBehavior
+import dev.kord.core.behavior.MemberBehavior
+import dev.kord.core.behavior.UserBehavior
+import dev.kord.core.behavior.channel.MessageChannelBehavior
+import dev.kord.core.entity.interaction.GuildApplicationCommandInteraction
+import dev.kord.core.event.interaction.ApplicationInteractionCreateEvent
+import org.koin.core.component.inject
+
+/**
+ * Base class representing the shared functionality for an application command's context.
+ *
+ * @param genericEvent Generic event object to populate data from.
+ * @param genericCommand Generic command object that this context belongs to.
+ */
+public abstract class ApplicationCommandContext(
+ public val genericEvent: ApplicationInteractionCreateEvent,
+ public val genericCommand: ApplicationCommand<*>
+) : CommandContext(genericCommand, genericEvent, genericCommand.name) {
+ /** Current bot setting object. **/
+ public val botSettings: ExtensibleBotBuilder by inject()
+
+ /** Channel this command was executed within. **/
+ public open lateinit var channel: MessageChannelBehavior
+
+ /** Guild this command was executed within, if any. **/
+ public open var guild: GuildBehavior? = null
+
+ /** Member that executed this command, if on a guild. **/
+ public open var member: MemberBehavior? = null
+
+ /** User that executed this command. **/
+ public open lateinit var user: UserBehavior
+
+ /** Called before processing, used to populate any extra variables from event data. **/
+ public override suspend fun populate() {
+ // NOTE: This must always be alphabetical, some latter calls rely on earlier ones
+
+ channel = getChannel()
+ guild = getGuild()
+ member = getMember()
+ user = getUser()
+ }
+
+ /** Extract channel information from event data, if that context is available. **/
+ public override suspend fun getChannel(): MessageChannelBehavior =
+ genericEvent.interaction.channel
+
+ /** Extract guild information from event data, if that context is available. **/
+ public override suspend fun getGuild(): GuildBehavior? =
+ genericEvent.interaction.data.guildId.value?.let { genericEvent.kord.unsafe.guild(it) }
+
+ /** Extract member information from event data, if that context is available. **/
+ public override suspend fun getMember(): MemberBehavior? =
+ (genericEvent.interaction as? GuildApplicationCommandInteraction)?.member
+
+ /** Extract user information from event data, if that context is available. **/
+ public override suspend fun getUser(): UserBehavior =
+ genericEvent.interaction.user
+}
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommandRegistry.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommandRegistry.kt
new file mode 100644
index 0000000000..e937d8064e
--- /dev/null
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/ApplicationCommandRegistry.kt
@@ -0,0 +1,499 @@
+@file:Suppress(
+ "UNCHECKED_CAST",
+ "TooGenericExceptionCaught",
+ "StringLiteralDuplication",
+)
+@file:OptIn(
+ KordUnsafe::class,
+ KordExperimental::class
+)
+
+package com.kotlindiscord.kord.extensions.commands.application
+
+import com.kotlindiscord.kord.extensions.ExtensibleBot
+import com.kotlindiscord.kord.extensions.commands.application.message.MessageCommand
+import com.kotlindiscord.kord.extensions.commands.application.slash.SlashCommand
+import com.kotlindiscord.kord.extensions.commands.application.slash.SlashCommandParser
+import com.kotlindiscord.kord.extensions.commands.application.user.UserCommand
+import com.kotlindiscord.kord.extensions.commands.converters.SlashCommandConverter
+import com.kotlindiscord.kord.extensions.i18n.TranslationsProvider
+import dev.kord.common.annotation.KordExperimental
+import dev.kord.common.annotation.KordUnsafe
+import dev.kord.common.entity.ApplicationCommandType
+import dev.kord.common.entity.Snowflake
+import dev.kord.core.Kord
+import dev.kord.core.behavior.createChatInputCommand
+import dev.kord.core.behavior.createMessageCommand
+import dev.kord.core.behavior.createUserCommand
+import dev.kord.core.entity.Guild
+import dev.kord.core.event.interaction.ChatInputCommandInteractionCreateEvent
+import dev.kord.core.event.interaction.MessageCommandInteractionCreateEvent
+import dev.kord.core.event.interaction.UserCommandInteractionCreateEvent
+import dev.kord.rest.builder.interaction.*
+import dev.kord.rest.request.KtorRequestException
+import mu.KLogger
+import mu.KotlinLogging
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import java.util.*
+
+/**
+ * Abstract class representing common behavior for application command registries.
+ *
+ * Deals with the registration and syncing of, and dispatching to, all application commands.
+ * Subtypes should build their functionality on top of this type.
+ *
+ * @see DefaultApplicationCommandRegistry
+ */
+public abstract class ApplicationCommandRegistry : KoinComponent {
+
+ protected val logger: KLogger = KotlinLogging.logger { }
+
+ /** Current instance of the bot. **/
+ public open val bot: ExtensibleBot by inject()
+
+ /** Kord instance, backing the ExtensibleBot. **/
+ public open val kord: Kord by inject()
+
+ /** Translations provider, for retrieving translations. **/
+ public open val translationsProvider: TranslationsProvider by inject()
+
+ /** Command parser to use for slash commands. **/
+ public val argumentParser: SlashCommandParser = SlashCommandParser()
+
+ /** Whether the initial sync has been finished, and commands should be registered directly. **/
+ public var initialised: Boolean = false
+
+ /** Quick access to the human-readable name for a Discord application command type. **/
+ public val ApplicationCommandType.name: String
+ get() = when (this) {
+ is ApplicationCommandType.Unknown -> "unknown"
+
+ ApplicationCommandType.ChatInput -> "slash"
+ ApplicationCommandType.Message -> "message"
+ ApplicationCommandType.User -> "user"
+ }
+
+ /** Handles the initial registration of commands, after extensions have been loaded. **/
+ public suspend fun initialRegistration() {
+ if (initialised) {
+ return
+ }
+
+ val commands: MutableList> = mutableListOf()
+
+ bot.extensions.values.forEach {
+ commands += it.messageCommands
+ commands += it.slashCommands
+ commands += it.userCommands
+ }
+
+ try {
+ initialize(commands)
+ } catch (t: Throwable) {
+ logger.error(t) { "Failed to initialize registry" }
+ }
+
+ initialised = true
+ }
+
+ /** Called once the initial registration started and all extensions are loaded. **/
+ protected abstract suspend fun initialize(commands: List>)
+
+ /** Register a [SlashCommand] to the registry.
+ *
+ * This method is only called after the [initialize] method and allows runtime modifications.
+ */
+ public abstract suspend fun register(command: SlashCommand<*, *>): SlashCommand<*, *>?
+
+ /**
+ * Register a [MessageCommand] to the registry.
+ *
+ * This method is only called after the [initialize] method and allows runtime modifications.
+ */
+ public abstract suspend fun register(command: MessageCommand<*>): MessageCommand<*>?
+
+ /** Register a [UserCommand] to the registry.
+ *
+ * This method is only called after the [initialize] method and allows runtime modifications.
+ */
+ public abstract suspend fun register(command: UserCommand<*>): UserCommand<*>?
+
+ /** Event handler for slash commands. **/
+ public abstract suspend fun handle(event: ChatInputCommandInteractionCreateEvent)
+
+ /** Event handler for message commands. **/
+ public abstract suspend fun handle(event: MessageCommandInteractionCreateEvent)
+
+ /** Event handler for user commands. **/
+ public abstract suspend fun handle(event: UserCommandInteractionCreateEvent)
+
+ /** Unregister a slash command. **/
+ public abstract suspend fun unregister(command: SlashCommand<*, *>, delete: Boolean = true): SlashCommand<*, *>?
+
+ /** Unregister a message command. **/
+ public abstract suspend fun unregister(command: MessageCommand<*>, delete: Boolean = true): MessageCommand<*>?
+
+ /** Unregister a user command. **/
+ public abstract suspend fun unregister(command: UserCommand<*>, delete: Boolean = true): UserCommand<*>?
+
+ // region: Utilities
+
+ /** Unregister a generic [ApplicationCommand]. **/
+ public open suspend fun unregisterGeneric(
+ command: ApplicationCommand<*>,
+ delete: Boolean = true,
+ ): ApplicationCommand<*>? =
+ when (command) {
+ is MessageCommand<*> -> unregister(command, delete)
+ is SlashCommand<*, *> -> unregister(command, delete)
+ is UserCommand<*> -> unregister(command, delete)
+
+ else -> error("Unsupported application command type: ${command.type.name}")
+ }
+
+ /** @suppress Internal function used to delete the given command from Discord. Used by [unregister]. **/
+ public open suspend fun deleteGeneric(
+ command: ApplicationCommand<*>,
+ discordCommandId: Snowflake,
+ ) {
+ try {
+ if (command.guildId != null) {
+ kord.unsafe.guildApplicationCommand(
+ command.guildId!!,
+ kord.resources.applicationId,
+ discordCommandId
+ ).delete()
+ } else {
+ kord.unsafe.globalApplicationCommand(kord.resources.applicationId, discordCommandId).delete()
+ }
+ } catch (e: KtorRequestException) {
+ logger.warn(e) {
+ "Failed to delete ${command.type.name} command ${command.name}" +
+ if (e.error?.message != null) {
+ "\n Discord error message: ${e.error?.message}"
+ } else {
+ ""
+ }
+ }
+ }
+ }
+
+ /** Register multiple slash commands. **/
+ public open suspend fun > registerAll(vararg commands: T): List =
+ commands.mapNotNull {
+ try {
+ when (it) {
+ is SlashCommand<*, *> -> register(it) as T
+ is MessageCommand<*> -> register(it) as T
+ is UserCommand<*> -> register(it) as T
+
+ else -> throw IllegalArgumentException(
+ "The registry does not know about this type of ApplicationCommand"
+ )
+ }
+ } catch (e: KtorRequestException) {
+ logger.warn(e) {
+ "Failed to register ${it.type.name} command: ${it.name}" +
+ if (e.error?.message != null) {
+ "\n Discord error message: ${e.error?.message}"
+ } else {
+ ""
+ }
+ }
+
+ null
+ } catch (t: Throwable) {
+ logger.warn(t) { "Failed to register ${it.type.name} command: ${it.name}" }
+
+ null
+ }
+ }
+
+ /**
+ * Creates a KordEx [ApplicationCommand] as discord command and returns the created command's id as [Snowflake].
+ */
+ public open suspend fun createDiscordCommand(command: ApplicationCommand<*>): Snowflake? = when (command) {
+ is SlashCommand<*, *> -> createDiscordSlashCommand(command)
+ is UserCommand<*> -> createDiscordUserCommand(command)
+ is MessageCommand<*> -> createDiscordMessageCommand(command)
+
+ else -> throw IllegalArgumentException("Unknown ApplicationCommand type")
+ }
+
+ /**
+ * Creates a KordEx [SlashCommand] as discord command and returns the created command's id as [Snowflake].
+ */
+ public open suspend fun createDiscordSlashCommand(command: SlashCommand<*, *>): Snowflake? {
+ val locale = bot.settings.i18nBuilder.defaultLocale
+
+ val guild = if (command.guildId != null) {
+ kord.getGuild(command.guildId!!)
+ } else {
+ null
+ }
+
+ val name = command.getTranslatedName(locale)
+ val description = command.getTranslatedDescription(locale)
+
+ val response = if (guild == null) {
+ // We're registering global commands here, if the guild is null
+
+ kord.createGlobalChatInputCommand(name, description) {
+ logger.trace { "Adding/updating global ${command.type.name} command: $name" }
+
+ this.register(locale, command)
+ }
+ } else {
+ // We're registering guild-specific commands here, if the guild is available
+
+ guild.createChatInputCommand(name, description) {
+ logger.trace { "Adding/updating guild-specific ${command.type.name} command: $name" }
+
+ this.register(locale, command)
+ }
+ }
+
+ injectPermissions(guild, command, response.id) ?: return null
+
+ return response.id
+ }
+
+ /**
+ * Creates a KordEx [UserCommand] as discord command and returns the created command's id as [Snowflake].
+ */
+ public open suspend fun createDiscordUserCommand(command: UserCommand<*>): Snowflake? {
+ val locale = bot.settings.i18nBuilder.defaultLocale
+
+ val guild = if (command.guildId != null) {
+ kord.getGuild(command.guildId!!)
+ } else {
+ null
+ }
+
+ val name = command.getTranslatedName(locale)
+
+ val response = if (guild == null) {
+ // We're registering global commands here, if the guild is null
+
+ kord.createGlobalUserCommand(name) {
+ logger.trace { "Adding/updating global ${command.type.name} command: $name" }
+
+ this.register(locale, command)
+ }
+ } else {
+ // We're registering guild-specific commands here, if the guild is available
+
+ guild.createUserCommand(name) {
+ logger.trace { "Adding/updating guild-specific ${command.type.name} command: $name" }
+
+ this.register(locale, command)
+ }
+ }
+
+ injectPermissions(guild, command, response.id) ?: return null
+
+ return response.id
+ }
+
+ /**
+ * Creates a KordEx [MessageCommand] as discord command and returns the created command's id as [Snowflake].
+ */
+ public open suspend fun createDiscordMessageCommand(command: MessageCommand<*>): Snowflake? {
+ val locale = bot.settings.i18nBuilder.defaultLocale
+
+ val guild = if (command.guildId != null) {
+ kord.getGuild(command.guildId!!)
+ } else {
+ null
+ }
+
+ val name = command.getTranslatedName(locale)
+
+ val response = if (guild == null) {
+ // We're registering global commands here, if the guild is null
+
+ kord.createGlobalMessageCommand(name) {
+ logger.trace { "Adding/updating global ${command.type.name} command: $name" }
+
+ this.register(locale, command)
+ }
+ } else {
+ // We're registering guild-specific commands here, if the guild is available
+
+ guild.createMessageCommand(name) {
+ logger.trace { "Adding/updating guild-specific ${command.type.name} command: $name" }
+
+ this.register(locale, command)
+ }
+ }
+
+ injectPermissions(guild, command, response.id) ?: return null
+
+ return response.id
+ }
+
+ // endregion
+
+ // region: Permissions
+
+ protected suspend fun > injectPermissions(
+ guild: Guild?,
+ command: T,
+ commandId: Snowflake
+ ): T? {
+ try {
+ if (guild != null) {
+ kord.editApplicationCommandPermissions(guild.id, commandId) {
+ injectRawPermissions(this, command)
+ }
+
+ logger.trace { "Applied permissions for command: ${command.name} ($command)" }
+ } else {
+ logger.warn { "Applying permissions to global application commands is currently not supported." }
+ }
+ } catch (e: KtorRequestException) {
+ logger.error(e) {
+ "Failed to apply application command permissions. This command will not be registered." +
+ if (e.error?.message != null) {
+ "\n Discord error message: ${e.error?.message}"
+ } else {
+ ""
+ }
+ }
+ } catch (t: Throwable) {
+ logger.error(t) {
+ "Failed to apply application command permissions. This command will not be registered."
+ }
+
+ return null
+ }
+ return command
+ }
+
+ protected fun injectRawPermissions(
+ builder: ApplicationCommandPermissionsModifyBuilder,
+ command: ApplicationCommand<*>
+ ) {
+ command.allowedUsers.map { builder.user(it, true) }
+ command.disallowedUsers.map { builder.user(it, false) }
+
+ command.allowedRoles.map { builder.role(it, true) }
+ command.disallowedRoles.map { builder.role(it, false) }
+ }
+
+ // endregion
+
+ // region: Extensions
+
+ /** Registration logic for slash commands, extracted for clarity. **/
+ public open suspend fun ChatInputCreateBuilder.register(locale: Locale, command: SlashCommand<*, *>) {
+ this.defaultPermission = command.guildId == null || command.allowByDefault
+
+ if (command.hasBody) {
+ val args = command.arguments?.invoke()
+
+ if (args != null) {
+ args.args.forEach { arg ->
+ val converter = arg.converter
+
+ if (converter !is SlashCommandConverter) {
+ error("Argument ${arg.displayName} does not support slash commands.")
+ }
+
+ if (this.options == null) this.options = mutableListOf()
+
+ val option = converter.toSlashOption(arg)
+
+ option.name = translationsProvider
+ .translate(option.name, locale, converter.bundle)
+ .lowercase()
+
+ this.options!! += option
+ }
+ }
+ } else {
+ command.subCommands.forEach {
+ val args = it.arguments?.invoke()?.args?.map { arg ->
+ val converter = arg.converter
+
+ if (converter !is SlashCommandConverter) {
+ error("Argument ${arg.displayName} does not support slash commands.")
+ }
+
+ val option = converter.toSlashOption(arg)
+
+ option.name = translationsProvider
+ .translate(option.name, locale, converter.bundle)
+ .lowercase()
+
+ option
+ }
+
+ this.subCommand(
+ it.name,
+ it.getTranslatedDescription(locale)
+ ) {
+ if (args != null) {
+ if (this.options == null) this.options = mutableListOf()
+
+ this.options!!.addAll(args)
+ }
+ }
+ }
+
+ command.groups.values.forEach { group ->
+ this.group(group.name, group.getTranslatedDescription(locale)) {
+ group.subCommands.forEach {
+ val args = it.arguments?.invoke()?.args?.map { arg ->
+ val converter = arg.converter
+
+ if (converter !is SlashCommandConverter) {
+ error("Argument ${arg.displayName} does not support slash commands.")
+ }
+
+ val option = converter.toSlashOption(arg)
+
+ option.name = translationsProvider
+ .translate(option.name, locale, converter.bundle)
+ .lowercase()
+
+ converter.toSlashOption(arg)
+ }
+
+ this.subCommand(
+ it.name,
+ it.getTranslatedDescription(locale)
+ ) {
+ if (args != null) {
+ if (this.options == null) this.options = mutableListOf()
+
+ this.options!!.addAll(args)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /** Registration logic for message commands, extracted for clarity. **/
+ @Suppress("UnusedPrivateMember") // Only for now...
+ public open fun MessageCommandCreateBuilder.register(locale: Locale, command: MessageCommand<*>) {
+ this.defaultPermission = command.guildId == null || command.allowByDefault
+ }
+
+ /** Registration logic for user commands, extracted for clarity. **/
+ @Suppress("UnusedPrivateMember") // Only for now...
+ public open fun UserCommandCreateBuilder.register(locale: Locale, command: UserCommand<*>) {
+ this.defaultPermission = command.guildId == null || command.allowByDefault
+ }
+
+ /** Check whether the type and name of an extension-registered application command matches a Discord one. **/
+ public open fun ApplicationCommand<*>.matches(
+ locale: Locale,
+ other: dev.kord.core.entity.application.ApplicationCommand
+ ): Boolean = type == other.type && getTranslatedName(locale).equals(other.name, true)
+
+ // endregion
+}
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/DefaultApplicationCommandRegistry.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/DefaultApplicationCommandRegistry.kt
new file mode 100644
index 0000000000..49dee9d56e
--- /dev/null
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/DefaultApplicationCommandRegistry.kt
@@ -0,0 +1,387 @@
+@file:Suppress(
+ "TooGenericExceptionCaught",
+ "StringLiteralDuplication",
+ "AnnotationSpacing",
+ "SpacingBetweenAnnotations"
+)
+
+package com.kotlindiscord.kord.extensions.commands.application
+
+import com.kotlindiscord.kord.extensions.commands.application.message.MessageCommand
+import com.kotlindiscord.kord.extensions.commands.application.slash.SlashCommand
+import com.kotlindiscord.kord.extensions.commands.application.user.UserCommand
+import dev.kord.common.entity.Snowflake
+import dev.kord.core.behavior.createApplicationCommands
+import dev.kord.core.event.interaction.ChatInputCommandInteractionCreateEvent
+import dev.kord.core.event.interaction.MessageCommandInteractionCreateEvent
+import dev.kord.core.event.interaction.UserCommandInteractionCreateEvent
+import dev.kord.rest.json.JsonErrorCode
+import dev.kord.rest.request.KtorRequestException
+import kotlinx.coroutines.flow.toList
+
+/** Registry for all Discord application commands. **/
+public open class DefaultApplicationCommandRegistry : ApplicationCommandRegistry() {
+
+ /** Mapping of Discord-side command ID to a message command object. **/
+ public open val messageCommands: MutableMap> = mutableMapOf()
+
+ /** Mapping of Discord-side command ID to a slash command object. **/
+ public open val slashCommands: MutableMap> = mutableMapOf()
+
+ /** Mapping of Discord-side command ID to a user command object. **/
+ public open val userCommands: MutableMap> = mutableMapOf()
+
+ public override suspend fun initialize(commands: List>) {
+ if (!bot.settings.applicationCommandsBuilder.register) {
+ logger.debug {
+ "Application command registration is disabled, pairing existing commands with extension commands"
+ }
+ }
+
+ try {
+ syncAll(true, commands)
+ } catch (t: Throwable) {
+ logger.error(t) { "Failed to synchronise application commands" }
+ }
+ }
+
+ // region: Untyped sync functions
+
+ /** Register multiple generic application commands. **/
+ public open suspend fun syncAll(removeOthers: Boolean = false, commands: List>) {
+ val groupedCommands = commands.groupBy { it.guildId }.toMutableMap()
+
+ if (removeOthers && !groupedCommands.containsKey(null)) {
+ groupedCommands[null] = listOf()
+ }
+
+ groupedCommands.forEach {
+ try {
+ sync(removeOthers, it.key, it.value)
+ } catch (e: KtorRequestException) {
+ logger.error(e) {
+ var message = if (it.key == null) {
+ "Failed to synchronise global application commands"
+ } else {
+ "Failed to synchronise application commands for guild with ID: ${it.key!!.asString}"
+ }
+
+ if (e.error?.message != null) {
+ message += "\n Discord error message: ${e.error?.message}"
+ }
+
+ if (e.error?.code == JsonErrorCode.MissingAccess) {
+ message += "\n Double-check that the bot was added to this guild with the " +
+ "`application.commands` scope enabled"
+ }
+
+ message
+ }
+ } catch (t: Throwable) {
+ logger.error(t) {
+ if (it.key == null) {
+ "Failed to synchronise global application commands"
+ } else {
+ "Failed to synchronise application commands for guild with ID: ${it.key!!.asString}"
+ }
+ }
+ }
+ }
+
+ val commandsWithPerms = (messageCommands + slashCommands + userCommands)
+ .filterValues {
+ it.allowedRoles.isNotEmpty() ||
+ it.allowedUsers.isNotEmpty() ||
+ it.disallowedRoles.isNotEmpty() ||
+ it.disallowedUsers.isNotEmpty() ||
+ !it.allowByDefault
+ }
+ .toList()
+ .groupBy { it.second.guildId }
+
+ try {
+ commandsWithPerms.forEach { (guildId, commands) ->
+ if (guildId != null) {
+ kord.bulkEditApplicationCommandPermissions(guildId) {
+ commands.forEach { (id, commandObj) ->
+ command(id) { injectRawPermissions(this, commandObj) }
+ }
+ }
+
+ logger.trace { "Applied permissions for ${commands.size} commands." }
+ } else {
+ logger.warn { "Applying permissions to global application commands is currently not supported." }
+ }
+ }
+ } catch (e: KtorRequestException) {
+ logger.error(e) {
+ "Failed to apply application command permissions - for this reason, all commands with configured" +
+ "permissions will be disabled." +
+ if (e.error?.message != null) {
+ "\n Discord error message: ${e.error?.message}"
+ } else {
+ ""
+ }
+ }
+ } catch (t: Throwable) {
+ logger.error(t) {
+ "Failed to apply application command permissions - for this reason, all commands with configured" +
+ "permissions will be disabled."
+ }
+
+ commandsWithPerms.forEach { (_, commands) ->
+ commands.forEach { (id, _) ->
+ messageCommands.remove(id)
+ slashCommands.remove(id)
+ userCommands.remove(id)
+ }
+ }
+ }
+ }
+
+ /** Register multiple generic application commands. **/
+ public open suspend fun sync(
+ removeOthers: Boolean = false,
+ guildId: Snowflake?,
+ commands: List>
+ ) {
+ // NOTE: Someday, discord will make real i18n possible, we hope...
+ val locale = bot.settings.i18nBuilder.defaultLocale
+
+ val guild = if (guildId != null) {
+ kord.getGuild(guildId)
+ ?: return logger.debug {
+ "Cannot register application commands for guild ID ${guildId.asString}, " +
+ "as it seems to be missing."
+ }
+ } else {
+ null
+ }
+
+ // Get guild commands if we're registering them (guild != null), otherwise get global commands
+ val registered = guild?.commands?.toList()
+ ?: kord.globalCommands.toList()
+
+ if (!bot.settings.applicationCommandsBuilder.register) {
+ commands.forEach { commandObj ->
+ val existingCommand = registered.firstOrNull { commandObj.matches(locale, it) }
+
+ if (existingCommand != null) {
+ when (commandObj) {
+ is MessageCommand<*> -> messageCommands[existingCommand.id] = commandObj
+ is SlashCommand<*, *> -> slashCommands[existingCommand.id] = commandObj
+ is UserCommand<*> -> userCommands[existingCommand.id] = commandObj
+ }
+ }
+ }
+
+ return // We're only syncing them up, there's no other API work to do
+ }
+
+ // Extension commands that haven't been registered yet
+ val toAdd = commands.filter { aC -> registered.all { dC -> !aC.matches(locale, dC) } }
+
+ // Extension commands that were previously registered
+ val toUpdate = commands.filter { aC -> registered.any { dC -> aC.matches(locale, dC) } }
+
+ // Registered Discord commands that haven't been provided by extensions
+ val toRemove = if (removeOthers) {
+ registered.filter { dC -> commands.all { aC -> !aC.matches(locale, dC) } }
+ } else {
+ listOf()
+ }
+
+ logger.info {
+ var message = if (guild == null) {
+ "Global application commands: ${toAdd.size} to add / " +
+ "${toUpdate.size} to update / " +
+ "${toRemove.size} to remove"
+ } else {
+ "Application commands for guild ${guild.name}: ${toAdd.size} to add / " +
+ "${toUpdate.size} to update / " +
+ "${toRemove.size} to remove"
+ }
+
+ if (!removeOthers) {
+ message += "\nThe `removeOthers` parameter is `false`, so no commands will be removed."
+ }
+
+ message
+ }
+
+ val toCreate = toAdd + toUpdate
+
+ @Suppress("IfThenToElvis") // Ultimately, this is far more readable
+ val response = if (guild == null) {
+ // We're registering global commands here, if the guild is null
+
+ kord.createGlobalApplicationCommands {
+ toCreate.forEach {
+ val name = it.getTranslatedName(locale)
+
+ logger.trace { "Adding/updating global ${it.type.name} command: $name" }
+
+ when (it) {
+ is MessageCommand<*> -> message(name) { this.register(locale, it) }
+ is UserCommand<*> -> user(name) { this.register(locale, it) }
+
+ is SlashCommand<*, *> -> input(
+ name, it.getTranslatedDescription(locale)
+ ) { this.register(locale, it) }
+ }
+ }
+ }.toList()
+ } else {
+ // We're registering guild-specific commands here, if the guild is available
+
+ guild.createApplicationCommands {
+ toCreate.forEach {
+ val name = it.getTranslatedName(locale)
+
+ logger.trace { "Adding/updating guild-specific ${it.type.name} command: $name" }
+
+ when (it) {
+ is MessageCommand<*> -> message(name) { this.register(locale, it) }
+ is UserCommand<*> -> user(name) { this.register(locale, it) }
+
+ is SlashCommand<*, *> -> input(
+ name, it.getTranslatedDescription(locale)
+ ) { this.register(locale, it) }
+ }
+ }
+ }.toList()
+ }
+
+ // Next, we need to associate all the commands we just registered with the commands in our extensions
+ toCreate.forEach { command ->
+ val match = response.first { command.matches(locale, it) }
+
+ when (command) {
+ is MessageCommand<*> -> messageCommands[match.id] = command
+ is SlashCommand<*, *> -> slashCommands[match.id] = command
+ is UserCommand<*> -> userCommands[match.id] = command
+ }
+ }
+
+ // Finally, we can remove anything that needs to be removed
+ toRemove.forEach {
+ logger.trace { "Removing ${it.type.name} command: ${it.name}" }
+ it.delete()
+ }
+
+ logger.info {
+ if (guild == null) {
+ "Finished synchronising global application commands"
+ } else {
+ "Finished synchronising application commands for guild ${guild.name}"
+ }
+ }
+ }
+
+ // endregion
+
+ // region: Typed registration functions
+
+ /** Register a message command. **/
+ public override suspend fun register(command: MessageCommand<*>): MessageCommand<*>? {
+ val commandId = createDiscordCommand(command) ?: return null
+
+ messageCommands[commandId] = command
+
+ return command
+ }
+
+ /** Register a slash command. **/
+ public override suspend fun register(command: SlashCommand<*, *>): SlashCommand<*, *>? {
+ val commandId = createDiscordCommand(command) ?: return null
+
+ slashCommands[commandId] = command
+
+ return command
+ }
+
+ /** Register a user command. **/
+ public override suspend fun register(command: UserCommand<*>): UserCommand<*>? {
+ val commandId = createDiscordCommand(command) ?: return null
+
+ userCommands[commandId] = command
+
+ return command
+ }
+
+ // endregion
+
+ // region: Unregistration functions
+
+ /** Unregister a message command. **/
+ public override suspend fun unregister(command: MessageCommand<*>, delete: Boolean): MessageCommand<*>? {
+ val filtered = messageCommands.filter { it.value == command }
+ val id = filtered.keys.firstOrNull() ?: return null
+
+ if (delete) {
+ deleteGeneric(command, id)
+ }
+
+ return messageCommands.remove(id)
+ }
+
+ /** Unregister a slash command. **/
+ public override suspend fun unregister(command: SlashCommand<*, *>, delete: Boolean): SlashCommand<*, *>? {
+ val filtered = slashCommands.filter { it.value == command }
+ val id = filtered.keys.firstOrNull() ?: return null
+
+ if (delete) {
+ deleteGeneric(command, id)
+ }
+
+ return slashCommands.remove(id)
+ }
+
+ /** Unregister a user command. **/
+ public override suspend fun unregister(command: UserCommand<*>, delete: Boolean): UserCommand<*>? {
+ val filtered = userCommands.filter { it.value == command }
+ val id = filtered.keys.firstOrNull() ?: return null
+
+ if (delete) {
+ deleteGeneric(command, id)
+ }
+
+ return userCommands.remove(id)
+ }
+
+ // endregion
+
+ // region: Event handlers
+
+ /** Event handler for message commands. **/
+ public override suspend fun handle(event: MessageCommandInteractionCreateEvent) {
+ val commandId = event.interaction.invokedCommandId
+ val command = messageCommands[commandId]
+
+ command ?: return logger.warn { "Received interaction for unknown message command: ${commandId.asString}" }
+
+ command.call(event)
+ }
+
+ /** Event handler for slash commands. **/
+ public override suspend fun handle(event: ChatInputCommandInteractionCreateEvent) {
+ val commandId = event.interaction.command.rootId
+ val command = slashCommands[commandId]
+
+ command ?: return logger.warn { "Received interaction for unknown slash command: ${commandId.asString}" }
+
+ command.call(event)
+ }
+
+ /** Event handler for user commands. **/
+ public override suspend fun handle(event: UserCommandInteractionCreateEvent) {
+ val commandId = event.interaction.invokedCommandId
+ val command = userCommands[commandId]
+
+ command ?: return logger.warn { "Received interaction for unknown user command: ${commandId.asString}" }
+
+ command.call(event)
+ }
+
+ // endregion
+}
diff --git a/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/StorageAwareApplicationCommandRegistry.kt b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/StorageAwareApplicationCommandRegistry.kt
new file mode 100644
index 0000000000..62052996b5
--- /dev/null
+++ b/kord-extensions/src/main/kotlin/com/kotlindiscord/kord/extensions/commands/application/StorageAwareApplicationCommandRegistry.kt
@@ -0,0 +1,138 @@
+@file:Suppress(
+ "UNCHECKED_CAST"
+)
+@file:OptIn(
+ ExperimentalCoroutinesApi::class
+)
+
+package com.kotlindiscord.kord.extensions.commands.application
+
+import com.kotlindiscord.kord.extensions.commands.application.message.MessageCommand
+import com.kotlindiscord.kord.extensions.commands.application.slash.SlashCommand
+import com.kotlindiscord.kord.extensions.commands.application.user.UserCommand
+import com.kotlindiscord.kord.extensions.registry.RegistryStorage
+import dev.kord.common.entity.Snowflake
+import dev.kord.core.event.interaction.ChatInputCommandInteractionCreateEvent
+import dev.kord.core.event.interaction.MessageCommandInteractionCreateEvent
+import dev.kord.core.event.interaction.UserCommandInteractionCreateEvent
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.toList
+
+/**
+ * [ApplicationCommandRegistry] which acts based off a specified storage interface.
+ *
+ * Discord lifecycles may not be implemented in this class and require manual updating.
+ */
+public open class StorageAwareApplicationCommandRegistry(
+ builder: () -> RegistryStorage>
+) : ApplicationCommandRegistry() {
+
+ protected open val commandRegistry: RegistryStorage