/*
* 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()
}
}
}