diff --git a/build.gradle.kts b/build.gradle.kts index c73fb17..71f8ef9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,9 @@ dependencies { implementation(libs.jda) implementation(libs.logger) implementation(libs.jackson.core) + implementation(libs.jackson.kotlin) implementation(libs.jackson.yaml) + implementation(kotlin("scripting-jsr223")) // Dangerous stuff!! testImplementation("org.jetbrains.kotlin:kotlin-test") } diff --git a/commands/example.yml b/commands/advanced-example.yml similarity index 58% rename from commands/example.yml rename to commands/advanced-example.yml index 9e703ef..3d33de8 100644 --- a/commands/example.yml +++ b/commands/advanced-example.yml @@ -23,6 +23,11 @@ input: type: BOOLEAN desc: is the user nice? required: false -# fancy stuff +# Yeah, you can write full-blown kotlin here +# That is why I will be manually reviewing each command that is added output: |- - I want to do something like ${options.user.asMention} is ${options["is-nice"]?.asBoolean ?: false} + import java.util.concurrent.ThreadLocalRandom + + val randomBool = ThreadLocalRandom.current().nextBoolean() + + "I want to do something like ${event.getOption("user")!!.asUser.asMention} is ${event.getOption("is-nice")?.asBoolean ?: randomBool}" diff --git a/commands/simple-example.yml b/commands/simple-example.yml new file mode 100644 index 0000000..f310249 --- /dev/null +++ b/commands/simple-example.yml @@ -0,0 +1,6 @@ +# name and description and output are required +name: simple-command +description: A simple example command +# Final line of output MUST be in quotes as that is what will be returned from the bot +output: |- + "Hi there! Wanna add your own commands? Submit a Pull Request on GitHub: " diff --git a/docker-compose.yml b/docker-compose.yml index 47314f7..1d598de 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,8 @@ version: "3.7" services: io: container_name: io + volumes: + - './commands:/oracle-bot/commands' build: context: . environment: diff --git a/settings.gradle.kts b/settings.gradle.kts index d9b9ea3..66c0a50 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,6 +14,7 @@ dependencyResolutionManagement { library("logger", "ch.qos.logback", "logback-classic").version("1.4.11") library("jackson-core", "com.fasterxml.jackson.core", "jackson-core").versionRef("jackson") + library("jackson-kotlin", "com.fasterxml.jackson.module", "jackson-module-kotlin").versionRef("jackson") library("jackson-yaml", "com.fasterxml.jackson.dataformat", "jackson-dataformat-yaml").versionRef("jackson") } } diff --git a/src/main/kotlin/me/duncte123/io/CommandManager.kt b/src/main/kotlin/me/duncte123/io/CommandManager.kt index f85b999..2a3730d 100644 --- a/src/main/kotlin/me/duncte123/io/CommandManager.kt +++ b/src/main/kotlin/me/duncte123/io/CommandManager.kt @@ -3,6 +3,8 @@ package me.duncte123.io import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import com.fasterxml.jackson.module.kotlin.KotlinFeature +import com.fasterxml.jackson.module.kotlin.KotlinModule import me.duncte123.io.commands.JjCommand import me.duncte123.io.commands.ReloadCommand import me.duncte123.io.commands.UserCommand @@ -17,6 +19,17 @@ import java.io.File class CommandManager { private val log = LoggerFactory.getLogger(CommandManager::class.java) private val jackson = ObjectMapper(YAMLFactory()) + .registerModule( + KotlinModule.Builder() + .withReflectionCacheSize(512) + .configure(KotlinFeature.NullToEmptyCollection, false) + .configure(KotlinFeature.NullToEmptyMap, false) + .configure(KotlinFeature.NullIsSameAsDefault, false) +// .configure(KotlinFeature.SingletonSupport, SingletonSupport.DISABLED) + .configure(KotlinFeature.SingletonSupport, false) + .configure(KotlinFeature.StrictNullChecks, false) + .build() + ) .addMixIn(OptionData::class.java, OptionDataMixin::class.java) private val commands = mutableMapOf() diff --git a/src/main/kotlin/me/duncte123/io/Listener.kt b/src/main/kotlin/me/duncte123/io/Listener.kt index fa3acbe..0e4aeed 100644 --- a/src/main/kotlin/me/duncte123/io/Listener.kt +++ b/src/main/kotlin/me/duncte123/io/Listener.kt @@ -13,7 +13,8 @@ class Listener : EventListener { is ReadyEvent -> onReady(event) is SlashCommandInteractionEvent -> onSlashCommandInteraction(event) is MessageReceivedEvent -> { - if (event.isFromType(ChannelType.PRIVATE)) { + // ALWAYS IGNORE BOTS + if (event.isFromType(ChannelType.PRIVATE) && !event.author.isBot) { event.message .reply("Hey you! Wishlist jock studio now! ") .queue() diff --git a/src/main/kotlin/me/duncte123/io/commands/UserCommand.kt b/src/main/kotlin/me/duncte123/io/commands/UserCommand.kt index 73a9805..6e531c7 100644 --- a/src/main/kotlin/me/duncte123/io/commands/UserCommand.kt +++ b/src/main/kotlin/me/duncte123/io/commands/UserCommand.kt @@ -6,6 +6,38 @@ import com.fasterxml.jackson.annotation.JsonProperty import me.duncte123.io.ICommand import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent import net.dv8tion.jda.api.interactions.commands.build.OptionData +import net.dv8tion.jda.api.requests.RestAction +import javax.script.ScriptEngine +import javax.script.ScriptEngineManager +import javax.script.SimpleBindings +import kotlin.math.min + +private val kotlinEngine: ScriptEngine by lazy { + ScriptEngineManager().getEngineByExtension("kts")!!.apply { + this.eval( + """ + import java.io.* + import java.lang.* + import java.math.* + import java.time.* + import java.util.* + import java.util.concurrent.* + import java.util.stream.* + import net.dv8tion.jda.api.* + import net.dv8tion.jda.api.entities.* + import net.dv8tion.jda.api.entities.channel.* + import net.dv8tion.jda.api.entities.channel.attribute.* + import net.dv8tion.jda.api.entities.channel.middleman.* + import net.dv8tion.jda.api.entities.channel.concrete.* + import net.dv8tion.jda.api.sharding.* + import net.dv8tion.jda.internal.entities.* + import net.dv8tion.jda.api.managers.* + import net.dv8tion.jda.internal.managers.* + import net.dv8tion.jda.api.utils.* + """.trimIndent() + ) + } +} @JsonIgnoreProperties(ignoreUnknown = true) data class UserCommand @JsonCreator constructor ( @@ -16,6 +48,57 @@ data class UserCommand @JsonCreator constructor ( @JsonProperty("output") val output: String, ) : ICommand { override fun execute(event: SlashCommandInteractionEvent) { - event.reply(output).queue() + event.deferReply(false).queue() + + val bindings = SimpleBindings() + + bindings["event"] = event + + val response = try { + kotlinEngine.eval(output, bindings) + } catch (e: Exception) { + e + } + + parseEvalResponse(response, event) + } + + private fun parseEvalResponse(out: Any?, event: SlashCommandInteractionEvent) { + val send: (message: String) -> Unit = { event.hook.sendMessage(it).queue() } + + when (out) { + null -> send("(command produced no output)") + + is Throwable -> { + send("ERROR: $out") + } + + is RestAction<*> -> { + out.queue({ + send("Rest action success: $it") + }) { + send("Rest action error: $it") + } + } + + else -> { + val toString = out.toString() + + if (toString.isEmpty() || toString.isBlank()) { + send("(command produced no output)") + return + } + + send(toString.truncate(1900)) + } + } + } + + private fun String.truncate(length: Int): String { + if (this.length < length) { + return this + } + + return this.substring(0, min(this.length, length - 3)) + "..." } }