From ec56e32be18c5dc0c1745f97706bcf81dfc6559f Mon Sep 17 00:00:00 2001 From: Simplxs <simplxsa@gmail.com> Date: Wed, 28 Feb 2024 12:55:27 +0800 Subject: [PATCH] refactor send_forward_msg --- .../src/main/java/protobuf/message/Ptt.kt | 18 +- .../main/java/protobuf/message/RichText.kt | 2 +- .../fuqiuluo/qqinterface/servlet/MsgSvc.kt | 247 ++++++++++++++--- .../fuqiuluo/qqinterface/servlet/PacketSvc.kt | 33 +-- .../qqinterface/servlet/msg/Converter.kt | 33 ++- .../qqinterface/servlet/msg/MessageSegment.kt | 6 +- .../servlet/msg/converter/ElemConverter.kt | 13 +- .../msg/converter/NtMsgElementConverter.kt | 20 +- .../servlet/msg/maker/ElemMaker.kt | 251 ++++++++++++------ .../servlet/msg/maker/NtMsgElementMaker.kt | 78 +++++- .../servlet/transfile/RichProtoSvc.kt | 23 +- .../fuqiuluo/shamrock/helper/MessageHelper.kt | 37 +-- .../remote/action/handlers/GetForwardMsg.kt | 16 +- .../remote/action/handlers/GetHistoryMsg.kt | 2 + .../shamrock/remote/action/handlers/GetMsg.kt | 1 + .../action/handlers/SendForwardMessage.kt | 227 +--------------- .../handlers/SendGroupForwardMessage.kt | 11 +- .../remote/action/handlers/SendMessage.kt | 127 ++++++--- .../handlers/SendPrivateForwardMessage.kt | 11 +- .../action/handlers/UploadMultiMessage.kt | 97 +++++++ .../shamrock/remote/api/MessageAction.kt | 146 ++++++---- .../shamrock/remote/service/data/Message.kt | 20 +- .../service/listener/PrimitiveListener.kt | 5 +- .../java/moe/fuqiuluo/shamrock/tools/Json.kt | 1 + 24 files changed, 891 insertions(+), 534 deletions(-) create mode 100644 xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadMultiMessage.kt diff --git a/protobuf/src/main/java/protobuf/message/Ptt.kt b/protobuf/src/main/java/protobuf/message/Ptt.kt index 2aedae4f..a310b0f3 100644 --- a/protobuf/src/main/java/protobuf/message/Ptt.kt +++ b/protobuf/src/main/java/protobuf/message/Ptt.kt @@ -7,9 +7,9 @@ import kotlinx.serialization.protobuf.ProtoNumber data class Ptt( @ProtoNumber(1) var fileType: UInt?=null, @ProtoNumber(2) var srcUin: ULong?=null, - @ProtoNumber(3) var fileUuid: ByteArray?=null, + @ProtoNumber(3) var fileUuid: String?=null, @ProtoNumber(4) var fileMd5: ByteArray?=null, - @ProtoNumber(5) var fileName: ByteArray?=null, + @ProtoNumber(5) var fileName: String?=null, @ProtoNumber(6) var fileSize: UInt?=null, @ProtoNumber(7) var reserve: ByteArray?=null, @ProtoNumber(8) var fileId: UInt?=null, @@ -22,11 +22,19 @@ data class Ptt( @ProtoNumber(15) var magicPttIndex: UInt?=null, @ProtoNumber(16) var voiceSwitch: UInt?=null, @ProtoNumber(17) var pttUrl: ByteArray?=null, - @ProtoNumber(18) var groupFileKey: ByteArray?=null, + @ProtoNumber(18) var groupFileKey: String?=null, @ProtoNumber(19) var time: UInt?=null, @ProtoNumber(20) var downPara: ByteArray?=null, @ProtoNumber(29) var format: UInt?=null, - @ProtoNumber(30) var pbReserve: ByteArray?=null, + @ProtoNumber(30) var pbReserve: PbReserve?=null, @ProtoNumber(31) var rptPttUrls: List<String>? = null, @ProtoNumber(32) var downloadFlag: UInt?=null, -) \ No newline at end of file +){ + companion object{ + @Serializable + data class PbReserve( + @ProtoNumber(2) var magic: Int?=null, + @ProtoNumber(7) var reserve: Int?=null, + ) + } +} \ No newline at end of file diff --git a/protobuf/src/main/java/protobuf/message/RichText.kt b/protobuf/src/main/java/protobuf/message/RichText.kt index 3e5c1280..f0f8bfb1 100644 --- a/protobuf/src/main/java/protobuf/message/RichText.kt +++ b/protobuf/src/main/java/protobuf/message/RichText.kt @@ -9,7 +9,7 @@ import kotlinx.serialization.protobuf.ProtoNumber @Serializable data class RichText( @ProtoNumber(1) val attr: Attr? = null, - @ProtoNumber(2) val elements: List<Elem>? = null, + @ProtoNumber(2) var elements: List<Elem>? = null, @ProtoNumber(3) val not_online_file: NotOnlineFile? = null, @ProtoNumber(4) val ptt: Ptt? = null, @ProtoNumber(5) val tmp_ptt: TmpPtt? = null, 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 92799001..3a1ad077 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt @@ -9,7 +9,9 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.json.JsonArray - +import kotlinx.serialization.json.JsonObject +import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment +import moe.fuqiuluo.qqinterface.servlet.msg.toJson import moe.fuqiuluo.qqinterface.servlet.msg.toListMap import moe.fuqiuluo.qqinterface.servlet.msg.toSegments import moe.fuqiuluo.shamrock.helper.ContactHelper @@ -25,10 +27,12 @@ import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher import moe.fuqiuluo.shamrock.xposed.helper.msgService import moe.fuqiuluo.symbols.decodeProtobuf import protobuf.auto.toByteArray -import protobuf.message.PushMsgBody +import protobuf.message.* import protobuf.message.longmsg.* +import java.util.* import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +import kotlin.random.Random internal object MsgSvc : BaseSvc() { private suspend fun prepareTempChatFromGroup( @@ -207,45 +211,195 @@ internal object MsgSvc : BaseSvc() { } val result = MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0)) - if (result.isFailure) { - LogCenter.log("sendToAio: " + result.exceptionOrNull()?.stackTraceToString(), Level.ERROR) - return result - } - val sendResult = result.getOrThrow() - return if (sendResult.isTimeout) { + .getOrElse { return Result.failure(it) } + return if (result.isTimeout) { // 发送失败,可能网络问题出现红色感叹号,重试 // 例如 rich media transfer failed delay(100) - MessageHelper.resendMsg(chatType, peedId, fromId, sendResult.qqMsgId, retryCnt, sendResult.msgHashId) + MessageHelper.resendMsg(chatType, peedId, fromId, result.qqMsgId, retryCnt, result.msgHashId) } else { - result + Result.success(result) } } suspend fun uploadMultiMsg( - uid: String, - groupUin: String?, - messages: List<PushMsgBody>, - ): Result<String> { + chatType: Int, + peerId: String, + fromId: String, + messages: JsonArray, + retryCnt: Int, + ): Result<MessageSegment> { + var i = -1 + val desc = MutableList(messages.size) { "" } + val forwardMsg = mutableMapOf<String, String>() + + val msgs = messages.mapNotNull { msg -> + kotlin.runCatching { + val data = msg.asJsonObject["data"].asJsonObject + if (data.containsKey("id")) { + val record = getMsg(data["id"].asInt).getOrElse { + error("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it") + } + PushMsgBody( + msgHead = ResponseHead( + peerUid = record.senderUid, + receiverUid = record.peerUid, + forward = ResponseForward( + friendName = record.sendNickName + ), + responseGrp = if (record.chatType == MsgConstant.KCHATTYPEGROUP) ResponseGrp( + groupCode = record.peerUin.toULong(), + memberCard = record.sendMemberName, + u1 = 2 + ) else null + ), + contentHead = ContentHead( + msgType = when (record.chatType) { + MsgConstant.KCHATTYPEC2C -> 9 + MsgConstant.KCHATTYPEGROUP -> 82 + else -> throw UnsupportedOperationException( + "Unsupported chatType: $chatType" + ) + }, + msgSubType = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null, + divSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null, + msgViaRandom = record.msgId, + sequence = record.msgSeq, // idk what this is(i++) + msgTime = record.msgTime, + u2 = 1, + u6 = 0, + u7 = 0, + msgSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) record.msgSeq else null, // seq for dm + forwardHead = ForwardHead( + u1 = 0, + u2 = 0, + u3 = 0, + ub641 = "", + avatar = "" + ) + ), + body = MsgBody( + richText = MessageHelper.messageArrayToRichText( + record.chatType, + record.msgId, + record.peerUin.toString(), + record.elements.toSegments( + record.chatType, + record.peerUin.toString(), + "0" + ).onEach { segment -> + if (segment.type == "forward") + forwardMsg[segment.data["filename"] as String] = + segment.data["id"] as String + }.toJson() + ).getOrElse { throw Exception("消息合成失败: $it") }.let { + desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": " + it.first + it.second + } + ) + ) + } else if (data.containsKey("content")) { + PushMsgBody( + msgHead = ResponseHead( + peer = data["uin"]?.asLong ?: TicketSvc.getUin().toLong(), + peerUid = data["uid"]?.asString ?: TicketSvc.getUid(), + receiverUid = TicketSvc.getUid(), + forward = ResponseForward( + friendName = data["name"]?.asStringOrNull ?: TicketSvc.getNickname() + ) + ), + contentHead = ContentHead( + msgType = 9, + msgSubType = 175, + divSeq = 175, + msgViaRandom = Random.nextLong(), + sequence = data["seq"]?.asLong ?: Random.nextLong(), + msgTime = data["time"]?.asLong ?: (System.currentTimeMillis() / 1000), + u2 = 1, + u6 = 0, + u7 = 0, + msgSeq = data["seq"]?.asLong ?: Random.nextLong(), + forwardHead = ForwardHead( + u1 = 0, + u2 = 0, + u3 = 2, + ub641 = "", + avatar = "" + ) + ), + body = MsgBody( + richText = MessageHelper.messageArrayToRichText( + chatType = chatType, + msgId = Random.nextLong(), + peerId = data["uin"]?.asString ?: TicketSvc.getUin(), + messageList = when (data["content"]) { + is JsonObject -> listOf(data["content"] as JsonObject).json + is JsonArray -> data["content"] as JsonArray + else -> MessageHelper.decodeCQCode(data["content"].asString) + }.onEach { element -> + val elementData = element.asJsonObject["data"].asJsonObject + if (element.asJsonObject["type"].asString == "forward") + forwardMsg[elementData["filename"].asString] = + elementData["id"].asString + } + ).getOrElse { throw Exception("消息合成失败: $it") }.let { + desc[++i] = + (data["name"].asStringOrNull ?: data["uin"].asStringOrNull + ?: TicketSvc.getNickname()) + ": " + it.first + it.second + } + ) + ) + } else { + error("消息节点缺少id或content字段") + } + }.getOrElse { + LogCenter.log("消息节点解析失败:$it", Level.WARN) + null + } + }.ifEmpty { return Result.failure(Exception("消息节点为空")) } + val payload = LongMsgPayload( - action = listOf( + action = mutableListOf( LongMsgAction( command = "MultiMsg", data = LongMsgContent( - body = messages + body = msgs ) ) - ) + ).apply { + forwardMsg.map { msg -> + addAll(getMultiMsg(msg.value).getOrElse { return Result.failure(Exception("无法获取嵌套转发消息: $it")) } + .map { action -> + if (action.command == "MultiMsg") LongMsgAction( + command = msg.key, + data = action.data + ) else action + }) + } + } ) LogCenter.log(payload.toByteArray().toHexString(), Level.DEBUG) val req = LongMsgReq( - sendInfo = SendLongMsgInfo( - type = if (groupUin == null) 1 else 3, - uid = LongMsgUid(groupUin ?: uid), - groupUin = groupUin?.toInt(), - payload = DeflateTools.gzip(payload.toByteArray()) - ), + sendInfo = when (chatType) { + MsgConstant.KCHATTYPEC2C -> SendLongMsgInfo( + type = 1, + uid = LongMsgUid(peerId), + payload = DeflateTools.gzip(payload.toByteArray()) + ) + + MsgConstant.KCHATTYPEGROUP -> SendLongMsgInfo( + type = 3, + uid = LongMsgUid(fromId), + groupUin = fromId.toInt(), + payload = DeflateTools.gzip(payload.toByteArray()) + ) + + else -> throw UnsupportedOperationException( + "Unsupported chatType: $chatType" + ) + }, setting = LongMsgSettings( field1 = 4, field2 = 2, @@ -253,17 +407,29 @@ internal object MsgSvc : BaseSvc() { field4 = 0 ) ) + val buffer = sendBufferAW( "trpc.group.long_msg_interface.MsgService.SsoSendLongMsg", true, req.toByteArray() ) ?: return Result.failure(Exception("unable to upload multi message")) val rsp = buffer.slice(4).decodeProtobuf<LongMsgRsp>() - return rsp.sendResult?.resId?.let { Result.success(it) } - ?: Result.failure(Exception("unable to upload multi message")) + val resId = rsp.sendResult?.resId ?: return Result.failure(Exception("unable to upload multi message")) + val filename = UUID.randomUUID().toString().uppercase() + return Result.success( + MessageSegment( + "forward", + mapOf( + "id" to resId, + "filename" to filename, + "summary" to "查看${desc.size}条转发消息", + "desc" to desc.slice(0..if (i < 3) i else 3).joinToString("\n") + ) + ) + ) } - suspend fun getMultiMsg(resId: String): Result<List<MessageDetail>> { + suspend fun getMultiMsg(resId: String): Result<List<LongMsgAction>> { val req = LongMsgReq( recvInfo = RecvLongMsgInfo( uid = LongMsgUid(TicketSvc.getUid()), @@ -284,11 +450,18 @@ internal object MsgSvc : BaseSvc() { ) ?: return Result.failure(Exception("unable to get multi message")) val rsp = buffer.slice(4).decodeProtobuf<LongMsgRsp>() val zippedPayload = DeflateTools.ungzip( - rsp.recvResult?.payload ?: return Result.failure(Exception("unable to get multi message")) + rsp.recvResult?.payload ?: return Result.failure(Exception("payload is empty")) ) LogCenter.log(zippedPayload.toHexString(), Level.DEBUG) - val payload = zippedPayload.decodeProtobuf<LongMsgPayload>() - payload.action?.forEach { + return Result.success( + zippedPayload.decodeProtobuf<LongMsgPayload>().action + ?: return Result.failure(Exception("action is empty")) + ) + } + + suspend fun getForwardMsg(resId: String): Result<List<MessageDetail>> { + val result = getMultiMsg(resId).getOrElse { return Result.failure(it) } + result.forEach { if (it.command == "MultiMsg") { return Result.success(it.data?.body?.map { msg -> val chatType = @@ -296,27 +469,29 @@ internal object MsgSvc : BaseSvc() { MessageDetail( time = msg.contentHead?.msgTime?.toInt() ?: 0, msgType = MessageHelper.obtainDetailTypeByMsgType(chatType), - msgId = 0, // MessageHelper.generateMsgIdHash(chatType, msg.content!!.msgViaRandom), msgViaRandom 为空 + msgId = 0, // msgViaRandom为空 tx不给 + qqMsgId = 0, msgSeq = msg.contentHead!!.msgSeq ?: 0, realId = msg.contentHead!!.msgSeq ?: 0, sender = MessageSender( msg.msgHead?.peer ?: 0, - msg.msgHead?.responseGrp?.memberCard?.ifEmpty { msg.msgHead?.forward?.friendName } - ?: msg.msgHead?.forward?.friendName ?: "", + msg.msgHead?.responseGrp?.memberCard ?: msg.msgHead?.forward?.friendName ?: "", "unknown", 0, msg.msgHead?.peerUid ?: "", msg.msgHead?.peerUid ?: "" ), - message = msg.body?.richText?.elements?.toSegments(chatType, msg.msgHead?.peer.toString(), "0") - ?.toListMap() ?: emptyList(), + message = msg.body?.richText?.toSegments( + chatType, + msg.msgHead?.peer.toString(), + "0" + )?.toListMap() ?: emptyList(), peerId = msg.msgHead?.peer ?: 0, groupId = if (chatType == MsgConstant.KCHATTYPEGROUP) msg.msgHead?.responseGrp?.groupCode?.toLong() ?: 0 else 0, targetId = if (chatType != MsgConstant.KCHATTYPEGROUP) msg.msgHead?.peer ?: 0 else 0 ) - } - ?: return Result.failure(Exception("Msg is empty"))) + } ?: return Result.failure(Exception("Msg is empty"))) } } return Result.failure(Exception("Can't find msg")) diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt index fc99577c..d74bf4e3 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt @@ -10,24 +10,18 @@ import io.ktor.utils.io.core.writeInt import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull import moe.fuqiuluo.qqinterface.servlet.msg.MessageTempHandler - import moe.fuqiuluo.shamrock.remote.action.handlers.GetHistoryMsg -import moe.fuqiuluo.shamrock.remote.service.listener.AioListener import moe.fuqiuluo.shamrock.tools.broadcast import moe.fuqiuluo.shamrock.utils.DeflateTools -import protobuf.message.element.LightAppElem -import protobuf.message.PushMsgBody -import protobuf.message.ContentHead -import protobuf.message.Elem -import protobuf.message.RichText -import protobuf.message.ResponseHead -import protobuf.message.MsgBody -import protobuf.push.MessagePush import mqq.app.MobileQQ import protobuf.auto.toByteArray +import protobuf.message.* +import protobuf.message.element.LightAppElem +import protobuf.push.MessagePush import kotlin.coroutines.resume +import kotlin.text.toByteArray -internal object PacketSvc: BaseSvc() { +internal object PacketSvc : BaseSvc() { /** * 伪造收到Json卡片消息 */ @@ -36,7 +30,7 @@ internal object PacketSvc: BaseSvc() { listOf( Elem( lightApp = LightAppElem((byteArrayOf(1) + DeflateTools.compress(content.toByteArray()))) - ) + ) ) } } @@ -44,7 +38,12 @@ internal object PacketSvc: BaseSvc() { private suspend fun fakeReceiveSelfMsg(msgService: IKernelMsgService, builder: () -> List<Elem>): Long { val latestMsg = withTimeoutOrNull(3000) { suspendCancellableCoroutine { - msgService.getMsgs(Contact(MsgConstant.KCHATTYPEC2C, app.currentUid, ""), 0L, 1, true) { code, why, msgs -> + msgService.getMsgs( + Contact(MsgConstant.KCHATTYPEC2C, app.currentUid, ""), + 0L, + 1, + true + ) { code, why, msgs -> it.resume(GetHistoryMsg.GetMsgResult(code, why, msgs)) } } @@ -72,9 +71,11 @@ internal object PacketSvc: BaseSvc() { u4 = msgSeq - 2, u5 = msgSeq ), - body = MsgBody(RichText( - elements = builder() - )) + body = MsgBody( + RichText( + elements = builder() + ) + ) ) ) diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt index 751ac028..9f647ac7 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt @@ -1,21 +1,46 @@ package moe.fuqiuluo.qqinterface.servlet.msg +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgElement import moe.fuqiuluo.qqinterface.servlet.msg.converter.ElemConverter import moe.fuqiuluo.qqinterface.servlet.msg.converter.NtMsgElementConverter +import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.MessageHelper +import moe.fuqiuluo.shamrock.tools.toHexString import protobuf.message.Elem +import protobuf.message.RichText -@JvmName("elemListToSegments") -internal suspend fun List<Elem>.toSegments( +@JvmName("richTextToSegments") +internal suspend fun RichText.toSegments( chatType: Int, peerId: String, subPeer: String ): List<MessageSegment> { val messageData = arrayListOf<MessageSegment>() - this.forEach { msg -> + if (ptt != null) { + val md5 = ptt!!.fileMd5!! + messageData.add( + MessageSegment( + "record", mapOf( + "file" to md5.toHexString(), + "url" to when (chatType) { + MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", ptt!!.fileUuid!!) + MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl( + "0", + md5, + ptt!!.groupFileKey!! + ) + + else -> throw UnsupportedOperationException("Not supported chat type: $chatType") + }, + "magic" to ptt!!.pbReserve?.magic, + ) + ) + ) + } + elements?.forEach { msg -> kotlin.runCatching { val elementType = if (msg.text != null) { 1 @@ -29,7 +54,7 @@ internal suspend fun List<Elem>.toSegments( 37 } else if (msg.srcMsg != null) { 45 - } else if (msg.lightApp != null) { + } else if (msg.lightApp != null) { 51 } else if (msg.commonElem != null) { 53 diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt index cea7d17a..9f5fd5c6 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt @@ -12,8 +12,8 @@ internal data class MessageSegment( ) { fun toJson(): JsonObject { return mapOf( - "type" to type.json, - "data" to data.json + "type" to type, + "data" to data ).json } } @@ -29,6 +29,6 @@ internal fun List<MessageSegment>.toListMap(): List<Map<String, JsonElement>> { mapOf( "type" to it.type.json, "data" to it.data.json - ).json + ) } } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt index 60ae7a44..a4683efe 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt @@ -13,11 +13,13 @@ import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.db.ImageDB import moe.fuqiuluo.shamrock.helper.db.ImageMapping import moe.fuqiuluo.shamrock.helper.db.MessageDB +import moe.fuqiuluo.shamrock.tools.asJsonArray import moe.fuqiuluo.shamrock.utils.DeflateTools import moe.fuqiuluo.shamrock.tools.asJsonObject import moe.fuqiuluo.shamrock.tools.asString import moe.fuqiuluo.shamrock.tools.toHexString import moe.fuqiuluo.symbols.decodeProtobuf +import moe.fuqiuluo.shamrock.tools.slice import protobuf.message.Elem import protobuf.message.element.commelem.ButtonExtra import protobuf.message.element.commelem.MarkdownExtra @@ -335,10 +337,8 @@ internal object ElemConverter { element: Elem ): MessageSegment { val data = element.lightApp!!.data!! - val jsonStr = - (if (data[0].toInt() == 1) DeflateTools.uncompress(data.sliceArray(1 until data.size)) else data.sliceArray( - 1 until data.size - )).toString() + val jsonStr = String(if (data[0].toInt() == 1) DeflateTools.uncompress(data.slice(1)) else data.slice(1)) + LogCenter.log(jsonStr, Level.DEBUG) val json = jsonStr.asJsonObject return when (json["app"].asString) { "com.tencent.multimsg" -> { @@ -346,7 +346,10 @@ internal object ElemConverter { MessageSegment( type = "forward", data = mapOf( - "id" to info["resid"].asString + "id" to info["resid"].asString, + "filename" to info["uniseq"].asString, + "summary" to info["summary"].asString, + "desc" to info["news"].asJsonArray.joinToString("\n") { it.asJsonObject["text"].asString } ) ) } diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/NtMsgElementConverter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/NtMsgElementConverter.kt index 365c1f35..03177adb 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/NtMsgElementConverter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/NtMsgElementConverter.kt @@ -2,10 +2,6 @@ package moe.fuqiuluo.qqinterface.servlet.msg.converter import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgElement -import kotlinx.serialization.json.add -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put -import kotlinx.serialization.json.putJsonArray import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc import moe.fuqiuluo.shamrock.helper.ContactHelper @@ -15,6 +11,7 @@ import moe.fuqiuluo.shamrock.helper.MessageHelper import moe.fuqiuluo.shamrock.helper.db.ImageDB import moe.fuqiuluo.shamrock.helper.db.ImageMapping import moe.fuqiuluo.shamrock.helper.db.MessageDB +import moe.fuqiuluo.shamrock.tools.asJsonArray import moe.fuqiuluo.shamrock.tools.asJsonObject import moe.fuqiuluo.shamrock.tools.asString import moe.fuqiuluo.shamrock.tools.hex2ByteArray @@ -242,16 +239,10 @@ internal object NtMsgElementConverter { data = hashMapOf( "file" to md5, "url" to when (chatType) { - MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPttDownUrl( - "0", - record.md5HexStr, - record.fileUuid - ) - MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid) - MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl( + MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl( "0", - record.md5HexStr, + md5.hex2ByteArray(), record.fileUuid ) @@ -343,7 +334,10 @@ internal object NtMsgElementConverter { MessageSegment( type = "forward", data = mapOf( - "id" to info["resid"].asString + "id" to info["resid"].asString, + "filename" to info["uniseq"].asString, + "summary" to info["summary"].asString, + "desc" to info["news"].asJsonArray.joinToString("\n") { it.asJsonObject["text"].asString } ) ) } diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/ElemMaker.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/ElemMaker.kt index 6c82cc10..441da395 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/ElemMaker.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/ElemMaker.kt @@ -13,64 +13,84 @@ import moe.fuqiuluo.qqinterface.servlet.msg.toJson import moe.fuqiuluo.qqinterface.servlet.msg.toSegments import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc import moe.fuqiuluo.shamrock.helper.* -import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToMessageElements +import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToRichText +import moe.fuqiuluo.shamrock.helper.MessageHelper.obtainMessageTypeByDetailType import moe.fuqiuluo.shamrock.tools.* import moe.fuqiuluo.shamrock.utils.DeflateTools import moe.fuqiuluo.shamrock.utils.FileUtils import protobuf.auto.toByteArray import protobuf.message.Elem +import protobuf.message.RichText import protobuf.message.element.* import protobuf.message.element.commelem.* import java.io.File import java.nio.ByteBuffer +import java.util.* import kotlin.random.Random import kotlin.random.nextULong import kotlin.time.Duration.Companion.seconds -internal typealias IElemMaker = suspend (Int, Long, String, JsonObject) -> Result<Elem> +internal typealias IElemMaker = suspend (ElemMaker, Int, Long, String, JsonObject) -> Unit -internal object ElemMaker { - private val makerArray = hashMapOf( - "text" to ElemMaker::createTextElem, - "at" to ElemMaker::createAtElem, - "face" to ElemMaker::createFaceElem, - "pic" to ElemMaker::createImageElem, - "image" to ElemMaker::createImageElem, +internal class ElemMaker { + companion object { + private val makerArray = hashMapOf( + "text" to ElemMaker::createTextElem, + "at" to ElemMaker::createAtElem, + "face" to ElemMaker::createFaceElem, + "pic" to ElemMaker::createImageElem, + "image" to ElemMaker::createImageElem, // "voice" to ElemMaker::createRecordElem, // "record" to ElemMaker::createRecordElem, // "video" to ElemMaker::createVideoElem, - "markdown" to ElemMaker::createMarkdownElem, - "button" to ElemMaker::createButtonElem, - "dice" to ElemMaker::createNewDiceElem, - "rps" to ElemMaker::createNewRpsElem, - "poke" to ElemMaker::createPokeElem, + "forward" to ElemMaker::createForwardStruct, + "json" to ElemMaker::createJsonElem, + "poke" to ElemMaker::createPokeElem, + "dice" to ElemMaker::createNewDiceElem, + "rps" to ElemMaker::createNewRpsElem, + "markdown" to ElemMaker::createMarkdownElem, + "button" to ElemMaker::createButtonElem, // "anonymous" to ElemMaker::createAnonymousElem, // "share" to ElemMaker::createShareElem, // "contact" to ElemMaker::createContactElem, // "location" to ElemMaker::createLocationElem, // "music" to ElemMaker::createMusicElem, - "reply" to ElemMaker::createReplyElem, + "reply" to ElemMaker::createReplyElem, // "touch" to ElemMaker::createTouchElem, - "weather" to ElemMaker::createWeatherElem, - "json" to ElemMaker::createJsonElem, - //"forward" to MessageMaker::createForwardElem, - //"multi_msg" to MessageMaker::createLongMsgStruct, - //"bubble_face" to ElemMaker::createBubbleFaceElem, - ) + "weather" to ElemMaker::createWeatherElem, + //"forward" to MessageMaker::createForwardElem, + //"multi_msg" to MessageMaker::createLongMsgStruct, + //"bubble_face" to ElemMaker::createBubbleFaceElem, + ) + + operator fun get(type: String): IElemMaker? = makerArray[type] + } - operator fun get(type: String): IElemMaker? = makerArray[type] + private var rich = RichText() + private val elems = mutableListOf<Elem>() + private var desc = "" + + fun getRich(): RichText { + rich.elements = elems + return rich + } + + fun getDesc(): String { + return desc + } private suspend fun createTextElem( chatType: Int, msgId: Long, peerId: String, data: JsonObject - ): Result<Elem> { + ) { data.checkAndThrow("text") val elem = Elem( text = TextMsg(data["text"].asString) ) - return Result.success(elem) + elems.add(elem) + desc += data["text"].asString } private suspend fun createAtElem( @@ -78,8 +98,8 @@ internal object ElemMaker { msgId: Long, peerId: String, data: JsonObject - ): Result<Elem> { - return when (chatType) { + ) { + when (chatType) { MsgConstant.KCHATTYPEGROUP -> { data.checkAndThrow("qq") @@ -105,15 +125,14 @@ internal object ElemMaker { peerId.toLong(), qq, true - ) - .let { - val info = it.getOrNull() - if (info == null) - LogCenter.log("无法获取群成员信息: $qqStr", Level.ERROR) - info?.troopnick - .ifNullOrEmpty(info?.friendnick) - .ifNullOrEmpty(qqStr) - }) + ).let { + val info = it.getOrNull() + if (info == null) + LogCenter.log("无法获取群成员信息: $qqStr", Level.ERROR) + else info.troopnick + .ifNullOrEmpty(info.friendnick) + .ifNullOrEmpty(qqStr) + }) } } @@ -126,7 +145,8 @@ internal object ElemMaker { val elem = Elem( text = TextMsg(str = display, attr6Buf = attr6.array()) ) - Result.success(elem) + elems.add(elem) + desc += display } MsgConstant.KCHATTYPEC2C -> { @@ -144,10 +164,11 @@ internal object ElemMaker { val elem = Elem( text = TextMsg(str = display) ) - Result.success(elem) + elems.add(elem) + desc += display } - else -> Result.failure(ActionMsgException) + else -> throw UnsupportedOperationException("Unsupported chatType($chatType) for AtMsg") } } @@ -156,7 +177,7 @@ internal object ElemMaker { msgId: Long, peerId: String, data: JsonObject - ): Result<Elem> { + ) { data.checkAndThrow("id") val faceId = data["id"].asInt val elem = if (data["big"].asBooleanOrNull == true) { @@ -183,7 +204,8 @@ internal object ElemMaker { ) ) } - return Result.success(elem) + elems.add(elem) + desc += "[表情]" } private suspend fun createImageElem( @@ -191,7 +213,7 @@ internal object ElemMaker { msgId: Long, peerId: String, data: JsonObject - ): Result<Elem> { + ) { val isOriginal = data["original"].asBooleanOrNull ?: true val isFlash = data["flash"].asBooleanOrNull ?: false val filePath = data["file"].asStringOrNull @@ -307,7 +329,8 @@ internal object ElemMaker { else -> throw LogicException("Not supported chatType($chatType) for PictureMsg") } - return Result.success(elem) + elems.add(elem) + desc += "[图片]" } private suspend fun createReplyElem( @@ -315,16 +338,15 @@ internal object ElemMaker { msgId: Long, peerId: String, data: JsonObject - ): Result<Elem> { + ) { data.checkAndThrow("id") val msgHash = data["id"].asInt val mapping = MessageHelper.getMsgMappingByHash(msgHash) - ?: return Result.failure(Exception("不存在该消息映射,无法回复消息")) + ?: throw Exception("不存在该消息映射,无法回复消息") if (mapping.qqMsgId == 0L) { // 貌似获取失败了,555 - LogCenter.log("无法获取被回复消息", Level.ERROR) - return Result.failure(Exception("无法获取被回复消息")) + throw Exception("无法获取被回复消息") } val elem = if (data.containsKey("text")) { @@ -351,16 +373,15 @@ internal object ElemMaker { ) } else { val msg = - MsgSvc.getMsgByQMsgId(chatType, mapping.peerId, mapping.qqMsgId).getOrNull() ?: return Result.failure( - Exception("无法获取被回复消息") - ) + MsgSvc.getMsgByQMsgId(chatType, mapping.peerId, mapping.qqMsgId).getOrNull() + ?: throw Exception("无法获取被回复消息") Elem( srcMsg = SourceMsg( origSeqs = listOf(msg.msgSeq.toInt()), senderUin = msg.senderUin.toULong(), time = msg.msgTime.toULong(), flag = 1u, - elems = messageArrayToMessageElements( + elems = messageArrayToRichText( msg.chatType, msg.msgId, msg.peerUin.toString(), @@ -369,7 +390,7 @@ internal object ElemMaker { if (msg.chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(), msg.channelId ?: msg.peerUin.toString() ).toJson() - ).second, + ).getOrElse { throw Exception("解析回复消息失败: $it") }.second.elements, type = 0u, pbReserve = SourceMsg.Companion.PbReserve( msgRand = Random.nextULong(), @@ -380,7 +401,8 @@ internal object ElemMaker { ) ) } - return Result.success(elem) + elems.add(elem) + desc += "[回复消息]" } private suspend fun createJsonElem( @@ -388,15 +410,82 @@ internal object ElemMaker { msgId: Long, peerId: String, data: JsonObject - ): Result<Elem> { + ) { data.checkAndThrow("data") val elem = Elem( lightApp = LightAppElem( - data = DeflateTools.compress(data.toString().toByteArray()) + data = byteArrayOf(1) + DeflateTools.compress(data.toString().toByteArray()) + ) + ) + elems.add(elem) + desc += "[Json消息]" + } + + private suspend fun createForwardStruct( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ) { + data.checkAndThrow("id") + val resId = data["id"].asString + val filename = data["filename"].asStringOrNull ?: UUID.randomUUID().toString().uppercase() + var summary = data["summary"].asStringOrNull + val descriptions = data["desc"].asStringOrNull + var news = descriptions?.split("\n")?.map { "text" to it } + + if (news == null || summary == null) { + val forwardMsg = MsgSvc.getForwardMsg(resId).getOrThrow() + if (news == null) { + news = forwardMsg.map { + "text" to it.sender.nickName + ": " + messageArrayToRichText( + obtainMessageTypeByDetailType(it.msgType), + it.qqMsgId, + it.peerId.toString(), + it.message.json + ).getOrThrow().first + } + } + if (summary == null) { + summary = "查看${forwardMsg.size}条转发消息" + } + } + + val json = mapOf( + "app" to "com.tencent.multimsg", + "config" to mapOf( + "autosize" to 1, + "forward" to 1, + "round" to 1, + "type" to "normal", + "width" to 300 + ), + "desc" to "[聊天记录]", + "extra" to mapOf( + "filename" to filename, + "tsum" to 2 + ).json.toString(), + "meta" to mapOf( + "detail" to mapOf( + "news" to news, + "resid" to resId, + "source" to "群聊的聊天记录", + "summary" to summary, + "uniseq" to filename + ) + ), + "prompt" to "[聊天记录]", + "ver" to "0.0.0.5", + "view" to "contact" + ) + val elem = Elem( + lightApp = LightAppElem( + data = byteArrayOf(1) + DeflateTools.compress(json.json.toString().toByteArray()) ) ) - return Result.success(elem) + elems.add(elem) + desc += "[聊天记录]" } private suspend fun createWeatherElem( @@ -404,7 +493,7 @@ internal object ElemMaker { msgId: Long, peerId: String, data: JsonObject - ): Result<Elem> { + ) { var code = data["code"].asIntOrNull if (code == null) { @@ -416,19 +505,22 @@ internal object ElemMaker { } if (code != null) { - WeatherSvc.fetchWeatherCard(code).onSuccess { -// OidbSvc.0xdc2_34 -// 00 00 00 DF 08 C2 1B 10 22 22 C4 01 0A B7 01 08 A2 E0 F2 2F 10 01 18 00 2A 02 08 01 58 FB 91 F6 AE 02 62 A1 01 08 01 52 08 E5 8C 97 E4 BA AC 20 20 5A 19 2D 33 C2 B0 2F 33 C2 B0 0A E7 A9 BA E6 B0 94 E8 B4 A8 E9 87 8F 3A E8 89 AF 62 11 5B E5 88 86 E4 BA AB 5D 20 E5 8C 97 E4 BA AC 20 20 6A 25 68 74 74 70 73 3A 2F 2F 77 65 61 74 68 65 72 2E 6D 70 2E 71 71 2E 63 6F 6D 2F 3F 73 74 3D 30 26 5F 77 76 3D 31 72 3E 68 74 74 70 73 3A 2F 2F 69 6D 67 63 61 63 68 65 2E 71 71 2E 63 6F 6D 2F 61 63 2F 71 71 77 65 61 74 68 65 72 2F 69 6D 61 67 65 2F 73 68 61 72 65 5F 69 63 6F 6E 2F 66 69 6E 65 2E 70 6E 67 12 08 08 01 10 FB 91 F6 AE 02 32 0D 61 6E 64 72 6F 69 64 20 39 2E 30 2E 38 - return createJsonElem( - chatType, msgId, peerId, it["weekStore"] - .asJsonObject["share"].asJsonObject + val weatherCard = WeatherSvc.fetchWeatherCard(code).getOrThrow() +// OidbSvc.0xdc2_34 +// 00 00 00 DF 08 C2 1B 10 22 22 C4 01 0A B7 01 08 A2 E0 F2 2F 10 01 18 00 2A 02 08 01 58 FB 91 F6 AE 02 62 A1 01 08 01 52 08 E5 8C 97 E4 BA AC 20 20 5A 19 2D 33 C2 B0 2F 33 C2 B0 0A E7 A9 BA E6 B0 94 E8 B4 A8 E9 87 8F 3A E8 89 AF 62 11 5B E5 88 86 E4 BA AB 5D 20 E5 8C 97 E4 BA AC 20 20 6A 25 68 74 74 70 73 3A 2F 2F 77 65 61 74 68 65 72 2E 6D 70 2E 71 71 2E 63 6F 6D 2F 3F 73 74 3D 30 26 5F 77 76 3D 31 72 3E 68 74 74 70 73 3A 2F 2F 69 6D 67 63 61 63 68 65 2E 71 71 2E 63 6F 6D 2F 61 63 2F 71 71 77 65 61 74 68 65 72 2F 69 6D 61 67 65 2F 73 68 61 72 65 5F 69 63 6F 6E 2F 66 69 6E 65 2E 70 6E 67 12 08 08 01 10 FB 91 F6 AE 02 32 0D 61 6E 64 72 6F 69 64 20 39 2E 30 2E 38 + val elem = Elem( + lightApp = LightAppElem( + data = byteArrayOf(1) + DeflateTools.compress( + weatherCard["weekStore"] + .asJsonObject["share"].asString.toByteArray() + ) ) - }.onFailure { - LogCenter.log("无法发送天气分享", Level.ERROR) - } + ) + elems.add(elem) + desc += "[天气卡片]" + } else { + throw LogicException("无法获取城市天气") } - - return Result.failure(ActionMsgException) } private suspend fun createPokeElem( @@ -436,7 +528,7 @@ internal object ElemMaker { msgId: Long, peerId: String, data: JsonObject - ): Result<Elem> { + ) { data.checkAndThrow("type", "id") val elem = Elem( commonElem = CommonElem( @@ -449,7 +541,8 @@ internal object ElemMaker { businessType = data["id"].asInt ) ) - return Result.success(elem) + elems.add(elem) + desc += "[戳一戳]" } private suspend fun createNewDiceElem( @@ -457,7 +550,7 @@ internal object ElemMaker { msgId: Long, peerId: String, data: JsonObject - ): Result<Elem> { + ) { val elem = Elem( commonElem = CommonElem( serviceType = 37, @@ -474,7 +567,8 @@ internal object ElemMaker { businessType = 2 ) ) - return Result.success(elem) + elems.add(elem) + desc += "[骰子]" } private suspend fun createNewRpsElem( @@ -482,7 +576,7 @@ internal object ElemMaker { msgId: Long, peerId: String, data: JsonObject - ): Result<Elem> { + ) { val elem = Elem( commonElem = CommonElem( serviceType = 37, @@ -499,7 +593,8 @@ internal object ElemMaker { businessType = 1 ) ) - return Result.success(elem) + elems.add(elem) + desc += "[包剪锤]" } private suspend fun createMarkdownElem( @@ -507,7 +602,7 @@ internal object ElemMaker { msgId: Long, peerId: String, data: JsonObject - ): Result<Elem> { + ) { data.checkAndThrow("content") val elem = Elem( commonElem = CommonElem( @@ -516,7 +611,8 @@ internal object ElemMaker { businessType = 1 ) ) - return Result.success(elem) + elems.add(elem) + desc += "[Markdown消息]" } private suspend fun createButtonElem( @@ -524,7 +620,7 @@ internal object ElemMaker { msgId: Long, peerId: String, data: JsonObject - ): Result<Elem> { + ) { data.checkAndThrow("buttons") val elem = Elem( commonElem = CommonElem( @@ -565,7 +661,8 @@ internal object ElemMaker { businessType = 1 ) ) - return Result.success(elem) + elems.add(elem) + desc += "[Button消息]" } private fun JsonObject.checkAndThrow(vararg key: String) { diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt index e09a97cf..4699dc8c 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt @@ -16,6 +16,7 @@ import kotlinx.serialization.json.JsonPrimitive import moe.fuqiuluo.qqinterface.servlet.CardSvc import moe.fuqiuluo.qqinterface.servlet.GroupSvc import moe.fuqiuluo.qqinterface.servlet.LbsSvc +import moe.fuqiuluo.qqinterface.servlet.MsgSvc import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc @@ -51,6 +52,8 @@ import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77 import tencent.im.oidb.cmd0xdc2.oidb_cmd0xdc2 import tencent.im.oidb.oidb_sso import java.io.File +import java.util.* +import kotlin.collections.ArrayList import kotlin.math.roundToInt import kotlin.random.Random import kotlin.random.nextInt @@ -80,10 +83,11 @@ internal object NtMsgElementMaker { "touch" to NtMsgElementMaker::createTouchElem, "weather" to NtMsgElementMaker::createWeatherElem, "json" to NtMsgElementMaker::createJsonElem, + "forward" to NtMsgElementMaker::createForwardStruct, "new_dice" to NtMsgElementMaker::createNewDiceElem, "new_rps" to NtMsgElementMaker::createNewRpsElem, "basketball" to NtMsgElementMaker::createBasketballElem, - //"multi_msg" to MessageMaker::createLongMsgStruct, + //"multi_msg" to NtMsgElementMaker::createLongMsgStruct, "bubble_face" to NtMsgElementMaker::createBubbleFaceElem, "button" to NtMsgElementMaker::createInlineKeywordElem, "inline_keyboard" to NtMsgElementMaker::createInlineKeywordElem @@ -91,6 +95,70 @@ internal object NtMsgElementMaker { operator fun get(type: String): IMsgElementMaker? = makerMap[type] + private suspend fun createForwardStruct( + chatType: Int, + msgId: Long, + peerId: String, + data: JsonObject + ): Result<MsgElement> { + data.checkAndThrow("id") + val resId = data["id"].asString + val filename = data["filename"].asStringOrNull ?: UUID.randomUUID().toString().uppercase() + var summary = data["summary"].asStringOrNull + val descriptions = data["desc"].asStringOrNull + var news = descriptions?.split("\n")?.map { "text" to it } + + if (news == null || summary == null) { + val forwardMsg = MsgSvc.getForwardMsg(resId).getOrElse { return Result.failure(it) } + if (news == null) { + news = forwardMsg.map { + "text" to it.sender.nickName + ": " + MessageHelper.messageArrayToRichText( + MessageHelper.obtainMessageTypeByDetailType(it.msgType), + it.qqMsgId, + it.peerId.toString(), + it.message.json + ).getOrThrow().first + } + } + if (summary == null) { + summary = "查看${forwardMsg.size}条转发消息" + } + } + + val json = mapOf( + "app" to "com.tencent.multimsg", + "config" to mapOf( + "autosize" to 1, + "forward" to 1, + "round" to 1, + "type" to "normal", + "width" to 300 + ), + "desc" to "[聊天记录]", + "extra" to mapOf( + "filename" to filename, + "tsum" to 2 + ).json.toString(), + "meta" to mapOf( + "detail" to mapOf( + "news" to news, + "resid" to resId, + "source" to "群聊的聊天记录", + "summary" to summary, + "uniseq" to filename + ) + ), + "prompt" to "[聊天记录]", + "ver" to "0.0.0.5", + "view" to "contact" + ) + return createJsonElem( + chatType, msgId, peerId, mapOf( + "data" to json + ).json + ) + } + private suspend fun createInlineKeywordElem( chatType: Int, msgId: Long, @@ -262,19 +330,21 @@ internal object NtMsgElementMaker { ): Result<MsgElement> { data.checkAndThrow("data") val jsonStr = data["data"].let { - if (it is JsonObject) it.asJsonObject.toString() else { - val str = it.asStringOrNull ?: "" + if (it is JsonObject) { + it.asJsonObject.toString() + } else { // 检查字符串是否是合法json,不然qq会闪退 try { + val str = it.asString val element = Json.decodeFromString<JsonElement>(str) if (element !is JsonObject) { return Result.failure(Exception("不合法的JSON字符串")) } + str } catch (err: Throwable) { LogCenter.log(err.stackTraceToString(), Level.ERROR) return Result.failure(Exception("不合法的JSON字符串")) } - str } } val element = MsgElement() diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt index 567a6994..f37a6cca 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt @@ -6,7 +6,6 @@ import com.tencent.mobileqq.transfile.FileMsg import com.tencent.mobileqq.transfile.api.IProtoReqManager import com.tencent.mobileqq.transfile.protohandler.RichProto import com.tencent.mobileqq.transfile.protohandler.RichProtoProc -import kotlinx.atomicfu.atomic import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.serialization.ExperimentalSerializationApi import moe.fuqiuluo.qqinterface.servlet.BaseSvc @@ -22,23 +21,9 @@ import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher import moe.fuqiuluo.symbols.decodeProtobuf import mqq.app.MobileQQ import protobuf.auto.toByteArray -import protobuf.oidb.TrpcOidb import protobuf.oidb.cmd0x11c5.C2CUserInfo import protobuf.oidb.cmd0x11c5.ChannelUserInfo -import protobuf.oidb.cmd0x11c5.ClientMeta -import protobuf.oidb.cmd0x11c5.CodecConfigReq -import protobuf.oidb.cmd0x11c5.CommonHead -import protobuf.oidb.cmd0x11c5.DownloadExt -import protobuf.oidb.cmd0x11c5.DownloadReq -import protobuf.oidb.cmd0x11c5.FileInfo -import protobuf.oidb.cmd0x11c5.FileType import protobuf.oidb.cmd0x11c5.GroupUserInfo -import protobuf.oidb.cmd0x11c5.IndexNode -import protobuf.oidb.cmd0x11c5.MultiMediaReqHead -import protobuf.oidb.cmd0x11c5.NtV2RichMediaReq -import protobuf.oidb.cmd0x11c5.NtV2RichMediaRsp -import protobuf.oidb.cmd0x11c5.SceneInfo -import protobuf.oidb.cmd0x11c5.VideoDownloadExt import protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody @@ -405,8 +390,8 @@ internal object RichProtoSvc: BaseSvc() { suspend fun getGroupPttDownUrl( peerId: String, - md5Hex: String, - fileUUId: String + md5: ByteArray, + groupFileKey: String ): String { return suspendCancellableCoroutine { val runtime = AppRuntimeFetcher.appRuntime @@ -417,8 +402,8 @@ internal object RichProtoSvc: BaseSvc() { groupPttDownReq.secondUin = peerId groupPttDownReq.uinType = FileMsg.UIN_TROOP groupPttDownReq.groupFileID = 0 - groupPttDownReq.groupFileKey = fileUUId - groupPttDownReq.md5 = md5Hex.hex2ByteArray() + groupPttDownReq.groupFileKey = groupFileKey + groupPttDownReq.md5 = md5 groupPttDownReq.voiceType = 1 groupPttDownReq.downType = 1 richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp -> 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 56a15258..0b990e48 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt @@ -21,12 +21,9 @@ import moe.fuqiuluo.qqinterface.servlet.msg.maker.NtMsgElementMaker import moe.fuqiuluo.shamrock.helper.db.MessageDB import moe.fuqiuluo.shamrock.helper.db.MessageMapping import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult -import moe.fuqiuluo.shamrock.tools.EmptyJsonObject -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 moe.fuqiuluo.shamrock.tools.* import protobuf.message.Elem +import protobuf.message.RichText import kotlin.coroutines.resume import kotlin.math.abs import kotlin.time.Duration.Companion.seconds @@ -75,7 +72,7 @@ internal object MessageHelper { } if (resendRet != 0 && resendRet != 4 // 使用OldBDH 100%触发 - ) { + ) { resendMsg(contact, msgId, retryCnt - 1, msgHashId) } else { Result.success(SendMsgResult(msgHashId, msgId, System.currentTimeMillis())) @@ -319,38 +316,30 @@ internal object MessageHelper { return hasActionMsg to msgList } - suspend fun messageArrayToMessageElements( + suspend fun messageArrayToRichText( chatType: Int, msgId: Long, peerId: String, messageList: JsonArray - ): Pair<Boolean, ArrayList<Elem>> { - val msgList = arrayListOf<Elem>() - var hasActionMsg = false - messageList.forEach { - val msg = it.jsonObject + ): Result<Pair<String, RichText>> { + val elemMaker = ElemMaker() + messageList.forEach { element -> + val msg = element.asJsonObject val maker = ElemMaker[msg["type"].asString] if (maker != null) { try { val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject - maker(chatType, msgId, peerId, data).onSuccess { msgElem -> - msgList.add(msgElem) - }.onFailure { - if (it.javaClass != ActionMsgException::class.java) { - throw it - } else { - hasActionMsg = true - } - } + maker(elemMaker, chatType, msgId, peerId, data) } catch (e: Throwable) { - LogCenter.log(e.stackTraceToString(), Level.ERROR) + if (e.javaClass != ActionMsgException::class.java) { + LogCenter.log(e.stackTraceToString(), Level.ERROR) + } } } else { LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR) - return false to arrayListOf() } } - return hasActionMsg to msgList + return Result.success(elemMaker.getDesc() to elemMaker.getRich()) } fun generateMsgIdHash(chatType: Int, msgId: Long): Int { diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetForwardMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetForwardMsg.kt index 54a2522f..f4a8d6b6 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetForwardMsg.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetForwardMsg.kt @@ -1,12 +1,10 @@ package moe.fuqiuluo.shamrock.remote.action.handlers -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement import moe.fuqiuluo.qqinterface.servlet.MsgSvc import moe.fuqiuluo.shamrock.remote.action.ActionSession import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.remote.service.data.MessageDetail +import moe.fuqiuluo.shamrock.remote.service.data.GetForwardMsgResult import moe.fuqiuluo.shamrock.tools.EmptyJsonString import moe.fuqiuluo.symbols.OneBotHandler @@ -21,14 +19,12 @@ internal object GetForwardMsg : IActionHandler() { resId: String, echo: JsonElement = EmptyJsonString ): String { - val result = MsgSvc.getMultiMsg(resId).getOrElse { return logic(it.toString(), echo) } - return ok(data = GetForwardMsgResult(result), echo = echo) + return ok( + data = GetForwardMsgResult( + msgs = MsgSvc.getForwardMsg(resId).getOrElse { return logic(it.toString(), echo = echo) }), + echo = echo + ) } - @Serializable - data class GetForwardMsgResult( - @SerialName("messages") val msgs: List<MessageDetail> - ) - override val requiredParams: Array<String> = arrayOf("id") } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt index 86432710..a587dbb0 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt @@ -69,6 +69,7 @@ internal object GetHistoryMsg : IActionHandler() { time = msg.msgTime.toInt(), msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType), msgId = msgHash, + qqMsgId = msg.msgId, msgSeq = msg.msgSeq, realId = msg.msgSeq, sender = MessageSender( @@ -92,6 +93,7 @@ internal object GetHistoryMsg : IActionHandler() { time = msg.msgTime.toInt(), msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType), msgId = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId), + qqMsgId = msg.msgId, msgSeq = msg.msgSeq, realId = msg.msgSeq, sender = MessageSender( diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetMsg.kt index 7657cafe..c7dfbe89 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetMsg.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetMsg.kt @@ -29,6 +29,7 @@ internal object GetMsg: IActionHandler() { time = msg.msgTime.toInt(), msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType), msgId = msgHash, + qqMsgId = msg.msgId, msgSeq = msg.msgSeq, realId = msg.msgSeq, sender = MessageSender( 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 069649c6..7acf84d9 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 @@ -3,20 +3,14 @@ package moe.fuqiuluo.shamrock.remote.action.handlers import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import kotlinx.serialization.json.* import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.qqinterface.servlet.TicketSvc -import moe.fuqiuluo.qqinterface.servlet.msg.toSegments -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.qqinterface.servlet.msg.toJson import moe.fuqiuluo.shamrock.helper.MessageHelper import moe.fuqiuluo.shamrock.helper.ParamsException 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.remote.service.data.SendForwardMessageResult import moe.fuqiuluo.shamrock.tools.* import moe.fuqiuluo.symbols.OneBotHandler -import protobuf.message.* -import java.util.* -import kotlin.random.Random @OneBotHandler("send_forward_msg") internal object SendForwardMessage : IActionHandler() { @@ -60,9 +54,10 @@ internal object SendForwardMessage : IActionHandler() { else -> error("unknown chat type: $chatType") }.toString() + val retryCnt = session.getIntOrNull("retry_cnt") ?: 5 return if (session.isArray("messages")) { val messages = session.getArray("messages") - invoke(chatType, peerId, messages, fromId, echo = session.echo) + invoke(chatType, peerId, fromId, messages, retryCnt, session.echo) } else { logic("未知格式合并转发消息", session.echo) } @@ -76,222 +71,22 @@ internal object SendForwardMessage : IActionHandler() { suspend operator fun invoke( chatType: Int, peerId: String, - messages: JsonArray, fromId: String = peerId, + messages: JsonArray, + retryCnt: Int, echo: JsonElement = EmptyJsonString ): String { - var uid: String? = null - var groupUin: String? = null - - var i = -1 - val desc = MutableList(messages.size) { "" } - - val msgs = messages.map { msg -> - kotlin.runCatching { - val data = msg.asJsonObject["data"].asJsonObject - if (data.containsKey("id")) { - val record = MsgSvc.getMsg(data["id"].asInt).getOrElse { - error("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it") - } - if (record.chatType == MsgConstant.KCHATTYPEGROUP) groupUin = record.peerUin.toString() - if (record.chatType == MsgConstant.KCHATTYPEC2C) uid = record.peerUid - PushMsgBody( - msgHead = ResponseHead( - peerUid = record.senderUid, - receiverUid = record.peerUid, - forward = ResponseForward( - friendName = record.sendNickName - ), - responseGrp = if (record.chatType == MsgConstant.KCHATTYPEGROUP) ResponseGrp( - groupCode = record.peerUin.toULong(), - memberCard = record.sendMemberName, - u1 = 2 - ) else null - ), - contentHead = ContentHead( - msgType = when (record.chatType) { - MsgConstant.KCHATTYPEC2C -> 9 - MsgConstant.KCHATTYPEGROUP -> 82 - else -> throw UnsupportedOperationException( - "Unsupported chatType: $chatType" - ) - }, - msgSubType = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null, - divSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null, - msgViaRandom = record.msgId, - sequence = record.msgSeq, // idk what this is(i++) - msgTime = record.msgTime, - u2 = 1, - u6 = 0, - u7 = 0, - msgSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) record.msgSeq else null, // seq for dm - forwardHead = ForwardHead( - u1 = 0, - u2 = 0, - u3 = 0, - ub641 = "", - avatar = "" - ) - ), - body = MsgBody( - richText = RichText( - elements = MessageHelper.messageArrayToMessageElements( - record.chatType, - record.msgId, - record.peerUin.toString(), - record.elements.toSegments( - record.chatType, - record.peerUin.toString(), - "0" - ).also { - desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": " - }.map { - desc[i] += when (it.type) { - "text" -> it.data["text"] as String - "at" -> "@${it.data["name"] as String? ?: it.data["qq"] as String}" - "face" -> "[表情]" - "pic", "image" -> "[图片]" - "voice", "record" -> "[语音]" - "video" -> "[视频]" - "node" -> "[合并转发消息]" - "markdown" -> "[Markdown消息]" - "button" -> "[Button类型]" - else -> "[未知消息类型]" - } - it.toJson() - }.json - ).also { - if (it.second.isEmpty() && !it.first) - error("消息合成失败,请查看日志或者检查输入。") - }.second - ) - ) - ) - } else if (data.containsKey("content")) { - PushMsgBody( - msgHead = ResponseHead( - peer = data["uin"]?.asLong ?: TicketSvc.getUin().toLong(), - peerUid = data["uid"]?.asString ?: TicketSvc.getUid(), - receiverUid = TicketSvc.getUid(), - forward = ResponseForward( - friendName = data["name"]?.asStringOrNull ?: TicketSvc.getNickname() - ) - ), - contentHead = ContentHead( - msgType = 9, - msgSubType = 175, - divSeq = 175, - msgViaRandom = Random.nextLong(), - sequence = data["seq"]?.asLong ?: Random.nextLong(), - msgTime = data["time"]?.asLong ?: (System.currentTimeMillis() / 1000), - u2 = 1, - u6 = 0, - u7 = 0, - msgSeq = data["seq"]?.asLong ?: Random.nextLong(), - forwardHead = ForwardHead( - u1 = 0, - u2 = 0, - u3 = 2, - ub641 = "", - avatar = "" - ) - ), - body = MsgBody( - richText = RichText( - elements = MessageHelper.messageArrayToMessageElements( - chatType = chatType, - msgId = Random.nextLong(), - peerId = data["uin"]?.asString ?: TicketSvc.getUin(), - messageList = 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 { - val type = it.asJsonObject["type"].asString - val itData = it.asJsonObject["data"].asJsonObject - desc[i] += when (type) { - "text" -> itData["text"].asString - "at" -> "@${itData["name"].asStringOrNull ?: itData["qq"].asString}" - "face" -> "[表情]" - "pic", "image" -> "[图片]" - "voice", "record" -> "[语音]" - "video" -> "[视频]" - "node" -> "[合并转发消息]" - "markdown" -> "[Markdown消息]" - "button" -> "[Button类型]" - else -> "[未知消息类型]" - } - } - ).also { - if (it.second.isEmpty() && !it.first) - error("消息合成失败,请查看日志或者检查输入。") - }.second - ) - ) - ) - } else { - error("消息节点缺少id或content字段") - } - }.getOrElse { - LogCenter.log("消息节点解析失败:$it", Level.WARN) - null - } - }.filterNotNull().ifEmpty { return logic("消息节点为空", echo) } - - kotlin.runCatching { - val resid = MsgSvc.uploadMultiMsg(uid ?: TicketSvc.getUid(), groupUin, msgs) + val message = MsgSvc.uploadMultiMsg(chatType, peerId, fromId, messages, retryCnt) .getOrElse { return logic(it.message ?: "", echo) } - val uniseq = UUID.randomUUID().toString().uppercase() - val result = MsgSvc.sendToAio( - chatType, peerId, - listOf( - mapOf( - "type" to "json", - "data" to mapOf( - "data" to mapOf( - "app" to "com.tencent.multimsg", - "config" to mapOf( - "autosize" to 1, - "forward" to 1, - "round" to 1, - "type" to "normal", - "width" to 300 - ), - "desc" to "[聊天记录]", - "extra" to mapOf( - "filename" to uniseq, - "tsum" to 2 - ).json.toString(), - "meta" to mapOf( - "detail" to mapOf( - "news" to desc.slice(0..if (i < 3) i else 3) - .map { mapOf("text" to it) }, - "resid" to resid, - "source" to "群聊的聊天记录", - "summary" to "查看${msgs.size}条转发消息", - "uniseq" to uniseq - ) - ), - "prompt" to "[聊天记录]", - "ver" to "0.0.0.5", - "view" to "contact" - ), - "resid" to resid - ) - ) - ).json, fromId, 3 - ).getOrElse { return logic(it.message ?: "", echo) } + val result = MsgSvc.sendToAio(chatType, peerId, listOf(message).toJson(), fromId, retryCnt) + .getOrElse { return logic(it.message ?: "", echo) } return ok( - ForwardMessageResult( + SendForwardMessageResult( msgId = result.msgHashId, - forwardId = resid + resId = message.data["id"] as String ), echo = echo ) }.onFailure { diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMessage.kt index 5ddbe16c..bac2caa5 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMessage.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMessage.kt @@ -6,12 +6,19 @@ import moe.fuqiuluo.shamrock.remote.action.IActionHandler import moe.fuqiuluo.symbols.OneBotHandler @OneBotHandler("send_group_forward_msg") -internal object SendGroupForwardMessage: IActionHandler() { +internal object SendGroupForwardMessage : IActionHandler() { override suspend fun internalHandle(session: ActionSession): String { val groupId = session.getLong("group_id") + val retryCnt = session.getIntOrNull("retry_cnt") ?: 5 return if (session.isArray("messages")) { val messages = session.getArray("messages") - SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId.toString(), messages, echo = session.echo) + SendForwardMessage( + MsgConstant.KCHATTYPEGROUP, + groupId.toString(), + messages = messages, + retryCnt = retryCnt, + echo = session.echo + ) } else { logic("未知格式合并转发消息", session.echo) } 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 4c2c4367..6c9ae7cd 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 @@ -21,7 +21,7 @@ import moe.fuqiuluo.shamrock.tools.jsonArray import moe.fuqiuluo.symbols.OneBotHandler @OneBotHandler("send_msg", ["send_message"]) -internal object SendMessage: IActionHandler() { +internal object SendMessage : IActionHandler() { override suspend fun internalHandle(session: ActionSession): String { val detailType = session.getStringOrNull("detail_type") ?: session.getStringOrNull("message_type") try { @@ -40,28 +40,61 @@ internal object SendMessage: IActionHandler() { return noParam("detail_type/message_type", session.echo) } } - val peerId = when(chatType) { - MsgConstant.KCHATTYPEGROUP -> session.getLongOrNull("group_id") ?: return noParam("group_id", session.echo) - MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("user_id") ?: return noParam("user_id", session.echo) + val peerId = when (chatType) { + MsgConstant.KCHATTYPEGROUP -> session.getLongOrNull("group_id") ?: return noParam( + "group_id", + session.echo + ) + + MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("user_id") + ?: return noParam("user_id", session.echo) + else -> error("unknown chat type: $chatType") }.toString() - val fromId = when(chatType) { - MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("group_id") ?: return noParam("group_id", session.echo) + val fromId = when (chatType) { + MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("group_id") + ?: return noParam("group_id", session.echo) + MsgConstant.KCHATTYPEC2C -> session.getLongOrNull("user_id") ?: return noParam("user_id", session.echo) else -> error("unknown chat type: $chatType") }.toString() - val retryCnt = session.getIntOrNull("retry_cnt") + val retryCnt = session.getIntOrNull("retry_cnt") ?: 5 val recallDuration = session.getLongOrNull("recall_duration") return if (session.isString("message")) { val autoEscape = session.getBooleanOrDefault("auto_escape", false) val message = session.getString("message") - invoke(chatType, peerId, message, autoEscape, echo = session.echo, fromId = fromId, retryCnt = retryCnt ?: 5, recallDuration = recallDuration) + invoke( + chatType, + peerId, + message, + autoEscape, + echo = session.echo, + fromId = fromId, + retryCnt = retryCnt, + recallDuration = recallDuration + ) } else if (session.isArray("message")) { val message = session.getArray("message") - invoke(chatType, peerId, message, session.echo, fromId = fromId, retryCnt ?: 5, recallDuration = recallDuration) + invoke( + chatType, + peerId, + message, + session.echo, + fromId = fromId, + retryCnt = retryCnt, + recallDuration = recallDuration + ) } else { val message = session.getObject("message") - invoke(chatType, peerId, listOf( message ).jsonArray, session.echo, fromId = fromId, retryCnt ?: 5, recallDuration = recallDuration) + invoke( + chatType, + peerId, + listOf(message).jsonArray, + session.echo, + fromId = fromId, + retryCnt = retryCnt, + recallDuration = recallDuration + ) } } catch (e: ParamsException) { return noParam(e.message!!, session.echo) @@ -82,14 +115,16 @@ internal object SendMessage: IActionHandler() { echo: JsonElement = EmptyJsonString ): String { val result = if (autoEscape) { - MsgSvc.sendToAio(chatType, peerId, listOf( - mapOf( - "type" to "text", - "data" to mapOf( - "text" to message + MsgSvc.sendToAio( + chatType, peerId, listOf( + mapOf( + "type" to "text", + "data" to mapOf( + "text" to message + ) ) - ) - ).json, fromId = fromId, retryCnt) + ).json, fromId = fromId, retryCnt + ) } else { val msg = MessageHelper.decodeCQCode(message) if (msg.isEmpty()) { @@ -98,44 +133,54 @@ internal object SendMessage: IActionHandler() { } else { MsgSvc.sendToAio(chatType, peerId, msg, fromId = fromId, retryCnt) } - } - if (result.isFailure) { - return logic(result.exceptionOrNull()?.message ?: "", echo) - } - val sendMsgResult = result.getOrThrow() - if (sendMsgResult.msgHashId <= 0) { + }.getOrElse{ return logic(it.message ?: "", echo)} + if (result.msgHashId <= 0) { return logic("send message failed", echo = echo) } - recallDuration?.let { autoRecall(sendMsgResult.msgHashId, it) } - return ok(MessageResult( - msgId = sendMsgResult.msgHashId, - time = (sendMsgResult.msgTime * 0.001).toLong() - ), echo = echo) + if (recallDuration != null) { + GlobalScope.launch(Dispatchers.Default) { + delay(recallDuration) + MsgSvc.recallMsg(result.msgHashId) + } + } + return ok( + MessageResult( + msgId = result.msgHashId, + time = (result.msgTime * 0.001).toLong() + ), echo = echo + ) } // 消息段格式消息 suspend operator fun invoke( - chatType: Int, peerId: String, message: JsonArray, echo: JsonElement = EmptyJsonString, fromId: String = peerId, retryCnt: Int, recallDuration: Long?, + chatType: Int, + peerId: String, + message: JsonArray, + echo: JsonElement = EmptyJsonString, + fromId: String = peerId, + retryCnt: Int, + recallDuration: Long?, ): String { //if (!ContactHelper.checkContactAvailable(chatType, peerId)) { // return logic("contact is not found", echo = echo) //} - val result = MsgSvc.sendToAio(chatType, peerId, message, fromId = fromId, retryCnt).getOrElse { return logic(it.message ?: "", echo) } + val result = MsgSvc.sendToAio(chatType, peerId, message, fromId, retryCnt) + .getOrElse { return logic(it.message ?: "", echo) } if (result.msgHashId <= 0) { return logic("send message failed", echo = echo) } - recallDuration?.let { autoRecall(result.msgHashId, it) } - return ok(MessageResult( - msgId = result.msgHashId, - time = (result.msgTime * 0.001).toLong() - ), echo) - } - - private fun autoRecall(msgHash: Int, duration: Long) { - GlobalScope.launch(Dispatchers.Default) { - delay(duration) - MsgSvc.recallMsg(msgHash) + if (recallDuration != null) { + GlobalScope.launch(Dispatchers.Default) { + delay(recallDuration) + MsgSvc.recallMsg(result.msgHashId) + } } + return ok( + MessageResult( + msgId = result.msgHashId, + time = (result.msgTime * 0.001).toLong() + ), echo + ) } override val requiredParams: Array<String> = arrayOf("message") diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateForwardMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateForwardMessage.kt index e4c70975..e857043f 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateForwardMessage.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateForwardMessage.kt @@ -9,9 +9,18 @@ import moe.fuqiuluo.symbols.OneBotHandler internal object SendPrivateForwardMessage : IActionHandler() { override suspend fun internalHandle(session: ActionSession): String { val userId = session.getLong("user_id") + val groupId = session.getLongOrNull("group_id") + val retryCnt = session.getIntOrNull("retry_cnt") ?: 5 return if (session.isArray("messages")) { val messages = session.getArray("messages") - SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId.toString(), messages, echo = session.echo) + SendForwardMessage( + MsgConstant.KCHATTYPEC2C, + userId.toString(), + groupId?.toString() ?: userId.toString(), + messages, + retryCnt, + echo = session.echo + ) } else { logic("未知格式合并转发消息", session.echo) } diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadMultiMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadMultiMessage.kt new file mode 100644 index 00000000..73667b65 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/UploadMultiMessage.kt @@ -0,0 +1,97 @@ +package moe.fuqiuluo.shamrock.remote.action.handlers + +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import kotlinx.serialization.json.* +import moe.fuqiuluo.qqinterface.servlet.MsgSvc +import moe.fuqiuluo.shamrock.helper.MessageHelper +import moe.fuqiuluo.shamrock.helper.ParamsException +import moe.fuqiuluo.shamrock.remote.action.ActionSession +import moe.fuqiuluo.shamrock.remote.action.IActionHandler +import moe.fuqiuluo.shamrock.remote.service.data.UploadForwardMessageResult +import moe.fuqiuluo.shamrock.tools.* +import moe.fuqiuluo.symbols.OneBotHandler + +@OneBotHandler("upload_multi_message") +internal object UploadMultiMessage : IActionHandler() { + override suspend fun internalHandle(session: ActionSession): String { + val detailType = session.getStringOrNull("detail_type") ?: session.getStringOrNull("message_type") + try { + val chatType = detailType?.let { + MessageHelper.obtainMessageTypeByDetailType(it) + } ?: run { + if (session.has("user_id")) { + if (session.has("group_id")) { + MsgConstant.KCHATTYPETEMPC2CFROMGROUP + } else { + MsgConstant.KCHATTYPEC2C + } + } else if (session.has("group_id")) { + MsgConstant.KCHATTYPEGROUP + } else { + return noParam("detail_type/message_type", session.echo) + } + } + val peerId = when (chatType) { + MsgConstant.KCHATTYPEGROUP -> session.getLongOrNull("group_id") ?: return noParam( + "group_id", + session.echo + ) + + MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("user_id") + ?: return noParam("user_id", session.echo) + + else -> error("unknown chat type: $chatType") + }.toString() + val fromId = when (chatType) { + MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("group_id") + ?: return noParam("group_id", session.echo) + + MsgConstant.KCHATTYPEC2C -> session.getLongOrNull("user_id") ?: return noParam( + "user_id", + session.echo + ) + + else -> error("unknown chat type: $chatType") + }.toString() + val retryCnt = session.getIntOrNull("retry_cnt") ?: 5 + return if (session.isArray("messages")) { + val messages = session.getArray("messages") + invoke(chatType, peerId, fromId, messages, retryCnt, echo = session.echo) + } else { + logic("未知格式合并转发消息", session.echo) + } + } catch (e: ParamsException) { + return noParam(e.message!!, session.echo) + } catch (e: Throwable) { + return logic(e.message ?: e.toString(), session.echo) + } + } + + suspend operator fun invoke( + chatType: Int, + peerId: String, + fromId: String = peerId, + messages: JsonArray, + retryCnt: Int, + echo: JsonElement = EmptyJsonString + ): String { + kotlin.runCatching { + val message = MsgSvc.uploadMultiMsg(chatType, peerId, fromId, messages, retryCnt) + .getOrElse { return logic(it.message ?: "", echo) } + + return ok( + UploadForwardMessageResult( + resId = message.data["id"] as String, + filename = message.data["filename"] as String, + summary = message.data["summary"] as String, + desc = message.data["desc"] as String + ), echo = echo + ) + }.onFailure { + return error("合并转发消息失败: $it", echo) + } + return logic("合并转发消息失败(unknown error)", echo) + } + + override val requiredParams: Array<String> = arrayOf("messages") +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt index 5470606b..694fcb9f 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt @@ -44,49 +44,6 @@ fun Routing.messageAction() { } } - route("/send_group_forward_(msg|message)".toRegex()) { - post { - val groupId = fetchPostOrNull("group_id") - val messages = fetchPostJsonArray("messages") - call.respondText( - SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId ?: "", messages), - ContentType.Application.Json - ) - } - get { - respond(false, Status.InternalHandlerError, "Not support GET method") - } - } - - route("/send_private_forward_(msg|message)".toRegex()) { - post { - val userId = fetchPostOrNull("user_id") - val messages = fetchPostJsonArray("messages") - call.respondText( - SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId ?: "", messages), - ContentType.Application.Json - ) - } - get { - respond(false, Status.InternalHandlerError, "Not support GET method") - } - } - - route("/send_forward_(msg|message)".toRegex()) { - post { - val userId = fetchPostOrNull("user_id") - val groupId = fetchPostOrNull("group_id") - val messages = fetchPostJsonArray("messages") - call.respondText( - SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId ?: groupId ?: "", messages), - ContentType.Application.Json - ) - } - get { - respond(false, Status.InternalHandlerError, "Not support GET method") - } - } - getOrPost("/get_forward_msg") { val id = fetchOrThrow("id") call.respondText(GetForwardMsg(id), ContentType.Application.Json) @@ -144,7 +101,7 @@ fun Routing.messageAction() { get { val msgType = fetchGetOrThrow("message_type") val message = fetchGetOrThrow("message") - val retryCnt = fetchGetOrNull("retry_cnt")?.toInt() ?: 3 + val retryCnt = fetchGetOrNull("retry_cnt")?.toInt() ?: 5 val autoEscape = fetchGetOrNull("auto_escape")?.toBooleanStrict() ?: false val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType) @@ -288,11 +245,11 @@ fun Routing.messageAction() { post { val userId = fetchPostOrThrow("user_id") val groupId = fetchPostOrNull("group_id") - val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 3 - val autoEscape = fetchPostOrNull("auto_escape")?.toBooleanStrict() ?: false val chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP - val fromId = groupId ?: userId + + val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 3 + val autoEscape = fetchPostOrNull("auto_escape")?.toBooleanStrict() ?: false val recallDuration = fetchPostOrNull("recall_duration")?.toLongOrNull() val result = if (isJsonData()) { @@ -302,8 +259,9 @@ fun Routing.messageAction() { peerId = userId, message = fetchPostJsonString("message"), autoEscape = autoEscape, - fromId = fromId, - retryCnt = retryCnt, recallDuration = recallDuration + fromId = groupId ?: userId, + retryCnt = retryCnt, + recallDuration = recallDuration ) } else { SendMessage( @@ -312,7 +270,7 @@ fun Routing.messageAction() { message = if (isJsonObject("message")) listOf(fetchPostJsonObject("message")).jsonArray else fetchPostJsonArray( "message" ), - fromId = groupId ?: userId ?: "", + fromId = groupId ?: userId, retryCnt = retryCnt, recallDuration = recallDuration ) @@ -323,12 +281,96 @@ fun Routing.messageAction() { peerId = userId, message = fetchPostOrThrow("message"), autoEscape = autoEscape, - fromId = fromId, - retryCnt = retryCnt, recallDuration = recallDuration + fromId = groupId ?: userId, + retryCnt = retryCnt, + recallDuration = recallDuration ) } call.respondText(result, ContentType.Application.Json) } } + + route("/upload_multi_(msg|message)".toRegex()) { + post { + val msgType = fetchPostOrThrow("message_type") + val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType) + val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5 + + val userId = fetchPostOrNull("user_id") + val groupId = fetchPostOrNull("group_id") + val messages = fetchPostJsonArray("messages") + call.respondText( + UploadMultiMessage( + chatType, + if (chatType == MsgConstant.KCHATTYPEC2C) userId!! else groupId!!, + groupId ?: userId ?: "", + messages, + retryCnt + ), + ContentType.Application.Json + ) + } + get { + respond(false, Status.InternalHandlerError, "Not support GET method") + } + } + + route("/send_forward_(msg|message)".toRegex()) { + post { + val msgType = fetchPostOrThrow("message_type") + val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType) + val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5 + + val userId = fetchPostOrNull("user_id") + val groupId = fetchPostOrNull("group_id") + val messages = fetchPostJsonArray("messages") + call.respondText( + SendForwardMessage( + chatType, + if (chatType == MsgConstant.KCHATTYPEC2C) userId!! else groupId!!, + groupId ?: userId ?: "", + messages, + retryCnt + ), + ContentType.Application.Json + ) + } + get { + respond(false, Status.InternalHandlerError, "Not support GET method") + } + } + + route("/send_private_forward_(msg|message)".toRegex()) { + post { + val userId = fetchPostOrThrow("user_id") + val groupId = fetchPostOrNull("group_id") + + val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5 + val messages = fetchPostJsonArray("messages") + call.respondText( + SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId, groupId ?: userId, messages, retryCnt), + ContentType.Application.Json + ) + } + get { + respond(false, Status.InternalHandlerError, "Not support GET method") + } + } + + route("/send_group_forward_(msg|message)".toRegex()) { + post { + val groupId = fetchPostOrThrow("group_id") + + val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5 + val messages = fetchPostJsonArray("messages") + call.respondText( + SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId, messages = messages, retryCnt = retryCnt), + ContentType.Application.Json + ) + } + get { + respond(false, Status.InternalHandlerError, "Not support GET method") + } + } } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt index 7dca35c6..91937299 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt @@ -9,10 +9,20 @@ internal data class MessageResult( @SerialName("message_id") val msgId: Int, @SerialName("time") val time: Long ) + +@Serializable +internal data class UploadForwardMessageResult( + @SerialName("res_id") val resId: String, + @SerialName("filename") val filename: String, + @SerialName("summary") val summary: String, + @SerialName("desc") val desc: String, +) + @Serializable -internal data class ForwardMessageResult( +internal data class SendForwardMessageResult( @SerialName("message_id") val msgId: Int, - @SerialName("forward_id") val forwardId: String + @SerialName("res_id") val resId: String, + @SerialName("forward_id") val forwardId: String = resId ) @Serializable @@ -20,6 +30,7 @@ internal data class MessageDetail( @SerialName("time") val time: Int, @SerialName("message_type") val msgType: String, @SerialName("message_id") val msgId: Int, + @SerialName("message_id_qq") val qqMsgId: Long, @SerialName("message_seq") val msgSeq: Long, @SerialName("real_id") val realId: Long, @SerialName("sender") val sender: MessageSender, @@ -29,6 +40,11 @@ internal data class MessageDetail( @SerialName("target_id") val targetId: Long = 0, ) +@Serializable +internal data class GetForwardMsgResult( + @SerialName("messages") val msgs: List<MessageDetail> +) + @Serializable internal data class MessageSender( @SerialName("user_id") val userId: Long, diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt index 987b49b2..1c5d4bbb 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt @@ -69,7 +69,6 @@ internal object PrimitiveListener { 528 -> when (subType) { 35 -> onFriendApply(msgTime, push.clientInfo!!, body) 39 -> onCardChange(msgTime, body) - // invite 68 -> onGroupApply(msgTime, contentHead, body) 138 -> onC2CRecall(msgTime, body) 290 -> onC2CPoke(msgTime, body) @@ -79,7 +78,7 @@ internal object PrimitiveListener { 12 -> onGroupBan(msgTime, body) 16 -> onGroupUniqueTitleChange(msgTime, body) 17 -> onGroupRecall(msgTime, body) - 20 -> onGroupPokeAndGroupSign(msgTime, body) + 20 -> onGroupCommonTips(msgTime, body) 21 -> onEssenceMessage(msgTime, push.clientInfo, body) } } @@ -281,7 +280,7 @@ internal object PrimitiveListener { } - private suspend fun onGroupPokeAndGroupSign(time: Long, body: MsgBody) { + private suspend fun onGroupCommonTips(time: Long, body: MsgBody) { val event = runCatching { body.msgContent!!.decodeProtobuf<GroupCommonTipsEvent>() }.getOrElse { 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 7e202750..f04a36a7 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt @@ -52,6 +52,7 @@ val Collection<Any?>.json: JsonArray is Number -> arrayList.add(it.json) is String -> arrayList.add(it.json) is Boolean -> arrayList.add(it.json) + is Pair<*, *> -> arrayList.add(mapOf(it as Pair<String, Any?>).json) is Map<*, *> -> arrayList.add((it as Map<String, Any?>).json) is Collection<*> -> arrayList.add((it as Collection<Any?>).json) else -> error("unknown array type: ${it::class.java}")