commit f167c995251e443a2c36992c6434b5f0dc4caedd Author: 筱傑 <840465812@qq.com> Date: Sun Aug 1 14:53:18 2021 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3542e01 --- /dev/null +++ b/.gitignore @@ -0,0 +1,121 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Cache of project +.gradletasknamecache + +**/build/ + +# Common working directory +run/ + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Local Test Launch point +src/test/kotlin/RunTerminal.kt diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..f744107 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + val kotlinVersion = "1.5.10" + kotlin("jvm") version kotlinVersion + kotlin("plugin.serialization") version kotlinVersion + + id("net.mamoe.mirai-console") version "2.6.7" +} + +group = "me.jie65535" +version = "0.1" + +repositories { + maven("https://maven.aliyun.com/repository/public") + mavenCentral() +} + +dependencies{ + implementation("org.jsoup:jsoup:1.13.1") + implementation("com.beust:klaxon:5.5") +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..29e08e8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..da9702f --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..e697d70 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "jcc" \ No newline at end of file diff --git a/src/main/kotlin/GlotAPI.kt b/src/main/kotlin/GlotAPI.kt new file mode 100644 index 0000000..d711424 --- /dev/null +++ b/src/main/kotlin/GlotAPI.kt @@ -0,0 +1,183 @@ +import com.beust.klaxon.Json +import com.beust.klaxon.Klaxon +import java.security.InvalidKeyException + +/** + * # glot.io api 封装 + * [https://glot.io/] 是一个开源的在线运行代码的网站 + * 它提供了免费API供外界使用,API文档见 [https://github.com/glotcode/glot/blob/master/api_docs] + * 本类是对该API文档的封装 + * 通过 [listLanguages] 获取支持在线运行的编程语言列表 + * ~~通过 [getVersion] 获取对应语言的最新版本请求地址~~ + * 通过 [getSupport] 判断指定编程语言是否支持 + * 通过 [getFilename] 来获取指定编程语言的文件名(runCode需要) + * 以上接口均有缓存,仅首次获取不同数据时会发起请求。因此,首次运行可能较慢。 + * 通过 [runCode] 运行代码 + * 若觉得原版 [runCode] 使用复杂,还可以使用另一个更简单的重载 [runCode] + * @suppress 注意,若传入不支持的语言,或者格式不正确,将无法正确识别 + * @author jie65535@github + */ +object GlotAPI { + private const val URL = "https://glot.io/" + private const val URL_NEW = "https://glot.io/new/" + private const val URL_API = URL + "api/" + private const val URL_LIST_LANGUAGES = URL_API + "run" + // 运行代码需要api token,这是的我帐号申请的,可以在[https://glot.io/auth/page/simple/register]注册帐号 + private const val API_TOKEN = "074ef4a7-7a94-47f2-9891-85511ef1fb52" + + data class Language(val name: String, val url: String) + data class CodeFile(val name: String, val content: String) + + data class RunCodeRequest(@Json(serializeNull = false) val stdin: String?, + @Json(serializeNull = false) val command: String?, + val files: List) + data class RunResult(val stdout: String, val stderr: String, val error: String) + + private var languages: List? = null + private val filenames: MutableMap = mutableMapOf() + // val fileExtensions: Map = mapOf("assembly" to "asm", "ats" to "dats", "bash" to "sh", "c" to "c", "clojure" to "clj", "cobol" to "cob", "coffeescript" to "coffee", "cpp" to "cpp", "crystal" to "cr", "csharp" to "cs", "d" to "d", "elixir" to "ex", "elm" to "elm", "erlang" to "erl", "fsharp" to "fs", "go" to "go", "groovy" to "groovy", "haskell" to "hs", "idris" to "idr", "java" to "java", "javascript" to "js", "julia" to "jl", "kotlin" to "kt", "lua" to "lua", "mercury" to "m", "nim" to "nim", "nix" to "nix", "ocaml" to "ml", "perl" to "pl", "php" to "php", "python" to "py", "raku" to "raku", "ruby" to "rb", "rust" to "rs", "scala" to "scala", "swift" to "swift", "typescript" to "ts", "plaintext" to "txt", ) + + /** + * 列出所有支持在线运行的语言(缓存) + * @return 返回支持的语言列表 示例: + * ```json + * [ + * { + * "name": "assembly", + * "url": "https://glot.io/api/run/assembly" + * }, + * { + * "name": "c", + * "url": "https://glot.io/api/run/c" + * } + * ] + * ``` + */ + fun listLanguages(): List { + if (languages == null) { + languages = Klaxon().parseArray(HttpUtil.get(URL_LIST_LANGUAGES)) ?: throw Exception("未获取到任何数据") + } + return languages!! + } + + /** + * 检查是否支持该语言在线编译 + * @param language 编程语言名字(忽略大小写) + * @return 是否支持 + */ + fun checkSupport(language: String): Boolean = listLanguages().any { it.name.equals(language, true) } + + /** + * 获取编程语言请求地址,若不支持将会抛出异常 + * @param language 编程语言名字(忽略大小写) + * @return 返回语言请求地址 + * @exception InvalidKeyException 不支持的语言 + */ + fun getSupport(language: String): Language = + listLanguages().find { it.name.equals(language, true) } ?: throw InvalidKeyException("不支持的语言") + + /** + * 获取指定编程语言文件名(缓存) + * @exception Exception 若不支持或无法获取,将抛出异常 + * @return 建议文件名(通常是main.c之类的,java比较特殊,是Main.java,所以需要请求,避免硬编码) + */ + fun getFilename(language: String): String { + val lang = getSupport(language) + if (filenames.containsKey(lang.name)) + return filenames[lang.name]!! + val document = HttpUtil.getDocument(URL_NEW + lang.name) + val filename = HttpUtil.documentSelect(document, ".filename").firstOrNull()?.text() ?: throw Exception("无法获取文件名") + filenames[lang.name] = filename + return filename + } + + /** + * # 运行代码 + * + * ## 简单示例: + * 请求 + * ```json + * { + * "files": [ + * { + * "name": "main.py", + * "content": "print(42)" + * } + * ] + * } + * ``` + * 响应 + * ```json + * { + * "stdout": "42\n", + * "stderr": "", + * "error": "" + * } + * ``` + * + * ## 读输入流示例: + * 请求 + * ```json + * { + * "stdin": "42", + * "files": [ + * { + * "name": "main.py", + * "content": "print(input('Number from stdin: '))" + * } + * ] + * } + * ``` + * 响应 + * ```json + * { + * "stdout": "Number from stdin: 42\n", + * "stderr": "", + * "error": "" + * } + * ``` + * + * ## 自定义运行命令示例: + * 请求 + * ```json + * { + * "command": "bash main.sh 42", + * "files": [ + * { + * "name": "main.sh", + * "content": "echo Number from arg: $1" + * } + * ] + * } + * ``` + * 响应 + * ```json + * { + * "stdout": "Number from arg: 42\n", + * "stderr": "", + * "error": "" + * } + * ``` + * @param language 要运行的编程语言 + * @param requestData 运行代码的请求数据 + * @return 返回运行结果 若执行了死循环或其它阻塞代码, + * 导致程序无法在限定时间内返回,将会报告超时异常 + */ + fun runCode(language: Language, requestData: RunCodeRequest): RunResult { + val response = HttpUtil.post(language.url + "/latest", Klaxon().toJsonString(requestData), mapOf("Authorization" to API_TOKEN)) + return Klaxon().parse(response) ?: throw Exception("未获取到任何数据") + } + + + /** + * # 运行代码 + * 更简单的运行代码重载 + * @param language 编程语言 + * @param code 程序代码 + * @param stdin 可选的输入缓冲区数据 + * @return 返回运行结果 若执行了死循环或其它阻塞代码, + * 导致程序无法在限定时间内返回,将会报告超时异常 + */ + fun runCode(language: String, code: String, stdin: String? = null): RunResult = + runCode(getSupport(language), RunCodeRequest(stdin, null, listOf(CodeFile(getFilename(language), code)))) +} \ No newline at end of file diff --git a/src/main/kotlin/HttpUtil.kt b/src/main/kotlin/HttpUtil.kt new file mode 100644 index 0000000..e415c4a --- /dev/null +++ b/src/main/kotlin/HttpUtil.kt @@ -0,0 +1,82 @@ +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.select.Elements +import java.io.File +import java.util.concurrent.TimeUnit + + +object HttpUtil { + private val JSON: MediaType? = "application/json; charset=utf-8".toMediaTypeOrNull() + private val okHttpClient: OkHttpClient by lazy { + OkHttpClient.Builder() + .readTimeout(10, TimeUnit.SECONDS) + .build() + } + + /** + * ### 下载图片 + */ + fun downloadImage(url: String, file: File): ByteArray { + val request = Request.Builder().url(url).build() + val imageByte = okHttpClient.newCall(request).execute().body!!.bytes() + val fileParent = file.parentFile + if (!fileParent.exists()) fileParent.mkdirs() + file.writeBytes(imageByte) + return imageByte + } + + /** + * ### 发送GET请求 + */ + fun get(url: String): String { + val request = Request.Builder().url(url).build() + return okHttpClient.newCall(request).execute().body!!.string() + } + + /** + * ### 发送带Json参数的POST请求 + */ + fun post(url: String, json: String): String { + val requestBody = json.toRequestBody(JSON) + val request = Request.Builder().url(url).post(requestBody).build() + return okHttpClient.newCall(request).execute().body!!.string() + } + /** + * ### 发送带Header与Json参数的POST请求 + */ + fun post(url: String, json: String, params: Map): String { + val requestBody = json.toRequestBody(JSON) + val requestBuilder = Request.Builder().url(url) + for (param in params) + requestBuilder.addHeader(param.key, param.value) + val request = requestBuilder.post(requestBody).build() + return okHttpClient.newCall(request).execute().body!!.string() + } + + /** + * ### 解析网页响应 + */ + fun parseBody(responseBody: String): Document { + return Jsoup.parse(responseBody) + } + + /** + * ### 发送GET请求并解析 + */ + fun getDocument(url: String): Document { + return parseBody(get(url)) + } + + /** + * ### Document 元素选择 + */ + fun documentSelect(document: Document, cssQuery: String): Elements { + return document.select(cssQuery) + } +} \ No newline at end of file diff --git a/src/main/kotlin/JCC.kt b/src/main/kotlin/JCC.kt new file mode 100644 index 0000000..b04f6cd --- /dev/null +++ b/src/main/kotlin/JCC.kt @@ -0,0 +1,99 @@ +import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register +import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregister +import net.mamoe.mirai.console.command.parse.CommandCallParser +import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription +import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin +import net.mamoe.mirai.contact.Group +import net.mamoe.mirai.contact.isBotMuted +import net.mamoe.mirai.event.globalEventChannel +import net.mamoe.mirai.event.subscribeMessages +import net.mamoe.mirai.message.data.At +import net.mamoe.mirai.message.data.MessageChainBuilder +import net.mamoe.mirai.utils.info +import okhttp3.internal.indexOfNonWhitespace + +object JCC : KotlinPlugin( + JvmPluginDescription( + id = "me.jie65535.jcc", + name = "J Compiler Collection", + version = "0.1", + ) { + author("jie65535") + info("""在线编译器集合""") + } +) { + const val CMD_PREFIX = "jcc" + + override fun onEnable() { + logger.info { "Plugin loaded" } + JccCommand.register() + + + globalEventChannel().subscribeMessages { + startsWith(CMD_PREFIX, false) reply { + if (subject is Group && (subject as Group).isBotMuted) + return@reply null + val msg = it.substring(CMD_PREFIX.length).trim() + if (msg.isNotEmpty()) { + val index = msg.indexOfFirst(Char::isWhitespace) + if (index >= 0) + { + val language = msg.substring(0, index) + val code = msg.substring(index).trim() + if (!GlotAPI.checkSupport(language)) + return@reply "不支持这种编程语言\n/jcc list #列出所有支持的编程语言" + if (code.isEmpty()) + return@reply "请输入要运行的代码" + try { + // subject.sendMessage("正在执行,请稍等...") + logger.info("请求执行代码") + val result = GlotAPI.runCode(language, code) + val builder = MessageChainBuilder() + var c = 0 + if (result.stdout.isNotEmpty()) c++ + if (result.stderr.isNotEmpty()) c++ + if (result.error.isNotEmpty()) c++ + val title = c >= 2 + var msgLength = 0 + if (subject is Group) { + builder.add(At(sender)) + builder.add("\n") + } + + if (result.error.isNotEmpty()) { + builder.add("error:\n") + builder.add(result.error) + msgLength += result.error.length + 7 + } + if (result.stdout.isNotEmpty()) { + if (title) builder.add("\nstdout:\n") + builder.add(result.stdout) + msgLength += result.stdout.length + } + if (result.stderr.isNotEmpty()) { + if (title) builder.add("\nstderr:\n") + builder.add(result.stderr) + msgLength += result.stderr.length + } + val messageChain = builder.build() + if (msgLength > 500) { + val messageContent = messageChain.contentToString() + return@reply "消息内容过长,已贴到Pastebin:\n" + UbuntuPastebinHelper.paste(messageContent) + } else { + return@reply messageChain + } + } catch (e: Exception) { + logger.warning(e) + return@reply "执行失败\n原因:${e.message}" + } + } + } + return@reply "请输入正确的命令!例如:\n$CMD_PREFIX python print(\"Hello world\")" + } + } + } + + override fun onDisable() { + JccCommand.unregister() + } +} \ No newline at end of file diff --git a/src/main/kotlin/JccCommand.kt b/src/main/kotlin/JccCommand.kt new file mode 100644 index 0000000..04ec356 --- /dev/null +++ b/src/main/kotlin/JccCommand.kt @@ -0,0 +1,24 @@ +import net.mamoe.mirai.console.command.CommandSender +import net.mamoe.mirai.console.command.CompositeCommand + +object JccCommand : CompositeCommand( + JCC, "jcc", + description = "在线编译器集合" +) { + @SubCommand + @Description("列出所有支持的编程语言") + suspend fun CommandSender.list() { + try { + sendMessage(GlotAPI.listLanguages().joinToString { it.name }) + } catch (e: Exception) { + sendMessage("执行失败\n${e.message}") + JCC.logger.warning(e) + } + } + + @SubCommand + @Description("帮助") + suspend fun CommandSender.help() { + sendMessage("直接调用jcc即可运行代码\n例如:jcc python print(\"Hello world\")\n其它指令:\n$usage") + } +} \ No newline at end of file diff --git a/src/main/kotlin/UbuntuPastebinHelper.kt b/src/main/kotlin/UbuntuPastebinHelper.kt new file mode 100644 index 0000000..90acf7f --- /dev/null +++ b/src/main/kotlin/UbuntuPastebinHelper.kt @@ -0,0 +1,82 @@ +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request +import org.jsoup.Jsoup +import java.io.IOException + +/** + * # ubuntu pastebin 帮助类 + * [https://paste.ubuntu.com] 是一个用于共享文档的网站 + * 由于pastebin本身没有对外提供api,所以本类使用解析html的方式实现 + * 通过 [getSyntaxList] 获取支持的语法列表(缓存) + * 通过 [get] 获取链接的内容 + * 通过 [paste] 来粘贴内容 + * + * @author jie65535@github + */ +object UbuntuPastebinHelper { + private const val URL = "https://paste.ubuntu.com" + private var syntaxList: Map? = null + /** + * 获取支持的语法列表(缓存) + * @return 返回一个map,其中key是给人看的,value是作为参数传递的 + */ + fun getSyntaxList(): Map { + if (syntaxList != null) + return syntaxList!! + val document = HttpUtil.getDocument(URL) + val element = HttpUtil.documentSelect(document, "select#id_syntax > option") + val map = mutableMapOf() + for (opt in element) + map[opt.text()] = opt.`val`() + syntaxList = map + return map + } + + /** + * 获取内容 + * @param url pastebin地址,如:https://paste.ubuntu.com/p/nmn8yKMtND/ + * @return 返回链接中贴的内容 + */ + fun get(url: String): String { + if (url.isEmpty() || !url.startsWith("https://paste.ubuntu.com/p/")) + throw Exception("非法的url") + val document = HttpUtil.getDocument(url) + return HttpUtil.documentSelect(document, ".paste > pre").text() + } + + /** + * 上传内容 + * @param content 上传内容 + * @param syntax 语法(例如c/cpp) 可以通过getSyntaxList得到所有支持的语法,传入pair的value 默认值:text + * @param poster 主题文本(最大长度30字符) 默认值:"temp" + * @param expiration 过期时间((empty)/day/week/month/year) 默认值:"day" + * @return 返回访问地址,如:https://paste.ubuntu.com/p/nmn8yKMtND/ + */ + fun paste(content: String, syntax: String = "text", poster: String = "temp", expiration: String = "day"): String? { + if (poster.length > 30) + throw Exception("poster length too long!") + if (content.isEmpty()) + throw Exception("content cannot be empty!") + val okHttpClient = OkHttpClient().newBuilder() + .followRedirects(false) + .build() + val requestBody = FormBody.Builder() + .add("poster", poster) + .add("syntax", syntax) + .add("expiration", expiration) + .add("content", content) + .build() + val request = Request.Builder() + .url(URL) + .post(requestBody) + .build() + val response = okHttpClient.newCall(request).execute() + if (response.code == 200) + throw Exception("请求已经成功,但无法执行动作,请检查参数") + return if (response.code == 302) + URL + response.header("Location") + else + throw IOException("请求失败,请检查网络或参数") + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin b/src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin new file mode 100644 index 0000000..d44a98c --- /dev/null +++ b/src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin @@ -0,0 +1 @@ +JCC \ No newline at end of file