3 Commits

Author SHA1 Message Date
35c82fcc51 Shamrock: fix #169 2023-12-21 00:55:56 +08:00
89a4912ed7 Shamrock: fix #166 2023-12-20 18:58:17 +08:00
aeabc66067 Shamrock: 兼容性正反向HTTP调整 2023-12-20 18:52:24 +08:00
6 changed files with 160 additions and 47 deletions

View File

@ -8,6 +8,7 @@ import android.os.Bundle
import androidx.core.content.ContextCompat.startActivity
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.parameter
import io.ktor.client.request.url
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
@ -58,7 +59,7 @@ object DashboardInitializer {
url("http://127.0.0.1:$servicePort/get_account_info")
val token = ShamrockConfig.getToken(context)
if (token.isNotBlank()) {
header("Authorization", "Bearer $token")
parameter("token", token)
}
}.let {
if (it.status == HttpStatusCode.OK) {

View File

@ -0,0 +1,8 @@
package moe.fuqiuluo.qqinterface.servlet
/**
* QQ收藏相关接口
*/
internal object QFavSvc: BaseSvc() {
}

View File

@ -1,6 +1,7 @@
package moe.fuqiuluo.shamrock.remote.api
import io.ktor.http.ContentType
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.request.httpVersion
import io.ktor.server.response.respondText
@ -8,8 +9,12 @@ import io.ktor.server.routing.Routing
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.route
import io.ktor.util.pipeline.PipelineContext
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import moe.fuqiuluo.shamrock.remote.HTTPServer
import moe.fuqiuluo.shamrock.remote.action.ActionManager
import moe.fuqiuluo.shamrock.remote.action.ActionSession
@ -18,12 +23,17 @@ import moe.fuqiuluo.shamrock.remote.entries.EmptyObject
import moe.fuqiuluo.shamrock.remote.entries.IndexData
import moe.fuqiuluo.shamrock.remote.entries.Status
import moe.fuqiuluo.shamrock.tools.EmptyJsonObject
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.tools.asJsonObjectOrNull
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.fetchOrNull
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
import moe.fuqiuluo.shamrock.tools.fetchPostJsonElement
import moe.fuqiuluo.shamrock.tools.fetchPostJsonElementOrNull
import moe.fuqiuluo.shamrock.tools.fetchPostJsonObject
import moe.fuqiuluo.shamrock.tools.fetchPostJsonObjectOrNull
import moe.fuqiuluo.shamrock.tools.isJsonArray
import moe.fuqiuluo.shamrock.tools.isJsonData
import moe.fuqiuluo.shamrock.tools.isJsonObject
import moe.fuqiuluo.shamrock.tools.isJsonString
import moe.fuqiuluo.shamrock.tools.json
@ -39,6 +49,31 @@ data class OldApiResult<T>(
val data: T? = null
)
suspend fun PipelineContext<Unit, ApplicationCall>.handleAsJsonObject(data: JsonObject) {
val action = data["action"].asString
val echo = data["echo"] ?: EmptyJsonString
call.attributes.put(ECHO_KEY, echo)
val params = data["params"].asJsonObjectOrNull ?: EmptyJsonObject
val handler = ActionManager[action]
if (handler == null) {
respond(false, Status.UnsupportedAction, EmptyObject, "不支持的Action", echo = echo)
} else {
call.respondText(handler.handle(ActionSession(params, echo)), ContentType.Application.Json)
}
}
suspend fun PipelineContext<Unit, ApplicationCall>.handleAsJsonArray(data: JsonArray) {
data.forEach {
when (it) {
is JsonArray -> handleAsJsonArray(it)
is JsonObject -> handleAsJsonObject(it)
else -> handleAsJsonObject(it.jsonObject)
}
}
}
fun Routing.echoVersion() {
route("/") {
get {
@ -49,6 +84,15 @@ fun Routing.echoVersion() {
)
}
post {
fetchPostJsonElementOrNull()?.let {
if (it is JsonArray) {
handleAsJsonArray(it)
return@post
} else if (it is JsonObject) {
handleAsJsonObject(it)
return@post
}
}
val action = fetchOrThrow("action")
val echo = if (isJsonObject("echo") || isJsonArray("echo")) {
fetchPostJsonElement("echo")

View File

@ -7,6 +7,9 @@ import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.server.application.call
import io.ktor.server.response.respondText
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
@ -14,6 +17,7 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import moe.fuqiuluo.qqinterface.servlet.msg.*
import moe.fuqiuluo.shamrock.remote.service.api.HttpTransmitServlet
@ -21,6 +25,11 @@ import moe.fuqiuluo.shamrock.remote.service.data.push.*
import moe.fuqiuluo.shamrock.tools.*
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.remote.action.ActionManager
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.config.ECHO_KEY
import moe.fuqiuluo.shamrock.remote.entries.EmptyObject
import moe.fuqiuluo.shamrock.remote.entries.Status
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
internal object HttpService: HttpTransmitServlet() {
@ -66,65 +75,86 @@ internal object HttpService: HttpTransmitServlet() {
private suspend fun handleQuicklyReply(record: MsgRecord, msgHash: Int, jsonText: String) {
try {
val data = Json.parseToJsonElement(jsonText).asJsonObject
if (data.containsKey("reply")) {
LogCenter.log({ "quickly reply successfully" }, Level.DEBUG)
val autoEscape = data["auto_escape"].asBooleanOrNull ?: false
val atSender = data["at_sender"].asBooleanOrNull ?: false
val autoReply = data["auto_reply"].asBooleanOrNull ?: true
val message = data["reply"]
if (message is JsonPrimitive) {
if (autoEscape) {
val msgList = mutableSetOf<JsonElement>()
msgList.add(mapOf(
"type" to "text",
"data" to mapOf(
"text" to message.asString
val data = Json.parseToJsonElement(jsonText)
if (data is JsonObject) {
if (data.containsKey("reply")) {
LogCenter.log({ "quickly reply successfully" }, Level.DEBUG)
val autoEscape = data["auto_escape"].asBooleanOrNull ?: false
val atSender = data["at_sender"].asBooleanOrNull ?: false
val autoReply = data["auto_reply"].asBooleanOrNull ?: true
val message = data["reply"]
if (message is JsonPrimitive) {
if (autoEscape) {
val msgList = mutableSetOf<JsonElement>()
msgList.add(mapOf(
"type" to "text",
"data" to mapOf(
"text" to message.asString
)
).json)
quicklyReply(
record,
msgList.jsonArray,
msgHash,
atSender,
autoReply
)
).json)
} else {
val messageArray = MessageHelper.decodeCQCode(message.asString)
quicklyReply(
record,
messageArray,
msgHash,
atSender,
autoReply
)
}
} else if (message is JsonArray) {
quicklyReply(
record,
msgList.jsonArray,
msgHash,
atSender,
autoReply
)
} else {
val messageArray = MessageHelper.decodeCQCode(message.asString)
quicklyReply(
record,
messageArray,
message,
msgHash,
atSender,
autoReply
)
}
} else if (message is JsonArray) {
quicklyReply(
record,
message,
msgHash,
atSender,
autoReply
)
}
}
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("delete") && data["delete"].asBoolean) {
MsgSvc.recallMsg(msgHash)
}
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("kick") && data["kick"].asBoolean) {
GroupSvc.kickMember(record.peerUin, false, record.senderUin)
}
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("ban") && data["ban"].asBoolean) {
val banTime = data["ban_duration"].asIntOrNull ?: (30 * 60)
if (banTime <= 0) return
GroupSvc.banMember(record.peerUin, record.senderUin, banTime)
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("delete") && data["delete"].asBoolean) {
MsgSvc.recallMsg(msgHash)
}
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("kick") && data["kick"].asBoolean) {
GroupSvc.kickMember(record.peerUin, false, record.senderUin)
}
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("ban") && data["ban"].asBoolean) {
val banTime = data["ban_duration"].asIntOrNull ?: (30 * 60)
if (banTime <= 0) return
GroupSvc.banMember(record.peerUin, record.senderUin, banTime)
}
} else if (data is JsonArray) {
data.forEach {
handleQuicklyActions(it.asJsonObject)
}
}
} catch (e: Throwable) {
LogCenter.log("处理快速操作错误: $e", Level.WARN)
}
}
private suspend fun handleQuicklyActions(data: JsonObject) {
val action = data["action"].asString
val echo = data["echo"] ?: EmptyJsonString
val params = data["params"].asJsonObjectOrNull ?: EmptyJsonObject
val handler = ActionManager[action]
if (handler == null) {
LogCenter.log("HTTP快速操作不支持的Action: $action", Level.WARN)
} else {
handler.handle(ActionSession(params, echo))
}
}
private suspend fun quicklyReply(
record: MsgRecord,
message: JsonArray,

View File

@ -30,7 +30,7 @@ internal abstract class HttpTransmitServlet : BaseTransmitServlet {
if (!allowTransmit()) return null
try {
if (address.startsWith("http://") || address.startsWith("https://")) {
return GlobalClient.post(address) {
val response = GlobalClient.post(address) {
contentType(ContentType.Application.Json)
setBody(body)
@ -44,6 +44,11 @@ internal abstract class HttpTransmitServlet : BaseTransmitServlet {
header("X-Client-Role", "Universal")
header("Sec-WebSocket-Protocol", "11.Shamrock")
}
return if (response.status.value == 204) {
null
} else {
response
}
} else {
LogCenter.log("HTTP推送地址错误: ${address}", Level.ERROR)
}

View File

@ -37,6 +37,8 @@ annotation class ShamrockDsl
private val keyIsJson = AttributeKey<Boolean>("isJson")
private val keyJsonObject = AttributeKey<JsonObject>("paramsJson")
private val keyJsonArray = AttributeKey<JsonArray>("paramsJsonArray")
private val keyJsonElement = AttributeKey<JsonElement>("paramsJsonElement")
private val keyParts = AttributeKey<Parameters>("paramsParts")
suspend fun ApplicationCall.fetch(key: String): String {
@ -167,7 +169,9 @@ suspend fun PipelineContext<Unit, ApplicationCall>.fetchPostOrThrow(key: String)
}
fun PipelineContext<Unit, ApplicationCall>.isJsonData(): Boolean {
return ContentType.Application.Json == call.request.contentType() || (keyIsJson in call.attributes && call.attributes[keyIsJson])
return ContentType.Application.Json == call.request.contentType()
|| (keyIsJson in call.attributes && call.attributes[keyIsJson])
|| (keyJsonElement in call.attributes)
}
suspend fun PipelineContext<Unit, ApplicationCall>.isJsonString(key: String): Boolean {
@ -245,12 +249,33 @@ suspend fun PipelineContext<Unit, ApplicationCall>.fetchPostJsonObjectOrNull(key
call.attributes[keyJsonObject]
} else {
Json.parseToJsonElement(call.receiveText()).jsonObject.also {
call.attributes.put(keyIsJson, true)
call.attributes.put(keyJsonObject, it)
}
}
return data[key].asJsonObjectOrNull
}
suspend fun PipelineContext<Unit, ApplicationCall>.fetchPostJsonElementOrNull(): JsonElement? {
return runCatching {
if (call.attributes.contains(keyJsonObject)) {
call.attributes[keyJsonObject]
} else if (call.attributes.contains(keyJsonArray)) {
call.attributes[keyJsonArray]
} else if (call.attributes.contains(keyJsonElement)) {
call.attributes[keyJsonElement]
} else {
Json.parseToJsonElement(call.receiveText()).also {
call.attributes.put(keyJsonElement, it)
if (it is JsonObject) {
call.attributes.put(keyJsonObject, it)
} else if (it is JsonArray) {
call.attributes.put(keyJsonArray, it)
}
}
}
}.getOrNull()
}
suspend fun PipelineContext<Unit, ApplicationCall>.fetchPostJsonArray(key: String): JsonArray {
val data = if (call.attributes.contains(keyJsonObject)) {