@file:OptIn(ExperimentalSerializationApi::class) package moe.fuqiuluo.qqinterface.servlet import com.tencent.mobileqq.app.QQAppInterface import com.tencent.mobileqq.transfile.HttpNetReq import com.tencent.mobileqq.transfile.INetEngineListener import com.tencent.mobileqq.transfile.NetReq import com.tencent.mobileqq.transfile.NetResp import com.tencent.mobileqq.transfile.ServerAddr import com.tencent.mobileqq.transfile.api.IHttpEngineService import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.io.core.BytePacketBuilder import kotlinx.io.core.readBytes import kotlinx.io.core.writeFully import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.encodeToByteArray import kotlinx.serialization.protobuf.ProtoBuf import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.tools.hex2ByteArray import moe.fuqiuluo.shamrock.tools.toHexString import moe.fuqiuluo.shamrock.utils.DeflateTools import moe.fuqiuluo.shamrock.utils.MD5 import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher import protobuf.fav.WeiyunAddRichMediaReq import protobuf.fav.WeiyunAuthor import protobuf.fav.WeiyunCollectCommInfo import protobuf.fav.WeiyunComm import protobuf.fav.WeiyunCommonReq import protobuf.fav.WeiyunFastUploadResourceReq import protobuf.fav.WeiyunGetFavContentReq import protobuf.fav.WeiyunGetFavListReq import protobuf.fav.WeiyunMsgHead import protobuf.fav.WeiyunPicInfo import protobuf.fav.WeiyunRichMediaContent import protobuf.fav.WeiyunRichMediaSummary import mqq.manager.TicketManager import oicq.wlogin_sdk.request.Ticket import oicq.wlogin_sdk.request.WtTicketPromise import oicq.wlogin_sdk.tools.ErrMsg import java.io.ByteArrayOutputStream import java.io.File import java.nio.ByteBuffer import kotlin.coroutines.resume /** * QQ收藏相关接口 */ internal object QFavSvc: BaseSvc() { private val SERVER_LIST_COLLECTOR = listOf(ServerAddr().also { it.isIpv6 = false it.mIp = "collector.weiyun.com" it.port = 80 }) private val SERVER_LIST_PICUP = listOf(ServerAddr().also { it.isIpv6 = false it.mIp = "pic.pieceup.qq.com" it.port = 80 }) private const val VERSION = 12820 private const val APPID = 30244 private const val SUB_APPID = 538116905 private const val MAJOR_VERSION = 8 private const val MINOR_VERSION = 9 private var seq = 1 suspend fun getItemList( category: Int, startPos: Int, pageSize: Int, ): Result { val baseReq = WeiyunCommonReq( getFavListReq = WeiyunGetFavListReq( type = 0u, bid = 0u, category = category.toUInt(), startTime = 0u, orderType = 0u, startPos = startPos.toUInt(), pageSize = pageSize.toUInt(), syncPolicy = 0u, reqSource = 0u ) ) return sendWeiyunReq(20000, baseReq) } suspend fun getItemContent( id: String ): Result { return sendWeiyunReq(20001, WeiyunCommonReq( getFavContentReq = WeiyunGetFavContentReq( cidList = arrayListOf(id) ) ) ) } suspend fun addImageMsg( uin: Long, name: String, groupId: Long = 0, groupName: String = "", picUrl: String, pid: String, width: Int, height: Int, size: Long, md5: String, ): Result { val md5Bytes = md5.hex2ByteArray() return sendWeiyunReq(20009, WeiyunCommonReq( addRichMediaReq = WeiyunAddRichMediaReq( commInfo = WeiyunCollectCommInfo( bid = 1u, category = 1u, author = WeiyunAuthor( type = if (groupId == 0L) 1u else 2u, numId = uin.toULong(), strId = name, groupId = groupId.toULong(), groupName = groupName ), createTime = System.currentTimeMillis().toULong() - 2000u, seq = System.currentTimeMillis().toULong() - 1000u, bizDataList = arrayListOf("""{"recordAudioOnly":false,"audioOnly":false,"fileOnly":false}""".toByteArray()), originalAppId = 0u, customGroupId = 0u ), summary = WeiyunRichMediaSummary( title = "", brief = "[图片]", picList = arrayListOf( WeiyunPicInfo( uri = picUrl, md5 = md5Bytes, sha1 = md5.toByteArray(), name = "", note = "", width = width.toUInt(), height = height.toUInt(), size = size.toULong(), type = 0u, picId = pid ) ), contentType = 1u ), richMediaContent = listOf( WeiyunRichMediaContent( rawData = """""".toByteArray(), picList = listOf( WeiyunPicInfo( uri = picUrl, md5 = md5Bytes, sha1 = md5.toByteArray(), name = "", note = "", width = width.toUInt(), height = height.toUInt(), size = size.toULong(), type = 0u, picId = pid ) ) ) ) ) ) ) } suspend fun applyUpImageMsg( uin: Long, name: String, groupId: Long = 0, groupName: String = "", width: Int, height: Int, image: File ): Result { if (!image.exists()) { return Result.failure(IllegalArgumentException("image file not exists")) } val md5 = MD5.genFileMd5(image.absolutePath) return sendWeiyunReq(20010, WeiyunCommonReq( fastUploadResourceReq = WeiyunFastUploadResourceReq( picInfoList = listOf( WeiyunPicInfo( md5 = md5, name = md5.toHexString(), width = width.toUInt(), height = height.toUInt(), size = image.length().toULong(), type = 1u, picId = "/storage/emulated/0/DCIM/temp.jpeg", owner = WeiyunAuthor( type = if (groupId == 0L) 1u else 2u, numId = uin.toULong(), strId = name, groupId = groupId.toULong(), groupName = groupName ) ) ), ) ) ) } suspend fun addRichMediaMsg( uin: Long, name: String, groupId: Long = 0, groupName: String = "", time: Long = System.currentTimeMillis(), content: String ): Result { return sendWeiyunReq(20009, WeiyunCommonReq( addRichMediaReq = WeiyunAddRichMediaReq( commInfo = WeiyunCollectCommInfo( bid = 1u, category = 1u, author = WeiyunAuthor( type = if (groupId == 0L) 1u else 2u, numId = uin.toULong(), strId = name, groupId = groupId.toULong(), groupName = groupName ), createTime = time.toULong() - 2000u, seq = time.toULong() - 1000u, originalAppId = 0u, customGroupId = 0u ), summary = WeiyunRichMediaSummary( brief = content, contentType = 1u ), richMediaContent = listOf( WeiyunRichMediaContent( rawData = content.textToHtml().toByteArray(), ) ) ) ) ) } private fun String.textToHtml(): String { return replace("\n", "

") } suspend fun sendPicUpBlock( fileSize: Long, offset: Long, block: ByteArray, blockSize: Long, sha: ByteArray, pid: String, outputStream: ByteArrayOutputStream = ByteArrayOutputStream(), ): Result { return suspendCancellableCoroutine { val httpNetReq = HttpNetReq() httpNetReq.userData = null httpNetReq.mCallback = object: INetEngineListener { override fun onResp(netResp: NetResp) { if (netResp.mHttpCode != 200 && netResp.mResult != 0 && netResp.mErrDesc.isNullOrEmpty()) { netResp.mErrDesc = netResp.mRespProperties["User-ErrMsg"] } netResp.mRespData = outputStream.toByteArray().copyOf() it.resume(Result.success(netResp)) } override fun onUpdateProgeress(netReq: NetReq, curr: Long, final: Long) {} } val vi = (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getA2(app.currentAccountUin) //LogCenter.log(pSKey) httpNetReq.mHttpMethod = HttpNetReq.HTTP_POST httpNetReq.mSendData = BytePacketBuilder().apply { writeInt(-1412589450) writeInt(10000) writeInt(0) writeInt(sha.size + 16 + blockSize.toInt()) writeShort(0) writeShort(sha.size.toShort()) writeFully(sha) writeInt(fileSize.toInt()) writeInt(offset.toInt()) writeInt(blockSize.toInt()) writeFully(block) }.build().readBytes() httpNetReq.mOutStream = outputStream httpNetReq.mStartDownOffset = 0L httpNetReq.mReqProperties["Shamrock"] = "true" httpNetReq.mReqProperties["Cookie"] = String.format("uin=%s;vt=%d;vi=%s;pid=%s;appid=%d", app.currentAccountUin, 8, vi, pid, APPID) httpNetReq.mReqProperties["host"] = "pic.pieceup.qq.com" httpNetReq.mReqProperties["Range"] = "bytes=0-" httpNetReq.mReqProperties["Content-Length"] = httpNetReq.mSendData.size.toString() httpNetReq.mReqProperties["Accept-Encoding"] = "gzip" httpNetReq.mReqProperties["Content-Encoding"] = "gzip" httpNetReq.mPrioty = 1 httpNetReq.mReqUrl = "https://pic.pieceup.qq.com/" httpNetReq.mServerList = SERVER_LIST_PICUP val service = AppRuntimeFetcher.appRuntime .getRuntimeService(IHttpEngineService::class.java, "qqfav") service.sendReq(httpNetReq) } } suspend fun sendWeiyunReq( cmd: Int, req: WeiyunCommonReq, outputStream: ByteArrayOutputStream = ByteArrayOutputStream(), ): Result { return suspendCancellableCoroutine { val httpNetReq = HttpNetReq() httpNetReq.userData = null httpNetReq.mCallback = object: INetEngineListener { override fun onResp(netResp: NetResp) { if (netResp.mHttpCode != 200 && netResp.mResult != 0 && netResp.mErrDesc.isNullOrEmpty()) { netResp.mErrDesc = netResp.mRespProperties["User-ErrMsg"] } netResp.mRespData = outputStream.toByteArray().copyOf() it.resume(Result.success(netResp)) } override fun onUpdateProgeress(netReq: NetReq, curr: Long, final: Long) {} } val pSKey = getWeiYunPSKey() httpNetReq.mHttpMethod = HttpNetReq.HTTP_POST httpNetReq.mSendData = DeflateTools.gzip(packData(packHead(cmd, pSKey), ProtoBuf.encodeToByteArray( WeiyunComm(req = req) ))) httpNetReq.mOutStream = outputStream httpNetReq.mStartDownOffset = 0L httpNetReq.mReqProperties["Shamrock"] = "true" httpNetReq.mReqProperties["Cookie"] = String.format("uin=%s;vt=%d;vi=%s;appid=%d", app.currentAccountUin, 27, pSKey, APPID) httpNetReq.mReqProperties["host"] = "collector.weiyun.com" httpNetReq.mReqProperties["Range"] = "bytes=0-" httpNetReq.mReqProperties["Content-Length"] = httpNetReq.mSendData.size.toString() httpNetReq.mReqProperties["Accept-Encoding"] = "gzip" httpNetReq.mReqProperties["Content-Encoding"] = "gzip" httpNetReq.mPrioty = 1 httpNetReq.mReqUrl = "https://collector.weiyun.com/collector.fcg" httpNetReq.mServerList = SERVER_LIST_COLLECTOR val service = AppRuntimeFetcher.appRuntime .getRuntimeService(IHttpEngineService::class.java, "qqfav") service.sendReq(httpNetReq) } } private fun packHead(cmd: Int, pskey: String): ByteArray { return ProtoBuf.encodeToByteArray( WeiyunMsgHead( uin = app.longAccountUin.toULong(), seq = seq++.toUInt(), type = 1u, cmd = cmd.toUInt(), appId = APPID.toUInt(), version = VERSION.toUInt(), netType = 3u, keyType = 27u, key = pskey.toByteArray(), majorVersion = MAJOR_VERSION.toUInt(), minorVersion = MINOR_VERSION.toUInt(), ) ) } private fun packData(head: ByteArray, body: ByteArray): ByteArray { val len = 16 + head.size + body.size val buf = ByteBuffer.allocate(len) buf.putInt(SUB_APPID) buf.putShort(1) buf.putInt(len) buf.putInt(body.size) buf.putShort(0) buf.put(head) buf.put(body) return buf.array() } private fun getWeiYunPSKey(): String { val pskey = (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager) .getPskey(app.currentAccountUin, 16L, arrayOf("weiyun.com"), WeiYunPSKeyPromise) return if (pskey != null) pskey.getPSkey("weiyun.com") else "" } private object WeiYunPSKeyPromise: WtTicketPromise { override fun Done(ticket: Ticket) { LogCenter.log("Fav: getPskeyPromise: done", Level.DEBUG) } override fun Failed(errMsg: ErrMsg) { LogCenter.log("Fav: getPskeyPromise: failed, $errMsg", Level.DEBUG) } override fun Timeout(errMsg: ErrMsg) { LogCenter.log("Fav: getPskeyPromise: timeout, $errMsg", Level.DEBUG) } } }