diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 417f49a..ed14bc3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -200,9 +200,20 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$serializationVersion") implementation("com.google.protobuf:protobuf-java:4.26.1") implementation("androidx.core:core-ktx:1.13.1") + + implementation(ktor("client", "core")) + implementation(ktor("client", "content-negotiation")) + implementation(ktor("client", "cio")) + implementation(ktor("serialization", "kotlinx-json")) + implementation(ktor("network", "tls-certificates")) + testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test:core:1.5.0") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test:runner:1.5.2") androidTestImplementation("androidx.test:rules:1.5.0") +} + +fun ktor(target: String, name: String): String { + return "io.ktor:ktor-$target-$name:2.3.3" } \ No newline at end of file diff --git a/app/src/main/java/moe/fuqiuluo/MsfHandler.kt b/app/src/main/java/moe/fuqiuluo/MsfHandler.kt new file mode 100644 index 0000000..6a51577 --- /dev/null +++ b/app/src/main/java/moe/fuqiuluo/MsfHandler.kt @@ -0,0 +1,72 @@ +package moe.fuqiuluo + +import com.tencent.qphone.base.remote.FromServiceMsg +import com.tencent.qphone.base.remote.ToServiceMsg +import de.robv.android.xposed.XposedBridge +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.coroutines.resume + +typealias MsfPush = (FromServiceMsg) -> Unit +typealias MsfResp = CancellableContinuation> + +internal object MSFHandler { + private val mPushHandlers = hashMapOf() + private val mRespHandler = hashMapOf() + private val mPushLock = Mutex() + private val mRespLock = Mutex() + + private val seq = atomic(0) + + fun nextSeq(): Int { + seq.compareAndSet(0xFFFFFFF, 0) + return seq.incrementAndGet() + } + + suspend fun registerPush(cmd: String, push: MsfPush) { + mPushLock.withLock { + mPushHandlers[cmd] = push + } + } + + suspend fun unregisterPush(cmd: String) { + mPushLock.withLock { + mPushHandlers.remove(cmd) + } + } + + suspend fun registerResp(cmd: Int, resp: MsfResp) { + mRespLock.withLock { + mRespHandler[cmd] = resp + } + } + + suspend fun unregisterResp(cmd: Int) { + mRespLock.withLock { + mRespHandler.remove(cmd) + } + } + + fun onPush(fromServiceMsg: FromServiceMsg) { + val cmd = fromServiceMsg.serviceCmd + if (cmd == "trpc.msg.olpush.OlPushService.MsgPush") { + //PrimitiveListener.onPush(fromServiceMsg) + } else { + val push = mPushHandlers[cmd] + push?.invoke(fromServiceMsg) + } + } + + fun onResp(toServiceMsg: ToServiceMsg, fromServiceMsg: FromServiceMsg) { + runCatching { + val cmd = toServiceMsg.getAttribute("qwq_uid") as? Int? + ?: return@runCatching + val resp = mRespHandler[cmd] + resp?.resume(toServiceMsg to fromServiceMsg) + }.onFailure { + XposedBridge.log("[QwQ] MSF.onResp failed: ${it.message}") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/fuqiuluo/QQInterfaces.kt b/app/src/main/java/moe/fuqiuluo/QQInterfaces.kt new file mode 100644 index 0000000..e13e78f --- /dev/null +++ b/app/src/main/java/moe/fuqiuluo/QQInterfaces.kt @@ -0,0 +1,136 @@ +package moe.fuqiuluo + +import android.os.Bundle +import com.tencent.common.app.AppInterface +import com.tencent.mobileqq.pb.ByteStringMicro +import com.tencent.qphone.base.remote.FromServiceMsg +import com.tencent.qphone.base.remote.ToServiceMsg +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.serialization.encodeToByteArray +import kotlinx.serialization.protobuf.ProtoBuf +import moe.fuqiuluo.entries.TrpcOidb +import moe.qwq.miko.tools.PlatformTools +import moe.qwq.miko.tools.PlatformTools.app +import mqq.app.MobileQQ +import tencent.im.oidb.oidb_sso +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +abstract class QQInterfaces { + + companion object { + fun sendToServiceMsg(to: ToServiceMsg) { + app.sendToService(to) + } + + suspend fun sendToServiceMsgAW( + to: ToServiceMsg, + timeout: Duration = 30.seconds + ): FromServiceMsg? { + val seq = MSFHandler.nextSeq() + to.addAttribute("qwq_uid", seq) + val resp: Pair? = withTimeoutOrNull(timeout) { + suspendCancellableCoroutine { continuation -> + GlobalScope.launch { + MSFHandler.registerResp(seq, continuation) + sendToServiceMsg(to) + } + } + } + if (resp == null) { + MSFHandler.unregisterResp(seq) + } + return resp?.second + } + + fun sendExtra(cmd: String, builder: (Bundle) -> Unit) { + val toServiceMsg = createToServiceMsg(cmd) + builder(toServiceMsg.extraData) + app.sendToService(toServiceMsg) + } + + fun createToServiceMsg(cmd: String): ToServiceMsg { + return ToServiceMsg("mobileqq.service", app.currentAccountUin, cmd) + } + + fun sendOidb(cmd: String, command: Int, service: Int, data: ByteArray, trpc: Boolean = false) { + val to = createToServiceMsg(cmd) + if (trpc) { + val oidb = TrpcOidb( + cmd = command, + service = service, + buffer = data, + flag = 1 + ) + to.putWupBuffer(ProtoBuf.encodeToByteArray(oidb)) + } else { + val oidb = oidb_sso.OIDBSSOPkg() + oidb.uint32_command.set(command) + oidb.uint32_service_type.set(service) + oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(data)) + oidb.str_client_version.set(PlatformTools.getClientVersion(MobileQQ.getContext())) + to.putWupBuffer(oidb.toByteArray()) + } + to.addAttribute("req_pb_protocol_flag", true) + app.sendToService(to) + } + + fun sendBuffer( + cmd: String, + isProto: Boolean, + data: ByteArray, + ) { + val toServiceMsg = createToServiceMsg(cmd) + toServiceMsg.putWupBuffer(data) + toServiceMsg.addAttribute("req_pb_protocol_flag", isProto) + sendToServiceMsg(toServiceMsg) + } + + @DelicateCoroutinesApi + suspend fun sendBufferAW( + cmd: String, + isProto: Boolean, + data: ByteArray, + timeout: Duration = 30.seconds + ): FromServiceMsg? { + val toServiceMsg = createToServiceMsg(cmd) + toServiceMsg.putWupBuffer(data) + toServiceMsg.addAttribute("req_pb_protocol_flag", isProto) + return sendToServiceMsgAW(toServiceMsg, timeout) + } + + @DelicateCoroutinesApi + suspend fun sendOidbAW( + cmd: String, + command: Int, + service: Int, + data: ByteArray, + trpc: Boolean = false, + timeout: Duration = 30.seconds + ): FromServiceMsg? { + val to = createToServiceMsg(cmd) + if (trpc) { + val oidb = TrpcOidb( + cmd = command, + service = service, + buffer = data, + flag = 1 + ) + to.putWupBuffer(ProtoBuf.encodeToByteArray(oidb)) + } else { + val oidb = oidb_sso.OIDBSSOPkg() + oidb.uint32_command.set(command) + oidb.uint32_service_type.set(service) + oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(data)) + oidb.str_client_version.set(PlatformTools.getClientVersion(MobileQQ.getContext())) + to.putWupBuffer(oidb.toByteArray()) + } + to.addAttribute("req_pb_protocol_flag", true) + return sendToServiceMsgAW(to, timeout) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/fuqiuluo/entries/MessagePush.kt b/app/src/main/java/moe/fuqiuluo/entries/MessagePush.kt index c9d65d4..0b21d03 100644 --- a/app/src/main/java/moe/fuqiuluo/entries/MessagePush.kt +++ b/app/src/main/java/moe/fuqiuluo/entries/MessagePush.kt @@ -20,7 +20,7 @@ data class Message( @Serializable data class MessageHead( - @ProtoNumber(1) val peerId: Long = Long.MIN_VALUE, + @ProtoNumber(1) val peerId: ULong = ULong.MIN_VALUE, @ProtoNumber(2) val peerUid: String? = null, @ProtoNumber(5) val targetId: Long = Long.MIN_VALUE, @ProtoNumber(6) val targetUid: String? = null diff --git a/app/src/main/java/moe/fuqiuluo/entries/NtV2RichMediaReq.kt b/app/src/main/java/moe/fuqiuluo/entries/NtV2RichMediaReq.kt new file mode 100644 index 0000000..1ac8b81 --- /dev/null +++ b/app/src/main/java/moe/fuqiuluo/entries/NtV2RichMediaReq.kt @@ -0,0 +1,272 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package moe.fuqiuluo.entries + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber + +@Serializable +data class NtV2RichMediaReq( + @ProtoNumber(1) val head: MultiMediaReqHead, + @ProtoNumber(2) val upload: UploadReq? = null, // 100 + @ProtoNumber(3) val download: DownloadReq? = null, + @ProtoNumber(4) val downloadRkey: DownloadRkeyReq? = null, + @ProtoNumber(5) val delete: DeleteReq? = null, + @ProtoNumber(6) val uploadCompleted: UploadCompletedReq? = null, + @ProtoNumber(7) val msgInfoAuth: MsgInfoAuthReq? = null, + @ProtoNumber(8) val uploadKeyRenewal: UploadKeyRenewalReq? = null, + @ProtoNumber(9) val downloadSafe: DownloadSafeReq? = null, + @ProtoNumber(99) val extension: ByteArray? = null, +) + +@Serializable +data class DownloadSafeReq( + @ProtoNumber(1) val index: IndexNode, +) + +@Serializable +data class UploadKeyRenewalReq( + @ProtoNumber(1) val oldUkey: String, + @ProtoNumber(2) val subType: UInt, +) + +@Serializable +data class MsgInfoAuthReq( + @ProtoNumber(1) val msg: ByteArray, + @ProtoNumber(2) val authTime: ULong, +) + +@Serializable +data class UploadCompletedReq( + @ProtoNumber(1) val srvSendMsg: Boolean, + @ProtoNumber(2) val clientRandomId: ULong, + @ProtoNumber(3) val msgInfo: MsgInfo, + @ProtoNumber(4) val clientSeq: UInt, +) + +@Serializable +data class MsgInfo( + @ProtoNumber(1) val msgInfoBody: List, + @ProtoNumber(2) val extBizInfo: ExtBizInfo?, +) + +@Serializable +data class MsgInfoBody( + @ProtoNumber(1) val index: IndexNode? = null, + @ProtoNumber(2) val picture: PictureInfo? = null, + @ProtoNumber(3) val video: VideoInfo? = null, + @ProtoNumber(4) val audio: AudioInfo? = null, + @ProtoNumber(5) val fileExist: Boolean? = null, + @ProtoNumber(6) val hashSum: ByteArray? = null, +) + +@Serializable +class VideoInfo + +@Serializable +class AudioInfo + + +@Serializable +data class PictureInfo( + @ProtoNumber(1) val urlPath: String, + @ProtoNumber(2) val ext: PicUrlExtInfo? = null, + @ProtoNumber(3) val domain: String? = null +) + +@Serializable +data class PicUrlExtInfo( + @ProtoNumber(1) val originalParameter: String? = null, + @ProtoNumber(2) val bigParameter: String? = null, + @ProtoNumber(3) val thumbParameter: String? = null +) + +@Serializable +data class DeleteReq( + @ProtoNumber(1) val index: List, + @ProtoNumber(2) val needRecallMsg: Boolean? = null, + @ProtoNumber(3) val msgSeq: ULong? = null, + @ProtoNumber(4) val msgRandom: ULong? = null, + @ProtoNumber(5) val msgTime: ULong? = null, +) + +@Serializable +data class DownloadRkeyReq( + @ProtoNumber(1) val types: List, + @ProtoNumber(2) val downloadType: Int +) + +@Serializable +data class UploadReq( + @ProtoNumber(1) val uploadInfo: List, + @ProtoNumber(2) val tryFastUploadCompleted: Boolean? = null, + @ProtoNumber(3) val srvSendMsg: Boolean? = null, + @ProtoNumber(4) val clientRandomId: ULong = ULong.MIN_VALUE, + @ProtoNumber(5) val compatQMsgSceneType: UInt? = null, + @ProtoNumber(6) val extBizInfo: ExtBizInfo? = null, + @ProtoNumber(7) val clientSeq: UInt? = null, + @ProtoNumber(8) val noNeedCompatMsg: Boolean? = null, +) + +@Serializable +data class ExtBizInfo( + @ProtoNumber(1) val pic: PicExtBizInfo? = null, + @ProtoNumber(2) val video: VideoExtBizInfo? = null, + @ProtoNumber(3) val ptt: PttExtBizInfo? = null, + @ProtoNumber(10) val busiType: UInt?, +) + +@Serializable +data class PttExtBizInfo( + @ProtoNumber(1) val srcUin: ULong, + @ProtoNumber(2) val pttScene: UInt, + @ProtoNumber(3) val pttType: UInt, + @ProtoNumber(4) val changeVoice: UInt, + @ProtoNumber(5) val waveform: ByteArray? = null, + @ProtoNumber(6) val autoConvertText: UInt? = null, + @ProtoNumber(11) val bytesReserve: ByteArray? = null, + @ProtoNumber(12) val bytesPbReserve: ByteArray? = null, + @ProtoNumber(13) val bytesGeneralFlags: ByteArray? = null, +) + +@Serializable +data class VideoExtBizInfo( + @ProtoNumber(1) val fromScene: UInt?, + @ProtoNumber(2) val toScene: UInt?, + @ProtoNumber(3) val bytesPbReserve: ByteArray?, +) + +@Serializable +data class PicExtBizInfo( + @ProtoNumber(1) val bizType: UInt?, + @ProtoNumber(2) val textSummary: String?, + @ProtoNumber(11) val bytesPbReserveC2c: ByteArray? = null, + @ProtoNumber(12) val bytesPbReserveTroop: ByteArray? = null, + @ProtoNumber(1001) val fromScene: UInt? = null, + @ProtoNumber(1002) val toScene: UInt? = null, + @ProtoNumber(1003) val oldFileId: UInt? = null, +) + +@Serializable +data class UploadInfo( + @ProtoNumber(1) val fileInfo: FileInfo, + @ProtoNumber(2) val subFileType: UInt +) + +@Serializable +data class FileInfo( + @ProtoNumber(1) val fileSize: ULong?, + @ProtoNumber(2) val md5: String?, + @ProtoNumber(3) val sha1: String?, + @ProtoNumber(4) val name: String?, + @ProtoNumber(5) val fileType: FileType?, + @ProtoNumber(6) val width: UInt?, + @ProtoNumber(7) val height: UInt?, + @ProtoNumber(8) val time: UInt?, + @ProtoNumber(9) val original: UInt?, +) + +@Serializable +data class FileType( + @ProtoNumber(1) val fileType: UInt = 0u, + @ProtoNumber(2) val picFormat: UInt = 0u, + @ProtoNumber(3) val videoFormat: UInt? = null, + @ProtoNumber(4) val voiceFormat: UInt? = null, +) + +@Serializable +data class DownloadReq( + @ProtoNumber(1) val index: IndexNode, + @ProtoNumber(2) val ext: DownloadExt, +) + +@Serializable +data class DownloadExt( + @ProtoNumber(1) val pic: PicDownloadExt? = null, + @ProtoNumber(2) val video: VideoDownloadExt, + @ProtoNumber(3) val voice: PttDownloadExt? = null, +) + +@Serializable +class PicDownloadExt + +@Serializable +class PttDownloadExt + +@Serializable +data class VideoDownloadExt( + @ProtoNumber(1) val busiType: UInt?, + @ProtoNumber(2) val sceneType: UInt? = null, + @ProtoNumber(3) val subBusiType: UInt?, + @ProtoNumber(4) val msgCodecConfig: CodecConfigReq, + @ProtoNumber(5) val flag: UInt?, +) + +@Serializable +data class CodecConfigReq( + @ProtoNumber(1) val platformChipinfo: String, + @ProtoNumber(2) val osVer: String, + @ProtoNumber(3) val deviceName: String, +) + +@Serializable +data class IndexNode( + @ProtoNumber(1) val fileInfo: FileInfo, + @ProtoNumber(2) val fileUuid: String, + @ProtoNumber(3) val storeId: UInt, // 0为旧服务器 1为nt服务器 + @ProtoNumber(4) val uploadTime: ULong, + @ProtoNumber(5) val ttl: ULong, + @ProtoNumber(6) val subType: UInt? = null, + @ProtoNumber(7) val storeAppId: UInt? = null +) + +@Serializable +data class MultiMediaReqHead( + @ProtoNumber(1) val commonHead: CommonHead, + @ProtoNumber(2) val sceneInfo: SceneInfo, + @ProtoNumber(3) val clientMeta: ClientMeta +) + +@Serializable +data class ClientMeta( + @ProtoNumber(1) val agentType: UInt, +) + +@Serializable +data class SceneInfo( + @ProtoNumber(101) val requestType: UInt, + @ProtoNumber(102) val businessType: UInt, + @ProtoNumber(103) val appType: UInt? = null, + @ProtoNumber(200) var sceneType: UInt? = null, + @ProtoNumber(201) var c2c: C2CUserInfo? = null, + @ProtoNumber(202) var grp: GroupUserInfo? = null, + @ProtoNumber(203) var channel: ChannelUserInfo? = null, + @ProtoNumber(205) val byteArr: ByteArray?= null +) + +@Serializable +data class ChannelUserInfo( + @ProtoNumber(1) val guildId: ULong, + @ProtoNumber(2) val channelId: ULong, + @ProtoNumber(3) val channelType: UInt, +) + +@Serializable +data class GroupUserInfo( + @ProtoNumber(1) val uin: ULong, +) + +@Serializable +data class C2CUserInfo( + @ProtoNumber(1) val accountType: UInt, + @ProtoNumber(2) val uid: String, + @ProtoNumber(3) val byteArr: ByteArray? = null +) + +@Serializable +data class CommonHead( + @ProtoNumber(1) val requestId: ULong, + @ProtoNumber(2) val cmd: UInt, + @ProtoNumber(3) val msg: String? = null +) diff --git a/app/src/main/java/moe/fuqiuluo/entries/NtV2RichMediaRsp.kt b/app/src/main/java/moe/fuqiuluo/entries/NtV2RichMediaRsp.kt new file mode 100644 index 0000000..5a24989 --- /dev/null +++ b/app/src/main/java/moe/fuqiuluo/entries/NtV2RichMediaRsp.kt @@ -0,0 +1,138 @@ +@file:OptIn(ExperimentalSerializationApi::class) +package moe.fuqiuluo.entries + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import moe.qwq.miko.ext.EMPTY_BYTE_ARRAY + +@Serializable +data class NtV2RichMediaRsp( + @ProtoNumber(1) val head: RspHead?, + @ProtoNumber(2) val upload: UploadRsp?, + @ProtoNumber(3) val download: DownloadRsp?, + @ProtoNumber(4) val downloadRkeyRsp: DownloadRkeyRsp?, + @ProtoNumber(5) val delete: DeleteRsp?, + @ProtoNumber(6) val uploadCompleted: UploadCompletedRsp?, + @ProtoNumber(7) val msgInfoAuth: MsgInfoAuthRsp?, + @ProtoNumber(8) val uploadKeyRenew: UploadKeyRenewalRsp?, + @ProtoNumber(9) val downloadSafe: DownloadSafeRsp?, + @ProtoNumber(99) val extension: ByteArray? = null, +) + +@Serializable +class DownloadSafeRsp + +@Serializable +data class UploadKeyRenewalRsp( + @ProtoNumber(1) val ukey: String?, + @ProtoNumber(2) val ukeyTtlSec: ULong?, +) + +@Serializable +data class MsgInfoAuthRsp( + @ProtoNumber(1) val authCode: UInt = 0u, + @ProtoNumber(2) val msg: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoNumber(3) val resultTime: ULong = 0u, +) + +@Serializable +data class UploadCompletedRsp( + @ProtoNumber(1) val msgSeq: ULong? +) + +@Serializable +class DeleteRsp + +@Serializable +data class DownloadRkeyRsp( + @ProtoNumber(1) val rkeys: List? +) + +@Serializable +data class RKeyInfo( + @ProtoNumber(1) val rkey: String, + @ProtoNumber(2) val rkeyTtlSec: ULong?, + @ProtoNumber(3) val storeId: UInt = 0u, + @ProtoNumber(4) val rkeyCreateTime: UInt?, + @ProtoNumber(5) val type: UInt, +) + +@Serializable +data class DownloadRsp( + @ProtoNumber(1) val rkeyParam: String?, + @ProtoNumber(2) val rkeyTtlSec: ULong?, + @ProtoNumber(3) val downloadInfo: DownloadInfo?, + @ProtoNumber(4) val rkeyCreateTime: UInt? +) + +@Serializable +data class DownloadInfo( + @ProtoNumber(1) val domain: String, + @ProtoNumber(2) val urlPath: String? = null, + @ProtoNumber(3) val httpsPort: Int = Int.MIN_VALUE, + @ProtoNumber(4) val ipv4: List, + @ProtoNumber(5) val ipv6: List, + @ProtoNumber(6) val picUrlExtInfo: PicUrlExtInfo?, + @ProtoNumber(7) val videoExtInfo: VideoExtInfo? = null, +) + +@Serializable +data class VideoExtInfo( + @ProtoNumber(1) val videoCodecFormat: UInt? = null, +) + +@Serializable +data class UploadRsp( + @ProtoNumber(1) val ukey: String?, + @ProtoNumber(2) val ukeyTtlSec: ULong?, + @ProtoNumber(3) val ipv4: List?, + @ProtoNumber(4) val ipv6: List?, + @ProtoNumber(5) val msgSeq: ULong?, + @ProtoNumber(6) val msgInfo: MsgInfo? = null, + @ProtoNumber(7) val ext: List? = null, + @ProtoNumber(8) val compatQMsg: ByteArray? = null, + @ProtoNumber(10) val subFileInfos: List? = null, +) + +@Serializable +data class SubFileInfo( + @ProtoNumber(1) val subType: UInt?, + @ProtoNumber(2) val ukey: String?, + @ProtoNumber(3) val ukeyTTLSec: ULong?, + @ProtoNumber(4) val ipv4: List?, + @ProtoNumber(5) val ipv6: List?, +) + +@Serializable +data class RichmediaStorageTransInfo( + @ProtoNumber(1) val subType: UInt = UInt.MIN_VALUE, + @ProtoNumber(2) val extType: UInt = UInt.MIN_VALUE, + @ProtoNumber(3) val extValue: ByteArray? = null, +) + +@Serializable +data class Ipv4( + @ProtoNumber(1) val outIp: Int = Int.MIN_VALUE, + @ProtoNumber(2) val outPort: Int = Int.MIN_VALUE, + @ProtoNumber(3) val inIp: Int = Int.MIN_VALUE, + @ProtoNumber(4) val inPort: Int = Int.MIN_VALUE, + @ProtoNumber(5) val ipType: Int = Int.MIN_VALUE, +) + +@Serializable +data class Ipv6( + @ProtoNumber(1) val outIp: ByteArray? = null, + @ProtoNumber(2) val outPort: Int = Int.MIN_VALUE, + @ProtoNumber(3) val inIp: ByteArray? = null, + @ProtoNumber(4) val inPort: Int = Int.MIN_VALUE, + @ProtoNumber(5) val ipType: Int = Int.MIN_VALUE, +) + +@Serializable +data class RspHead( + @ProtoNumber(1) val commonHead: CommonHead?, + @ProtoNumber(2) val retCode: UInt = 0u, + @ProtoNumber(3) val msg: String? +) + diff --git a/app/src/main/java/moe/fuqiuluo/entries/TrpcOidb.kt b/app/src/main/java/moe/fuqiuluo/entries/TrpcOidb.kt new file mode 100644 index 0000000..ea7e7c0 --- /dev/null +++ b/app/src/main/java/moe/fuqiuluo/entries/TrpcOidb.kt @@ -0,0 +1,15 @@ +package moe.fuqiuluo.entries + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber + +@Serializable +data class TrpcOidb( + @ProtoNumber(1) val cmd: Int = Int.MIN_VALUE, + @ProtoNumber(2) val service: Int = Int.MIN_VALUE, + @ProtoNumber(3) val result: UInt? = null, + @ProtoNumber(4) val buffer: ByteArray? = null, + @ProtoNumber(5) val msg: String? = null, + //@ProtoNumber(11) val traceParams: Map = mapOf(), + @ProtoNumber(12) val flag: Int = Int.MIN_VALUE, +) \ No newline at end of file diff --git a/app/src/main/java/moe/qwq/miko/ActionManager.kt b/app/src/main/java/moe/qwq/miko/ActionManager.kt index e0bc540..9cc9cf5 100644 --- a/app/src/main/java/moe/qwq/miko/ActionManager.kt +++ b/app/src/main/java/moe/qwq/miko/ActionManager.kt @@ -7,6 +7,7 @@ import moe.qwq.miko.internals.hooks.BrowserAccessRestrictions import moe.qwq.miko.actions.FetchService import moe.qwq.miko.actions.IAction import moe.qwq.miko.actions.PacketHijacker +import moe.qwq.miko.actions.PatchMsfCore import moe.qwq.miko.actions.WebJsBridge import moe.qwq.miko.internals.hooks.* @@ -16,6 +17,7 @@ object ActionManager { WebJsBridge::class.java, // ALWAYS RUN FetchService::class.java, // ALWAYS RUN PacketHijacker::class.java, // ALWAYS RUN + PatchMsfCore::class.java, // ALWAYS RUN OneClickLike::class.java, ForceTabletMode::class.java, diff --git a/app/src/main/java/moe/qwq/miko/actions/PatchMsfCore.kt b/app/src/main/java/moe/qwq/miko/actions/PatchMsfCore.kt new file mode 100644 index 0000000..33cabb6 --- /dev/null +++ b/app/src/main/java/moe/qwq/miko/actions/PatchMsfCore.kt @@ -0,0 +1,39 @@ +package moe.qwq.miko.actions + +import android.content.Context +import com.tencent.common.app.AppInterface +import com.tencent.mobileqq.msf.sdk.MsfMessagePair +import de.robv.android.xposed.XposedBridge +import moe.fuqiuluo.MSFHandler.onPush +import moe.fuqiuluo.MSFHandler.onResp +import moe.fuqiuluo.processor.HookAction +import moe.fuqiuluo.xposed.loader.LuoClassloader +import moe.qwq.miko.ext.hookMethod +import moe.qwq.miko.tools.PlatformTools.app + +@HookAction("注入MSF收包任务") +class PatchMsfCore: AlwaysRunAction() { + override fun onRun(ctx: Context) { + runCatching { + val MSFRespHandleTask = LuoClassloader.load("mqq.app.msghandle.MSFRespHandleTask") + if (MSFRespHandleTask == null) { + XposedBridge.log("[QwQ] 无法注入MSFRespHandleTask!") + } else { + val msfPair = MSFRespHandleTask.declaredFields.first { + it.type == MsfMessagePair::class.java + } + msfPair.isAccessible = true + MSFRespHandleTask.hookMethod("run").before { + val pair = msfPair.get(it.thisObject) as MsfMessagePair + if (pair.toServiceMsg == null) { + onPush(pair.fromServiceMsg) + } else { + onResp(pair.toServiceMsg, pair.fromServiceMsg) + } + } + } + }.onFailure { + XposedBridge.log(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/qwq/miko/ext/Kotlinx.kt b/app/src/main/java/moe/qwq/miko/ext/Kotlinx.kt index 9886016..c5a37ac 100644 --- a/app/src/main/java/moe/qwq/miko/ext/Kotlinx.kt +++ b/app/src/main/java/moe/qwq/miko/ext/Kotlinx.kt @@ -47,8 +47,8 @@ fun ByteArray.slice(off: Int, length: Int = size - off): ByteArray { .let { s -> if (uppercase) s.lowercase(Locale.getDefault()) else s } } ?: "null" -fun String?.ifNullOrEmpty(defaultValue: String?): String? { - return if (this.isNullOrEmpty()) defaultValue else this +fun String?.ifNullOrEmpty(defaultValue: () -> String?): String? { + return if (this.isNullOrEmpty()) defaultValue() else this } @JvmOverloads fun String.hex2ByteArray(replace: Boolean = false): ByteArray { diff --git a/app/src/main/java/moe/qwq/miko/ext/Trpc.kt b/app/src/main/java/moe/qwq/miko/ext/Trpc.kt new file mode 100644 index 0000000..4b8280a --- /dev/null +++ b/app/src/main/java/moe/qwq/miko/ext/Trpc.kt @@ -0,0 +1,37 @@ +package moe.qwq.miko.ext + +import com.tencent.qphone.base.remote.FromServiceMsg +import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.protobuf.ProtoBuf +import moe.fuqiuluo.entries.TrpcOidb +import moe.qwq.miko.tools.DeflateTools +import tencent.im.oidb.oidb_sso +import kotlin.reflect.KClass + +inline fun ByteArray.decodeProtobuf(to: KClass? = null): T { + return ProtoBuf.decodeFromByteArray(this) +} + +fun FromServiceMsg.decodeToOidb(): oidb_sso.OIDBSSOPkg { + return kotlin.runCatching { + oidb_sso.OIDBSSOPkg().mergeFrom(wupBuffer.slice(4).let { + if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it + }) + }.getOrElse { + oidb_sso.OIDBSSOPkg().mergeFrom(wupBuffer.let { + 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/app/src/main/java/moe/qwq/miko/ext/Xposed.kt b/app/src/main/java/moe/qwq/miko/ext/Xposed.kt index 7ce8eba..a360ae3 100644 --- a/app/src/main/java/moe/qwq/miko/ext/Xposed.kt +++ b/app/src/main/java/moe/qwq/miko/ext/Xposed.kt @@ -41,6 +41,10 @@ internal fun Class<*>.hookMethod(name: String): XCHook { } } +internal fun Class<*>.hookMethod(name: String, hook: XC_MethodHook) { + XposedBridge.hookAllMethods(this, name, hook) +} + internal fun beforeHook(ver: Int = XCallback.PRIORITY_DEFAULT, block: (param: XC_MethodHook.MethodHookParam) -> Unit): XC_MethodHook { return object :XC_MethodHook(ver) { override fun beforeHookedMethod(param: MethodHookParam) { diff --git a/app/src/main/java/moe/qwq/miko/internals/AioListener.kt b/app/src/main/java/moe/qwq/miko/internals/AioListener.kt index 6187ed7..d454a18 100644 --- a/app/src/main/java/moe/qwq/miko/internals/AioListener.kt +++ b/app/src/main/java/moe/qwq/miko/internals/AioListener.kt @@ -159,7 +159,7 @@ override fun onRecvMsg(recordLisrt: ArrayList) { } val recallData = ProtoBuf.decodeFromByteArray(buffer) - val groupCode = GroupHelper.groupUin2GroupCode(message.msgHead.peerId) + val groupCode = GroupHelper.groupUin2GroupCode(message.msgHead.peerId.toLong()) val msgUid = message.content.msgUid val targetUid = recallData.operation.msgInfo?.senderUid ?: "" val operatorUid = recallData.operation.operatorUid ?: "" @@ -181,7 +181,7 @@ override fun onRecvMsg(recordLisrt: ArrayList) { var targetNick: String? = null if (targetNick == null) { targetNick = (if (targetUid.isEmpty()) null else GroupHelper.getTroopMemberInfoByUin(groupCode, target.toLong()).getOrNull())?.let { - it.troopnick.ifNullOrEmpty(it.friendnick) + it.troopnick.ifNullOrEmpty { it.friendnick } } ?: targetUid } @@ -197,7 +197,7 @@ override fun onRecvMsg(recordLisrt: ArrayList) { if (operatorNick == null) { operatorNick = (if (operatorUid.isEmpty()) null else GroupHelper.getTroopMemberInfoByUin(groupCode, operator.toLong()).getOrNull())?.let { - it.troopnick.ifNullOrEmpty(it.friendnick) + it.troopnick.ifNullOrEmpty { it.friendnick } } ?: operatorUid } diff --git a/app/src/main/java/moe/qwq/miko/internals/helper/NTServiceFetcher.kt b/app/src/main/java/moe/qwq/miko/internals/helper/NTServiceFetcher.kt index 06508c5..d08a805 100644 --- a/app/src/main/java/moe/qwq/miko/internals/helper/NTServiceFetcher.kt +++ b/app/src/main/java/moe/qwq/miko/internals/helper/NTServiceFetcher.kt @@ -1,4 +1,5 @@ -@file:OptIn(ExperimentalSerializationApi::class) +@file:OptIn(ExperimentalSerializationApi::class, DelicateCoroutinesApi::class) +@file:Suppress("UNCHECKED_CAST") package moe.qwq.miko.internals.helper @@ -7,18 +8,24 @@ import com.tencent.qqnt.kernel.api.IKernelService import com.tencent.qqnt.kernel.api.impl.MsgService import com.tencent.qqnt.kernel.nativeinterface.MsgRecord import de.robv.android.xposed.XposedBridge +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.decodeFromByteArray import kotlinx.serialization.protobuf.ProtoBuf import moe.fuqiuluo.entries.MessagePush +import moe.qwq.miko.ext.beforeHook import moe.qwq.miko.ext.hookMethod import moe.qwq.miko.internals.AioListener import moe.qwq.miko.internals.hooks.MessageHook +import java.lang.reflect.Method internal object NTServiceFetcher { private lateinit var iKernelService: IKernelService private var curKernelHash = 0 - private var isMsgListenerHookLoaded = false + //private var isMsgListenerHookLoaded = false fun onFetch(service: IKernelService) { val msgService = service.msgService ?: return @@ -37,21 +44,30 @@ internal object NTServiceFetcher { private fun initNTKernel(msgService: MsgService) { XposedBridge.log("[QwQ] Init NT Kernel.") - msgService.javaClass.hookMethod("addMsgListener").before { +/* msgService.javaClass.hookMethod("addMsgListener").before { val listener = it.args[0] if (isMsgListenerHookLoaded) return@before - listener.javaClass.hookMethod("onRecvMsg").before { - val msgs = it.args[0] as ArrayList - msgs.forEach { msg -> + + val hookV1 = beforeHook { + val newMsgs = arrayListOf() + (it.args[0] as ArrayList).forEach { msg -> MessageHook.tryHandleMessageDecrypt(msg) + newMsgs.add(msg) } + it.args[0] = newMsgs } - - listener.javaClass.hookMethod("onAddSendMsg").before { - val record = it.args[0] as MsgRecord - MessageHook.tryHandleMessageDecrypt(record) + val hookV2 = beforeHook { + val msg = it.args[0] as MsgRecord + MessageHook.tryHandleMessageDecrypt(msg) } - } + + listener.javaClass.hookMethod("onRecvMsg", hookV1) + listener.javaClass.hookMethod("onMsgInfoListAdd", hookV1) + listener.javaClass.hookMethod("onMsgInfoListUpdate", hookV1) + listener.javaClass.hookMethod("onAddSendMsg", hookV2) + + isMsgListenerHookLoaded = true + }*/ kernelService.wrapperSession.javaClass.hookMethod("onMsfPush").before { runCatching { diff --git a/app/src/main/java/moe/qwq/miko/internals/helper/RichProtoHelper.kt b/app/src/main/java/moe/qwq/miko/internals/helper/RichProtoHelper.kt new file mode 100644 index 0000000..bc82a53 --- /dev/null +++ b/app/src/main/java/moe/qwq/miko/internals/helper/RichProtoHelper.kt @@ -0,0 +1,191 @@ +@file:OptIn(DelicateCoroutinesApi::class, ExperimentalStdlibApi::class) +package moe.qwq.miko.internals.helper + +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.kernel.nativeinterface.PicElement +import de.robv.android.xposed.XposedBridge +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.serialization.encodeToByteArray +import kotlinx.serialization.protobuf.ProtoBuf +import moe.fuqiuluo.QQInterfaces +import moe.fuqiuluo.entries.* +import moe.qwq.miko.ext.decodeProtobuf +import moe.qwq.miko.ext.decodeToTrpcOidb + +private const val GPRO_PIC = "gchat.qpic.cn" +private const val MULTIMEDIA_DOMAIN = "multimedia.nt.qq.com.cn" +private const val C2C_PIC = "c2cpicdw.qpic.cn" + +object RichProtoHelper: QQInterfaces() { + private lateinit var cacheRkey: DownloadRkeyRsp + private var lastReqTime = 0L + + private suspend fun getTempNtRKey(): Result { + if (System.currentTimeMillis() - lastReqTime < 60_000) { + return Result.success(cacheRkey) + } + runCatching { + val req = ProtoBuf.encodeToByteArray(NtV2RichMediaReq( + head = MultiMediaReqHead( + commonHead = CommonHead( + requestId = 1u, + cmd = 202u + ), + sceneInfo = SceneInfo( + requestType = 2u, + businessType = 1u, + sceneType = 0u, + ), + clientMeta = ClientMeta(2u) + ), + downloadRkey = DownloadRkeyReq( + types = listOf(10, 20), + downloadType = 2 + ) + )) + 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 { + cacheRkey = it + lastReqTime = System.currentTimeMillis() + return Result.success(it) + } + }.onFailure { + return Result.failure(it) + } + return Result.failure(Exception("failed to fetch NtTempRKey")) + } + + 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") || storeId == 1 + if (isNtServer) { + val tmpRKey = getTempNtRKey().onFailure { + XposedBridge.log(it) + } + 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" + } else { + XposedBridge.log("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") + } + } + + fun getGroupPicDownUrl( + originalUrl: String, + md5: String, + peer: String = "", + fileId: String = "", + sha: String = "", + fileSize: ULong = 0uL, + width: UInt = 0u, + height: UInt = 0u + ): String { + val domain = GPRO_PIC + if (originalUrl.isNotEmpty()) { + return "https://$domain$originalUrl" + } + return "https://$domain/gchatpic_new/0/0-0-${md5.uppercase()}/0?term=2" + } + + fun getC2CPicDownUrl( + originalUrl: String, + md5: String, + peer: String = "", + fileId: String = "", + sha: String = "", + fileSize: ULong = 0uL, + width: UInt = 0u, + height: UInt = 0u, + storeId: Int = 0 + ): String { + val domain = C2C_PIC + if (originalUrl.isNotEmpty()) { + return "https://$domain$originalUrl" + } + return "https://$domain/offpic_new/0/0-0-${md5}/0?term=2" + } + + fun getGuildPicDownUrl( + originalUrl: String, + md5: String, + peer: String = "", + subPeer: String = "", + fileId: String = "", + sha: String = "", + fileSize: ULong = 0uL, + width: UInt = 0u, + height: UInt = 0u + ): String { + val domain = GPRO_PIC + if (originalUrl.isNotEmpty()) { + return "https://$domain$originalUrl" + } + return "https://$domain/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2" + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/qwq/miko/internals/hooks/MessageHook.kt b/app/src/main/java/moe/qwq/miko/internals/hooks/MessageHook.kt index d08f485..3100027 100644 --- a/app/src/main/java/moe/qwq/miko/internals/hooks/MessageHook.kt +++ b/app/src/main/java/moe/qwq/miko/internals/hooks/MessageHook.kt @@ -1,24 +1,28 @@ package moe.qwq.miko.internals.hooks import android.content.Context -import com.tencent.mobileqq.qroute.QRoute import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgRecord +import com.tencent.qqnt.kernel.nativeinterface.PicElement import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo import com.tencent.qqnt.kernel.nativeinterface.TextElement -import com.tencent.qqnt.msg.api.IMsgService import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XposedBridge +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import moe.fuqiuluo.processor.HookAction import moe.qwq.miko.actions.ActionProcess import moe.qwq.miko.actions.IAction +import moe.qwq.miko.ext.ifNullOrEmpty import moe.qwq.miko.internals.helper.MessageCrypt import moe.qwq.miko.internals.helper.NTServiceFetcher +import moe.qwq.miko.internals.helper.RichProtoHelper import moe.qwq.miko.internals.helper.msgService import moe.qwq.miko.internals.setting.QwQSetting +import moe.qwq.miko.tools.DownloadUtils import moe.qwq.miko.tools.PlatformTools -import mqq.app.MobileQQ +import moe.qwq.miko.tools.PlatformTools.QQ_9_0_8_VER import java.io.File import java.io.RandomAccessFile @@ -34,18 +38,60 @@ class MessageHook: IAction { val msgService = NTServiceFetcher.kernelService.msgService!! val originalPath = msgService.getRichMediaFilePathForMobileQQSend( RichMediaFilePathInfo(2, 0, pic.md5HexStr, "", 1, 0, null, "", true) - ) ?: return - val originalFile = RandomAccessFile(originalPath, "r") - val length = originalFile.length() - originalFile.seek(length - 12) - val dataSize = originalFile.readInt() - val hash = originalFile.readInt() - val magic = originalFile.readInt() + )!! + if (decodeLocalMsg(originalPath, encrypt, pic, record)) return // 解密失败?文件不存在,大概率是这个图片没有被缓存在本地 +/* val md5 = (pic.md5HexStr ?: pic.fileName + .replace("{", "") + .replace("}", "") + .replace("-", "").split(".")[0]) + .uppercase() + var storeId = 0 + if (PlatformTools.getQQVersionCode() > QQ_9_0_8_VER) { + storeId = pic.storeID + } + val originalUrl = pic.originImageUrl ?: "" + val downloadUrl = RichProtoHelper.getTempPicDownloadUrl( + record.chatType, originalUrl, md5, pic, 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 + } + ) + val originalFileTmp = File("$originalPath.1") + DownloadUtils.download(downloadUrl, originalFileTmp) + *//*if(!decodeLocalMsg(originalFileTmp.absolutePath, encrypt, pic, record)) { + XposedBridge.log("[QwQ] 解密消息失败,无法解密消息: $originalFileTmp, ${originalFileTmp.exists()}, $downloadUrl") + } else { + //File(originalPath).let { if (!it.exists()) originalFileTmp.renameTo(it) } + }*/ + } + + private fun decodeLocalMsg( + originalPath: String, + encrypt: String, + pic: PicElement, + record: MsgRecord + ): Boolean { + if (!File(originalPath).exists()) return false + val originalRandomFile = RandomAccessFile(originalPath, "r") + val length = originalRandomFile.length() + originalRandomFile.seek(length - 12) + val dataSize = originalRandomFile.readInt() + val hash = originalRandomFile.readInt() + val magic = originalRandomFile.readInt() if (magic == 0x114514 && hash == (encrypt + record.senderUin).hashCode()) { val data = ByteArray(dataSize) - originalFile.seek(length - 12 - dataSize) - originalFile.read(data) - originalFile.close() + originalRandomFile.seek(length - 12 - dataSize) + originalRandomFile.read(data) + originalRandomFile.close() MessageCrypt.decrypt(data, encrypt).onSuccess { record.elements.clear() record.elements.addAll(it) @@ -53,6 +99,7 @@ class MessageHook: IAction { XposedBridge.log("消息解密失败: ${it.stackTraceToString()}") } } + return true } } diff --git a/app/src/main/java/moe/qwq/miko/tools/DeflateTools.kt b/app/src/main/java/moe/qwq/miko/tools/DeflateTools.kt new file mode 100644 index 0000000..bfba7b2 --- /dev/null +++ b/app/src/main/java/moe/qwq/miko/tools/DeflateTools.kt @@ -0,0 +1,100 @@ +package moe.qwq.miko.tools + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.util.zip.Deflater +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream +import java.util.zip.Inflater + + +object DeflateTools { + fun uncompress(inputByte: ByteArray?): ByteArray { + var len: Int + val infill = Inflater() + infill.setInput(inputByte) + val bos = ByteArrayOutputStream() + val outByte = ByteArray(1024) + try { + while (!infill.finished()) { + len = infill.inflate(outByte) + if (len == 0) { + break + } + bos.write(outByte, 0, len) + } + infill.end() + } catch (e: Exception) { + e.printStackTrace() + } finally { + try { + bos.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + return bos.toByteArray() + } + + fun compress(inputByte: ByteArray?): ByteArray { + var len: Int + val defile = Deflater() + defile.setInput(inputByte) + defile.finish() + val bos = ByteArrayOutputStream() + val outputByte = ByteArray(1024) + try { + while (!defile.finished()) { + len = defile.deflate(outputByte) + bos.write(outputByte, 0, len) + } + defile.end() + } finally { + try { + bos.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + return bos.toByteArray() + } + + fun gzip(data: ByteArray): ByteArray { + val input = ByteArrayInputStream(data) + val outputStream = ByteArrayOutputStream() + try { + val cache = ByteArray(1024) + val stream = GZIPOutputStream(outputStream) + while (true) { + val read = input.read(cache, 0, 1024) + if (read == -1) { + break + } + stream.write(cache, 0, read) + } + stream.flush() + stream.close() + return outputStream.toByteArray() + } finally { + outputStream.close() + input.close() + } + } + + fun ungzip(bytes: ByteArray): ByteArray { + val out = ByteArrayOutputStream() + val `in` = ByteArrayInputStream(bytes) + try { + val ungzip = GZIPInputStream(`in`) + val buffer = ByteArray(256) + var n: Int + while (ungzip.read(buffer).also { n = it } >= 0) { + out.write(buffer, 0, n) + } + } catch (e: java.lang.Exception) { + e.printStackTrace() + } + return out.toByteArray() + } +} \ No newline at end of file diff --git a/app/src/main/java/moe/qwq/miko/tools/DownloadUtils.kt b/app/src/main/java/moe/qwq/miko/tools/DownloadUtils.kt new file mode 100644 index 0000000..f8b8e0b --- /dev/null +++ b/app/src/main/java/moe/qwq/miko/tools/DownloadUtils.kt @@ -0,0 +1,164 @@ +@file:OptIn(DelicateCoroutinesApi::class, ObsoleteCoroutinesApi::class) +package moe.qwq.miko.tools + +import de.robv.android.xposed.XposedBridge +import io.ktor.client.HttpClient +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsChannel +import io.ktor.http.HttpStatusCode +import io.ktor.serialization.kotlinx.json.json +import io.ktor.utils.io.jvm.javaio.toInputStream +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.ObsoleteCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.serialization.json.Json +import java.io.File +import java.io.RandomAccessFile +import java.net.HttpURLConnection +import java.net.URL +import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.minutes + +object DownloadUtils { + val GlobalClient: HttpClient by lazy { + HttpClient { + //install(HttpCookies) + install(HttpTimeout) { + requestTimeoutMillis = 15000 + connectTimeoutMillis = 15000 + socketTimeoutMillis = 15000 + } + install(ContentNegotiation) { + json(Json { + prettyPrint = true + isLenient = true + }) + } + } + } + private const val MAX_THREAD = 4 + + suspend fun download( + urlAdr: String, + dest: File, + threadCount: Int = MAX_THREAD, + headers: Map = mapOf() + ): Boolean { + if (!dest.exists()) { + dest.createNewFile() + } + var threadCnt = if(threadCount == 0) MAX_THREAD else threadCount + val url = URL(urlAdr) + val connection = withContext(Dispatchers.IO) { url.openConnection() } as HttpURLConnection + headers.forEach { (k, v) -> + connection.setRequestProperty(k, v) + } + connection.requestMethod = "GET" + connection.connectTimeout = 5000 + val responseCode = connection.responseCode + if (responseCode == 200) { + val contentLength = connection.contentLength + if (contentLength <= 0) { + return downloadByKtor(url, dest) + } else { + withContext(Dispatchers.IO) { + val raf = RandomAccessFile(dest, "rw") + raf.setLength(contentLength.toLong()) + raf.close() + } + } + if (contentLength <= 1024 * 1024) { + threadCnt = 1 + } + var blockSize = (contentLength * (1.0 / threadCnt)).roundToInt() + connection.disconnect() + val progress = atomic(0) + val channel = Channel() + var processed = 0 + repeat(threadCnt) { + if (processed + blockSize != contentLength && it == threadCnt - 1) { + blockSize = contentLength - processed + } + val start = processed + val end = processed + blockSize - 1 + GlobalScope.launch(Dispatchers.IO) { + reallyDownload(url, start, end, dest, channel) + } + processed += blockSize + } + withTimeoutOrNull(1.minutes) { + while (progress.value < contentLength) { + if(progress.addAndGet(channel.receive()) >= contentLength) { + break + } + } + return@withTimeoutOrNull true + } ?: dest.delete() + return true + } + return false + } + + private suspend fun downloadByKtor(url: URL, dest: File): Boolean { + val respond = GlobalClient.get(url) + if (respond.status == HttpStatusCode.OK) { + val channel = respond.bodyAsChannel() + withContext(Dispatchers.IO) { + dest.outputStream().use { + channel.toInputStream().use { input -> + input.copyTo(it) + } + } + } + return true + } else { + XposedBridge.log("[QwQ] Download failed: ${respond.status}") + } + return false + } + + private suspend fun reallyDownload(url: URL, start: Int, end: Int, dest: File, channel: Channel) { + val openConnection: HttpURLConnection = withContext(Dispatchers.IO) { url.openConnection() } as HttpURLConnection + openConnection.requestMethod = "GET" + openConnection.connectTimeout = 5000 + openConnection.setRequestProperty("range", "bytes=$start-$end") + val responseCode = openConnection.responseCode + if (responseCode == 206) { + val inputStream = openConnection.inputStream + val raf = withContext(Dispatchers.IO) { + RandomAccessFile(dest, "rw").also { + it.seek(start.toLong()) + } + } + var len: Int + val buf = ByteArray(1024) + var flag = true + while (flag) { + len = withContext(Dispatchers.IO) { + inputStream.read(buf) + } + flag = len != -1 + if (flag) { + withContext(Dispatchers.IO) { + raf.write(buf, 0, len) + } + channel.send(len) + } + } + withContext(Dispatchers.IO) { + inputStream.close() + raf.close() + } + } + openConnection.disconnect() + } + +} \ No newline at end of file diff --git a/qqinterface/src/main/java/com/tencent/mobileqq/msf/sdk/MsfMessagePair.java b/qqinterface/src/main/java/com/tencent/mobileqq/msf/sdk/MsfMessagePair.java new file mode 100644 index 0000000..e6c82db --- /dev/null +++ b/qqinterface/src/main/java/com/tencent/mobileqq/msf/sdk/MsfMessagePair.java @@ -0,0 +1,18 @@ +package com.tencent.mobileqq.msf.sdk; + +import com.tencent.qphone.base.remote.FromServiceMsg; +import com.tencent.qphone.base.remote.ToServiceMsg; + +public class MsfMessagePair { + public FromServiceMsg fromServiceMsg; + public String sendProcess; + public ToServiceMsg toServiceMsg; + + public MsfMessagePair(String str, ToServiceMsg toServiceMsg, FromServiceMsg fromServiceMsg) { + + } + + public MsfMessagePair(ToServiceMsg toServiceMsg, FromServiceMsg fromServiceMsg) { + + } +} \ No newline at end of file