Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add eval command to bot #1

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright (c) 2021-2021 solonovamax <[email protected]>
*
* 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
*
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
14 changes: 7 additions & 7 deletions src/main/kotlin/ca/solostudios/polybot/PolyBot.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright (c) 2021-2021 solonovamax <[email protected]>
*
* 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
*
Expand Down Expand Up @@ -306,11 +306,11 @@ class PolyBot(val config: PolyConfig, builder: InlineJDABuilder) {
}

fun userReference(userId: Long): BackedReference<User?, Long> {
return BackedReference(userId, { jda.getUserById(it) }, { it?.idLong ?: 0 })
return BackedReference(userId, { jda.retrieveUserById(it).complete() }, { it?.idLong ?: 0 })
}

fun polyUserReference(userId: Long): BackedReference<PolyUser?, Long> {
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? {
Expand All @@ -323,22 +323,22 @@ class PolyBot(val config: PolyConfig, builder: InlineJDABuilder) {

fun memberReference(guildId: Long, userId: Long): BackedReference<Member?, Pair<Long, Long>> {
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<PolyMember?, Pair<Long, Long>> {
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<Emote?, Long> {
Expand Down
190 changes: 189 additions & 1 deletion src/main/kotlin/ca/solostudios/polybot/commands/BotAdminCommands.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright (c) 2021-2021 solonovamax <[email protected]>
*
* The file BotAdminCommands.kt is part of PolyhedralBot
* Last modified on 25-10-2021 05:05 p.m.
* Last modified on 28-10-2021 07:58 p.m.
*
* MIT License
*
Expand Down Expand Up @@ -37,16 +37,44 @@ 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.ByteArrayOutputStream
import java.io.PrintStream
import java.time.Duration
import javax.script.ScriptEngineManager
import kotlinx.coroutines.TimeoutCancellationException
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
@PolyCategory(BOT_ADMIN_CATEGORY)
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")
Expand Down Expand Up @@ -77,4 +105,164 @@ 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)

try {
val result = withTimeout(Duration.ofSeconds(10)) {
val (err, out, result) = captureOutAndErr {
evalString<KotlinAdminScript>(code) {
providedProperties(
"user" to author,
"message" to message,
"logger" to logger,
"bot" to bot,
"jda" to bot.jda,
)
}
}

// result as ResultWithDiagnostics.Success


return@withTimeout buildString {
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("```")
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) {

}
}

/**
* 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 <reified T : Any> evalString(
source: String,
noinline configure: ScriptEvaluationConfiguration.Builder.() -> Unit
): ResultWithDiagnostics<EvaluationResult> {
val actualConfiguration = createJvmCompilationConfigurationFromTemplate<T>()
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<PolyBot>())

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.*",
)

}
}

/**
* 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<EvaluationResult>): Triple<String, String, ResultWithDiagnostics<EvaluationResult>> {
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<EvaluationResult>
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)
}
}