From 61852812e93aa2343265b0215c8c905063747c6c Mon Sep 17 00:00:00 2001 From: solonovamax Date: Mon, 25 Oct 2021 22:16:34 -0400 Subject: [PATCH 1/2] Eval command go brrrrr Signed-off-by: solonovamax --- build.gradle.kts | 10 +- .../kotlin/ca/solostudios/polybot/PolyBot.kt | 14 +-- .../polybot/commands/BotAdminCommands.kt | 112 +++++++++++++++++- 3 files changed, 124 insertions(+), 12 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index d967123..8cf4f9c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,7 @@ * Copyright (c) 2021-2021 solonovamax * * The file build.gradle.kts is part of PolyhedralBot - * Last modified on 25-10-2021 05:59 p.m. + * Last modified on 25-10-2021 08:19 p.m. * * MIT License * @@ -127,8 +127,10 @@ dependencies { // Kotlin implementation(kotlin("stdlib", KOTLIN_VERSION)) implementation(kotlin("reflect", KOTLIN_VERSION)) // Reflection stuff - implementation(kotlin("script-runtime", KOTLIN_VERSION)) // For executing scripts at runtime - implementation(kotlin("script-util", KOTLIN_VERSION)) + implementation(kotlin("script-util", KOTLIN_VERSION)) // For executing scripts at runtime + implementation(kotlin("script-runtime", KOTLIN_VERSION)) + implementation(kotlin("scripting-jsr223", KOTLIN_VERSION)) + implementation(kotlin("scripting-jvm-host", KOTLIN_VERSION)) implementation(kotlin("compiler-embeddable", KOTLIN_VERSION)) implementation(kotlin("scripting-compiler-embeddable", KOTLIN_VERSION)) // Kotlin Serialization @@ -293,7 +295,7 @@ tasks { it.moduleGroup == "org.jetbrains.exposed" } exclude { - it.moduleName == "kotlin-reflect" + it.moduleName == "kotlin-reflect" || it.moduleName.startsWith("kotlin-script") } exclude { it.moduleGroup == "org.apache.lucene" && (it.moduleName == "lucene-core" || it.moduleName == "lucene-sandbox") diff --git a/src/main/kotlin/ca/solostudios/polybot/PolyBot.kt b/src/main/kotlin/ca/solostudios/polybot/PolyBot.kt index 98eab4b..a6cda72 100644 --- a/src/main/kotlin/ca/solostudios/polybot/PolyBot.kt +++ b/src/main/kotlin/ca/solostudios/polybot/PolyBot.kt @@ -3,7 +3,7 @@ * Copyright (c) 2021-2021 solonovamax * * The file PolyBot.kt is part of PolyhedralBot - * Last modified on 25-10-2021 05:05 p.m. + * Last modified on 25-10-2021 10:16 p.m. * * MIT License * @@ -306,11 +306,11 @@ class PolyBot(val config: PolyConfig, builder: InlineJDABuilder) { } fun userReference(userId: Long): BackedReference { - return BackedReference(userId, { jda.getUserById(it) }, { it?.idLong ?: 0 }) + return BackedReference(userId, { jda.retrieveUserById(it).complete() }, { it?.idLong ?: 0 }) } fun polyUserReference(userId: Long): BackedReference { - return BackedReference(userId, { jda.getUserById(it)?.poly(this) }, { it?.id ?: 0 }) + return BackedReference(userId, { jda.retrieveUserById(it).complete()?.poly(this) }, { it?.id ?: 0 }) } fun user(userId: Long): User? { @@ -323,22 +323,22 @@ class PolyBot(val config: PolyConfig, builder: InlineJDABuilder) { fun memberReference(guildId: Long, userId: Long): BackedReference> { return BackedReference(guildId to userId, - { jda.getGuildById(it.first)?.getMemberById(it.second) }, + { jda.getGuildById(it.first)?.retrieveMemberById(it.second)?.complete() }, { it?.let { it.idLong to it.guild.idLong } ?: (0L to 0L) }) } fun polyMemberReference(guildId: Long, userId: Long): BackedReference> { return BackedReference(guildId to userId, - { jda.getGuildById(it.first)?.getMemberById(it.second)?.poly(this) }, + { jda.getGuildById(it.first)?.retrieveMemberById(it.second)?.complete()?.poly(this) }, { it?.let { it.id to it.guild.id } ?: (0L to 0L) }) } fun member(guildId: Long, userId: Long): Member? { - return jda.getGuildById(guildId)?.getMemberById(userId) + return jda.getGuildById(guildId)?.retrieveMemberById(userId)?.complete() } fun polyMember(guildId: Long, userId: Long): PolyMember? { - return jda.getGuildById(guildId)?.getMemberById(userId)?.poly(this) + return jda.getGuildById(guildId)?.retrieveMemberById(userId)?.complete()?.poly(this) } fun emoteReference(emoteId: Long): BackedReference { diff --git a/src/main/kotlin/ca/solostudios/polybot/commands/BotAdminCommands.kt b/src/main/kotlin/ca/solostudios/polybot/commands/BotAdminCommands.kt index a7d3672..86a9376 100644 --- a/src/main/kotlin/ca/solostudios/polybot/commands/BotAdminCommands.kt +++ b/src/main/kotlin/ca/solostudios/polybot/commands/BotAdminCommands.kt @@ -3,7 +3,7 @@ * Copyright (c) 2021-2021 solonovamax * * The file BotAdminCommands.kt is part of PolyhedralBot - * Last modified on 25-10-2021 05:05 p.m. + * Last modified on 25-10-2021 10:16 p.m. * * MIT License * @@ -37,9 +37,19 @@ import ca.solostudios.polybot.cloud.commands.annotations.JDAUserPermission import ca.solostudios.polybot.cloud.commands.annotations.PolyCategory import ca.solostudios.polybot.entities.PolyMessage import ca.solostudios.polybot.entities.PolyUser +import cloud.commandframework.annotations.Argument import cloud.commandframework.annotations.CommandDescription import cloud.commandframework.annotations.CommandMethod import cloud.commandframework.annotations.Hidden +import cloud.commandframework.annotations.specifier.Greedy +import java.io.StringWriter +import java.time.Duration +import javax.script.ScriptContext +import javax.script.ScriptEngineManager +import javax.script.SimpleScriptContext +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.launch +import kotlinx.coroutines.time.withTimeout import org.slf4j.kotlin.* @Hidden @@ -47,6 +57,7 @@ import org.slf4j.kotlin.* @PolyCategory(BOT_ADMIN_CATEGORY) class BotAdminCommands(bot: PolyBot) : PolyCommands(bot) { private val logger by getLogger() + private val engine = ScriptEngineManager().getEngineByName("kotlin") @CommandName("Shutdown Bot") @CommandMethod("shutdown") @@ -77,4 +88,103 @@ class BotAdminCommands(bot: PolyBot) : PolyCommands(bot) { logger.info { "Update request was triggered by ${author.tag} (${author.id})" } bot.shutdown(ExitCodes.EXIT_CODE_UPDATE) } + + @CommandName("Eval") + @CommandMethod("eval [code]") + @JDAUserPermission(ownerOnly = true) + suspend fun eval(message: PolyMessage, + author: PolyUser, + @Greedy + @Argument(value = "code", description = "Code to evaluate") + code: String) { + assert(author.isOwner) // sanity check + assert(author.id in bot.config.botConfig.ownerIds) + + val outputWriter = StringWriter() + val errorWriter = StringWriter() + + bot.scope.launch { } + + try { + val result = withTimeout(Duration.ofSeconds(10)) { + val context = SimpleScriptContext() + + + context.reader = "".reader() + context.writer = outputWriter + context.errorWriter = errorWriter + + + val scope = engine.createBindings().also { context.setBindings(it, ScriptContext.ENGINE_SCOPE) } + + + scope["user"] = author + scope["message"] = message + scope["logger"] = logger + scope["bot"] = bot + scope["jda"] = bot.jda + + if (message.fromGuild) { + scope["guild"] = message.guild + scope["author"] = message.member + } else { + scope["author"] = author + } + + val source = "@file:kotlin.OptIn(kotlin.time.ExperimentalTime::class)\n" + + imports.joinToString(separator = "\n", postfix = "\n") { "import $it" } + code + + logger.info { source } + + val output: String? = engine.eval(source, scope)?.toString() + val out = outputWriter.toString() + val err = errorWriter.toString() + + return@withTimeout buildString { + if (!output.isNullOrEmpty()) { + appendLine("\nReturn value:") + appendLine("```") + appendLine(output) + appendLine("```") + } + + if (out.isNotEmpty()) { + appendLine("\nConsole output:") + appendLine("```") + appendLine(out) + appendLine("```") + } + + if (err.isNotEmpty()) { + appendLine("\nErr output:") + appendLine("```") + appendLine(out) + appendLine("```") + } + } + } + + logger.info { "Result of eval: $result" } + + message.reply("Evaluation completed successfully.") + + if (result.isNotEmpty()) + message.reply(result.takeIf { it.length <= 4000 } ?: result.substring(0, 4000)) + } catch (e: TimeoutCancellationException) { + + } + } + + companion object { + val imports = listOf( + "kotlin.*", + "kotlinx.*", + "kotlinx.coroutines.*", + "ca.solostudios.polybot.*", + "ca.solostudios.polybot.util.*", + "ca.solostudios.polybot.entities.*", + "net.dv8tion.jda.api.*", + "org.slf4j.kotlin.*", + ) + } } \ No newline at end of file From 8302d92f75230643bbdf729c58634cad49f2eeef Mon Sep 17 00:00:00 2001 From: solonovamax Date: Thu, 28 Oct 2021 19:58:27 -0400 Subject: [PATCH 2/2] Begin moving to Kotlin's script api instead of Java JSR 223 Signed-off-by: solonovamax --- .../polybot/commands/BotAdminCommands.kt | 204 ++++++++++++------ ...tAdminCommands.KotlinAdminScript.classname | 0 2 files changed, 141 insertions(+), 63 deletions(-) create mode 100644 src/main/resources/META-INF/kotlin/script/templates/ca.solostudios.polybot.commands.BotAdminCommands.KotlinAdminScript.classname diff --git a/src/main/kotlin/ca/solostudios/polybot/commands/BotAdminCommands.kt b/src/main/kotlin/ca/solostudios/polybot/commands/BotAdminCommands.kt index 86a9376..e46de5b 100644 --- a/src/main/kotlin/ca/solostudios/polybot/commands/BotAdminCommands.kt +++ b/src/main/kotlin/ca/solostudios/polybot/commands/BotAdminCommands.kt @@ -3,7 +3,7 @@ * Copyright (c) 2021-2021 solonovamax * * The file BotAdminCommands.kt is part of PolyhedralBot - * Last modified on 25-10-2021 10:16 p.m. + * Last modified on 28-10-2021 07:58 p.m. * * MIT License * @@ -42,15 +42,31 @@ import cloud.commandframework.annotations.CommandDescription import cloud.commandframework.annotations.CommandMethod import cloud.commandframework.annotations.Hidden import cloud.commandframework.annotations.specifier.Greedy -import java.io.StringWriter +import java.io.ByteArrayOutputStream +import java.io.PrintStream import java.time.Duration -import javax.script.ScriptContext import javax.script.ScriptEngineManager -import javax.script.SimpleScriptContext import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.launch import kotlinx.coroutines.time.withTimeout +import net.dv8tion.jda.api.JDA import org.slf4j.kotlin.* +import kotlin.script.experimental.annotations.KotlinScript +import kotlin.script.experimental.api.EvaluationResult +import kotlin.script.experimental.api.ResultWithDiagnostics +import kotlin.script.experimental.api.ScriptAcceptedLocation +import kotlin.script.experimental.api.ScriptCompilationConfiguration +import kotlin.script.experimental.api.ScriptEvaluationConfiguration +import kotlin.script.experimental.api.acceptedLocations +import kotlin.script.experimental.api.defaultImports +import kotlin.script.experimental.api.ide +import kotlin.script.experimental.api.providedProperties +import kotlin.script.experimental.host.toScriptSource +import kotlin.script.experimental.jvm.dependenciesFromClassContext +import kotlin.script.experimental.jvm.jvm +import kotlin.script.experimental.jvm.updateClasspath +import kotlin.script.experimental.jvm.util.classpathFromClass +import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost +import kotlin.script.experimental.jvmhost.createJvmCompilationConfigurationFromTemplate @Hidden @PolyCommandContainer @@ -58,6 +74,7 @@ import org.slf4j.kotlin.* class BotAdminCommands(bot: PolyBot) : PolyCommands(bot) { private val logger by getLogger() private val engine = ScriptEngineManager().getEngineByName("kotlin") + private val scriptingHost = BasicJvmScriptingHost() @CommandName("Shutdown Bot") @CommandMethod("shutdown") @@ -100,61 +117,44 @@ class BotAdminCommands(bot: PolyBot) : PolyCommands(bot) { assert(author.isOwner) // sanity check assert(author.id in bot.config.botConfig.ownerIds) - val outputWriter = StringWriter() - val errorWriter = StringWriter() - - bot.scope.launch { } - try { val result = withTimeout(Duration.ofSeconds(10)) { - val context = SimpleScriptContext() - - - context.reader = "".reader() - context.writer = outputWriter - context.errorWriter = errorWriter - - - val scope = engine.createBindings().also { context.setBindings(it, ScriptContext.ENGINE_SCOPE) } - - - scope["user"] = author - scope["message"] = message - scope["logger"] = logger - scope["bot"] = bot - scope["jda"] = bot.jda - - if (message.fromGuild) { - scope["guild"] = message.guild - scope["author"] = message.member - } else { - scope["author"] = author + val (err, out, result) = captureOutAndErr { + evalString(code) { + providedProperties( + "user" to author, + "message" to message, + "logger" to logger, + "bot" to bot, + "jda" to bot.jda, + ) + } } - - val source = "@file:kotlin.OptIn(kotlin.time.ExperimentalTime::class)\n" + - imports.joinToString(separator = "\n", postfix = "\n") { "import $it" } + code - - logger.info { source } - - val output: String? = engine.eval(source, scope)?.toString() - val out = outputWriter.toString() - val err = errorWriter.toString() - + + // result as ResultWithDiagnostics.Success + + return@withTimeout buildString { - if (!output.isNullOrEmpty()) { - appendLine("\nReturn value:") - appendLine("```") - appendLine(output) - appendLine("```") - } - + append(result) + + // if (!output.isNullOrEmpty()) { + // val returnValue = result.value.returnValue + // returnValue as ResultValue.Value + // + // returnValue.toString() + // appendLine("\nReturn value:") + // appendLine("```") + // appendLine(output) + // appendLine("```") + // } + if (out.isNotEmpty()) { appendLine("\nConsole output:") appendLine("```") appendLine(out) appendLine("```") } - + if (err.isNotEmpty()) { appendLine("\nErr output:") appendLine("```") @@ -165,26 +165,104 @@ class BotAdminCommands(bot: PolyBot) : PolyCommands(bot) { } logger.info { "Result of eval: $result" } - + message.reply("Evaluation completed successfully.") - + if (result.isNotEmpty()) message.reply(result.takeIf { it.length <= 4000 } ?: result.substring(0, 4000)) } catch (e: TimeoutCancellationException) { + + } + } + + /** + * From [Jetbrains/kotlin](https://github.com/JetBrains/kotlin/blob/master/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/TestScriptDefinitions.kt#L62). + * + * @param T + * @param source + * @param configure + * @receiver + * @return + */ + private inline fun evalString( + source: String, + noinline configure: ScriptEvaluationConfiguration.Builder.() -> Unit + ): ResultWithDiagnostics { + val actualConfiguration = createJvmCompilationConfigurationFromTemplate() + return scriptingHost.eval(source.toScriptSource(), actualConfiguration, ScriptEvaluationConfiguration(configure)) + } + + @KotlinScript( + displayName = "PolyScript", + compilationConfiguration = KotlinAdminScriptCompilationConfiguration::class, + ) + abstract class KotlinAdminScript + + class KotlinAdminScriptCompilationConfiguration : ScriptCompilationConfiguration( + { + defaultImports(DEFAULT_EVAL_IMPORTS) + + updateClasspath(classpathFromClass()) + + providedProperties( + "bot" to PolyBot::class, + "logger" to KLogger::class, + "jda" to JDA::class, + // "guild" to PolyGuild::class, + // "member" to PolyMember::class, + "user" to PolyUser::class, + "message" to PolyMessage::class + ) + + jvm { + dependenciesFromClassContext(contextClass = PolyBot::class, wholeClasspath = true) + } + ide { + acceptedLocations(ScriptAcceptedLocation.Everywhere) + } + } + ) { + companion object { + val DEFAULT_EVAL_IMPORTS = listOf( + "kotlin.*", + "kotlinx.*", + "kotlinx.coroutines.*", + "ca.solostudios.polybot.*", + "ca.solostudios.polybot.util.*", + "ca.solostudios.polybot.entities.*", + "net.dv8tion.jda.api.*", + "org.slf4j.kotlin.*", + ) } } - companion object { - val imports = listOf( - "kotlin.*", - "kotlinx.*", - "kotlinx.coroutines.*", - "ca.solostudios.polybot.*", - "ca.solostudios.polybot.util.*", - "ca.solostudios.polybot.entities.*", - "net.dv8tion.jda.api.*", - "org.slf4j.kotlin.*", - ) + /** + * Taken from [Jetbrains/kotlin](https://github.com/JetBrains/kotlin/blob/master/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt#L519). + * + * @param body + * @receiver + * @return + */ + private fun captureOutAndErr(body: () -> ResultWithDiagnostics): Triple> { + val outStream = ByteArrayOutputStream() + val errStream = ByteArrayOutputStream() + + val prevOut = System.out + val prevErr = System.err + System.setOut(PrintStream(outStream)) + System.setErr(PrintStream(errStream)) + lateinit var res: ResultWithDiagnostics + try { + res = body() + } finally { + System.out.flush() + System.err.flush() + System.setOut(prevOut) + System.setErr(prevErr) + } + + + return Triple(outStream.toString().trim(), errStream.toString().trim(), res) } } \ No newline at end of file diff --git a/src/main/resources/META-INF/kotlin/script/templates/ca.solostudios.polybot.commands.BotAdminCommands.KotlinAdminScript.classname b/src/main/resources/META-INF/kotlin/script/templates/ca.solostudios.polybot.commands.BotAdminCommands.KotlinAdminScript.classname new file mode 100644 index 0000000..e69de29