From 1c65aab673f29b355c9f699f8b5a70b86d8b1924 Mon Sep 17 00:00:00 2001 From: Simplxs Date: Mon, 19 Feb 2024 04:13:18 +0800 Subject: [PATCH] =?UTF-8?q?refactor=20send=5Fforward=5Fmsg(=E6=9A=82?= =?UTF-8?q?=E6=9C=AA=E5=AE=8C=E6=88=90=20=E8=AF=B7=E5=8B=BF=E4=BD=BF?= =?UTF-8?q?=E7=94=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/protobuf/message/MessageBody.kt | 1 + .../java/protobuf/message/MessageContent.kt | 16 +- .../java/protobuf/message/MessageElement.kt | 1 + .../main/java/protobuf/message/MessageHead.kt | 7 + .../protobuf/message/element/FaceElement.kt | 9 + .../java/protobuf/message/longmsg/LongMsg.kt | 62 ++++ .../message/longmsg/LongMsgPayload.kt | 32 ++ .../fuqiuluo/qqinterface/servlet/MsgSvc.kt | 100 ++++-- .../fuqiuluo/qqinterface/servlet/TicketSvc.kt | 8 + .../servlet/msg/MessageElementMaker.kt | 96 ++++++ .../{MessageMaker.kt => MsgElementMaker.kt} | 246 ++++++++++----- .../msg/convert/MessageElemConverter.kt | 3 - .../fuqiuluo/shamrock/helper/MessageHelper.kt | 46 ++- .../action/handlers/SendForwardMessage.kt | 285 ++++++++++++------ .../remote/action/handlers/SendMessage.kt | 14 +- .../remote/service/listener/AioListener.kt | 3 +- .../java/moe/fuqiuluo/shamrock/tools/Json.kt | 18 +- 17 files changed, 714 insertions(+), 233 deletions(-) create mode 100644 protobuf/src/main/java/protobuf/message/element/FaceElement.kt create mode 100644 protobuf/src/main/java/protobuf/message/longmsg/LongMsg.kt create mode 100644 protobuf/src/main/java/protobuf/message/longmsg/LongMsgPayload.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageElementMaker.kt rename xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/{MessageMaker.kt => MsgElementMaker.kt} (85%) diff --git a/protobuf/src/main/java/protobuf/message/MessageBody.kt b/protobuf/src/main/java/protobuf/message/MessageBody.kt index ff29da6..398b2e5 100644 --- a/protobuf/src/main/java/protobuf/message/MessageBody.kt +++ b/protobuf/src/main/java/protobuf/message/MessageBody.kt @@ -7,4 +7,5 @@ import kotlinx.serialization.protobuf.ProtoNumber data class MessageBody( @ProtoNumber(1) val rich: RichMessage? = null, @ProtoNumber(2) val rawBuffer: ByteArray? = null, + @ProtoNumber(3) val MsgEncryptContent: ByteArray? = null ) \ No newline at end of file diff --git a/protobuf/src/main/java/protobuf/message/MessageContent.kt b/protobuf/src/main/java/protobuf/message/MessageContent.kt index 69600db..3bd4e3b 100644 --- a/protobuf/src/main/java/protobuf/message/MessageContent.kt +++ b/protobuf/src/main/java/protobuf/message/MessageContent.kt @@ -11,8 +11,20 @@ data class MessageContent( @ProtoNumber(5) val msgSeq: Long = Long.MIN_VALUE, @ProtoNumber(6) val msgTime: Long? = null, @ProtoNumber(7) val u2: Int? = null, + @ProtoNumber(8) val u6: Int? = null, + @ProtoNumber(9) val u7: Int? = null, @ProtoNumber(11) val u3: Long? = null, @ProtoNumber(12) val msgRandom: Long = Long.MIN_VALUE, @ProtoNumber(14) val u4: Long? = null, - @ProtoNumber(28) val u5: Long? = null, -) \ No newline at end of file + @ProtoNumber(15) val forwardHead: ForwardHead? = null, + @ProtoNumber(28) val u5: Long? = null +) + +@Serializable +data class ForwardHead( + @ProtoNumber(1) val u1: Int? = null, + @ProtoNumber(2) val u2: Int? = null, + @ProtoNumber(3) val u3: Int? = null, + @ProtoNumber(4) val u4: String? = null, + @ProtoNumber(5) val Avatar: String? = null +) diff --git a/protobuf/src/main/java/protobuf/message/MessageElement.kt b/protobuf/src/main/java/protobuf/message/MessageElement.kt index e29c76c..70f8437 100644 --- a/protobuf/src/main/java/protobuf/message/MessageElement.kt +++ b/protobuf/src/main/java/protobuf/message/MessageElement.kt @@ -7,6 +7,7 @@ import protobuf.message.element.* @Serializable data class MessageElement( @ProtoNumber(1) val text: TextElement? = null, + @ProtoNumber(2) val face: FaceElement? = null, @ProtoNumber(51) val json: JsonElement? = null, @ProtoNumber(53) val commElem: CommonElement? = null, ) \ No newline at end of file diff --git a/protobuf/src/main/java/protobuf/message/MessageHead.kt b/protobuf/src/main/java/protobuf/message/MessageHead.kt index 8b22916..2c0f70c 100644 --- a/protobuf/src/main/java/protobuf/message/MessageHead.kt +++ b/protobuf/src/main/java/protobuf/message/MessageHead.kt @@ -13,12 +13,19 @@ data class MessageHead( @ProtoNumber(4) val appId: Int = Int.MIN_VALUE, @ProtoNumber(5) val receiver: Long? = null, @ProtoNumber(6) val receiverUid: String? = null, + @ProtoNumber(7) val forward: MessageForward? = null, @ProtoNumber(8) val groupInfo: GroupInfo? = null, ) +@Serializable +data class MessageForward( + @ProtoNumber(6) val friendName: String? = null, +) + @Serializable data class GroupInfo( @ProtoNumber(1) val groupCode: ULong = ULong.MIN_VALUE, @ProtoNumber(4) val memberCard: String? = null, + @ProtoNumber(5) val u1: Int? = null, @ProtoNumber(7) val groupName: String? = null, ) \ No newline at end of file diff --git a/protobuf/src/main/java/protobuf/message/element/FaceElement.kt b/protobuf/src/main/java/protobuf/message/element/FaceElement.kt new file mode 100644 index 0000000..9899328 --- /dev/null +++ b/protobuf/src/main/java/protobuf/message/element/FaceElement.kt @@ -0,0 +1,9 @@ +package protobuf.message.element + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber + +@Serializable +data class FaceElement( + @ProtoNumber(1) val id: Int? = null, +) \ No newline at end of file diff --git a/protobuf/src/main/java/protobuf/message/longmsg/LongMsg.kt b/protobuf/src/main/java/protobuf/message/longmsg/LongMsg.kt new file mode 100644 index 0000000..d1098cb --- /dev/null +++ b/protobuf/src/main/java/protobuf/message/longmsg/LongMsg.kt @@ -0,0 +1,62 @@ +@file:OptIn(ExperimentalSerializationApi::class) +package protobuf.message.longmsg + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber + + +@Serializable +data class LongMsgSettings( + @ProtoNumber(1) val field1: Int? = null, + @ProtoNumber(2) val field2: Int? = null, + @ProtoNumber(3) val field3: Int? = null, + @ProtoNumber(4) val field4: Int? = null, +) + +@Serializable +data class LongMsgUid( + @ProtoNumber(2) val uid: String? = null, +) + +@Serializable +data class RecvLongMsgInfo( + @ProtoNumber(1) val uid: LongMsgUid? = null, + @ProtoNumber(2) val resId: String? = null, + @ProtoNumber(3) val acquire: Boolean? = null, +) + +@Serializable +data class SendLongMsgInfo( + @ProtoNumber(1) val type: Int? = null, + @ProtoNumber(2) val uid: LongMsgUid? = null, + @ProtoNumber(3) val groupUin: Int? = null, + @ProtoNumber(4) val payload: ByteArray? = null, +) + +@Serializable +data class LongMsgReq( + @ProtoNumber(1) val recvInfo: RecvLongMsgInfo? = null, + @ProtoNumber(2) val sendInfo: SendLongMsgInfo? = null, + @ProtoNumber(15) val setting: LongMsgSettings? = null, +) + +@Serializable +data class LongMsgRsp( + @ProtoNumber(1) val recvResult: RecvLongMsgResult? = null, + @ProtoNumber(2) val sendResult: SendLongMsgResult? = null, + @ProtoNumber(15) val setting: LongMsgSettings? = null +) { + companion object { + @Serializable + data class SendLongMsgResult( + @ProtoNumber(3) val resId: String? = null, + ) + + @Serializable + data class RecvLongMsgResult( + @ProtoNumber(3) val resId: String? = null, + @ProtoNumber(4) val payload: ByteArray? = null, + ) + } +} diff --git a/protobuf/src/main/java/protobuf/message/longmsg/LongMsgPayload.kt b/protobuf/src/main/java/protobuf/message/longmsg/LongMsgPayload.kt new file mode 100644 index 0000000..c96cce8 --- /dev/null +++ b/protobuf/src/main/java/protobuf/message/longmsg/LongMsgPayload.kt @@ -0,0 +1,32 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package protobuf.message.longmsg + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import protobuf.message.MessageBody +import protobuf.message.MessageContent +import protobuf.message.MessageHead + +@Serializable +data class PushMsgBody( + @ProtoNumber(1) val head: MessageHead? = null, + @ProtoNumber(2) val content: MessageContent? = null, + @ProtoNumber(3) val body: MessageBody? = null +) + +@Serializable +data class LongMsgContent( + @ProtoNumber(1) val body: List? = null +) + +@Serializable +data class LongMsgAction( + @ProtoNumber(1) val command: String? = null, + @ProtoNumber(2) val data: LongMsgContent? = null +) +@Serializable +data class LongMsgPayload( + @ProtoNumber(2) val action: LongMsgAction? = null +) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt index 2c36dd9..c304035 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt @@ -5,12 +5,7 @@ package moe.fuqiuluo.qqinterface.servlet import com.tencent.mobileqq.qroute.QRoute import com.tencent.mobileqq.troop.api.ITroopMemberNameService import com.tencent.qqnt.kernel.api.IKernelService -import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import com.tencent.qqnt.kernel.nativeinterface.MsgRecord -import com.tencent.qqnt.kernel.nativeinterface.TempChatGameSession -import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo -import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo +import com.tencent.qqnt.kernel.nativeinterface.* import com.tencent.qqnt.msg.api.IMsgService import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope @@ -18,21 +13,33 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.encodeToByteArray import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.protobuf.ProtoBuf +import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments import moe.fuqiuluo.shamrock.helper.ContactHelper import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.helper.SendMsgException +import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToMessageElements import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult -import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY +import moe.fuqiuluo.shamrock.tools.* +import moe.fuqiuluo.shamrock.utils.DeflateTools import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher import moe.fuqiuluo.shamrock.xposed.helper.msgService +import protobuf.message.* +import protobuf.message.longmsg.* +import tencent.mobileim.structmsg.structmsg.SystemMsg import java.util.UUID +import kotlin.collections.slice import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +import kotlin.random.Random +import kotlin.random.nextLong -internal object MsgSvc: BaseSvc() { +internal object MsgSvc : BaseSvc() { suspend fun prepareTempChatFromGroup( groupId: String, peerId: String @@ -40,16 +47,19 @@ internal object MsgSvc: BaseSvc() { LogCenter.log("主动临时消息,创建临时会话。", Level.INFO) val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService ?: return Result.failure(Exception("获取消息服务失败")) - msgService.prepareTempChat(TempChatPrepareInfo( - MsgConstant.KCHATTYPETEMPC2CFROMGROUP, - ContactHelper.getUidByUinAsync(peerId = peerId.toLong()), - app.getRuntimeService(ITroopMemberNameService::class.java, "all").getTroopMemberNameRemarkFirst(groupId, peerId), - groupId, - EMPTY_BYTE_ARRAY, - app.currentUid, - "", - TempChatGameSession() - )) { code, reason -> + msgService.prepareTempChat( + TempChatPrepareInfo( + MsgConstant.KCHATTYPETEMPC2CFROMGROUP, + ContactHelper.getUidByUinAsync(peerId = peerId.toLong()), + app.getRuntimeService(ITroopMemberNameService::class.java, "all") + .getTroopMemberNameRemarkFirst(groupId, peerId), + groupId, + EMPTY_BYTE_ARRAY, + app.currentUid, + "", + TempChatGameSession() + ) + ) { code, reason -> if (code != 0) { LogCenter.log("临时会话创建失败: $code, $reason", Level.ERROR) } @@ -83,7 +93,7 @@ internal object MsgSvc: BaseSvc() { ?: return Result.failure(Exception("没有对应消息映射,消息获取失败")) val peerId = mapping.peerId - val contact = MessageHelper.generateContact(mapping.chatType, peerId, mapping.subPeerId ?: "") + val contact = MessageHelper.generateContact(mapping.chatType, peerId, mapping.subPeerId) val msg = withTimeoutOrNull(5000) { val service = QRoute.api(IMsgService::class.java) @@ -174,7 +184,7 @@ internal object MsgSvc: BaseSvc() { val mapping = MessageHelper.getMsgMappingByHash(msgHash) ?: return -1 to "无法找到消息映射" - val contact = MessageHelper.generateContact(mapping.chatType, mapping.peerId, mapping.subPeerId ?: "") + val contact = MessageHelper.generateContact(mapping.chatType, mapping.peerId, mapping.subPeerId) return suspendCancellableCoroutine { continuation -> msgService.recallMsg(contact, arrayListOf(mapping.qqMsgId)) { code, why -> @@ -204,7 +214,8 @@ internal object MsgSvc: BaseSvc() { } } } - val result = MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0)) + val result = + MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0)) if (result.isFailure) { LogCenter.log("sendToAio: " + result.exceptionOrNull()?.stackTraceToString(), Level.ERROR) return result @@ -220,13 +231,54 @@ internal object MsgSvc: BaseSvc() { } } + suspend fun sendMultiMsg( + uid: String, + groupUin: String?, + messages: List, + ): Result { + val payload = LongMsgPayload( + action = LongMsgAction( + command = "MultiMsg", + data = LongMsgContent( + messages + ) + ) + ) + + val req = LongMsgReq( + sendInfo = SendLongMsgInfo( + type = if (groupUin == null) 1 else 3, + uid = LongMsgUid(groupUin ?: uid), + groupUin = groupUin?.toInt(), + payload = DeflateTools.gzip(ProtoBuf.encodeToByteArray(payload)) + ), + setting = LongMsgSettings( + field1 = 4, + field2 = 2, + field3 = 9, + field4 = 0 + ) + ) + val buffer = sendBufferAW( + "trpc.group.long_msg_interface.MsgService.SsoSendLongMsg", + true, + ProtoBuf.encodeToByteArray(req) + ) ?: return Result.failure(Exception("unable to upload multi message")) + val rsp = ProtoBuf.decodeFromByteArray(buffer.slice(4)) + return rsp.sendResult?.resId?.let { Result.success(it) } ?: Result.failure(Exception("unable to upload multi message")) + } + suspend fun getMultiMsg(resId: String): Result> { + // trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg + // 00 00 00 70 0A 60 0A 1A 12 18 75 5F 35 5A 5A 53 6F 38 63 4D 71 70 49 79 63 75 57 5F 78 43 4C 48 6E 77 12 40 4D 6F 61 44 38 77 2B 55 74 43 42 55 45 4C 4F 66 7A 61 72 69 43 7A 4F 5A 44 57 4B 43 6D 68 45 74 4F 65 54 6C 46 66 44 70 2F 73 61 56 77 50 2F 44 52 37 72 4A 2B 4B 4B 47 30 65 71 2B 6C 4B 58 34 18 03 7A 08 08 02 10 02 18 09 20 00 + val kernelService = NTServiceFetcher.kernelService val sessionService = kernelService.wrapperSession val msgService = sessionService.msgService val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin()) - val content = "{\"app\":\"com.tencent.multimsg\",\"config\":{\"autosize\":1,\"forward\":1,\"round\":1,\"type\":\"normal\",\"width\":300},\"desc\":\"[聊天记录]\",\"extra\":\"\",\"meta\":{\"detail\":{\"news\":[{\"text\":\"Shamrock: 这是条假消息!\"}],\"resid\":\"$resId\",\"source\":\"聊天记录\",\"summary\":\"转发消息\",\"uniseq\":\"${UUID.randomUUID()}\"}},\"prompt\":\"[聊天记录]\",\"ver\":\"0.0.0.5\",\"view\":\"contact\"}" + val content = + "{\"app\":\"com.tencent.multimsg\",\"config\":{\"autosize\":1,\"forward\":1,\"round\":1,\"type\":\"normal\",\"width\":300},\"desc\":\"[聊天记录]\",\"extra\":\"\",\"meta\":{\"detail\":{\"news\":[{\"text\":\"Shamrock: 这是条假消息!\"}],\"resid\":\"$resId\",\"source\":\"聊天记录\",\"summary\":\"转发消息\",\"uniseq\":\"${UUID.randomUUID()}\"}},\"prompt\":\"[聊天记录]\",\"ver\":\"0.0.0.5\",\"view\":\"contact\"}" val msgId = PacketSvc.fakeSelfRecvJsonMsg(msgService, content) if (msgId < 0) { return Result.failure(Exception("获取合并转发消息ID失败")) @@ -261,7 +313,7 @@ internal object MsgSvc: BaseSvc() { class MessageCallback( private val peerId: String, var msgHash: Int - ): IOperateCallback { + ) : IOperateCallback { override fun onResult(code: Int, reason: String?) { if (code != 0 && msgHash != 0) { MessageHelper.removeMsgByHashCode(msgHash) diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/TicketSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/TicketSvc.kt index 26e4564..6817021 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/TicketSvc.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/TicketSvc.kt @@ -50,6 +50,14 @@ internal object TicketSvc: BaseSvc() { return app.longAccountUin } + fun getUid(): String { + return app.currentUid.ifBlank { "u_" } + } + + fun getNickname(): String { + return app.currentNickname + } + fun getCookie(): String { val uin = getUin() val skey = getRealSkey(uin) diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageElementMaker.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageElementMaker.kt new file mode 100644 index 0000000..45c00c9 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageElementMaker.kt @@ -0,0 +1,96 @@ +package moe.fuqiuluo.qqinterface.servlet.msg + +import kotlinx.serialization.json.JsonObject +import moe.fuqiuluo.shamrock.helper.ParamsException +import moe.fuqiuluo.shamrock.tools.asInt +import moe.fuqiuluo.shamrock.tools.asString +import moe.fuqiuluo.shamrock.utils.DeflateTools +import protobuf.message.MessageElement +import protobuf.message.element.FaceElement +import protobuf.message.element.TextElement + + +internal typealias IMessageMaker = suspend (Int, Long, String, JsonObject) -> Result + +internal object MessageElementMaker { + private val makerArray = hashMapOf( + "text" to MessageElementMaker::createTextElem, + "face" to MessageElementMaker::createFaceElem, +// "pic" to MessageElementMaker::createImageElem, +// "image" to MessageElementMaker::createImageElem, +// "voice" to MessageElementMaker::createRecordElem, +// "record" to MessageElementMaker::createRecordElem, +// "at" to MessageElementMaker::createAtElem, +// "video" to MessageElementMaker::createVideoElem, +// "markdown" to MessageElementMaker::createMarkdownElem, +// "dice" to MessageElementMaker::createDiceElem, +// "rps" to MessageElementMaker::createRpsElem, +// "poke" to MessageElementMaker::createPokeElem, +// "anonymous" to MessageElementMaker::createAnonymousElem, +// "share" to MessageElementMaker::createShareElem, +// "contact" to MessageElementMaker::createContactElem, +// "location" to MessageElementMaker::createLocationElem, +// "music" to MessageElementMaker::createMusicElem, +// "reply" to MessageElementMaker::createReplyElem, +// "touch" to MessageElementMaker::createTouchElem, +// "weather" to MessageElementMaker::createWeatherElem, + "json" to MessageElementMaker::createJsonElem, + //"new_dice" to MessageElementMaker::createNewDiceElem, + //"new_rps" to MessageElementMaker::createNewRpsElem, + //"basketball" to MessageElementMaker::createBasketballElem, + //"node" to MessageMaker::createNodeElem, + //"multi_msg" to MessageMaker::createLongMsgStruct, + //"bubble_face" to MessageElementMaker::createBubbleFaceElem, + ) + + private suspend fun createTextElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { + data.checkAndThrow("text") + val elem = MessageElement( + text = TextElement(data["text"].asString) + ) + return Result.success(elem) + } + + private suspend fun createFaceElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { + data.checkAndThrow("id") + val elem = MessageElement( + face = FaceElement(data["id"].asInt) + ) + return Result.success(elem) + } + + private suspend fun createJsonElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { + data.checkAndThrow("data") + + val elem = MessageElement( + json = protobuf.message.element.JsonElement( + data = DeflateTools.compress(data.toString().toByteArray()) + ) + ) + return Result.success(elem) + } + + private fun JsonObject.checkAndThrow(vararg key: String) { + key.forEach { + if (!containsKey(it)) throw ParamsException(it) + } + } + + operator fun get(type: String): IMessageMaker? = makerArray[type] + +} diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageMaker.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MsgElementMaker.kt similarity index 85% rename from xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageMaker.kt rename to xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MsgElementMaker.kt index f67ace8..b8785a3 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageMaker.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MsgElementMaker.kt @@ -82,38 +82,38 @@ import kotlin.math.roundToInt import kotlin.random.Random import kotlin.random.nextInt -internal typealias IMaker = suspend (Int, Long, String, JsonObject) -> Result +internal typealias IMsgMaker = suspend (Int, Long, String, JsonObject) -> Result -internal object MessageMaker { - private val makerArray = mutableMapOf( - "text" to MessageMaker::createTextElem, - "face" to MessageMaker::createFaceElem, - "pic" to MessageMaker::createImageElem, - "image" to MessageMaker::createImageElem, - "voice" to MessageMaker::createRecordElem, - "record" to MessageMaker::createRecordElem, - "at" to MessageMaker::createAtElem, - "video" to MessageMaker::createVideoElem, - "markdown" to MessageMaker::createMarkdownElem, - "dice" to MessageMaker::createDiceElem, - "rps" to MessageMaker::createRpsElem, - "poke" to MessageMaker::createPokeElem, - "anonymous" to MessageMaker::createAnonymousElem, - "share" to MessageMaker::createShareElem, - "contact" to MessageMaker::createContactElem, - "location" to MessageMaker::createLocationElem, - "music" to MessageMaker::createMusicElem, - "reply" to MessageMaker::createReplyElem, - "touch" to MessageMaker::createTouchElem, - "weather" to MessageMaker::createWeatherElem, - "json" to MessageMaker::createJsonElem, - "new_dice" to MessageMaker::createNewDiceElem, - "new_rps" to MessageMaker::createNewRpsElem, - "basketball" to MessageMaker::createBasketballElem, +internal object MsgElementMaker { + private val makerArray = hashMapOf( + "text" to MsgElementMaker::createTextElem, + "face" to MsgElementMaker::createFaceElem, + "pic" to MsgElementMaker::createImageElem, + "image" to MsgElementMaker::createImageElem, + "voice" to MsgElementMaker::createRecordElem, + "record" to MsgElementMaker::createRecordElem, + "at" to MsgElementMaker::createAtElem, + "video" to MsgElementMaker::createVideoElem, + "markdown" to MsgElementMaker::createMarkdownElem, + "dice" to MsgElementMaker::createDiceElem, + "rps" to MsgElementMaker::createRpsElem, + "poke" to MsgElementMaker::createPokeElem, + "anonymous" to MsgElementMaker::createAnonymousElem, + "share" to MsgElementMaker::createShareElem, + "contact" to MsgElementMaker::createContactElem, + "location" to MsgElementMaker::createLocationElem, + "music" to MsgElementMaker::createMusicElem, + "reply" to MsgElementMaker::createReplyElem, + "touch" to MsgElementMaker::createTouchElem, + "weather" to MsgElementMaker::createWeatherElem, + "json" to MsgElementMaker::createJsonElem, + "new_dice" to MsgElementMaker::createNewDiceElem, + "new_rps" to MsgElementMaker::createNewRpsElem, + "basketball" to MsgElementMaker::createBasketballElem, //"node" to MessageMaker::createNodeElem, //"multi_msg" to MessageMaker::createLongMsgStruct, - "bubble_face" to MessageMaker::createBubbleFaceElem, - "inline_keyboard" to MessageMaker::createInlineKeywordElem + "bubble_face" to MsgElementMaker::createBubbleFaceElem, + "inline_keyboard" to MsgElementMaker::createInlineKeywordElem ) private suspend fun createInlineKeywordElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { @@ -169,7 +169,12 @@ internal object MessageMaker { return Result.success(elem) } - private suspend fun createBubbleFaceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createBubbleFaceElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { data.checkAndThrow("id", "count") val faceId = data["id"].asInt val local = QQSysFaceUtil.convertToLocal(faceId) @@ -202,21 +207,13 @@ internal object MessageMaker { // SendForwardMessage(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin(), data["content"].asJsonArray) // // } - /**\ - * msgElement.setFaceElement(new FaceElement()); - * msgElement.getFaceElement().setFaceIndex(114); - * msgElement.getFaceElement().setFaceText("/篮球"); - * msgElement.getFaceElement().setFaceType(3); - * msgElement.getFaceElement().setPackId("1"); - * msgElement.getFaceElement().setStickerId("13"); - * msgElement.getFaceElement().setRandomType(1); - * msgElement.getFaceElement().setImageType(1); - * msgElement.getFaceElement().setStickerType(2); - * msgElement.getFaceElement().setSourceType(1); - * msgElement.getFaceElement().setSurpriseId(""); - * msgElement.getFaceElement().setResultId(String.valueOf(new Random().nextInt(5) + 1)); - */ - private suspend fun createBasketballElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + + private suspend fun createBasketballElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { val elem = MsgElement() elem.elementType = MsgConstant.KELEMTYPEFACE val face = FaceElement() @@ -227,14 +224,19 @@ internal object MessageMaker { face.stickerId = "13" face.sourceType = 1 face.stickerType = 2 - face.resultId = Random.nextInt(1 .. 5).toString() + face.resultId = Random.nextInt(1..5).toString() face.surpriseId = "" face.randomType = 1 elem.faceElement = face return Result.success(elem) } - private suspend fun createNewRpsElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createNewRpsElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { val elem = MsgElement() elem.elementType = MsgConstant.KELEMTYPEFACE val face = FaceElement() @@ -252,7 +254,12 @@ internal object MessageMaker { return Result.success(elem) } - private suspend fun createNewDiceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createNewDiceElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { val elem = MsgElement() elem.elementType = MsgConstant.KELEMTYPEFACE val face = FaceElement() @@ -370,7 +377,7 @@ internal object MessageMaker { LogCenter.log("无法获取被回复消息", Level.ERROR) } - if(data.containsKey("text")) { + if (data.containsKey("text")) { data.checkAndThrow("qq", "time", "seq") reply.replayMsgSeq = data["seq"].asLong reply.sourceMsgText = data["text"].asString @@ -389,21 +396,23 @@ internal object MessageMaker { ): Result { data.checkAndThrow("type") - when(val type = data["type"].asString) { + when (val type = data["type"].asString) { "qq" -> { data.checkAndThrow("id") val id = data["id"].asString - if(!MusicHelper.tryShareQQMusicById(chatType, peerId.toLong(), msgId, id)) { + if (!MusicHelper.tryShareQQMusicById(chatType, peerId.toLong(), msgId, id)) { LogCenter.log("无法发送QQ音乐分享", Level.ERROR) } } + "163" -> { data.checkAndThrow("id") val id = data["id"].asString - if(!MusicHelper.tryShare163MusicById(chatType, peerId.toLong(), msgId, id)) { + if (!MusicHelper.tryShare163MusicById(chatType, peerId.toLong(), msgId, id)) { LogCenter.log("无法发送网易云音乐分享", Level.ERROR) } } + "custom" -> { data.checkAndThrow("url", "audio", "title") ArkMsgSvc.tryShareMusic( @@ -418,13 +427,19 @@ internal object MessageMaker { data["audio"].asString ) } + else -> LogCenter.log("不支持的音乐分享类型: $type", Level.ERROR) } return Result.failure(ActionMsgException) } - private suspend fun createLocationElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createLocationElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { data.checkAndThrow("lat", "lon") val lat = data["lat"].asString.toDouble() @@ -437,7 +452,12 @@ internal object MessageMaker { return Result.failure(ActionMsgException) } - private suspend fun createContactElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createContactElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { data.checkAndThrow("id") val type = data["type"].asStringOrNull ?: data["kind"].asStringOrNull val id = data["id"].asString @@ -448,10 +468,12 @@ internal object MessageMaker { val ark = ArkElement(CardSvc.getSharePrivateArkMsg(id.toLong()), null, null) elem.arkElement = ark } + "group" -> { val ark = ArkElement(GroupSvc.getShareTroopArkMsg(id.toLong()), null, null) elem.arkElement = ark } + else -> throw IllegalParamsException("type") } @@ -460,7 +482,12 @@ internal object MessageMaker { return Result.success(elem) } - private suspend fun createShareElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createShareElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { data.checkAndThrow("title", "url") val url = data["url"].asString @@ -525,11 +552,21 @@ internal object MessageMaker { return Result.failure(ActionMsgException) } - private suspend fun createAnonymousElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createAnonymousElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { return Result.failure(ActionMsgException) } - private suspend fun createPokeElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createPokeElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { data.checkAndThrow("type", "id") val elem = MsgElement() val face = FaceElement() @@ -544,8 +581,8 @@ internal object MessageMaker { face.vaspokeName = "" face.vaspokeMinver = "" face.pokeStrength = (data["strength"].asIntOrNull ?: data["cnt"].asIntOrNull - ?: data["count"].asIntOrNull ?: data["time"].asIntOrNull ?: 0).also { - if(it < 0 || it > 3) throw IllegalParamsException("strength") + ?: data["count"].asIntOrNull ?: data["time"].asIntOrNull ?: 0).also { + if (it < 0 || it > 3) throw IllegalParamsException("strength") } face.msgType = 0 face.faceBubbleCount = 0 @@ -556,7 +593,12 @@ internal object MessageMaker { return Result.success(elem) } - private suspend fun createFaceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createFaceElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { data.checkAndThrow("id") val serverId = data["id"].asInt @@ -575,15 +617,15 @@ internal object MessageMaker { face.faceIndex = serverId face.faceText = QQSysFaceUtil.getFaceDescription(QQSysFaceUtil.convertToLocal(serverId)) if (serverId == 394) { - face.stickerId = 40.toString() + face.stickerId = "40" face.packId = "1" face.sourceType = 1 face.stickerType = 3 face.randomType = 1 - face.resultId = data["result"].asStringOrNull ?: Random.nextInt(1 .. 5).toString() + face.resultId = data["result"].asStringOrNull ?: Random.nextInt(1..5).toString() } else if (big) { face.imageType = 0 - face.stickerId = 30.toString() + face.stickerId = "30" face.packId = "1" face.sourceType = 1 face.stickerType = 1 @@ -597,7 +639,12 @@ internal object MessageMaker { return Result.success(elem) } - private suspend fun createRpsElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createRpsElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { val elem = MsgElement() elem.elementType = MsgConstant.KELEMTYPEMARKETFACE val market = MarketFaceElement( @@ -612,7 +659,12 @@ internal object MessageMaker { return Result.success(elem) } - private suspend fun createDiceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createDiceElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { val elem = MsgElement() elem.elementType = MsgConstant.KELEMTYPEMARKETFACE val market = MarketFaceElement( @@ -627,7 +679,12 @@ internal object MessageMaker { return Result.success(elem) } - private suspend fun createMarkdownElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createMarkdownElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { data.checkAndThrow("content") val elem = MsgElement() elem.elementType = MsgConstant.KELEMTYPEMARKDOWN @@ -636,7 +693,12 @@ internal object MessageMaker { return Result.success(elem) } - private suspend fun createVideoElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createVideoElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { data.checkAndThrow("file") val file = data["file"].asString.let { @@ -672,7 +734,8 @@ internal object MessageMaker { ) if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize( originalPath - ) != file.length()) { + ) != file.length() + ) { QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath) AudioUtils.obtainVideoCover(file.absolutePath, thumbPath!!) } @@ -711,23 +774,26 @@ internal object MessageMaker { val qq = data["qq"].asString val at = TextElement() - when(qq) { + when (qq) { "0", "all" -> { at.content = "@全体成员" at.atType = MsgConstant.ATTYPEALL at.atNtUid = "0" } + "online" -> { at.content = "@在线成员" at.atType = MsgConstant.ATTYPEONLINE at.atNtUid = "0" } + "admin" -> { at.content = "@管理员" at.atRoleId = 1 at.atType = MsgConstant.ATTYPEROLE at.atNtUid = "0" } + else -> { val name = data["name"].asStringOrNull if (name == null) { @@ -757,7 +823,12 @@ internal object MessageMaker { return Result.success(elem) } - private suspend fun createRecordElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createRecordElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { var file = data["file"].asStringOrNull?.let { val md5 = it.replace(regex = "[{}\\-]".toRegex(), replacement = "") .replace(" ", "") @@ -785,11 +856,13 @@ internal object MessageMaker { ptt.duration = QRoute.api(IAIOPttApi::class.java) .getPttFileDuration(file.absolutePath) } + MediaType.Amr -> { LogCenter.log({ "Amr: $file" }, Level.DEBUG) ptt.duration = AudioUtils.getDurationSec(file) ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR } + MediaType.Pcm -> { LogCenter.log({ "Pcm To Silk: $file" }, Level.DEBUG) val result = AudioUtils.pcmToSilk(file) @@ -797,6 +870,7 @@ internal object MessageMaker { file = result.first ptt.formatType = MsgConstant.KPTTFORMATTYPESILK } + else -> { LogCenter.log({ "Audio To SILK: $file" }, Level.DEBUG) val result = AudioUtils.audioToSilk(file) @@ -813,12 +887,13 @@ internal object MessageMaker { // QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath) //} - if(!(Transfer with when (chatType) { - MsgConstant.KCHATTYPEGROUP -> Troop(peerId) - MsgConstant.KCHATTYPEC2C -> Private(peerId) - MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> Private(peerId) - else -> error("Not supported chatType($chatType) for RecordMsg") - } trans VoiceResource(file))) { + if (!(Transfer with when (chatType) { + MsgConstant.KCHATTYPEGROUP -> Troop(peerId) + MsgConstant.KCHATTYPEC2C -> Private(peerId) + MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> Private(peerId) + else -> error("Not supported chatType($chatType) for RecordMsg") + } trans VoiceResource(file)) + ) { return Result.failure(RuntimeException("上传语音失败: $file")) } @@ -848,7 +923,12 @@ internal object MessageMaker { return Result.success(elem) } - private suspend fun createImageElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createImageElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { val isOriginal = data["original"].asBooleanOrNull ?: true val isFlash = data["flash"].asBooleanOrNull ?: false val filePath = data["file"].asStringOrNull @@ -889,7 +969,8 @@ internal object MessageMaker { ) if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize( originalPath - ) != file.length()) { + ) != file.length() + ) { val thumbPath = msgService.getRichMediaFilePathForMobileQQSend( RichMediaFilePathInfo( 2, 0, pic.md5HexStr, file.name, 2, 720, null, "", true @@ -927,7 +1008,12 @@ internal object MessageMaker { return Result.success(elem) } - private suspend fun createTextElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result { + private suspend fun createTextElem( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result { data.checkAndThrow("text") val elem = MsgElement() elem.elementType = MsgConstant.KELEMTYPETEXT @@ -943,5 +1029,5 @@ internal object MessageMaker { } } - operator fun get(type: String): IMaker? = makerArray[type] + operator fun get(type: String): IMsgMaker? = makerArray[type] } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageElemConverter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageElemConverter.kt index fec1e56..7175abf 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageElemConverter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageElemConverter.kt @@ -1,6 +1,5 @@ package moe.fuqiuluo.qqinterface.servlet.msg.convert -import com.tencent.mobileqq.qmmkv.QMMKV import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgElement import kotlinx.serialization.json.add @@ -19,8 +18,6 @@ import moe.fuqiuluo.shamrock.tools.asJsonObject import moe.fuqiuluo.shamrock.tools.asString import moe.fuqiuluo.shamrock.tools.hex2ByteArray import moe.fuqiuluo.shamrock.tools.json -import mqq.app.MobileQQ -import kotlin.jvm.internal.Intrinsics internal sealed class MessageElemConverter: IMessageConvert { /** diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt index d4e1e84..01590fa 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt @@ -16,7 +16,8 @@ import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonObject import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.qqinterface.servlet.msg.MessageMaker +import moe.fuqiuluo.qqinterface.servlet.msg.MessageElementMaker +import moe.fuqiuluo.qqinterface.servlet.msg.MsgElementMaker import moe.fuqiuluo.shamrock.helper.db.MessageDB import moe.fuqiuluo.shamrock.helper.db.MessageMapping import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult @@ -26,8 +27,8 @@ import moe.fuqiuluo.shamrock.tools.asJsonObjectOrNull import moe.fuqiuluo.shamrock.tools.asString import moe.fuqiuluo.shamrock.tools.json import moe.fuqiuluo.shamrock.tools.jsonArray +import protobuf.message.MessageElement import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine import kotlin.math.abs internal object MessageHelper { @@ -39,7 +40,7 @@ internal object MessageHelper { fromId: String = peerId ): SendMsgResult { val uniseq = generateMsgId(chatType) - val msg = messageArrayToMessageElements(chatType, uniseq.qqMsgId, peerId, decodeCQCode(message)).also { + val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, decodeCQCode(message)).also { if (it.second.isEmpty() && !it.first) { error("消息合成失败,请查看日志或者检查输入。") } else if (it.second.isEmpty()) { @@ -82,7 +83,7 @@ internal object MessageHelper { callback: IOperateCallback ): Result { val uniseq = generateMsgId(chatType) - val msg = messageArrayToMessageElements(chatType, uniseq.qqMsgId, peerId, message).also { + val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, message).also { if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。") }.second.filter { it.elementType != -1 @@ -166,7 +167,7 @@ internal object MessageHelper { fromId: String = peerId ): SendMsgResult { val uniseq = generateMsgId(chatType) - val msg = messageArrayToMessageElements(chatType, uniseq.qqMsgId, peerId, message).also { + val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, message).also { if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。") }.second.filter { it.elementType != -1 @@ -224,7 +225,7 @@ internal object MessageHelper { fromId: String = peerId ): SendMsgResult { val uniseq = generateMsgId(chatType) - val msg = messageArrayToMessageElements(chatType, uniseq.qqMsgId, peerId, message).also { + val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, message).also { if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。") }.second.filter { it.elementType != -1 @@ -276,12 +277,41 @@ internal object MessageHelper { } } - suspend fun messageArrayToMessageElements(chatType: Int, msgId: Long, targetUin: String, messageList: JsonArray): Pair> { + suspend fun messageArrayToMsgElements(chatType: Int, msgId: Long, targetUin: String, messageList: JsonArray): Pair> { val msgList = arrayListOf() var hasActionMsg = false messageList.forEach { val msg = it.jsonObject - val maker = MessageMaker[msg["type"].asString] + val maker = MsgElementMaker[msg["type"].asString] + if (maker != null) { + try { + val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject + maker(chatType, msgId, targetUin, data).onSuccess { msgElem -> + msgList.add(msgElem) + }.onFailure { + if (it.javaClass != ActionMsgException::class.java) { + throw it + } else { + hasActionMsg = true + } + } + } catch (e: Throwable) { + LogCenter.log(e.stackTraceToString(), Level.ERROR) + } + } else { + LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR) + return false to arrayListOf() + } + } + return hasActionMsg to msgList + } + + suspend fun messageArrayToMessageElements(chatType: Int, msgId: Long, targetUin: String, messageList: JsonArray): Pair> { + val msgList = arrayListOf() + var hasActionMsg = false + messageList.forEach { + val msg = it.jsonObject + val maker = MessageElementMaker[msg["type"].asString] if (maker != null) { try { val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt index 9ba85b8..7dfd4fb 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt @@ -1,7 +1,6 @@ package moe.fuqiuluo.shamrock.remote.action.handlers import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import com.tencent.qqnt.kernel.nativeinterface.MultiMsgInfo import kotlinx.serialization.json.* import moe.fuqiuluo.qqinterface.servlet.MsgSvc import moe.fuqiuluo.qqinterface.servlet.TicketSvc @@ -14,8 +13,11 @@ import moe.fuqiuluo.shamrock.remote.action.ActionSession import moe.fuqiuluo.shamrock.remote.action.IActionHandler import moe.fuqiuluo.shamrock.remote.service.data.ForwardMessageResult import moe.fuqiuluo.shamrock.tools.* -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher import moe.fuqiuluo.symbols.OneBotHandler +import protobuf.message.* +import protobuf.message.longmsg.PushMsgBody +import java.util.* +import kotlin.random.Random @OneBotHandler("send_forward_msg") internal object SendForwardMessage : IActionHandler() { @@ -37,14 +39,26 @@ internal object SendForwardMessage : IActionHandler() { return noParam("detail_type/message_type", session.echo) } } - val peerId = when(chatType) { - MsgConstant.KCHATTYPEGROUP -> session.getStringOrNull("group_id") ?: return noParam("group_id", session.echo) - MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("user_id") ?: return noParam("user_id", session.echo) + val peerId = when (chatType) { + MsgConstant.KCHATTYPEGROUP -> session.getStringOrNull("group_id") ?: return noParam( + "group_id", + session.echo + ) + + MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("user_id") + ?: return noParam("user_id", session.echo) + else -> error("unknown chat type: $chatType") } - val fromId = when(chatType) { - MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("group_id") ?: return noParam("group_id", session.echo) - MsgConstant.KCHATTYPEC2C -> session.getStringOrNull("user_id") ?: return noParam("user_id", session.echo) + val fromId = when (chatType) { + MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("group_id") + ?: return noParam("group_id", session.echo) + + MsgConstant.KCHATTYPEC2C -> session.getStringOrNull("user_id") ?: return noParam( + "user_id", + session.echo + ) + else -> error("unknown chat type: $chatType") } return if (session.isArray("messages")) { @@ -68,103 +82,184 @@ internal object SendForwardMessage : IActionHandler() { echo: JsonElement = EmptyJsonString ): String { kotlin.runCatching { - val kernelService = NTServiceFetcher.kernelService - val sessionService = kernelService.wrapperSession - val msgService = sessionService.msgService - val selfUin = TicketSvc.getUin() + var uid: String? = null + var groupUin: String? = null - val multiNodes = messages.map { - if (it.asJsonObject["type"].asStringOrNull != "node") { - LogCenter.log("包含非node类型节点", Level.WARN) - return@map null - } - if (it.asJsonObject["data"] !is JsonObject) { - LogCenter.log("data字段错误", Level.WARN) - return@map null - } - it.asJsonObject["data"].asJsonObject.let { data -> - if (data.containsKey("id")) { - val record = MsgSvc.getMsg(data["id"].asInt).getOrNull() - if (record == null) { - LogCenter.log("合并转发消息节点消息获取失败:${data["id"]}", Level.WARN) - return@map null - } else { - record.peerName to record.toSegments().map { segment -> - segment.toJson() - }.json - } - } else if (data.containsKey("content")) { - (data["name"].asStringOrNull ?: "Anno") to when (val raw = data["content"]) { - is JsonObject -> raw.asJsonArray - is JsonArray -> raw.asJsonArray - else -> MessageHelper.decodeCQCode(raw.asString) - } - } else { - LogCenter.log("消息节点缺少id或content字段", Level.WARN) + var i = -1 + val desc = MutableList(messages.size) { "" } + + val msgs = messages.map { msg -> + val data = msg.asJsonObject["data"].asJsonObject + if (data.containsKey("id")) { + val record = MsgSvc.getMsg(data["id"].asInt).getOrElse { + LogCenter.log("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it", Level.WARN) return@map null } - }.let { node -> - val content = node.second.map { msg -> - when (msg.asJsonObject["type"].asStringOrNull ?: "text") { - "at" -> { - buildJsonObject { - put("type", "text") - putJsonObject("data") { - put( - "text", "@${ - msg.asJsonObject["data"].asJsonObject["name"].asStringOrNull.ifNullOrEmpty( - msg.asJsonObject["data"].asJsonObject["qq"].asString - ) - }" - ) + uid = record.peerUid + if (record.chatType == MsgConstant.KCHATTYPEGROUP) groupUin = record.peerUin.toString() + PushMsgBody( + head = MessageHead( + peerUid = record.senderUid, + groupInfo = if (record.chatType == MsgConstant.KCHATTYPEGROUP) GroupInfo( + groupCode = record.peerUin.toULong(), + memberCard = record.sendMemberName, + u1 = 2 + ) else null + ), + content = MessageContent( + msgType = record.msgType, + msgViaRandom = record.msgId, + msgSeq = record.msgSeq, + msgTime = record.msgTime, + u2 = 1, + u6 = 0, + u7 = 0, + forwardHead = ForwardHead( + u1 = 0, + u2 = 0, + u3 = if (record.chatType == MsgConstant.KCHATTYPEGROUP) 0 else 2, + u4 = "", + Avatar = "" + ) + ), + body = MessageBody( + rich = RichMessage( + elements = MessageHelper.messageArrayToMessageElements( + record.chatType, + record.msgId, + record.peerUin.toString(), + record.elements.toSegments(record.chatType, record.peerUin.toString(), "0").also { + desc[++i] = record.peerName + ": " + }.map { + when (it.type) { + "text" -> desc[i] += it.data["text"] as String + + "at" -> desc[i] += "@${it.data["name"] as String? ?: it.data["qq"] as String}" + + "face" -> desc[i] += "[表情]" + + "voice" -> desc[i] += "[语音]" + + "node" -> desc[i] += "[合并转发消息]" + } + + it.toJson() + }.json + ).also { + if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。") + }.second + ) + ) + ) + } else if (data.containsKey("content")) { + PushMsgBody( + head = MessageHead( + peerUid = data["uid"]?.asString ?: TicketSvc.getUid() + ), + content = MessageContent( + msgType = 529, + msgViaRandom = 4, + msgSeq = data["seq"]?.asLong ?: 0, + msgTime = System.currentTimeMillis() / 1000, + u2 = 1, + u6 = 0, + u7 = 0, + forwardHead = ForwardHead( + u1 = 0, + u2 = 0, + u3 = 2, + u4 = "", + Avatar = "" + ) + ), + body = MessageBody( + rich = RichMessage( + elements = MessageHelper.messageArrayToMessageElements( + 1, + Random.nextLong(), + data["uin"]?.asString ?: TicketSvc.getUin(), + when (data["content"]) { + is JsonObject -> listOf(data["content"] as JsonObject).json + is JsonArray -> data["content"] as JsonArray + else -> MessageHelper.decodeCQCode(data["content"].asString) + }.also { + desc[++i] = "${ + data["name"].asStringOrNull ?: data["uin"].asStringOrNull + ?: TicketSvc.getNickname() + }: " + }.onEach { + when (it.asJsonObject["type"].asString) { + "text" -> desc[i] += it.asJsonObject["data"].asJsonObject["text"].asString + + "at" -> desc[i] += "@${it.asJsonObject["data"].asJsonObject["name"].asStringOrNull ?: it.asJsonObject["data"].asJsonObject["qq"].asString}" + + "face" -> desc[i] += "[表情]" + + "voice" -> desc[i] += "[语音]" + + "node" -> desc[i] += "[合并转发消息]" + } } - } - } - - "voice" -> { - buildJsonObject { - put("type", "text") - putJsonObject("data") { - put("text", "[语音]") - } - } - } - - "node" -> { - LogCenter.log("合并转发消息暂时不支持嵌套", Level.WARN) - buildJsonObject { - put("type", "text") - putJsonObject("data") { - put("text", "[合并转发消息]") - } - } - } - - else -> msg - } - }.json - - val result = MessageHelper.sendMessageNoCb(MsgConstant.KCHATTYPEC2C, selfUin, content) - if (result.qqMsgId == 0L) { - LogCenter.log("合并转发消息节点消息发送失败", Level.WARN) - return@map null - } - result.qqMsgId to node.first + ).also { + if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。") + }.second + ) + ) + ) + } else { + LogCenter.log("消息节点缺少id或content字段", Level.WARN) + null } - }.filterNotNull() + }.filterNotNull().ifEmpty { return logic("消息节点为空", echo) } - val from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin) - val to = MessageHelper.generateContact(chatType, peerId, fromId) - val uniseq = MessageHelper.generateMsgId(chatType) - msgService.multiForwardMsg(ArrayList().apply { - multiNodes.forEach { add(MultiMsgInfo(it.first, it.second)) } - }.also { it.reverse() }, from, to, MsgSvc.MessageCallback(peerId, uniseq.msgHashId)) + val resid = MsgSvc.sendMultiMsg(uid ?: TicketSvc.getUid(), groupUin, msgs) + .getOrElse { return logic(it.message ?: "", echo) } + val uniseq = UUID.randomUUID().toString().uppercase() + + val result = MsgSvc.sendToAio( + chatType, peerId, + listOf( + hashMapOf( + "type" to "json", "data" to hashMapOf( + "data" to hashMapOf( + "app" to "com.tencent.multimsg", + "config" to hashMapOf( + "autosize" to 1, + "forward" to 1, + "round" to 1, + "type" to "normal", + "width" to 300 + ).json, + "desc" to "[聊天记录]", + "extra" to hashMapOf( + "filename" to uniseq, + "tsum" to 2 + ).json.toString() + "\n", + "meta" to hashMapOf( + "detail" to hashMapOf( + "news" to desc.slice(0..if (i < 3) i else 3) + .map { hashMapOf("text" to it).json }.json, + "resid" to resid, + "source" to "群聊的聊天记录", + "summary" to "查看${msgs.size}条转发消息", + "uniseq" to uniseq + ).json + ).json, + "prompt" to "[聊天记录]", + "ver" to "0.0.0.5", + "view" to "contact" + ).json, + "resid" to resid + ).json + ).json + ).json, fromId, 3 + ).getOrElse { return logic(it.message ?: "", echo) } return ok( ForwardMessageResult( - msgId = uniseq.msgHashId, - forwardId = "" + msgId = result.msgHashId, + forwardId = resid ), echo = echo ) }.onFailure { diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMessage.kt index de80347..7d10a2a 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMessage.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMessage.kt @@ -120,18 +120,14 @@ internal object SendMessage: IActionHandler() { //if (!ContactHelper.checkContactAvailable(chatType, peerId)) { // return logic("contact is not found", echo = echo) //} - val result = MsgSvc.sendToAio(chatType, peerId, message, fromId = fromId, retryCnt) - if (result.isFailure) { - return logic(result.exceptionOrNull()?.message ?: "", echo) - } - val sendMsgResult = result.getOrThrow() - if (sendMsgResult.msgHashId <= 0) { + val result = MsgSvc.sendToAio(chatType, peerId, message, fromId = fromId, retryCnt).getOrElse { return logic(it.message ?: "", echo) } + if (result.msgHashId <= 0) { return logic("send message failed", echo = echo) } - recallDuration?.let { autoRecall(sendMsgResult.msgHashId, it) } + recallDuration?.let { autoRecall(result.msgHashId, it) } return ok(MessageResult( - msgId = sendMsgResult.msgHashId, - time = (sendMsgResult.msgTime * 0.001).toLong() + msgId = result.msgHashId, + time = (result.msgTime * 0.001).toLong() ), echo) } diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt index 5ee1edd..2b418c1 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt @@ -144,7 +144,7 @@ internal object AioListener : IKernelMsgListener { } MsgConstant.KCHATTYPEGUILD -> { - LogCenter.log("频道消息(guildId = ${record.guildId}, sender=${record.senderUid}, id = [$msgHash | ${record.msgId}], msg = $rawMsg)") + LogCenter.log("频道消息(guildId = ${record.guildId}, sender = ${record.senderUid}, id = [$msgHash | ${record.msgId}], msg = $rawMsg)") if (!GlobalEventTransmitter.MessageTransmitter .transGuildMessage(record, record.elements, rawMsg, msgHash, postType = postType) ) { @@ -169,7 +169,6 @@ internal object AioListener : IKernelMsgListener { GlobalScope.launch { try { val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId) - val peerId = when (record.chatType) { MsgConstant.KCHATTYPEGUILD -> record.guildId else -> record.peerUin.toString() diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt index e587f3d..f4e1fba 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt @@ -65,16 +65,14 @@ val Map.json: JsonObject get() { val map = hashMapOf() forEach { (key, any) -> - if (any != null) { - when (any) { - is JsonElement -> map[key] = any - is Number -> map[key] = any.json - is String -> map[key] = any.json - is Boolean -> map[key] = any.json - is Map<*, *> -> map[key] = (any as Map).json - is Collection<*> -> map[key] = (any as Collection).json - else -> error("unknown object type: ${any::class.java}") - } + when (any) { + is JsonElement -> map[key] = any + is Number -> map[key] = any.json + is String -> map[key] = any.json + is Boolean -> map[key] = any.json + is Map<*, *> -> map[key] = (any as Map).json + is Collection<*> -> map[key] = (any as Collection).json + else -> error("unknown object type: ${any::class.java}") } } return map.jsonObject