/* * JGrasscutterCommand * Copyright (C) 2022 jie65535 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package top.jie65535.mirai.opencommand import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import top.jie65535.mirai.utils.UnsafeOkHttpClient @OptIn(ExperimentalSerializationApi::class) object OpenCommandApi { private val httpClient = UnsafeOkHttpClient.getUnsafeOkHttpClient().build() private val json = Json { ignoreUnknownKeys = true encodeDefaults = true } @Serializable private data class PingRequest( val action: String = "ping" ) @Serializable private data class SendCodeRequest( @SerialName("data") val uid: Int, val action: String = "sendCode" ) @Serializable private data class VerifyRequest( val token: String, @SerialName("data") val code: Int, val action: String = "verify" ) @Serializable private data class CommandRequest( val token: String, @SerialName("data") val command: String, val action: String = "command", ) @Serializable private data class StringResponse( val retcode: Int, val message: String, val data: String? ) class HttpException(val code: Int, message: String) : Exception(message) class InvokeException(val code: Int, message: String) : Exception(message) private val JSON = "application/json; charset=utf-8".toMediaType() /** * 向服务器发起opencommand请求 * @param host 服务器地址 * @param jsonBody 请求正文(Json) * @return 如果一切正常,返回相应数据,可能为空 * @exception InvokeException HTTP请求执行成功,但opencommand插件调用失败 * @exception HttpException HTTP请求完成,但HTTP响应错误代码(例如404、500等) * @exception IOException 如果由于取消、连接问题或超时而无法执行请求。由于网络可能在交换期间发生故障,因此远程服务器可能在故障之前接受了请求 */ private suspend fun doRequest(host: String, jsonBody: String): String? { val api = "$host/opencommand/api" val request = Request.Builder() .url(api) .post(jsonBody.toRequestBody(JSON)) .build() // JGrasscutterCommand.logger.debug("POST to $api Body $jsonBody") return withContext(Dispatchers.IO) { httpClient.newCall(request).execute().use { httpResponse -> val responseBody = httpResponse.body if (httpResponse.code == 200 && responseBody != null) { val response = json.decodeFromStream(responseBody.byteStream()) if (response.retcode != 200) { throw InvokeException(response.retcode, response.data ?: response.message) } else { return@use response.data } } else { throw HttpException(httpResponse.code, httpResponse.message) } } } } /** * 测试链接 * @param host 服务器地址 */ suspend fun ping(host: String) { doRequest(host, json.encodeToString(PingRequest())) } /** * 发送验证码 * @param host 服务器地址 * @param uid 目标玩家UID * @return 返回临时Token用于验证 */ suspend fun sendCode(host: String, uid: Int): String { return doRequest(host, json.encodeToString(SendCodeRequest(uid)))!! } /** * 验证身份,验证失败时将抛出异常,异常详情参考doRequest描述 * @param host 服务器地址 * @param token 发送验证码时返回的临时令牌 * @param code 用户输入的验证代码 * @see doRequest */ suspend fun verify(host: String, token: String, code: Int) { doRequest(host, json.encodeToString(VerifyRequest(token, code))) } /** * 运行命令,成功时返回命令执行结果,失败时抛出异常,异常详情参考doRequest描述 * @param host 服务器地址 * @param token 持久令牌 * @param command 命令行 * @return 命令执行结果 * @see doRequest */ suspend fun runCommand(host: String, token: String, command: String): String { val ret = doRequest(host, json.encodeToString(CommandRequest(token, command))) return if (ret.isNullOrEmpty()) "OK" else ret } /** * 运行命令,成功时返回命令执行结果,失败时抛出异常,异常详情参考doRequest描述 * 允许单次执行多条命令,用换行(\n)分隔 * @param host 服务器地址 * @param token 持久令牌 * @param rawCommands 命令行 * @return 命令执行结果 * @see doRequest */ suspend fun runCommands(host: String, token: String, rawCommands: String): String { // 去除首尾空白、命令前缀。使用api执行命令不需要前缀 val commands = rawCommands.splitToSequence('\n') .map { it.trim().trimStart('/').trimStart('!') } .toList() return if (commands.isEmpty()) throw IllegalArgumentException("命令不能为空!") else if (commands.size == 1) { val ret = doRequest(host, json.encodeToString(CommandRequest(token, commands[0]))) if (ret.isNullOrEmpty()) "OK" else ret } else { val msg = StringBuilder() var okCount = 0 for (cmd in commands) { val ret = doRequest(host, json.encodeToString(CommandRequest(token, cmd))) if (ret.isNullOrEmpty()) { if (okCount++ == 0) msg.append("OK") } else { if (okCount > 0) { if (okCount > 1) { msg.append('*').append(okCount) } msg.appendLine() okCount = 0 } msg.appendLine(ret) } } if (okCount > 1) // OK*n msg.append('*').append(okCount) else if (msg[msg.length-1] == '\n') // 移除额外的换行 msg.deleteCharAt(msg.length-1) msg.toString() } } }