From bc967cf926edb3b6665f2e8527031d69ba4c4096 Mon Sep 17 00:00:00 2001 From: WhiteChi Date: Fri, 10 Nov 2023 00:34:51 +0800 Subject: [PATCH] `Shamrock`: Support du chat de groupe et transfert de messages --- app/build.gradle.kts | 29 ++-- .../com/tencent/mobileqq/pb/MessageMicro.java | 44 ++++++ .../nativeinterface/IKernelMsgService.java | 4 + .../kernel/nativeinterface/MultiMsgInfo.java | 6 +- xposed/build.gradle.kts | 1 + .../qqinterface/entries/NtMessagePush.kt | 5 + .../fuqiuluo/qqinterface/servlet/BaseSvc.kt | 2 +- .../fuqiuluo/qqinterface/servlet/MsgSvc.kt | 47 +++++++ .../fuqiuluo/qqinterface/servlet/PacketSvc.kt | 98 ++++++++++++++ .../qqinterface/servlet/msg/LongMsg.kt | 103 -------------- .../servlet/msg/convert/MessageConvert.kt | 4 +- .../fuqiuluo/shamrock/helper/MessageHelper.kt | 103 +++++++++++++- .../remote/action/handlers/GetForwardMsg.kt | 54 ++++++-- .../action/handlers/SendGroupForwardMsg.kt | 126 ++++++++++++++---- .../shamrock/remote/api/MessageAction.kt | 5 + .../remote/service/listener/AioListener.kt | 21 ++- .../xposed/actions/HookWrapperCodec.kt | 33 ++++- 17 files changed, 510 insertions(+), 175 deletions(-) create mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/entries/NtMessagePush.kt create mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/LongMsg.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2ba786d..7d342a7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -173,24 +173,19 @@ dependencies { implementation("io.coil-kt:coil:2.4.0") implementation("io.coil-kt:coil-compose:2.4.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") implementation("org.jetbrains.kotlinx:kotlinx-io-jvm:0.1.16") - implementation("io.ktor:ktor-server-core:2.3.3") - implementation("io.ktor:ktor-server-host-common:2.3.3") - implementation("io.ktor:ktor-server-status-pages:2.3.3") - implementation("io.ktor:ktor-server-netty:2.3.3") - implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.3") - implementation("io.ktor:ktor-server-content-negotiation:2.3.3") - implementation("io.ktor:ktor-client-core:2.3.3") - implementation("io.ktor:ktor-client-cio:2.3.3") - implementation("io.ktor:ktor-client-content-negotiation:2.3.3") - implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.3") - // useless - //implementation ("com.maxkeppeler.sheets-compose-dialogs:core:1.2.0") - //implementation ("com.maxkeppeler.sheets-compose-dialogs:info:1.2.0") - //implementation ("com.maxkeppeler.sheets-compose-dialogs:input:1.2.0") - //implementation ("com.maxkeppeler.sheets-compose-dialogs:list:1.2.0") - //implementation ("com.maxkeppeler.sheets-compose-dialogs:state:1.2.0") + + val ktorVersion = "2.3.3" + implementation("io.ktor:ktor-server-core:$ktorVersion") + implementation("io.ktor:ktor-server-host-common:$ktorVersion") + implementation("io.ktor:ktor-server-status-pages:$ktorVersion") + implementation("io.ktor:ktor-server-netty:$ktorVersion") + implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-client-cio:$ktorVersion") + implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + //implementation("io.ktor:ktor-serialization-kotlinx-protobuf:$ktorVersion") implementation(project(":xposed")) diff --git a/qqinterface/src/main/java/com/tencent/mobileqq/pb/MessageMicro.java b/qqinterface/src/main/java/com/tencent/mobileqq/pb/MessageMicro.java index 8c09ea2..aba5ba0 100644 --- a/qqinterface/src/main/java/com/tencent/mobileqq/pb/MessageMicro.java +++ b/qqinterface/src/main/java/com/tencent/mobileqq/pb/MessageMicro.java @@ -1,6 +1,50 @@ package com.tencent.mobileqq.pb; +import java.lang.reflect.Field; +import java.util.Arrays; + public class MessageMicro> { + public static final class FieldMap { + private Object[] defaultValues; + private Field[] fields; + private int[] tags; + + FieldMap(int[] iArr, String[] strArr, Object[] objArr, Class cls) { + this.tags = iArr; + this.defaultValues = objArr; + this.fields = new Field[iArr.length]; + for (int i2 = 0; i2 < iArr.length; i2++) { + try { + this.fields[i2] = cls.getField(strArr[i2]); + } catch (Exception e2) { + e2.printStackTrace(); + } + } + } + + void clear(MessageMicro messageMicro) { + } + + > void copyFields(U u, U u2) { + } + + Field get(int i2) { + int binarySearch = Arrays.binarySearch(this.tags, i2); + if (binarySearch < 0) { + return null; + } + return this.fields[binarySearch]; + } + + int getSerializedSize(MessageMicro messageMicro) { + return 0; + } + } + + public static FieldMap initFieldMap(int[] iArr, String[] strArr, Object[] objArr, Class cls) { + return new FieldMap(iArr, strArr, objArr, cls); + } + public final T mergeFrom(byte[] bArr) { return null; } diff --git a/qqinterface/src/main/java/com/tencent/qqnt/kernel/nativeinterface/IKernelMsgService.java b/qqinterface/src/main/java/com/tencent/qqnt/kernel/nativeinterface/IKernelMsgService.java index ae35d22..ac9af0c 100644 --- a/qqinterface/src/main/java/com/tencent/qqnt/kernel/nativeinterface/IKernelMsgService.java +++ b/qqinterface/src/main/java/com/tencent/qqnt/kernel/nativeinterface/IKernelMsgService.java @@ -30,6 +30,10 @@ public interface IKernelMsgService { void getMultiMsg(Contact contact, long rootMsgId, long parentMsgId, IGetMultiMsgCallback cb); + void multiForwardMsg(ArrayList arrayList, Contact from, Contact to, IOperateCallback cb); + + void setAllC2CAndGroupMsgRead(IOperateCallback cb); + void clearMsgRecords(Contact contact, IClearMsgRecordsCallback cb); String createUidFromTinyId(long j2, long j3); diff --git a/qqinterface/src/main/java/com/tencent/qqnt/kernel/nativeinterface/MultiMsgInfo.java b/qqinterface/src/main/java/com/tencent/qqnt/kernel/nativeinterface/MultiMsgInfo.java index 618d8bd..1b118d3 100644 --- a/qqinterface/src/main/java/com/tencent/qqnt/kernel/nativeinterface/MultiMsgInfo.java +++ b/qqinterface/src/main/java/com/tencent/qqnt/kernel/nativeinterface/MultiMsgInfo.java @@ -1,7 +1,5 @@ package com.tencent.qqnt.kernel.nativeinterface; -/* compiled from: P */ -/* loaded from: classes2.dex */ public final class MultiMsgInfo { long msgId; String senderShowName; @@ -21,8 +19,6 @@ public final class MultiMsgInfo { return "MultiMsgInfo{msgId=" + this.msgId + ",senderShowName=" + this.senderShowName + ",}"; } - public MultiMsgInfo(long j2, String str) { - this.msgId = j2; - this.senderShowName = str; + public MultiMsgInfo(long msgId, String showName) { } } diff --git a/xposed/build.gradle.kts b/xposed/build.gradle.kts index b6f5fea..6819f6b 100644 --- a/xposed/build.gradle.kts +++ b/xposed/build.gradle.kts @@ -74,6 +74,7 @@ dependencies { implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + //implementation("io.ktor:ktor-serialization-kotlinx-protobuf:$ktorVersion") implementation("io.ktor:ktor-network-tls-certificates:$ktorVersion") /** diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/entries/NtMessagePush.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/entries/NtMessagePush.kt new file mode 100644 index 0000000..47d01fc --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/entries/NtMessagePush.kt @@ -0,0 +1,5 @@ +@file:OptIn(ExperimentalSerializationApi::class) +package moe.fuqiuluo.qqinterface.entries + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/BaseSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/BaseSvc.kt index 0811816..03bf15d 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/BaseSvc.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/BaseSvc.kt @@ -101,7 +101,7 @@ internal abstract class BaseSvc { app.sendToService(to) } - fun sendBuffer(cmd: String, isPb: Boolean, buffer: ByteArray, seq: Int) { + fun sendBuffer(cmd: String, isPb: Boolean, buffer: ByteArray, seq: Int = MsfCore.getNextSeq()) { val toServiceMsg = ToServiceMsg("mobileqq.service", app.currentUin, cmd) toServiceMsg.putWupBuffer(buffer) toServiceMsg.addAttribute("req_pb_protocol_flag", isPb) 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 4bdcc93..054a713 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt @@ -1,3 +1,5 @@ +@file:OptIn(DelicateCoroutinesApi::class) + package moe.fuqiuluo.qqinterface.servlet import com.tencent.mobileqq.qroute.QRoute @@ -9,9 +11,15 @@ import com.tencent.qqnt.kernel.nativeinterface.MsgRecord import com.tencent.qqnt.kernel.nativeinterface.TempChatGameSession import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo import com.tencent.qqnt.msg.api.IMsgService +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.time.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.json.JsonArray +import moe.fuqiuluo.proto.protobufOf import moe.fuqiuluo.shamrock.helper.ContactHelper import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter @@ -19,6 +27,7 @@ import moe.fuqiuluo.shamrock.helper.MessageHelper import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher import moe.fuqiuluo.shamrock.xposed.helper.msgService +import java.util.UUID import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -183,6 +192,44 @@ internal object MsgSvc: BaseSvc() { return MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, MessageCallback(peedId, 0), fromId) } + suspend fun getMultiMsg(resId: String): Result> { + 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 msgId = PacketSvc.fakeSelfRecvJsonMsg(msgService, content) + if (msgId < 0) { + return Result.failure(Exception("获取合并转发消息ID失败")) + } + val msgList = withTimeoutOrNull(5000L) { + suspendCancellableCoroutine> { + val job = GlobalScope.launch { + var hasResult = false + while (!hasResult) { + msgService.getMultiMsg(contact, msgId, msgId) { code, why, msgList -> + if (code == 0) { + it.resume(msgList) + hasResult = true + } else { + LogCenter.log("获取合并转发消息失败: $code($why): $msgId", Level.ERROR) + } + } + delay(200) + } + } + it.invokeOnCancellation { + job.cancel() + } + } + } ?: return Result.failure(Exception("获取合并转发消息失败")) + + //msgService.deleteMsg(contact, arrayListOf(msgId), null) + + return Result.success(msgList) + } + class MessageCallback( private val peerId: String, var msgHash: Int diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt new file mode 100644 index 0000000..7fd8ccc --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt @@ -0,0 +1,98 @@ +package moe.fuqiuluo.qqinterface.servlet + +import com.tencent.mobileqq.msf.core.MsfCore +import com.tencent.qqnt.kernel.nativeinterface.Contact +import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgService +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import io.ktor.utils.io.core.BytePacketBuilder +import io.ktor.utils.io.core.readBytes +import io.ktor.utils.io.core.writeFully +import io.ktor.utils.io.core.writeInt +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import moe.fuqiuluo.proto.protobufOf +import moe.fuqiuluo.shamrock.helper.MessageHelper +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 mqq.app.MobileQQ +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine +import kotlin.math.abs +import kotlin.random.Random +import kotlin.random.nextInt +import kotlin.random.nextLong + +internal object PacketSvc: BaseSvc() { + /** + * 伪造收到Json卡片消息 + */ + suspend fun fakeSelfRecvJsonMsg(msgService: IKernelMsgService, content: String): Long { + return fakeReceiveSelfMsg(msgService) { arrayOf( + mapOf( + 51 to 1 to (byteArrayOf(1) + DeflateTools.compress(content.toByteArray())) + ) + ) } + } + + private suspend fun fakeReceiveSelfMsg(msgService: IKernelMsgService, builder: () -> Array>): Long { + val latestMsg = withTimeoutOrNull(3000) { + suspendCancellableCoroutine { + msgService.getMsgs(Contact(MsgConstant.KCHATTYPEC2C, app.currentUid, ""), 0L, 1, true) { code, why, msgs -> + it.resume(GetHistoryMsg.GetMsgResult(code, why, msgs)) + } + } + }?.data?.firstOrNull() + val msgSeq = (latestMsg?.msgSeq ?: 0) + 1 + fakeReceive("trpc.msg.olpush.OlPushService.MsgPush", 10000, protobufOf( + 1 to mapOf( + 1 to mapOf( + 1 to app.currentUin.toLong(), + 2 to app.currentUid, + 3 to 1001, + 5 to app.currentUin.toLong(), + 6 to app.currentUid + ), + 2 to mapOf( + 1 to 166, + 3 to 11, + 4 to msgSeq, + 5 to msgSeq, + 6 to (System.currentTimeMillis() / 1000).toInt(), + 7 to 1, + 11 to msgSeq, + 12 to msgService.getMsgUniqueId(System.currentTimeMillis()), + 14 to msgSeq - 2, + 28 to msgSeq + ), + 3 to 1 to 2 to builder() + ) + ).toByteArray()) + return withTimeoutOrNull(5000L) { + suspendCancellableCoroutine { + AioListener.messageLessListenerMap[msgSeq] = { + it.resume(this.msgId) + } + } + } ?: -1L + } + + /** + * 伪造QQ收到某个包 + */ + private fun fakeReceive(cmd: String, seq: Int, buffer: ByteArray) { + MobileQQ.getContext().broadcast("msf") { + putExtra("__cmd", "fake_packet") + putExtra("package_cmd", cmd) + putExtra("package_uin", app.currentUin) + putExtra("package_seq", seq) + val wupBuffer = BytePacketBuilder().apply { + writeInt(buffer.size + 4) + writeFully(buffer) + }.build() + putExtra("package_buffer", wupBuffer.readBytes()) + wupBuffer.release() + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/LongMsg.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/LongMsg.kt deleted file mode 100644 index f63c351..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/LongMsg.kt +++ /dev/null @@ -1,103 +0,0 @@ -package moe.fuqiuluo.qqinterface.servlet.msg - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import com.tencent.qqnt.kernel.nativeinterface.MsgRecord -import moe.fuqiuluo.proto.ProtoUtils -import moe.fuqiuluo.proto.asUtf8String -import moe.fuqiuluo.proto.protobufOf -import moe.fuqiuluo.qqinterface.servlet.BaseSvc -import moe.fuqiuluo.shamrock.tools.slice -import moe.fuqiuluo.shamrock.utils.DeflateTools - -internal object LongMsgHelper: BaseSvc() { - private const val GROUP_LONG_MSG_CMD = "trpc.group.long_msg_interface.MsgService.SsoSendLongMsg" - - suspend fun uploadGroupMsg(groupId: String, msgs: List): String { - val reqBody = protobufOf( - 2 to mapOf( - 1 to 3, - 2 to 2 to groupId, - 3 to groupId.toLong(), - 4 to DeflateTools.gzip(toGroupByteArray(msgs)) - ), - 15 to mapOf( - 1 to 4, - 2 to 2, - 3 to 9, - 4 to 0 - ) - ).toByteArray() - val buffer = sendBufferAW(GROUP_LONG_MSG_CMD, true, reqBody) - ?: error("获取消息资源ID失败") - val pb = ProtoUtils.decodeFromByteArray(buffer.slice(4)) - return pb[2, 3].asUtf8String - } - - private fun toGroupByteArray(msgs: List): ByteArray { - return protobufOf( - 2 to mapOf( - 1 to "MultiMsg", - 2 to 1 to msgs.map { record -> - mapOf( - 1 to mapOf( - 2 to record.senderUid, - 8 to mapOf( - 1 to record.peerUin, - 4 to record.sendNickName, - 5 to 2 - ) - ), - 2 to mapOf( - 1 to 82, - 4 to record.msgRandom, - 5 to record.msgSeq, - 6 to record.msgTime, - 7 to 1, - 8 to 0, - 9 to 0, - 15 to mapOf( - 1 to 0, - 2 to 0, - 3 to 0, - 4 to "", - 5 to "" - ) - ), - 3 to mapOf( - 1 to 2 to (record.elements.map { - when (val type = it.elementType) { - MsgConstant.KELEMTYPETEXT -> mapOf(1 to 1 to it.textElement.content) - - else -> error("不支持的合并转发消息类型: $type") - } - } as ArrayList).also { - it.add(0, mapOf( - 37 to mapOf( - 1 to 8, - 16 to 0, - 17 to 0, - 19 to mapOf( - 15 to 65536, - 25 to 0, - 30 to 0, - 31 to 0, - 34 to 0, - 41 to 0, - 52 to 64, - 54 to 1, - 55 to 0, - 72 to 0, - 73 to 1 to 0, - 96 to 0 - ) - ) - )) - } - ) - ) - } - ) - ).toByteArray() - } - -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageConvert.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageConvert.kt index 7234f73..f8dbacd 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageConvert.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageConvert.kt @@ -53,8 +53,8 @@ internal object MessageConvert { MsgConstant.KELEMTYPEREPLY to MessageElemConverter.ReplyConverter, MsgConstant.KELEMTYPEGRAYTIP to MessageElemConverter.GrayTipsConverter, MsgConstant.KELEMTYPEFILE to MessageElemConverter.FileConverter, - MsgConstant.KELEMTYPEMULTIFORWARD to MessageElemConverter.XmlMultiMsgConverter, - MsgConstant.KELEMTYPESTRUCTLONGMSG to MessageElemConverter.XmlLongMsgConverter, + //MsgConstant.KELEMTYPEMULTIFORWARD to MessageElemConverter.XmlMultiMsgConverter, + //MsgConstant.KELEMTYPESTRUCTLONGMSG to MessageElemConverter.XmlLongMsgConverter, ) } 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 6d8fff8..9b7d69e 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt @@ -23,6 +23,22 @@ import moe.fuqiuluo.shamrock.tools.jsonArray import kotlin.math.abs internal object MessageHelper { + suspend fun sendMessageWithoutMsgId( + chatType: Int, + peerId: String, + message: String, + callback: IOperateCallback, + fromId: String = peerId + ): Pair { + val uniseq = generateMsgId(chatType) + val msg = messageArrayToMessageElements(chatType, uniseq.second, peerId, decodeCQCode(message)).also { + if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。") + }.second.filter { + it.elementType != -1 + } as ArrayList + return sendMessageWithoutMsgId(chatType, peerId, msg, callback, fromId) + } + suspend fun sendMessageWithoutMsgId( chatType: Int, peerId: String, @@ -31,14 +47,31 @@ internal object MessageHelper { fromId: String = peerId ): Pair { val uniseq = generateMsgId(chatType) - var nonMsg: Boolean val msg = messageArrayToMessageElements(chatType, uniseq.second, peerId, message).also { if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。") }.second.filter { it.elementType != -1 - }.also { - nonMsg = it.isEmpty() - } + } as ArrayList + return sendMessageWithoutMsgId(chatType, peerId, msg, callback, fromId) + } + + suspend fun sendMessageWithoutMsgId( + chatType: Int, + peerId: String, + message: ArrayList, + callback: IOperateCallback, + fromId: String = peerId + ): Pair { + return sendMessageWithoutMsgId(generateContact(chatType, peerId, fromId), message, callback) + } + + fun sendMessageWithoutMsgId( + contact: Contact, + message: ArrayList, + callback: IOperateCallback + ): Pair { + val uniseq = generateMsgId(contact.chatType) + val nonMsg: Boolean = message.isEmpty() return if (!nonMsg) { val service = QRoute.api(IMsgService::class.java) if(callback is MsgSvc.MessageCallback) { @@ -46,9 +79,9 @@ internal object MessageHelper { } service.sendMsg( - generateContact(chatType, peerId, fromId), + contact, uniseq.second, - msg as ArrayList, + message, callback ) System.currentTimeMillis() to uniseq.first @@ -57,6 +90,64 @@ internal object MessageHelper { } } + suspend fun sendMessageWithMsgId( + chatType: Int, + peerId: String, + message: JsonArray, + callback: IOperateCallback, + fromId: String = peerId + ): Pair { + val uniseq = generateMsgId(chatType) + val msg = messageArrayToMessageElements(chatType, uniseq.second, peerId, message).also { + if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。") + }.second.filter { + it.elementType != -1 + } as ArrayList + val contact = generateContact(chatType, peerId, fromId) + val nonMsg: Boolean = message.isEmpty() + return if (!nonMsg) { + val service = QRoute.api(IMsgService::class.java) + if(callback is MsgSvc.MessageCallback) { + callback.msgHash = uniseq.first + } + + service.sendMsg( + contact, + uniseq.second, + msg, + callback + ) + uniseq.second to uniseq.first + } else { + uniseq.second to 0 + } + } + + fun sendMessageWithMsgId( + contact: Contact, + message: ArrayList, + callback: IOperateCallback + ): Pair { + val uniseq = generateMsgId(contact.chatType) + val nonMsg: Boolean = message.isEmpty() + return if (!nonMsg) { + val service = QRoute.api(IMsgService::class.java) + if(callback is MsgSvc.MessageCallback) { + callback.msgHash = uniseq.first + } + + service.sendMsg( + contact, + uniseq.second, + message, + callback + ) + uniseq.second to uniseq.first + } else { + 0L to 0 + } + } + suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact { val peerId = if (MsgConstant.KCHATTYPEC2C == chatType || MsgConstant.KCHATTYPETEMPC2CFROMGROUP == chatType) { ContactHelper.getUidByUinAsync(id.toLong()) 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 d946426..d7fc921 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,22 +1,58 @@ package moe.fuqiuluo.shamrock.remote.action.handlers +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonElement +import moe.fuqiuluo.qqinterface.servlet.MsgSvc +import moe.fuqiuluo.qqinterface.servlet.msg.convert.MessageConvert +import moe.fuqiuluo.shamrock.helper.MessageHelper import moe.fuqiuluo.shamrock.remote.action.ActionSession import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher +import moe.fuqiuluo.shamrock.remote.service.data.MessageDetail +import moe.fuqiuluo.shamrock.remote.service.data.MessageSender +import moe.fuqiuluo.shamrock.tools.EmptyJsonString internal object GetForwardMsg: IActionHandler() { override suspend fun internalHandle(session: ActionSession): String { val id = session.getString("id") - - val kernelService = NTServiceFetcher.kernelService - val sessionService = kernelService.wrapperSession - val msgService = sessionService.msgService - - //msgService.getMultiMsg() - - return error("不支持实现,请提交ISSUE!", session.echo) + return invoke(id, session.echo) } + suspend operator fun invoke( + resId: String, + echo: JsonElement = EmptyJsonString + ): String { + val result = MsgSvc.getMultiMsg(resId) + if (result.isFailure) { + return logic(result.exceptionOrNull().toString(), echo) + } + + return ok(data = GetForwardMsgResult(result.getOrThrow().map { msg -> + val msgHash = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId) + MessageDetail( + time = msg.msgTime.toInt(), + msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType), + msgId = msgHash, + realId = msg.msgSeq.toInt(), + sender = MessageSender( + msg.senderUin, msg.sendNickName, "unknown", 0, msg.senderUid + ), + message = MessageConvert.convertMessageRecordToMsgSegment(msg).map { + it.toJson() + }, + peerId = msg.peerUin, + groupId = if (msg.chatType == MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0, + targetId = if (msg.chatType != MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0 + ) + }), echo = echo) + } + + @Serializable + data class GetForwardMsgResult( + @SerialName("messages") val msgs: List + ) + override val requiredParams: Array = arrayOf("id") override fun path(): String = "get_forward_msg" diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMsg.kt index 29367b2..d6cb336 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMsg.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMsg.kt @@ -1,41 +1,117 @@ package moe.fuqiuluo.shamrock.remote.action.handlers -import com.tencent.qqnt.kernel.nativeinterface.MsgRecord +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.kernel.nativeinterface.MultiMsgInfo +import kotlinx.atomicfu.atomic +import kotlinx.serialization.json.JsonArray 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.tools.asInt -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.qqinterface.servlet.msg.LongMsgHelper +import moe.fuqiuluo.qqinterface.servlet.TicketSvc +import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.helper.MessageHelper +import moe.fuqiuluo.shamrock.tools.EmptyJsonObject import moe.fuqiuluo.shamrock.tools.EmptyJsonString +import moe.fuqiuluo.shamrock.tools.asInt +import moe.fuqiuluo.shamrock.tools.asJsonObject +import moe.fuqiuluo.shamrock.tools.asString +import moe.fuqiuluo.shamrock.tools.json +import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher internal object SendGroupForwardMsg: IActionHandler() { override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getLong("group_id") - val hashList = session.getArrayOrNull("seqs")?.map { it.asInt } - if (hashList != null) { - val msgs = hashList.map { - MsgSvc.getMsg(it).getOrNull() + val groupId = session.getString("group_id") + if (session.isArray("messages")) { + val messages = session.getArray("messages") + return invoke(messages, groupId, session.echo) + } + return logic("未知格式合并转发消息", session.echo) + } + + suspend operator fun invoke( + message: JsonArray, + groupId: String, + echo: JsonElement = EmptyJsonString + ): String { + kotlin.runCatching { + val kernelService = NTServiceFetcher.kernelService + val sessionService = kernelService.wrapperSession + val msgService = sessionService.msgService + val selfUin = TicketSvc.getUin() + + val msgs = message.map { + it.asJsonObject["data"].asJsonObject.let { + if (it.containsKey("content")) + MessageNode(it["name"].asString, it["content"]) + else MessageIdNode(it["id"].asInt) + } + }.map { + if (it is MessageIdNode) { + val recordResult = MsgSvc.getMsg(it.id) + if (recordResult.isFailure) { + EmptyNode + } else { + val record = recordResult.getOrThrow() + MessageNode( + name = record.sendMemberName + .ifBlank { record.sendNickName } + .ifBlank { record.sendRemarkName } + .ifBlank { record.peerName }, + content = record.toSegments().map { segment -> + segment.toJson() + }.json + ) + } + } else { + it as MessageNode + } + }.filter { + it.content != null } - val resId = LongMsgHelper.uploadGroupMsg(groupId.toString(), msgs.filterNotNull()) - return ok(mapOf("res_id" to resId), session.echo) + + var forwardMsgCallback: (() -> Unit)? = null + val availableMsgSize = atomic(0) + val msgIds = msgs.map { + it.name to MessageHelper.sendMessageWithMsgId(MsgConstant.KCHATTYPEC2C, selfUin, it.content!!.let { msg -> + if (msg is JsonArray) msg else MessageHelper.decodeCQCode(msg.asString) + }, { _, _ -> + if (availableMsgSize.incrementAndGet() == msgs.size) { + forwardMsgCallback?.invoke() + } + }).first + } + + val from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin) + val to = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId) + forwardMsgCallback = { + msgService.multiForwardMsg(ArrayList(msgIds.size).apply { + msgIds.forEach { add(MultiMsgInfo(it.second, it.first)) } + }, from, to) { code, why -> + if (code != 0) + LogCenter.log("合并转发消息:$code($why)", Level.WARN) + } + } + return ok(data = EmptyJsonObject, echo = echo) + }.onFailure { + return error("error: $it", echo) } - - - return "xxx" + return logic("合并转发消息失败(unknown error)", echo) } - operator fun invoke(msgs: List, echo: JsonElement = EmptyJsonString): String { - if (msgs.isEmpty()) { - return logic("消息为空", echo) - } else if (msgs.size > 100) { - return logic("消息数量过多", echo) - } - - - - TODO() - } + override val requiredParams: Array = arrayOf("group_id") override fun path(): String = "send_group_forward_msg" + + class MessageIdNode( + val id: Int + ): Node + open class MessageNode( + val name: String, + val content: JsonElement? + ): Node + object EmptyNode: MessageNode("", null) + interface Node } \ 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 6cf1451..2efd95c 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 @@ -22,6 +22,11 @@ import moe.fuqiuluo.shamrock.tools.isJsonData import moe.fuqiuluo.shamrock.tools.isJsonString fun Routing.messageAction() { + getOrPost("/get_forward_msg") { + val id = fetchOrThrow("id") + call.respondText(GetForwardMsg(id)) + } + getOrPost("/get_group_msg_history") { val peerId = fetchOrThrow("group_id") val cnt = fetchOrNull("count")?.toInt() ?: 20 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 971d277..ee9e573 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 @@ -16,9 +16,13 @@ import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter import moe.fuqiuluo.shamrock.remote.service.data.push.MessageTempSource import moe.fuqiuluo.shamrock.remote.service.data.push.PostType import java.util.ArrayList -import java.util.HashMap +import java.util.Collections +import kotlin.collections.HashMap internal object AioListener: IKernelMsgListener { + // 通过MSG SEQ临时监听器 + internal val messageLessListenerMap = Collections.synchronizedMap(HashMap Unit>()) + override fun onRecvMsg(msgList: ArrayList) { if (msgList.isEmpty()) return @@ -33,6 +37,15 @@ internal object AioListener: IKernelMsgListener { try { if (record.chatType == MsgConstant.KCHATTYPEGUILD) return // TODO: 频道消息暂不处理 + messageLessListenerMap.firstNotNullOfOrNull { + if(it.key == record.msgSeq) it else null + }?.let { + it.value(record) + messageLessListenerMap.remove(it.key) + } + + if (record.msgSeq < 0) return + val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId) MessageHelper.saveMsgMapping( @@ -48,6 +61,10 @@ internal object AioListener: IKernelMsgListener { val rawMsg = record.elements.toCQCode(record.chatType, record.peerUin.toString()) if (rawMsg.isEmpty()) return + //if (rawMsg.contains("forward")) { + // LogCenter.log(record.extInfoForUI.decodeToString(), Level.WARN) + //} + when (record.chatType) { MsgConstant.KCHATTYPEGROUP -> { LogCenter.log("群消息(group = ${record.peerName}(${record.peerUin}), uin = ${record.senderUin}, id = $msgHash|${record.msgSeq}, msg = $rawMsg)") @@ -63,7 +80,7 @@ internal object AioListener: IKernelMsgListener { } } MsgConstant.KCHATTYPEC2C -> { - LogCenter.log("私聊消息(private = ${record.senderUin}, id = $msgHash|${record.msgSeq}, msg = $rawMsg)") + LogCenter.log("私聊消息(private = ${record.senderUin}, id = [$msgHash | ${record.msgId} | ${record.msgSeq}], msg = $rawMsg)") ShamrockConfig.getPrivateRule()?.let { rule -> if (rule.black?.contains(record.peerUin) == true) return if (rule.white?.contains(record.peerUin) == false) return diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/HookWrapperCodec.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/HookWrapperCodec.kt index b307c07..57f311c 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/HookWrapperCodec.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/HookWrapperCodec.kt @@ -8,6 +8,7 @@ import com.tencent.qphone.base.remote.ToServiceMsg import com.tencent.qphone.base.util.CodecWarpper import kotlinx.atomicfu.atomic import kotlinx.coroutines.DelicateCoroutinesApi +import moe.fuqiuluo.qqinterface.servlet.TicketSvc import moe.fuqiuluo.shamrock.remote.service.PacketReceiver import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY @@ -15,7 +16,10 @@ import moe.fuqiuluo.shamrock.tools.hookMethod import moe.fuqiuluo.shamrock.tools.slice import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.utils.PlatformUtils +import moe.fuqiuluo.shamrock.xposed.helper.internal.DynamicReceiver +import moe.fuqiuluo.shamrock.xposed.helper.internal.IPCRequest + +private const val MAGIC_APP_ID = 114514 internal class HookWrapperCodec: IAction { private val IgnoredCmd = arrayOf( @@ -54,12 +58,12 @@ internal class HookWrapperCodec: IAction { val isInit = atomic(false) CodecWarpper::class.java.hookMethod("init").after { if (isInit.value) return@after - hookReceive(it.thisObject.javaClass) + hookReceive(it.thisObject, it.thisObject.javaClass) isInit.lazySet(true) } CodecWarpper::class.java.hookMethod("nativeOnReceData").before { if (isInit.value) return@before - hookReceive(it.thisObject.javaClass) + hookReceive(it.thisObject, it.thisObject.javaClass) isInit.lazySet(true) } } catch (e: Exception) { @@ -67,7 +71,26 @@ internal class HookWrapperCodec: IAction { } } - private fun hookReceive(thizClass: Class<*>) { + private fun hookReceive(thiz: Any, thizClass: Class<*>) { + val onResponse = thizClass.getDeclaredMethod("onResponse", Integer.TYPE, Any::class.java, Integer.TYPE) + //LogCenter.log("HookWrapperCodec: onResponse = $onResponse", Level.INFO) + DynamicReceiver.register("fake_packet", IPCRequest { + val uin = it.getStringExtra("package_uin")!! + val cmd = it.getStringExtra("package_cmd")!! + val seq = it.getIntExtra("package_seq", 0) + val buffer = it.getByteArrayExtra("package_buffer")!! + //LogCenter.log("伪造收包(cmd = $cmd)") + + val from = FromServiceMsg() + from.requestSsoSeq = seq + from.putWupBuffer(buffer) + from.serviceCmd = cmd + from.appId = MAGIC_APP_ID + from.setMsgSuccess() + from.uin = uin + from.appSeq = seq + onResponse.invoke(thiz, 0, from, 0) + }) thizClass.hookMethod("onResponse").before { val from = it.args[1] as FromServiceMsg try { @@ -88,7 +111,7 @@ internal class HookWrapperCodec: IAction { } merge.BusiBuffVec.set(busiBufVec) from.putWupBuffer(merge.toByteArray()) - } else { + } else if (from.appId != MAGIC_APP_ID) { if (from.serviceCmd in IgnoredCmd && ShamrockConfig.isInjectPacket()) { from.serviceCmd = "ShamrockInjectedCmd" from.putWupBuffer(EMPTY_BYTE_ARRAY)