diff --git a/processor/src/main/java/moe/fuqiuluo/ksp/impl/ProtobufProcessor.kt b/processor/src/main/java/moe/fuqiuluo/ksp/impl/ProtobufProcessor.kt index 8078c5c..7cf97dc 100644 --- a/processor/src/main/java/moe/fuqiuluo/ksp/impl/ProtobufProcessor.kt +++ b/processor/src/main/java/moe/fuqiuluo/ksp/impl/ProtobufProcessor.kt @@ -32,9 +32,9 @@ class ProtobufProcessor( }.toList() if (actions.isNotEmpty()) { - actions.forEach { clz -> - if (clz.isInternal()) return@forEach - if (clz.isPrivate()) return@forEach + actions.forEachIndexed { index, clz -> + if (clz.isInternal()) return@forEachIndexed + if (clz.isPrivate()) return@forEachIndexed val packageName = "protobuf.auto" val fileSpecBuilder = FileSpec.scriptBuilder("FastProtobuf", packageName) @@ -58,7 +58,7 @@ class ProtobufProcessor( codeGenerator.createNewFile( dependencies = Dependencies.ALL_FILES, packageName = packageName, - fileName = clz.simpleName.asString() + "\$FP" + fileName = "FP${clz.simpleName.asString().hashCode()}" ).use { outputStream -> outputStream.writer().use { fileSpecBuilder.build().writeTo(it) diff --git a/protobuf/src/main/java/protobuf/oidb/TrpcOidb.kt b/protobuf/src/main/java/protobuf/oidb/TrpcOidb.kt index 31da9eb..d182ef4 100644 --- a/protobuf/src/main/java/protobuf/oidb/TrpcOidb.kt +++ b/protobuf/src/main/java/protobuf/oidb/TrpcOidb.kt @@ -9,5 +9,6 @@ data class TrpcOidb( @ProtoNumber(1) val cmd: Int = Int.MIN_VALUE, @ProtoNumber(2) val service: Int = Int.MIN_VALUE, @ProtoNumber(4) val buffer: ByteArray, + //@ProtoNumber(11) val traceParams: Map = mapOf(), @ProtoNumber(12) val flag: Int = Int.MIN_VALUE, ): Protobuf \ No newline at end of file diff --git a/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/Oidb0x11c5.kt b/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/Oidb0x11c5.kt new file mode 100644 index 0000000..3ab5620 --- /dev/null +++ b/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/Oidb0x11c5.kt @@ -0,0 +1,135 @@ +@file:OptIn(ExperimentalSerializationApi::class) +package protobuf.oidb.cmd0x11c5 + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import moe.fuqiuluo.symbols.Protobuf + +@Serializable +data class Oidb0x11c5Req( + @ProtoNumber(1) val head: MultiMediaRoutingHead, + @ProtoNumber(3) val dataInfo: MultiMediaDataInfo +): Protobuf + +@Serializable +data class Oidb0x11c5Resp( + @ProtoNumber(1) val head: Head, + @ProtoNumber(3) val result: DownloadResult? +): Protobuf { + companion object { + @Serializable + data class Head( + //@ProtoNumber(1) val request: Request, + @ProtoNumber(3) val msg: String + ) { + /*companion object { + @Serializable + data class Request( + @ProtoNumber(1) val u1: UInt, + @ProtoNumber(2) val u2: UInt + ) + }*/ + } + + @Serializable + data class DownloadResult( + @ProtoNumber(1) val rkeyParam: String, + @ProtoNumber(2) val expire: UInt, + ) + } +} + +@Serializable +data class MultiMediaDataInfo( + @ProtoNumber(1) val multiMedia: MultiMedia, + @ProtoNumber(2) val ext: EXT, +) { + companion object { + @Serializable + data class MultiMedia( + @ProtoNumber(1) val picture: Picture, + @ProtoNumber(2) val fileId: String, + @ProtoNumber(3) val u1: UInt, + @ProtoNumber(4) val u2: UInt, + @ProtoNumber(5) val u3: UInt, + @ProtoNumber(6) val u4: UInt, + ) + + @Serializable + data class Picture( + @ProtoNumber(1) val size: ULong, + @ProtoNumber(2) val md5: String, + @ProtoNumber(3) val sha: String, + @ProtoNumber(4) val fileName: String, + @ProtoNumber(5) val u1: U3, + @ProtoNumber(6) val width: UInt, + @ProtoNumber(7) val height: UInt, + @ProtoNumber(8) val u2: UInt, + @ProtoNumber(9) val u3: UInt, + ) + + @Serializable + data class U3( + @ProtoNumber(1) val u1: UInt, + @ProtoNumber(2) val u2: UInt, + @ProtoNumber(3) val u3: UInt, + @ProtoNumber(4) val u4: UInt, + ) + + @Serializable + data class EXT( + @ProtoNumber(2) val u1: U1, + ) + + @Serializable + data class U1( + @ProtoNumber(1) val u1: UInt, + @ProtoNumber(3) val u2: UInt, + @ProtoNumber(4) val u3: U2, + @ProtoNumber(5) val u4: UInt, + ) + + @Serializable + data class U2( + @ProtoNumber(1) val u1: ByteArray, + @ProtoNumber(2) val u2: ByteArray, + @ProtoNumber(3) val u3: ByteArray, + ) + + } +} + +@Serializable +data class MultiMediaRoutingHead( + @ProtoNumber(1) val request: Request, + @ProtoNumber(2) val peerUser: PeerUser, + @ProtoNumber(3) val u1: U1 +) { + companion object { + @Serializable + data class U1( + @ProtoNumber(1) val u1: UInt, + ) + + @Serializable + data class Request( + @ProtoNumber(1) val u1: UInt, + @ProtoNumber(2) val u2: UInt + ) + + @Serializable + data class PeerUser( + @ProtoNumber(101) val u1: UInt, + @ProtoNumber(102) val u2: UInt, + @ProtoNumber(200) val u3: UInt, + @ProtoNumber(201) val peer: Peer, + ) + + @Serializable + data class Peer( + @ProtoNumber(1) val u1: UInt, + @ProtoNumber(2) val uid: String + ) + } +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/msgelement/MsgElementConverter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/msgelement/MsgElementConverter.kt index 8a7ea23..a0060fd 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/msgelement/MsgElementConverter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/msgelement/MsgElementConverter.kt @@ -184,19 +184,20 @@ internal object MsgElementConverter { element: MsgElement ): MessageSegment { val image = element.picElement - val md5 = image.md5HexStr ?: image.fileName + val md5 = (image.md5HexStr ?: image.fileName .replace("{", "") .replace("}", "") - .replace("-", "").split(".")[0] + .replace("-", "").split(".")[0]) + .uppercase() ImageDB.getInstance().imageMappingDao().insert( - ImageMapping(md5.uppercase(), chatType, image.fileSize) + ImageMapping(md5, chatType, image.fileSize) ) //LogCenter.log(image.toString()) val originalUrl = image.originImageUrl ?: "" - //LogCenter.log({ "receive image: $image" }, Level.DEBUG) + LogCenter.log({ "receive image: $image" }, Level.DEBUG) return MessageSegment( type = "image", @@ -208,7 +209,16 @@ internal object MsgElementConverter { md5 ) - MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(originalUrl, md5) + 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 = peerId + ) MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(originalUrl, md5) else -> throw UnsupportedOperationException("Not supported chat type: $chatType") }, 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 27f2592..4471cae 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,15 +6,15 @@ 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 io.ktor.util.Identity.decode +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.decodeFromByteArray -import kotlinx.serialization.encodeToByteArray import moe.fuqiuluo.qqinterface.servlet.BaseSvc +import moe.fuqiuluo.shamrock.helper.ContactHelper import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY import moe.fuqiuluo.shamrock.tools.hex2ByteArray import moe.fuqiuluo.shamrock.tools.slice import moe.fuqiuluo.shamrock.tools.toHexString @@ -23,6 +23,11 @@ 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.MultiMediaDataInfo +import protobuf.oidb.cmd0x11c5.MultiMediaRoutingHead +import protobuf.oidb.cmd0x11c5.Oidb0x11c5Req +import protobuf.oidb.cmd0x11c5.Oidb0x11c5Resp import protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody @@ -34,22 +39,11 @@ import tencent.im.oidb.oidb_sso import kotlin.coroutines.resume private const val GPRO_PIC = "gchat.qpic.cn" -private const val GPRO_PIC_NT = "multimedia.nt.qq.com.cn" +private const val MULTIMEDIA_DOMAIN = "multimedia.nt.qq.com.cn" private const val C2C_PIC = "c2cpicdw.qpic.cn" internal object RichProtoSvc: BaseSvc() { var multiMediaRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64" - /*@Deprecated("Use RichProtoSvc.getQQDns instead", ReplaceWith("getQQDns(domain)")) - fun getQQDns(domain: String) { - val bundle = Bundle() - bundle.putString("domain", "xxx") - bundle.putInt("businessType", 1) - val result = BinderMethodProxy - .callServer(QIPCClientHelper.getInstance().client, "InnerDnsModule", "reqDomain2IpList", bundle) - if (result.isSuccess) { - val ipList: ArrayList = result.data.getParcelableArrayList("ip")!! - } - }*/ suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String { val buffer = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, Oidb0xfc2ReqBody( @@ -167,7 +161,7 @@ internal object RichProtoSvc: BaseSvc() { md5: String, ): String { val isNtServer = originalUrl.startsWith("/download") - val domain = if (isNtServer) GPRO_PIC_NT else GPRO_PIC + val domain = if (isNtServer) MULTIMEDIA_DOMAIN else GPRO_PIC if (originalUrl.isNotEmpty()) { if (isNtServer && !originalUrl.contains("rkey=")) { return "https://$domain$originalUrl&rkey=$multiMediaRKey" @@ -177,19 +171,38 @@ internal object RichProtoSvc: BaseSvc() { return "https://$domain/gchatpic_new/0/0-0-${md5.uppercase()}/0?term=2" } - fun getC2CPicDownUrl( + suspend fun getC2CPicDownUrl( originalUrl: String, - md5: String + md5: String, + peer: String = "", + fileId: String = "", + sha: String = "", + fileSize: ULong = 0uL, + width: UInt = 0u, + height: UInt = 0u ): String { val isNtServer = originalUrl.startsWith("/download") - val domain = if (isNtServer) GPRO_PIC_NT else C2C_PIC + val domain = if (isNtServer) MULTIMEDIA_DOMAIN else C2C_PIC if (originalUrl.isNotEmpty()) { + if (fileId.isNotEmpty()) getC2CNtPicRKey( + peer = ContactHelper.getUidByUinAsync(peer.toLong()), + fileId = fileId, + md5 = md5, + sha = sha, + fileSize = fileSize, + width = width, + height = height + ).onSuccess { + if (isNtServer && !originalUrl.contains("rkey=")) { + return "https://$domain$originalUrl$it" + } + } if (isNtServer && !originalUrl.contains("rkey=")) { return "https://$domain$originalUrl&rkey=$multiMediaRKey" } return "https://$domain$originalUrl" } - return "https://$$domain/offpic_new/0/123-0-${md5.uppercase()}/0?term=2" + return "https://$$domain/offpic_new/0/123-0-${md5}/0?term=2" } fun getGuildPicDownUrl( @@ -197,7 +210,7 @@ internal object RichProtoSvc: BaseSvc() { md5: String ): String { val isNtServer = originalUrl.startsWith("/download") - val domain = if (isNtServer) GPRO_PIC_NT else GPRO_PIC + val domain = if (isNtServer) MULTIMEDIA_DOMAIN else GPRO_PIC if (originalUrl.isNotEmpty()) { if (isNtServer && !originalUrl.contains("rkey=")) { return "https://$domain$originalUrl&rkey=$multiMediaRKey" @@ -207,6 +220,71 @@ internal object RichProtoSvc: BaseSvc() { return "https://$domain/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2" } + suspend fun getC2CNtPicRKey( + peer: String, + fileId: String, + md5: String, + sha: String, + fileSize: ULong, + width: UInt, + height: UInt + ): Result { + val req = run { + Oidb0x11c5Req( + MultiMediaRoutingHead( + request = MultiMediaRoutingHead.Companion.Request(u1 = 2u, u2 = 200u), + peerUser = MultiMediaRoutingHead.Companion.PeerUser(u1 = 2u, u2 = 1u, u3 = 1u, peer = MultiMediaRoutingHead.Companion.Peer( + u1 = 2u, + uid = peer + )), + u1 = MultiMediaRoutingHead.Companion.U1(2u) + ), + MultiMediaDataInfo( + MultiMediaDataInfo.Companion.MultiMedia( + MultiMediaDataInfo.Companion.Picture( + size = fileSize, + md5 = md5.lowercase(), + sha = sha.lowercase(), + fileName = "${md5}.jpg", + u1 = MultiMediaDataInfo.Companion.U3( + u1 = 1u, + u2 = 1000u, + u3 = 0u, + u4 = 0u + ), + width = width, + height = height, + u2 = 0u, + u3 = 1u + ), + fileId = fileId, + u1 = 1u, + u2 = 0u, + u3 = 0u, + u4 = 0u + ), + MultiMediaDataInfo.Companion.EXT( + u1 = MultiMediaDataInfo.Companion.U1( + u1 = 0u, + u2 = 0u, + u3 = MultiMediaDataInfo.Companion.U2( + u1 = EMPTY_BYTE_ARRAY, + u2 = EMPTY_BYTE_ARRAY, + u3 = EMPTY_BYTE_ARRAY + ), + u4 = 1u + ) + ) + ) + ) + }.toByteArray() + val buffer = sendOidbAW("OidbSvcTrpcTcp.0x11c5_200", 0x11c5, 200, req, true)?.slice(4) + buffer?.decodeProtobuf()?.buffer?.decodeProtobuf()?.result?.rkeyParam?.let { + return Result.success(it) + } + return Result.failure(Exception("unable to get c2c nt pic")) + } + suspend fun getC2CVideoDownUrl( peerId: String, md5: ByteArray, diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetImage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetImage.kt index 1965315..6456918 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetImage.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetImage.kt @@ -18,7 +18,7 @@ internal object GetImage: IActionHandler() { return invoke(file, echo) } - operator fun invoke(file: String, echo: JsonElement = EmptyJsonString): String { + suspend operator fun invoke(file: String, echo: JsonElement = EmptyJsonString): String { val fileMd5 = file .replace("{", "") .replace("}", "")