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 androidx.core.content.ContextCompat.startActivity
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.request.header import io.ktor.client.request.header
import io.ktor.client.request.parameter
import io.ktor.client.request.url import io.ktor.client.request.url
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@ -58,7 +59,7 @@ object DashboardInitializer {
url("http://127.0.0.1:$servicePort/get_account_info") url("http://127.0.0.1:$servicePort/get_account_info")
val token = ShamrockConfig.getToken(context) val token = ShamrockConfig.getToken(context)
if (token.isNotBlank()) { if (token.isNotBlank()) {
header("Authorization", "Bearer $token") parameter("token", token)
} }
}.let { }.let {
if (it.status == HttpStatusCode.OK) { 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 package moe.fuqiuluo.shamrock.remote.api
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call import io.ktor.server.application.call
import io.ktor.server.request.httpVersion import io.ktor.server.request.httpVersion
import io.ktor.server.response.respondText 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.get
import io.ktor.server.routing.post import io.ktor.server.routing.post
import io.ktor.server.routing.route import io.ktor.server.routing.route
import io.ktor.util.pipeline.PipelineContext
import kotlinx.serialization.Contextual import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable 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.HTTPServer
import moe.fuqiuluo.shamrock.remote.action.ActionManager import moe.fuqiuluo.shamrock.remote.action.ActionManager
import moe.fuqiuluo.shamrock.remote.action.ActionSession 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.IndexData
import moe.fuqiuluo.shamrock.remote.entries.Status import moe.fuqiuluo.shamrock.remote.entries.Status
import moe.fuqiuluo.shamrock.tools.EmptyJsonObject 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.fetchOrNull
import moe.fuqiuluo.shamrock.tools.fetchOrThrow import moe.fuqiuluo.shamrock.tools.fetchOrThrow
import moe.fuqiuluo.shamrock.tools.fetchPostJsonElement import moe.fuqiuluo.shamrock.tools.fetchPostJsonElement
import moe.fuqiuluo.shamrock.tools.fetchPostJsonElementOrNull
import moe.fuqiuluo.shamrock.tools.fetchPostJsonObject import moe.fuqiuluo.shamrock.tools.fetchPostJsonObject
import moe.fuqiuluo.shamrock.tools.fetchPostJsonObjectOrNull import moe.fuqiuluo.shamrock.tools.fetchPostJsonObjectOrNull
import moe.fuqiuluo.shamrock.tools.isJsonArray import moe.fuqiuluo.shamrock.tools.isJsonArray
import moe.fuqiuluo.shamrock.tools.isJsonData
import moe.fuqiuluo.shamrock.tools.isJsonObject import moe.fuqiuluo.shamrock.tools.isJsonObject
import moe.fuqiuluo.shamrock.tools.isJsonString import moe.fuqiuluo.shamrock.tools.isJsonString
import moe.fuqiuluo.shamrock.tools.json import moe.fuqiuluo.shamrock.tools.json
@ -39,6 +49,31 @@ data class OldApiResult<T>(
val data: T? = null 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() { fun Routing.echoVersion() {
route("/") { route("/") {
get { get {
@ -49,6 +84,15 @@ fun Routing.echoVersion() {
) )
} }
post { 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 action = fetchOrThrow("action")
val echo = if (isJsonObject("echo") || isJsonArray("echo")) { val echo = if (isJsonObject("echo") || isJsonArray("echo")) {
fetchPostJsonElement("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.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.MsgSvc import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import io.ktor.client.statement.bodyAsText 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.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -14,6 +17,7 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import moe.fuqiuluo.qqinterface.servlet.msg.* import moe.fuqiuluo.qqinterface.servlet.msg.*
import moe.fuqiuluo.shamrock.remote.service.api.HttpTransmitServlet 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.tools.*
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter 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 import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
internal object HttpService: HttpTransmitServlet() { internal object HttpService: HttpTransmitServlet() {
@ -66,65 +75,86 @@ internal object HttpService: HttpTransmitServlet() {
private suspend fun handleQuicklyReply(record: MsgRecord, msgHash: Int, jsonText: String) { private suspend fun handleQuicklyReply(record: MsgRecord, msgHash: Int, jsonText: String) {
try { try {
val data = Json.parseToJsonElement(jsonText).asJsonObject val data = Json.parseToJsonElement(jsonText)
if (data.containsKey("reply")) {
LogCenter.log({ "quickly reply successfully" }, Level.DEBUG) if (data is JsonObject) {
val autoEscape = data["auto_escape"].asBooleanOrNull ?: false if (data.containsKey("reply")) {
val atSender = data["at_sender"].asBooleanOrNull ?: false LogCenter.log({ "quickly reply successfully" }, Level.DEBUG)
val autoReply = data["auto_reply"].asBooleanOrNull ?: true val autoEscape = data["auto_escape"].asBooleanOrNull ?: false
val message = data["reply"] val atSender = data["at_sender"].asBooleanOrNull ?: false
if (message is JsonPrimitive) { val autoReply = data["auto_reply"].asBooleanOrNull ?: true
if (autoEscape) { val message = data["reply"]
val msgList = mutableSetOf<JsonElement>() if (message is JsonPrimitive) {
msgList.add(mapOf( if (autoEscape) {
"type" to "text", val msgList = mutableSetOf<JsonElement>()
"data" to mapOf( msgList.add(mapOf(
"text" to message.asString "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( quicklyReply(
record, record,
msgList.jsonArray, message,
msgHash,
atSender,
autoReply
)
} else {
val messageArray = MessageHelper.decodeCQCode(message.asString)
quicklyReply(
record,
messageArray,
msgHash, msgHash,
atSender, atSender,
autoReply autoReply
) )
} }
} else if (message is JsonArray) {
quicklyReply(
record,
message,
msgHash,
atSender,
autoReply
)
} }
} if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("delete") && data["delete"].asBoolean) {
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("delete") && data["delete"].asBoolean) { MsgSvc.recallMsg(msgHash)
MsgSvc.recallMsg(msgHash) }
} if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("kick") && data["kick"].asBoolean) {
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("kick") && data["kick"].asBoolean) { GroupSvc.kickMember(record.peerUin, false, record.senderUin)
GroupSvc.kickMember(record.peerUin, false, record.senderUin) }
} if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("ban") && data["ban"].asBoolean) {
if (MsgConstant.KCHATTYPEGROUP == record.chatType && data.containsKey("ban") && data["ban"].asBoolean) { val banTime = data["ban_duration"].asIntOrNull ?: (30 * 60)
val banTime = data["ban_duration"].asIntOrNull ?: (30 * 60) if (banTime <= 0) return
if (banTime <= 0) return GroupSvc.banMember(record.peerUin, record.senderUin, banTime)
GroupSvc.banMember(record.peerUin, record.senderUin, banTime) }
} else if (data is JsonArray) {
data.forEach {
handleQuicklyActions(it.asJsonObject)
}
} }
} catch (e: Throwable) { } catch (e: Throwable) {
LogCenter.log("处理快速操作错误: $e", Level.WARN) 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( private suspend fun quicklyReply(
record: MsgRecord, record: MsgRecord,
message: JsonArray, message: JsonArray,

View File

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

View File

@ -37,6 +37,8 @@ annotation class ShamrockDsl
private val keyIsJson = AttributeKey<Boolean>("isJson") private val keyIsJson = AttributeKey<Boolean>("isJson")
private val keyJsonObject = AttributeKey<JsonObject>("paramsJson") private val keyJsonObject = AttributeKey<JsonObject>("paramsJson")
private val keyJsonArray = AttributeKey<JsonArray>("paramsJsonArray")
private val keyJsonElement = AttributeKey<JsonElement>("paramsJsonElement")
private val keyParts = AttributeKey<Parameters>("paramsParts") private val keyParts = AttributeKey<Parameters>("paramsParts")
suspend fun ApplicationCall.fetch(key: String): String { 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 { 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 { suspend fun PipelineContext<Unit, ApplicationCall>.isJsonString(key: String): Boolean {
@ -245,12 +249,33 @@ suspend fun PipelineContext<Unit, ApplicationCall>.fetchPostJsonObjectOrNull(key
call.attributes[keyJsonObject] call.attributes[keyJsonObject]
} else { } else {
Json.parseToJsonElement(call.receiveText()).jsonObject.also { Json.parseToJsonElement(call.receiveText()).jsonObject.also {
call.attributes.put(keyIsJson, true)
call.attributes.put(keyJsonObject, it) call.attributes.put(keyJsonObject, it)
} }
} }
return data[key].asJsonObjectOrNull 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 { suspend fun PipelineContext<Unit, ApplicationCall>.fetchPostJsonArray(key: String): JsonArray {
val data = if (call.attributes.contains(keyJsonObject)) { val data = if (call.attributes.contains(keyJsonObject)) {