diff --git a/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaReq.kt b/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaReq.kt index 569c9223..df01afe1 100644 --- a/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaReq.kt +++ b/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaReq.kt @@ -94,7 +94,8 @@ data class DeleteReq( @Serializable data class DownloadRkeyReq( - @ProtoNumber(1) val types: List + @ProtoNumber(1) val types: List, + @ProtoNumber(2) val downloadType: Int ) @Serializable diff --git a/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt b/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt index e12c0095..1704a9cf 100644 --- a/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt +++ b/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt @@ -52,11 +52,11 @@ data class DownloadRkeyRsp( @Serializable data class RKeyInfo( - @ProtoNumber(1) val rkey: String?, + @ProtoNumber(1) val rkey: String, @ProtoNumber(2) val rkeyTtlSec: ULong?, @ProtoNumber(3) val storeId: UInt = 0u, @ProtoNumber(4) val rkeyCreateTime: UInt?, - @ProtoNumber(4) val type: UInt?, + @ProtoNumber(4) val type: UInt, ) @Serializable diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Trpc.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Trpc.kt index b5f6771d..d9e1f840 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Trpc.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Trpc.kt @@ -4,6 +4,8 @@ import com.tencent.qphone.base.remote.FromServiceMsg import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.utils.DeflateTools +import moe.fuqiuluo.symbols.decodeProtobuf +import protobuf.oidb.TrpcOidb import tencent.im.oidb.oidb_sso fun FromServiceMsg.decodeToOidb(): oidb_sso.OIDBSSOPkg { @@ -16,4 +18,16 @@ fun FromServiceMsg.decodeToOidb(): oidb_sso.OIDBSSOPkg { if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it }) } +} + +fun FromServiceMsg.decodeToTrpcOidb(): TrpcOidb { + return kotlin.runCatching { + wupBuffer.slice(4).let { + if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it + }.decodeProtobuf() + }.getOrElse { + wupBuffer.let { + if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it + }.decodeProtobuf() + } } \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt b/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt index 91e961ed..5db9d16f 100644 --- a/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt +++ b/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.withTimeoutOrNull import moe.fuqiuluo.shamrock.config.ResourceGroup import moe.fuqiuluo.shamrock.config.ShamrockConfig import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.tools.decodeToTrpcOidb import moe.fuqiuluo.shamrock.tools.hex2ByteArray import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty import moe.fuqiuluo.shamrock.tools.slice @@ -36,6 +37,8 @@ import protobuf.oidb.cmd0x11c5.CodecConfigReq import protobuf.oidb.cmd0x11c5.CommonHead import protobuf.oidb.cmd0x11c5.DownloadExt import protobuf.oidb.cmd0x11c5.DownloadReq +import protobuf.oidb.cmd0x11c5.DownloadRkeyReq +import protobuf.oidb.cmd0x11c5.DownloadRkeyRsp import protobuf.oidb.cmd0x11c5.FileInfo import protobuf.oidb.cmd0x11c5.FileType import protobuf.oidb.cmd0x11c5.IndexNode @@ -285,6 +288,44 @@ internal object NtV2RichMediaSvc: QQInterfaces() { return Result.success(result) } + suspend fun getTempNtRKey(): Result { + runCatching { + val req = NtV2RichMediaReq( + head = MultiMediaReqHead( + commonHead = CommonHead( + requestId = requestIdSeq.incrementAndGet().toULong(), + cmd = 202u + ), + sceneInfo = SceneInfo( + requestType = 2u, + businessType = 1u, + sceneType = 0u, + ), + clientMeta = ClientMeta(2u) + ), + downloadRkey = DownloadRkeyReq( + types = listOf(10, 20), + downloadType = 2 + ) + ).toByteArray() + val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x9067_202", 0x9067, 202, req, true) + if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) { + return Result.failure(Exception("failed to fetch NtTempRKey: ${fromServiceMsg?.wupBuffer?.toHexString()}")) + } + val trpc = fromServiceMsg.decodeToTrpcOidb() + if (trpc.buffer == null) { + return Result.failure(Exception("failed to fetch NtTempRKey: ${trpc.msg}")) + } + + trpc.buffer?.decodeProtobuf()?.downloadRkeyRsp?.let { + return Result.success(it) + } + }.onFailure { + return Result.failure(it) + } + return Result.failure(Exception("failed to fetch NtTempRKey")) + } + /** * 获取NT图片的RKEY */ @@ -353,13 +394,9 @@ internal object NtV2RichMediaSvc: QQInterfaces() { ).toByteArray() val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x11c5_200", 4549, 200, req, true) if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) { - return Result.failure(Exception("unable to get multimedia pic info: ${fromServiceMsg?.wupBuffer}")) - } - val trpc = kotlin.runCatching { - fromServiceMsg.wupBuffer.decodeProtobuf() - }.getOrElse { - fromServiceMsg.wupBuffer.slice(4).decodeProtobuf() + return Result.failure(Exception("unable to get multimedia pic info: ${fromServiceMsg?.wupBuffer?.toHexString()}")) } + val trpc = fromServiceMsg.decodeToTrpcOidb() if (trpc.buffer == null) { return Result.failure(Exception("unable to get multimedia pic info: ${trpc.msg}")) } @@ -457,11 +494,7 @@ internal object NtV2RichMediaSvc: QQInterfaces() { if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) { return Result.failure(Exception("unable to request upload nt pic")) } - val trpc = kotlin.runCatching { - fromServiceMsg.wupBuffer.decodeProtobuf() - }.getOrElse { - fromServiceMsg.wupBuffer.slice(4).decodeProtobuf() - } + val trpc = fromServiceMsg.decodeToTrpcOidb() if (trpc.buffer == null) { return Result.failure(Exception("unable to request upload nt pic: ${trpc.msg}")) } diff --git a/xposed/src/main/java/qq/service/bdh/RichProtoSvc.kt b/xposed/src/main/java/qq/service/bdh/RichProtoSvc.kt index 0139221e..d2b666e6 100644 --- a/xposed/src/main/java/qq/service/bdh/RichProtoSvc.kt +++ b/xposed/src/main/java/qq/service/bdh/RichProtoSvc.kt @@ -6,12 +6,16 @@ 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 com.tencent.qqnt.kernel.nativeinterface.Image +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.kernel.nativeinterface.MsgRecord +import com.tencent.qqnt.kernel.nativeinterface.PicElement import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.serialization.ExperimentalSerializationApi import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.tools.decodeToOidb -import moe.fuqiuluo.shamrock.tools.slice +import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty import moe.fuqiuluo.shamrock.tools.toHexString import moe.fuqiuluo.shamrock.utils.PlatformUtils import moe.fuqiuluo.symbols.decodeProtobuf @@ -29,7 +33,6 @@ import qq.service.contact.ContactHelper import tencent.im.cs.cmd0x346.cmd0x346 import tencent.im.oidb.cmd0x6d6.oidb_0x6d6 import tencent.im.oidb.cmd0xe37.cmd0xe37 -import tencent.im.oidb.oidb_sso import kotlin.coroutines.resume private const val GPRO_PIC = "gchat.qpic.cn" @@ -150,6 +153,75 @@ internal object RichProtoSvc: QQInterfaces() { } } + suspend fun getTempPicDownloadUrl( + chatType: Int, + originalUrl: String, + md5: String, + image: PicElement, + storeId: Int = 0, + peer: String? = null, + subPeer: String? = null, + ): String { + val isNtServer = originalUrl.startsWith("/download") + if (isNtServer) { + val tmpRKey = NtV2RichMediaSvc.getTempNtRKey() + if (tmpRKey.isSuccess) { + val tmpRKeyRsp = tmpRKey.getOrThrow() + val tmpRKeyMap = hashMapOf() + tmpRKeyRsp.rkeys?.forEach { rKeyInfo -> + tmpRKeyMap[rKeyInfo.type] = rKeyInfo.rkey + } + val rkey = tmpRKeyMap[when(chatType) { + MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> 10u + MsgConstant.KCHATTYPEC2C -> 20u + MsgConstant.KCHATTYPEGUILD -> 10u + else -> 0u + }] + if (rkey != null) { + return "https://$MULTIMEDIA_DOMAIN$originalUrl$rkey" + } + } + } + return when (chatType) { + MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> getGroupPicDownUrl( + originalUrl = originalUrl, + md5 = md5, + fileId = image.fileUuid, + width = image.picWidth.toUInt(), + height = image.picHeight.toUInt(), + sha = "", + fileSize = image.fileSize.toULong(), + peer = peer ?: "0" + ) + + MsgConstant.KCHATTYPEC2C -> getC2CPicDownUrl( + originalUrl = originalUrl, + md5 = md5, + fileId = image.fileUuid, + width = image.picWidth.toUInt(), + height = image.picHeight.toUInt(), + sha = "", + fileSize = image.fileSize.toULong(), + peer = peer ?: "0", + storeId = storeId + ) + + MsgConstant.KCHATTYPEGUILD -> getGuildPicDownUrl( + originalUrl = originalUrl, + md5 = md5, + fileId = image.fileUuid, + width = image.picWidth.toUInt(), + height = image.picHeight.toUInt(), + sha = "", + fileSize = image.fileSize.toULong(), + peer = peer ?: "0", + subPeer = subPeer ?: "0" + ) + + else -> throw UnsupportedOperationException("Not supported chat type: $chatType") + } + } + suspend fun getGroupPicDownUrl( originalUrl: String, md5: String, diff --git a/xposed/src/main/java/qq/service/internals/AioListener.kt b/xposed/src/main/java/qq/service/internals/AioListener.kt index 9defeb2d..29475ce9 100644 --- a/xposed/src/main/java/qq/service/internals/AioListener.kt +++ b/xposed/src/main/java/qq/service/internals/AioListener.kt @@ -13,8 +13,11 @@ import moe.fuqiuluo.shamrock.config.AliveReply import moe.fuqiuluo.shamrock.config.get import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.helper.db.ImageDB +import moe.fuqiuluo.shamrock.helper.db.ImageMapping import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter import moe.fuqiuluo.shamrock.utils.PlatformUtils +import moe.fuqiuluo.shamrock.utils.PlatformUtils.QQ_9_0_8_VER import qq.service.bdh.RichProtoSvc import qq.service.file.GroupFileHelper import qq.service.group.GroupHelper @@ -34,49 +37,82 @@ object AioListener : SimpleKernelMsgListener() { } } - private suspend fun onMsg(record: MsgRecord) { - if (AliveReply.get()) { - val texts = record.elements.filter { it.elementType == MsgConstant.KELEMTYPETEXT } - val text = texts.joinToString { it.textElement.content } - if (texts.isNotEmpty() && text == "ping") { - val contact = MessageHelper.generateContact(record) + private suspend fun debugTest(record: MsgRecord, text: String) { + if (record.chatType == MsgConstant.KCHATTYPEGROUP && text == ".shamrock.members") { + val contact = MessageHelper.generateContact(record) + GroupHelper.getGroupMemberList(record.peerUin, true).onSuccess { MessageHelper.sendMessage(contact, arrayListOf( MsgElement().apply { elementType = MsgConstant.KELEMTYPETEXT textElement = TextElement().apply { - content = "pong" + content = "memberCount: ${it.size}" } } ), 3, MessageHelper.generateMsgId(record.chatType)) - return + }.onFailure { + LogCenter.log("获取群成员列表失败: $it", Level.ERROR) + } + } else if (record.chatType == MsgConstant.KCHATTYPEGROUP && text == ".shamrock.root_files") { + val contact = MessageHelper.generateContact(record) + val files = GroupFileHelper.getGroupFiles(record.peerUin) + MessageHelper.sendMessage(contact, arrayListOf( + MsgElement().apply { + elementType = MsgConstant.KELEMTYPETEXT + textElement = TextElement().apply { + content = "foldersCount: ${files.foldersCount}\nfilesCount: ${files.filesCount}" + } + } + ), 3, MessageHelper.generateMsgId(record.chatType)) + } else if (record.chatType == MsgConstant.KCHATTYPEGROUP && text == ".shamrock.pic_url") { + val contact = MessageHelper.generateContact(record) + val pic = record.elements.filter { + it.elementType == MsgConstant.KELEMTYPEPIC + }.map { + val image = it.picElement + val md5 = (image.md5HexStr ?: image.fileName + .replace("{", "") + .replace("}", "") + .replace("-", "").split(".")[0]) + .uppercase() + var storeId = 0 + if (PlatformUtils.getQQVersionCode() > QQ_9_0_8_VER) { + storeId = image.storeID + } + val originalUrl = image.originImageUrl ?: "" + return@map RichProtoSvc.getTempPicDownloadUrl(record.chatType, originalUrl, md5, image, storeId) } - if (record.chatType == MsgConstant.KCHATTYPEGROUP && text == ".shamrock.members") { - val contact = MessageHelper.generateContact(record) - GroupHelper.getGroupMemberList(record.peerUin, true).onSuccess { - MessageHelper.sendMessage(contact, arrayListOf( - MsgElement().apply { - elementType = MsgConstant.KELEMTYPETEXT - textElement = TextElement().apply { - content = "memberCount: ${it.size}" - } - } - ), 3, MessageHelper.generateMsgId(record.chatType)) - }.onFailure { - LogCenter.log("获取群成员列表失败: $it", Level.ERROR) + MessageHelper.sendMessage(contact, arrayListOf( + MsgElement().apply { + elementType = MsgConstant.KELEMTYPETEXT + textElement = TextElement().apply { + content = "picUrl: \n${ + pic.joinToString("\n") + }" + } } - } else if (record.chatType == MsgConstant.KCHATTYPEGROUP && text == ".shamrock.root_files") { + ), 3, MessageHelper.generateMsgId(record.chatType)) + } + + } + + private suspend fun onMsg(record: MsgRecord) { + if (AliveReply.get()) { + val texts = record.elements.filter { it.elementType == MsgConstant.KELEMTYPETEXT } + val text = texts.joinToString { it.textElement.content } + if (texts.isNotEmpty() && text == "ping") { val contact = MessageHelper.generateContact(record) - val files = GroupFileHelper.getGroupFiles(record.peerUin) MessageHelper.sendMessage(contact, arrayListOf( MsgElement().apply { elementType = MsgConstant.KELEMTYPETEXT textElement = TextElement().apply { - content = "foldersCount: ${files.foldersCount}\nfilesCount: ${files.filesCount}" + content = "pong" } } ), 3, MessageHelper.generateMsgId(record.chatType)) + return } + debugTest(record, text) } when (record.chatType) { MsgConstant.KCHATTYPEGROUP -> { diff --git a/xposed/src/main/java/qq/service/msg/MsgConvertor.kt b/xposed/src/main/java/qq/service/msg/MsgConvertor.kt index fbc2ac93..106f7373 100644 --- a/xposed/src/main/java/qq/service/msg/MsgConvertor.kt +++ b/xposed/src/main/java/qq/service/msg/MsgConvertor.kt @@ -157,44 +157,20 @@ private object MsgConvertor { elem.type = ElementType.IMAGE elem.setImage(ImageElement.newBuilder().apply { this.file = ByteString.copyFromUtf8(md5) - this.fileUrl = when (record.chatType) { - MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl( - originalUrl = originalUrl, - md5 = md5, - fileId = image.fileUuid, - width = image.picWidth.toUInt(), - height = image.picHeight.toUInt(), - sha = "", - fileSize = image.fileSize.toULong(), - peer = record.peerUin.toString() - ) - - MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl( - originalUrl = originalUrl, - md5 = md5, - fileId = image.fileUuid, - width = image.picWidth.toUInt(), - height = image.picHeight.toUInt(), - sha = "", - fileSize = image.fileSize.toULong(), - peer = record.senderUin.toString(), - storeId = storeId - ) - - MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl( - originalUrl = originalUrl, - md5 = md5, - fileId = image.fileUuid, - width = image.picWidth.toUInt(), - height = image.picHeight.toUInt(), - sha = "", - fileSize = image.fileSize.toULong(), - peer = record.channelId.ifNullOrEmpty { record.peerUin.toString() } ?: "0", - subPeer = record.guildId ?: "0" - ) - - else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}") - } + this.fileUrl = RichProtoSvc.getTempPicDownloadUrl(record.chatType, originalUrl, md5, image, storeId, + peer = when(record.chatType) { + MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> record.peerUin.toString() + MsgConstant.KCHATTYPEC2C -> record.senderUin.toString() + MsgConstant.KCHATTYPEGUILD -> record.channelId.ifNullOrEmpty { record.peerUin.toString() } ?: "0" + else -> null + }, + subPeer = when(record.chatType) { + MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> null + MsgConstant.KCHATTYPEC2C -> null + MsgConstant.KCHATTYPEGUILD -> record.guildId ?: "0" + else -> null + } + ) this.fileType = if (image.isFlashPic == true) ImageElement.ImageType.FLASH else if (image.original) ImageElement.ImageType.ORIGIN else ImageElement.ImageType.COMMON this.subType = image.picSubType diff --git a/xposed/src/main/java/qq/service/msg/ReqMessageConvertor.kt b/xposed/src/main/java/qq/service/msg/ReqMessageConvertor.kt index 108bdd00..6b53a2f6 100644 --- a/xposed/src/main/java/qq/service/msg/ReqMessageConvertor.kt +++ b/xposed/src/main/java/qq/service/msg/ReqMessageConvertor.kt @@ -136,44 +136,7 @@ private object ReqMsgConvertor { val elem = Element.newBuilder() elem.setImage(ImageElement.newBuilder().apply { this.fileMd5 = md5 - this.fileUrl = when (contact.chatType) { - MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl( - originalUrl = originalUrl, - md5 = md5, - fileId = image.fileUuid, - width = image.picWidth.toUInt(), - height = image.picHeight.toUInt(), - sha = "", - fileSize = image.fileSize.toULong(), - peer = contact.longPeer().toString() - ) - - MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl( - originalUrl = originalUrl, - md5 = md5, - fileId = image.fileUuid, - width = image.picWidth.toUInt(), - height = image.picHeight.toUInt(), - sha = "", - fileSize = image.fileSize.toULong(), - peer = contact.longPeer().toString(), - storeId = storeId - ) - - MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl( - originalUrl = originalUrl, - md5 = md5, - fileId = image.fileUuid, - width = image.picWidth.toUInt(), - height = image.picHeight.toUInt(), - sha = "", - fileSize = image.fileSize.toULong(), - peer = contact.longPeer().toString(), - subPeer = "0" - ) - - else -> throw UnsupportedOperationException("Not supported chat type: ${contact.chatType}") - } + this.fileUrl = RichProtoSvc.getTempPicDownloadUrl(contact.chatType, originalUrl, md5, image, storeId, contact.peerUid, contact.guildId) this.fileType = if (image.isFlashPic == true) ImageElement.ImageType.FLASH else if (image.original) ImageElement.ImageType.ORIGIN else ImageElement.ImageType.COMMON this.subType = image.picSubType