4 Commits

10 changed files with 364 additions and 70 deletions

View File

@ -176,9 +176,6 @@ internal object MsgSvc: BaseSvc() {
fromId: String = peedId,
retryCnt: Int = 3
): Result<Pair<Long, Int>> {
//LogCenter.log(message.toString(), Level.ERROR)
//callback.msgHash = result.second 什么垃圾代码万一cb比你快你不就寄了
// 主动临时消息
when (chatType) {
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
@ -188,13 +185,7 @@ internal object MsgSvc: BaseSvc() {
}
}
}
val result = MessageHelper.sendMessageWithoutMsgId(
chatType,
peedId,
message,
fromId,
MessageCallback(peedId, 0)
)
val result = MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0))
return if (result.isFailure
&& result.exceptionOrNull()?.javaClass == SendMsgException::class.java
&& retryCnt > 0) {

View File

@ -51,6 +51,47 @@ internal object QFavSvc: BaseSvc() {
private const val MINOR_VERSION = 9
private var seq = 1
suspend fun getItemList(
category: Int,
startPos: Int,
pageSize: Int,
): Result<NetResp> {
val data = protobufMapOf {
it[1] = mapOf(
20000 to mapOf(
/**
* "type", "bid", "category", "start_time", "order_type", "start_pos", "page_size", "sync_policy", "req_source"
*/
1 to 0,
2 to 0,
3 to category,
//4 to System.currentTimeMillis() - 1000 * 60,
//4 to System.currentTimeMillis(),
4 to 0,
5 to 0,
6 to startPos,
7 to pageSize,
8 to 0,
9 to 0
)
)
}.toByteArray()
return sendWeiyunReq(20000, data)
}
suspend fun getItemContent(
id: String
): Result<NetResp> {
val data = protobufMapOf {
it[1] = mapOf(
20001 to mapOf(
1 to id
)
)
}.toByteArray()
return sendWeiyunReq(20001, data)
}
suspend fun addImageMsg(
uin: Long,
name: String,
@ -215,7 +256,7 @@ internal object QFavSvc: BaseSvc() {
* 4 => pic_list
* 5 => file_list
*/
2 to content
2 to content.textToHtml()
)
)
)
@ -223,6 +264,10 @@ internal object QFavSvc: BaseSvc() {
return sendWeiyunReq(20009, data)
}
private fun String.textToHtml(): String {
return replace("\n", "<div><br/></div>")
}
suspend fun sendPicUpBlock(
fileSize: Long,
offset: Long,
@ -300,7 +345,6 @@ internal object QFavSvc: BaseSvc() {
override fun onUpdateProgeress(netReq: NetReq, curr: Long, final: Long) {}
}
val pSKey = getWeiYunPSKey()
//LogCenter.log(pSKey)
httpNetReq.mHttpMethod = HttpNetReq.HTTP_POST
httpNetReq.mSendData = DeflateTools.gzip(packData(packHead(cmd, pSKey), body))
httpNetReq.mOutStream = outputStream

View File

@ -9,6 +9,7 @@ import com.tencent.qqnt.msg.api.IMsgService
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
@ -38,7 +39,11 @@ internal object MessageHelper {
): Pair<Long, Int> {
val uniseq = generateMsgId(chatType)
val msg = messageArrayToMessageElements(chatType, uniseq.second, peerId, decodeCQCode(message)).also {
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
if (it.second.isEmpty() && !it.first) {
error("消息合成失败,请查看日志或者检查输入。")
} else if (it.second.isEmpty()) {
return System.currentTimeMillis() to 0
}
}.second.filter {
it.elementType != -1
} as ArrayList<MsgElement>
@ -59,6 +64,12 @@ internal object MessageHelper {
}.second.filter {
it.elementType != -1
} as ArrayList<MsgElement>
// ActionMsg No Care
if (msg.isEmpty()) {
return Result.success(System.currentTimeMillis() to 0)
}
val totalSize = msg.filter {
it.elementType == MsgConstant.KELEMTYPEPIC ||
it.elementType == MsgConstant.KELEMTYPEPTT ||
@ -67,11 +78,11 @@ internal object MessageHelper {
(it.picElement?.fileSize ?: 0) + (it.pttElement?.fileSize
?: 0) + (it.videoElement?.fileSize ?: 0)
}.reduceOrNull { a, b -> a + b } ?: 0
val estimateTime = (totalSize / (300 * 1024)) * 1000 + 2000
val estimateTime = (totalSize / (300 * 1024)) * 1000 + 5000
lateinit var sendResultPair: Pair<Long, Int>
val sendRet = withTimeoutOrNull<Pair<Int, String>>(estimateTime) {
suspendCoroutine {
suspendCancellableCoroutine {
GlobalScope.launch {
sendResultPair = sendMessageWithoutMsgId(
chatType,

View File

@ -52,7 +52,7 @@ internal object ActionManager {
GetWeatherCityCode, GetWeather,
// FAV
FavAddRichMediaMsg, FavAddImageMsg,
FavAddTextMsg, FavAddImageMsg, FavGetItemContent, FavGetItemList,
// OTHER
GetDeviceBattery, DownloadFile

View File

@ -3,6 +3,8 @@ package moe.fuqiuluo.shamrock.remote.action.handlers
import android.graphics.BitmapFactory
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.proto.ProtoUtils
import moe.fuqiuluo.proto.asUtf8String
@ -40,8 +42,14 @@ internal object FavAddImageMsg: IActionHandler() {
FileUtils.parseAndSave(it)
}
}
val options = BitmapFactory.Options()
BitmapFactory.decodeFile(image.absolutePath, options)
lateinit var picUrl: String
lateinit var picId: String
lateinit var itemId: String
lateinit var md5: String
QFavSvc.applyUpImageMsg(uin, nickName,
image = image,
groupName = groupName,
@ -49,14 +57,12 @@ internal object FavAddImageMsg: IActionHandler() {
width = options.outWidth,
height = options.outHeight
).onSuccess {
return if (it.mHttpCode == 200 && it.mResult == 0) {
if (it.mHttpCode == 200 && it.mResult == 0) {
val readPacket = ByteReadPacket(DeflateTools.ungzip(it.mRespData))
readPacket.discardExact(6)
val allLength = readPacket.readInt()
val dataLength = readPacket.readInt()
val headLength = allLength - dataLength - 16
//LogCenter.log("上传图片请求成功: ${DeflateTools.ungzip(it.mRespData).toHexString()}")
//LogCenter.log("图片上传响应: allLength=$allLength, dataLength=$dataLength, headLength=$headLength")
readPacket.discardExact(2)
ByteArray(headLength).also {
readPacket.readFully(it, 0, it.size)
@ -66,55 +72,82 @@ internal object FavAddImageMsg: IActionHandler() {
}
val pb = ProtoUtils.decodeFromByteArray(data)
val resp = pb[2, 20010, 1, 2]
val picUrl = resp[1].asUtf8String
val picId = resp[11].asUtf8String
val md5 = resp[4].asUtf8String
val sha = CryptTools
.getSHA1("/storage/emulated/0/Android/data/com.tencent.mobileqq/Tencent/QQ_Collection/pic/" + md5.uppercase() + "_0")
image.inputStream().use {
var offset = 0L
val block = ByteArray(131072)
var rest = image.length()
do {
val length = if (rest <= 131072) rest else 131072L
if(it.read(block, 0, length.toInt()) != -1) {
QFavSvc.sendPicUpBlock(
fileSize = image.length(),
offset = offset,
block = block,
blockSize = length,
pid = picId,
sha = sha
).onFailure {
return error(it.message ?: it.toString(), echo)
}
offset += length
rest -= length
} else {
rest = -1
}
} while (rest > 0)
}
QFavSvc.addImageMsg(
uin, nickName, groupId, groupName, picUrl, picId, options.outWidth, options.outHeight, image.length(), md5.uppercase()
).onFailure {
return error(it.message ?: it.toString(), echo)
}
ok(picUrl, echo)
picUrl = resp[1].asUtf8String
picId = resp[11].asUtf8String
md5 = resp[4].asUtf8String
} else {
logic(it.mErrDesc, echo)
return logic(it.mErrDesc, echo)
}
}.onFailure {
return error(it.message ?: it.toString(), echo)
}
return ok("请求已提交", echo)
val sha = CryptTools
.getSHA1("/storage/emulated/0/Android/data/com.tencent.mobileqq/Tencent/QQ_Collection/pic/" + md5.uppercase() + "_0")
image.inputStream().use {
var offset = 0L
val block = ByteArray(131072)
var rest = image.length()
do {
val length = if (rest <= 131072) rest else 131072L
if(it.read(block, 0, length.toInt()) != -1) {
QFavSvc.sendPicUpBlock(
fileSize = image.length(),
offset = offset,
block = block,
blockSize = length,
pid = picId,
sha = sha
).onFailure {
return error(it.message ?: it.toString(), echo)
}
offset += length
rest -= length
} else {
rest = -1
}
} while (rest > 0)
}
QFavSvc.addImageMsg(
uin, nickName, groupId, groupName, picUrl, picId, options.outWidth, options.outHeight, image.length(), md5.uppercase()
).onFailure {
return error(it.message ?: it.toString(), echo)
}.onSuccess {
if (it.mHttpCode == 200 && it.mResult == 0) {
val readPacket = ByteReadPacket(DeflateTools.ungzip(it.mRespData))
readPacket.discardExact(6)
val allLength = readPacket.readInt()
val dataLength = readPacket.readInt()
val headLength = allLength - dataLength - 16
readPacket.discardExact(2)
ByteArray(headLength).also {
readPacket.readFully(it, 0, it.size)
}
val data = ByteArray(dataLength).also {
readPacket.readFully(it, 0, it.size)
}
val pb = ProtoUtils.decodeFromByteArray(data)
itemId = pb[2, 20009, 1].asUtf8String
}
}
return ok(PicInfo(
picUrl = picUrl,
picId = picId,
id = itemId
), echo)
}
override fun path(): String = "fav.add_image_msg"
override val requiredParams: Array<String> = arrayOf("user_id", "nick", "file")
@Serializable
private data class PicInfo(
@SerialName("pic_url") val picUrl: String,
@SerialName("pic_id") val picId: String,
@SerialName("id") val id: String
)
}

View File

@ -1,12 +1,21 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.proto.ProtoUtils
import moe.fuqiuluo.proto.asUtf8String
import moe.fuqiuluo.qqinterface.servlet.QFavSvc
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.DeflateTools
internal object FavAddRichMediaMsg: IActionHandler() {
internal object FavAddTextMsg: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val uin = session.getLong("user_id")
val nickName = session.getString("nick")
@ -33,7 +42,23 @@ internal object FavAddRichMediaMsg: IActionHandler() {
groupId = groupId
).onSuccess {
return if (it.mHttpCode == 200 && it.mResult == 0) {
ok("成功", echo)
val readPacket = ByteReadPacket(DeflateTools.ungzip(it.mRespData))
readPacket.discardExact(6)
val allLength = readPacket.readInt()
val dataLength = readPacket.readInt()
val headLength = allLength - dataLength - 16
readPacket.discardExact(2)
ByteArray(headLength).also {
readPacket.readFully(it, 0, it.size)
}
val data = ByteArray(dataLength).also {
readPacket.readFully(it, 0, it.size)
}
val pb = ProtoUtils.decodeFromByteArray(data)
ok(data = QFavItem(
pb[2, 20009, 1].asUtf8String
), echo)
} else {
logic(it.mErrDesc, echo)
}
@ -41,7 +66,12 @@ internal object FavAddRichMediaMsg: IActionHandler() {
return ok("请求已提交", echo)
}
override fun path(): String = "fav.add_rich_media_msg"
override fun path(): String = "fav.add_text_msg"
override val requiredParams: Array<String> = arrayOf("user_id", "nick", "content")
@Serializable
private data class QFavItem(
@SerialName("id") val id: String
)
}

View File

@ -0,0 +1,57 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.proto.ProtoUtils
import moe.fuqiuluo.proto.asUtf8String
import moe.fuqiuluo.qqinterface.servlet.QFavSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.DeflateTools
internal object FavGetItemContent: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val id = session.getString("id")
return invoke(id, session.echo)
}
suspend operator fun invoke(
id: String,
echo: JsonElement = EmptyJsonString
): String {
val respData = DeflateTools.ungzip(QFavSvc.getItemContent(id).onSuccess {
if (it.mHttpCode != 200 || it.mResult != 0) {
return logic(it.mErrDesc, echo)
}
}.getOrThrow().mRespData)
val readPacket = ByteReadPacket(respData)
readPacket.discardExact(6)
val allLength = readPacket.readInt()
val dataLength = readPacket.readInt()
val headLength = allLength - dataLength - 16
readPacket.discardExact(2)
ByteArray(headLength).also {
readPacket.readFully(it, 0, it.size)
}
val data = ByteArray(dataLength).also {
readPacket.readFully(it, 0, it.size)
}
val pb = ProtoUtils.decodeFromByteArray(data)
return ok(ItemContent(pb[2, 20001, 1, 8, 2].asUtf8String))
}
override fun path(): String = "fav.get_item_content"
override val requiredParams: Array<String> = arrayOf("id")
@Serializable
private data class ItemContent(
@SerialName("content") val content: String
)
}

View File

@ -0,0 +1,117 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.proto.ProtoUtils
import moe.fuqiuluo.proto.asInt
import moe.fuqiuluo.proto.asList
import moe.fuqiuluo.proto.asLong
import moe.fuqiuluo.proto.asMap
import moe.fuqiuluo.proto.asULong
import moe.fuqiuluo.proto.asUtf8String
import moe.fuqiuluo.qqinterface.servlet.QFavSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.DeflateTools
internal object FavGetItemList: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val category = session.getInt("category")
val startPos = session.getInt("start_pos")
val pageSize = session.getInt("page_size")
return invoke(category, startPos, pageSize, session.echo)
}
suspend operator fun invoke(
category: Int,
startPos: Int,
pageSize: Int,
echo: JsonElement = EmptyJsonString
): String {
if (pageSize <= 1) {
return logic("page_size must be greater than 1", echo)
}
val result = DeflateTools.ungzip(QFavSvc.getItemList(
category = category,
startPos = startPos,
pageSize = pageSize
).onSuccess {
if (it.mHttpCode != 200 || it.mResult != 0) {
return logic("fav.get_item_list failed", echo)
}
}.getOrThrow().mRespData)
val readPacket = ByteReadPacket(result)
readPacket.discardExact(6)
val allLength = readPacket.readInt()
val dataLength = readPacket.readInt()
val headLength = allLength - dataLength - 16
readPacket.discardExact(2)
ByteArray(headLength).also {
readPacket.readFully(it, 0, it.size)
}
val data = ByteArray(dataLength).also {
readPacket.readFully(it, 0, it.size)
}
val pb = ProtoUtils.decodeFromByteArray(data)
val itemList = arrayListOf<Item>()
val rawItemList = pb[2, 20000, 1].asList
rawItemList.value.forEach {
val item = it.asMap
val itemId = item[1].asUtf8String
val authorType = item[4, 1].asInt
val author = item[4, 2].asULong
val authorName = item[4, 3].asUtf8String
val groupName: String
val groupId: Long
if (authorType == 2) {
groupName = item[4, 5].asUtf8String
groupId = item[4, 4].asULong
} else {
groupName = ""
groupId = 0L
}
val clientVersion = item[7].asUtf8String
val time = item[9].asLong
itemList.add(Item(
id = itemId,
authorType = authorType,
author = author,
authorName = authorName,
groupName = groupName,
groupId = groupId,
clientVersion = clientVersion,
time = time
))
}
return ok(ItemList(itemList), echo)
}
override val requiredParams: Array<String> = arrayOf("category", "start_pos", "page_size")
override fun path(): String = "fav.get_item_list"
@Serializable
private data class ItemList(
val items: List<Item>
)
@Serializable
private data class Item(
@SerialName("id") val id: String,
@SerialName("author_type") val authorType: Int,
@SerialName("author") val author: Long,
@SerialName("author_name") val authorName: String,
@SerialName("group_name") val groupName: String,
@SerialName("group_id") val groupId: Long,
@SerialName("client_version") val clientVersion: String,
@SerialName("time") val time: Long
)
}

View File

@ -5,11 +5,9 @@ import io.ktor.server.application.call
import io.ktor.server.response.respondText
import io.ktor.server.routing.Routing
import moe.fuqiuluo.shamrock.remote.action.handlers.FavAddImageMsg
import moe.fuqiuluo.shamrock.remote.action.handlers.FavAddRichMediaMsg
import moe.fuqiuluo.shamrock.remote.action.handlers.GetFriendList
import moe.fuqiuluo.shamrock.remote.action.handlers.GetFriendSystemMsg
import moe.fuqiuluo.shamrock.remote.action.handlers.GetStrangerInfo
import moe.fuqiuluo.shamrock.remote.action.handlers.IsBlackListUin
import moe.fuqiuluo.shamrock.remote.action.handlers.FavAddTextMsg
import moe.fuqiuluo.shamrock.remote.action.handlers.FavGetItemContent
import moe.fuqiuluo.shamrock.remote.action.handlers.FavGetItemList
import moe.fuqiuluo.shamrock.tools.fetchOrNull
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
import moe.fuqiuluo.shamrock.tools.getOrPost
@ -24,7 +22,7 @@ fun Routing.fav() {
val content = call.fetchOrThrow("content")
val groupName = call.fetchOrNull("group_name") ?: ""
val groupId = call.fetchOrNull("group_id")?.toLong() ?: 0L
call.respondText(FavAddRichMediaMsg(uin, nickName, time, content, groupName, groupId), ContentType.Application.Json)
call.respondText(FavAddTextMsg(uin, nickName, time, content, groupName, groupId), ContentType.Application.Json)
}
getOrPost("/fav/add_image_msg") {
@ -35,4 +33,16 @@ fun Routing.fav() {
val groupId = call.fetchOrNull("group_id")?.toLong() ?: 0L
call.respondText(FavAddImageMsg(uin, nickName, file, groupName, groupId), ContentType.Application.Json)
}
getOrPost("/fav/get_item_content") {
val id = call.fetchOrThrow("id")
call.respondText(FavGetItemContent(id), ContentType.Application.Json)
}
getOrPost("/fav/get_item_list") {
val category = call.fetchOrThrow("category").toInt()
val startPos = call.fetchOrThrow("start_pos").toInt()
val pageSize = call.fetchOrThrow("page_size").toInt()
call.respondText(FavGetItemList(category, startPos, pageSize), ContentType.Application.Json)
}
}

View File

@ -16,6 +16,7 @@ import mqq.app.MobileQQ
internal class HookForDebug: IAction {
override fun invoke(ctx: Context) {
/*
val httpEngineService = AppRuntimeFetcher.appRuntime
.getRuntimeService(IHttpEngineService::class.java, "all")
httpEngineService.javaClass.hookMethod("sendReq").before {
@ -28,7 +29,7 @@ internal class HookForDebug: IAction {
LogCenter.log("请求地址: ${req.mReqUrl}")
LogCenter.log("请求: ${req.toInnerValuesString(NetReq::class.java)}")
}
}
}*/
}
}