Shamrock: 支持NT图片合并转发

Signed-off-by: 白池 <whitechi73@outlook.com>
This commit is contained in:
白池 2024-03-02 18:03:13 +08:00
parent 661680e60b
commit 27f837adbe
10 changed files with 271 additions and 296 deletions

View File

@ -31,7 +31,7 @@ data class RecvLongMsgInfo(
data class SendLongMsgInfo( data class SendLongMsgInfo(
@ProtoNumber(1) val type: Int? = null, @ProtoNumber(1) val type: Int? = null,
@ProtoNumber(2) val uid: LongMsgUid? = null, @ProtoNumber(2) val uid: LongMsgUid? = null,
@ProtoNumber(3) val groupUin: Int? = null, @ProtoNumber(3) val groupUin: ULong? = null,
@ProtoNumber(4) val payload: ByteArray? = null, @ProtoNumber(4) val payload: ByteArray? = null,
) )

View File

@ -1,55 +1 @@
@file:OptIn(ExperimentalSerializationApi::class)
package protobuf.message.multimedia package protobuf.message.multimedia
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import moe.fuqiuluo.symbols.Protobuf
@Serializable
data class RichMediaForPicData(
@ProtoNumber(1) val info: MediaInfo?,
@ProtoNumber(2) val display: DisplayMediaInfo?,
): Protobuf<RichMediaForPicData> {
companion object {
@Serializable
data class MediaInfo(
@ProtoNumber(1) val picture: Picture? = null,
)
@Serializable
data class Picture(
@ProtoNumber(1) val info: PictureInfo? = null,
@ProtoNumber(2) val fileId: String? = null,
@ProtoNumber(4) val time: ULong? = null,
)
@Serializable
data class PictureInfo(
@ProtoNumber(2) val md5Hex: String? = null,
@ProtoNumber(3) val sha: String? = null,
@ProtoNumber(4) val name: String? = null,
@ProtoNumber(6) val width: Int? = null,
@ProtoNumber(7) val height: Int? = null,
)
}
}
@Serializable
data class DisplayMediaInfo(
@ProtoNumber(1) val show: Show? = null,
) {
companion object {
@Serializable
data class Show(
@ProtoNumber(2) val text: String? = null,
@ProtoNumber(12) val download: Download? = null
)
@Serializable
data class Download(
@ProtoNumber(30) val url: String? = null,
)
}
}

View File

@ -49,8 +49,8 @@ data class UploadCompletedReq(
@Serializable @Serializable
data class MsgInfo( data class MsgInfo(
@ProtoNumber(1) val msgInfoBody: List<MsgInfoBody>, @ProtoNumber(1) val msgInfoBody: List<MsgInfoBody>,
@ProtoNumber(2) val extBizInfo: ExtBizInfo, @ProtoNumber(2) val extBizInfo: ExtBizInfo?,
) ): Protobuf<MsgInfo>
@Serializable @Serializable
data class MsgInfoBody( data class MsgInfoBody(
@ -106,7 +106,7 @@ data class UploadReq(
@ProtoNumber(5) val compatQMsgSceneType: UInt? = null, @ProtoNumber(5) val compatQMsgSceneType: UInt? = null,
@ProtoNumber(6) val extBizInfo: ExtBizInfo? = null, @ProtoNumber(6) val extBizInfo: ExtBizInfo? = null,
@ProtoNumber(7) val clientSeq: UInt? = null, @ProtoNumber(7) val clientSeq: UInt? = null,
@ProtoNumber(8) val noNeedCompatMsg: Boolean = false, @ProtoNumber(8) val noNeedCompatMsg: Boolean? = null,
) )
@Serializable @Serializable
@ -114,7 +114,7 @@ data class ExtBizInfo(
@ProtoNumber(1) val pic: PicExtBizInfo? = null, @ProtoNumber(1) val pic: PicExtBizInfo? = null,
@ProtoNumber(2) val video: VideoExtBizInfo? = null, @ProtoNumber(2) val video: VideoExtBizInfo? = null,
@ProtoNumber(3) val ptt: PttExtBizInfo? = null, @ProtoNumber(3) val ptt: PttExtBizInfo? = null,
@ProtoNumber(10) val busiType: UInt, @ProtoNumber(10) val busiType: UInt?,
) )
@Serializable @Serializable
@ -132,15 +132,15 @@ data class PttExtBizInfo(
@Serializable @Serializable
data class VideoExtBizInfo( data class VideoExtBizInfo(
@ProtoNumber(1) val fromScene: UInt, @ProtoNumber(1) val fromScene: UInt?,
@ProtoNumber(2) val toScene: UInt, @ProtoNumber(2) val toScene: UInt?,
@ProtoNumber(3) val bytesPbReserve: ByteArray, @ProtoNumber(3) val bytesPbReserve: ByteArray?,
) )
@Serializable @Serializable
data class PicExtBizInfo( data class PicExtBizInfo(
@ProtoNumber(1) val bizType: UInt, @ProtoNumber(1) val bizType: UInt?,
@ProtoNumber(2) val textSummary: String, @ProtoNumber(2) val textSummary: String?,
@ProtoNumber(11) val bytesPbReserveC2c: ByteArray? = null, @ProtoNumber(11) val bytesPbReserveC2c: ByteArray? = null,
@ProtoNumber(12) val bytesPbReserveTroop: ByteArray? = null, @ProtoNumber(12) val bytesPbReserveTroop: ByteArray? = null,
@ProtoNumber(1001) val fromScene: UInt? = null, @ProtoNumber(1001) val fromScene: UInt? = null,
@ -156,15 +156,15 @@ data class UploadInfo(
@Serializable @Serializable
data class FileInfo( data class FileInfo(
@ProtoNumber(1) val fileSize: ULong, @ProtoNumber(1) val fileSize: ULong?,
@ProtoNumber(2) val md5: String, @ProtoNumber(2) val md5: String?,
@ProtoNumber(3) val sha1: String, @ProtoNumber(3) val sha1: String?,
@ProtoNumber(4) val name: String, @ProtoNumber(4) val name: String?,
@ProtoNumber(5) val fileType: FileType, @ProtoNumber(5) val fileType: FileType?,
@ProtoNumber(6) val width: UInt, @ProtoNumber(6) val width: UInt?,
@ProtoNumber(7) val height: UInt, @ProtoNumber(7) val height: UInt?,
@ProtoNumber(8) val time: UInt, @ProtoNumber(8) val time: UInt?,
@ProtoNumber(9) val original: UInt, @ProtoNumber(9) val original: UInt?,
) )
@Serializable @Serializable
@ -217,7 +217,7 @@ data class IndexNode(
@ProtoNumber(3) val storeId: UInt, // 0为旧服务器 1为nt服务器 @ProtoNumber(3) val storeId: UInt, // 0为旧服务器 1为nt服务器
@ProtoNumber(4) val uploadTime: ULong, @ProtoNumber(4) val uploadTime: ULong,
@ProtoNumber(5) val ttl: ULong, @ProtoNumber(5) val ttl: ULong,
@ProtoNumber(6) val subType: UInt, @ProtoNumber(6) val subType: UInt? = null,
@ProtoNumber(7) val storeAppId: UInt? = null @ProtoNumber(7) val storeAppId: UInt? = null
) )

View File

@ -26,8 +26,8 @@ class DownloadSafeRsp
@Serializable @Serializable
data class UploadKeyRenewalRsp( data class UploadKeyRenewalRsp(
@ProtoNumber(1) val ukey: String, @ProtoNumber(1) val ukey: String?,
@ProtoNumber(2) val ukeyTtlSec: ULong, @ProtoNumber(2) val ukeyTtlSec: ULong?,
) )
@Serializable @Serializable
@ -39,7 +39,7 @@ data class MsgInfoAuthRsp(
@Serializable @Serializable
data class UploadCompletedRsp( data class UploadCompletedRsp(
@ProtoNumber(1) val msgSeq: ULong @ProtoNumber(1) val msgSeq: ULong?
) )
@Serializable @Serializable
@ -47,13 +47,13 @@ class DeleteRsp
@Serializable @Serializable
data class DownloadRkeyRsp( data class DownloadRkeyRsp(
@ProtoNumber(1) val rkeys: List<RKeyInfo> @ProtoNumber(1) val rkeys: List<RKeyInfo>?
) )
@Serializable @Serializable
data class RKeyInfo( data class RKeyInfo(
@ProtoNumber(1) val rkey: String, @ProtoNumber(1) val rkey: String?,
@ProtoNumber(2) val rkeyTtlSec: ULong, @ProtoNumber(2) val rkeyTtlSec: ULong?,
@ProtoNumber(3) val storeId: UInt = 0u, @ProtoNumber(3) val storeId: UInt = 0u,
@ProtoNumber(4) val rkeyCreateTime: UInt?, @ProtoNumber(4) val rkeyCreateTime: UInt?,
@ProtoNumber(4) val type: UInt?, @ProtoNumber(4) val type: UInt?,
@ -61,8 +61,8 @@ data class RKeyInfo(
@Serializable @Serializable
data class DownloadRsp( data class DownloadRsp(
@ProtoNumber(1) val rkeyParam: String, @ProtoNumber(1) val rkeyParam: String?,
@ProtoNumber(2) val rkeyTtlSec: ULong, @ProtoNumber(2) val rkeyTtlSec: ULong?,
@ProtoNumber(3) val downloadInfo: DownloadInfo?, @ProtoNumber(3) val downloadInfo: DownloadInfo?,
@ProtoNumber(4) val rkeyCreateTime: UInt? @ProtoNumber(4) val rkeyCreateTime: UInt?
) )
@ -80,16 +80,16 @@ data class DownloadInfo(
@Serializable @Serializable
data class VideoExtInfo( data class VideoExtInfo(
@ProtoNumber(1) val videoCodecFormat: UInt, @ProtoNumber(1) val videoCodecFormat: UInt? = null,
) )
@Serializable @Serializable
data class UploadRsp( data class UploadRsp(
@ProtoNumber(1) val ukey: String, @ProtoNumber(1) val ukey: String?,
@ProtoNumber(2) val ukeyTtlSec: ULong, @ProtoNumber(2) val ukeyTtlSec: ULong?,
@ProtoNumber(3) val ipv4: List<Ipv4>, @ProtoNumber(3) val ipv4: List<Ipv4>?,
@ProtoNumber(4) val ipv6: List<Ipv6>, @ProtoNumber(4) val ipv6: List<Ipv6>?,
@ProtoNumber(5) val msgSeq: ULong, @ProtoNumber(5) val msgSeq: ULong?,
@ProtoNumber(6) val msgInfo: MsgInfo? = null, @ProtoNumber(6) val msgInfo: MsgInfo? = null,
@ProtoNumber(7) val ext: List<RichmediaStorageTransInfo>? = null, @ProtoNumber(7) val ext: List<RichmediaStorageTransInfo>? = null,
@ProtoNumber(8) val compatQMsg: ByteArray? = null, @ProtoNumber(8) val compatQMsg: ByteArray? = null,
@ -98,11 +98,11 @@ data class UploadRsp(
@Serializable @Serializable
data class SubFileInfo( data class SubFileInfo(
@ProtoNumber(1) val subType: UInt, @ProtoNumber(1) val subType: UInt?,
@ProtoNumber(2) val ukey: String, @ProtoNumber(2) val ukey: String?,
@ProtoNumber(3) val ukeyTTLSec: ULong, @ProtoNumber(3) val ukeyTTLSec: ULong?,
@ProtoNumber(4) val ipv4: List<Ipv4>, @ProtoNumber(4) val ipv4: List<Ipv4>?,
@ProtoNumber(5) val ipv6: List<Ipv6>, @ProtoNumber(5) val ipv6: List<Ipv6>?,
) )
@Serializable @Serializable
@ -132,8 +132,8 @@ data class Ipv6(
@Serializable @Serializable
data class RspHead( data class RspHead(
@ProtoNumber(1) val commonHead: CommonHead, @ProtoNumber(1) val commonHead: CommonHead?,
@ProtoNumber(2) val retCode: UInt = 0u, @ProtoNumber(2) val retCode: UInt = 0u,
@ProtoNumber(3) val msg: String @ProtoNumber(3) val msg: String?
) )

View File

@ -225,9 +225,22 @@ internal object MsgSvc : BaseSvc() {
suspend fun uploadMultiMsg( suspend fun uploadMultiMsg(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
fromId: String, fromId: String = peerId,
messages: JsonArray,
retryCnt: Int
): Result<MessageSegment> {
return uploadMultiMsg(chatType, peerId, fromId, messages).onFailure {
if (retryCnt > 0) {
return uploadMultiMsg(chatType, peerId, fromId, messages, retryCnt - 1)
}
}
}
private suspend fun uploadMultiMsg(
chatType: Int,
peerId: String,
fromId: String = peerId,
messages: JsonArray, messages: JsonArray,
retryCnt: Int,
): Result<MessageSegment> { ): Result<MessageSegment> {
var i = -1 var i = -1
val desc = MutableList(messages.size) { "" } val desc = MutableList(messages.size) { "" }
@ -237,9 +250,10 @@ internal object MsgSvc : BaseSvc() {
kotlin.runCatching { kotlin.runCatching {
val data = msg.asJsonObject["data"].asJsonObject val data = msg.asJsonObject["data"].asJsonObject
if (data.containsKey("id")) { if (data.containsKey("id")) {
val record = getMsg(data["id"].asInt).getOrElse { val msgId = data["id"].asInt
val record = getMsg(msgId).onFailure {
error("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it") error("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it")
} }.getOrThrow()
PushMsgBody( PushMsgBody(
msgHead = ResponseHead( msgHead = ResponseHead(
peerUid = record.senderUid, peerUid = record.senderUid,
@ -257,9 +271,7 @@ internal object MsgSvc : BaseSvc() {
msgType = when (record.chatType) { msgType = when (record.chatType) {
MsgConstant.KCHATTYPEC2C -> 9 MsgConstant.KCHATTYPEC2C -> 9
MsgConstant.KCHATTYPEGROUP -> 82 MsgConstant.KCHATTYPEGROUP -> 82
else -> throw UnsupportedOperationException( else -> throw UnsupportedOperationException("Unsupported chatType: $chatType")
"Unsupported chatType: $chatType"
)
}, },
msgSubType = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null, msgSubType = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
divSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null, divSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
@ -288,14 +300,16 @@ internal object MsgSvc : BaseSvc() {
record.peerUin.toString(), record.peerUin.toString(),
"0" "0"
).onEach { segment -> ).onEach { segment ->
if (segment.type == "forward") if (segment.type == "forward") {
forwardMsg[segment.data["filename"] as String] = forwardMsg[segment.data["filename"] as String] =
segment.data["id"] as String segment.data["id"] as String
}
}.toJson() }.toJson()
).getOrElse { throw Exception("消息合成失败: $it") }.let { ).onFailure {
error("消息合成失败: ${it.stackTraceToString()}")
}.onSuccess {
desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": " + it.first desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": " + it.first
it.second }.getOrThrow().second
}
) )
) )
} else if (data.containsKey("content")) { } else if (data.containsKey("content")) {
@ -343,22 +357,21 @@ internal object MsgSvc : BaseSvc() {
elementData["id"].asString elementData["id"].asString
} }
} }
).getOrElse { error("消息合成失败: $it") }.let { ).onSuccess {
desc[++i] = (data["name"].asStringOrNull ?: data["uin"].asStringOrNull desc[++i] = (data["name"].asStringOrNull ?: data["uin"].asStringOrNull
?: TicketSvc.getNickname()) + ": " + it.first ?: TicketSvc.getNickname()) + ": " + it.first
it.second }.onFailure {
} error("消息合成失败: ${it.stackTraceToString()}")
}.getOrThrow().second
) )
) )
} else { } else error("消息节点缺少id或content字段")
error("消息节点缺少id或content字段")
}
}.onFailure { }.onFailure {
LogCenter.log("消息节点解析失败:${it.stackTraceToString()}", Level.WARN) LogCenter.log("消息节点解析失败:${it.stackTraceToString()}", Level.WARN)
}.getOrElse { }.getOrNull()
null }.ifEmpty {
} return Result.failure(Exception("消息节点为空"))
}.ifEmpty { return Result.failure(Exception("消息节点为空")) } }
val payload = LongMsgPayload( val payload = LongMsgPayload(
action = mutableListOf( action = mutableListOf(
@ -380,26 +393,22 @@ internal object MsgSvc : BaseSvc() {
} }
} }
) )
LogCenter.log(payload.toByteArray().toHexString(), Level.DEBUG) LogCenter.log({ payload.toByteArray().toHexString() }, Level.DEBUG)
val req = LongMsgReq( val req = LongMsgReq(
sendInfo = when (chatType) { sendInfo = when (chatType) {
MsgConstant.KCHATTYPEC2C -> SendLongMsgInfo( MsgConstant.KCHATTYPEC2C -> SendLongMsgInfo(
type = 1, type = 1,
uid = LongMsgUid(peerId), uid = LongMsgUid(if(peerId.startsWith("u_")) peerId else ContactHelper.getUidByUinAsync(peerId.toLong()) ),
payload = DeflateTools.gzip(payload.toByteArray()) payload = DeflateTools.gzip(payload.toByteArray())
) )
MsgConstant.KCHATTYPEGROUP -> SendLongMsgInfo( MsgConstant.KCHATTYPEGROUP -> SendLongMsgInfo(
type = 3, type = 3,
uid = LongMsgUid(fromId), uid = LongMsgUid(fromId),
groupUin = fromId.toInt(), groupUin = fromId.toULong(),
payload = DeflateTools.gzip(payload.toByteArray()) payload = DeflateTools.gzip(payload.toByteArray())
) )
else -> throw UnsupportedOperationException("Unsupported chatType: $chatType")
else -> throw UnsupportedOperationException(
"Unsupported chatType: $chatType"
)
}, },
setting = LongMsgSettings( setting = LongMsgSettings(
field1 = 4, field1 = 4,
@ -407,27 +416,25 @@ internal object MsgSvc : BaseSvc() {
field3 = 9, field3 = 9,
field4 = 0 field4 = 0
) )
) ).toByteArray()
val buffer = sendBufferAW( val buffer = sendBufferAW("trpc.group.long_msg_interface.MsgService.SsoSendLongMsg", true, req, timeout = 30_000)
"trpc.group.long_msg_interface.MsgService.SsoSendLongMsg", ?: return Result.failure(Exception("unable to upload multi message, response timeout"))
true, val rsp = runCatching {
req.toByteArray() buffer.slice(4).decodeProtobuf<LongMsgRsp>()
) ?: return Result.failure(Exception("unable to upload multi message")) }.getOrElse {
val rsp = buffer.slice(4).decodeProtobuf<LongMsgRsp>() buffer.decodeProtobuf<LongMsgRsp>()
}
val resId = rsp.sendResult?.resId ?: return Result.failure(Exception("unable to upload multi message")) val resId = rsp.sendResult?.resId ?: return Result.failure(Exception("unable to upload multi message"))
val filename = UUID.randomUUID().toString().uppercase() return Result.success(MessageSegment(
return Result.success( type = "forward",
MessageSegment( data = mapOf(
"forward", "id" to resId,
mapOf( "filename" to UUID.randomUUID().toString(),
"id" to resId, "summary" to "查看${desc.size}条转发消息",
"filename" to filename, "desc" to desc.slice(0..if (i < 3) i else 3).joinToString("\n")
"summary" to "查看${desc.size}条转发消息",
"desc" to desc.slice(0..if (i < 3) i else 3).joinToString("\n")
)
) )
) ))
} }
suspend fun getMultiMsg(resId: String): Result<List<LongMsgAction>> { suspend fun getMultiMsg(resId: String): Result<List<LongMsgAction>> {

View File

@ -24,6 +24,8 @@ import protobuf.message.Ptt
import protobuf.message.RichText import protobuf.message.RichText
import protobuf.message.element.* import protobuf.message.element.*
import protobuf.message.element.commelem.* import protobuf.message.element.commelem.*
import protobuf.oidb.cmd0x11c5.C2CUserInfo
import protobuf.oidb.cmd0x11c5.GroupUserInfo
import java.io.File import java.io.File
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.* import java.util.*
@ -68,7 +70,7 @@ internal class ElemMaker {
private var rich = RichText() private var rich = RichText()
private val elems = mutableListOf<Elem>() private val elems = mutableListOf<Elem>()
private var desc = "" private var summary = StringBuilder()
fun getRich(): RichText { fun getRich(): RichText {
rich.elements = elems rich.elements = elems
@ -76,7 +78,7 @@ internal class ElemMaker {
} }
fun getDesc(): String { fun getDesc(): String {
return desc return summary.toString()
} }
private suspend fun createTextElem( private suspend fun createTextElem(
@ -86,11 +88,12 @@ internal class ElemMaker {
data: JsonObject data: JsonObject
) { ) {
data.checkAndThrow("text") data.checkAndThrow("text")
val text = data["text"].asString
val elem = Elem( val elem = Elem(
text = TextMsg(data["text"].asString) text = TextMsg(text)
) )
elems.add(elem) elems.add(elem)
desc += data["text"].asString summary.append(text)
} }
private suspend fun createAtElem( private suspend fun createAtElem(
@ -102,7 +105,6 @@ internal class ElemMaker {
when (chatType) { when (chatType) {
MsgConstant.KCHATTYPEGROUP -> { MsgConstant.KCHATTYPEGROUP -> {
data.checkAndThrow("qq") data.checkAndThrow("qq")
val qq: Long val qq: Long
val type: Int val type: Int
val display = when (val qqStr = data["qq"].asString) { val display = when (val qqStr = data["qq"].asString) {
@ -146,7 +148,7 @@ internal class ElemMaker {
text = TextMsg(str = display, attr6Buf = attr6.array()) text = TextMsg(str = display, attr6Buf = attr6.array())
) )
elems.add(elem) elems.add(elem)
desc += display summary.append(display)
} }
MsgConstant.KCHATTYPEC2C -> { MsgConstant.KCHATTYPEC2C -> {
@ -165,7 +167,7 @@ internal class ElemMaker {
text = TextMsg(str = display) text = TextMsg(str = display)
) )
elems.add(elem) elems.add(elem)
desc += display summary.append(display)
} }
else -> throw UnsupportedOperationException("Unsupported chatType($chatType) for AtMsg") else -> throw UnsupportedOperationException("Unsupported chatType($chatType) for AtMsg")
@ -205,7 +207,7 @@ internal class ElemMaker {
) )
} }
elems.add(elem) elems.add(elem)
desc += "[表情]" summary.append("[表情]")
} }
private suspend fun createImageElem( private suspend fun createImageElem(
@ -215,7 +217,6 @@ internal class ElemMaker {
data: JsonObject data: JsonObject
) { ) {
val isOriginal = data["original"].asBooleanOrNull ?: true val isOriginal = data["original"].asBooleanOrNull ?: true
val isFlash = data["flash"].asBooleanOrNull ?: false
val filePath = data["file"].asStringOrNull val filePath = data["file"].asStringOrNull
val url = data["url"].asStringOrNull val url = data["url"].asStringOrNull
var file: File? = null var file: File? = null
@ -255,82 +256,111 @@ internal class ElemMaker {
picHeight = options.outWidth picHeight = options.outWidth
} }
val uploadRet = NtV2RichMediaSvc.tryUploadResourceByNt( val fileInfo = NtV2RichMediaSvc.tryUploadResourceByNt(
chatType = chatType, chatType = chatType,
elementType = MsgConstant.KELEMTYPEPIC, elementType = MsgConstant.KELEMTYPEPIC,
resources = arrayListOf(file), resources = arrayListOf(file),
timeout = 30.seconds timeout = 30.seconds
).getOrThrow().first() ).getOrThrow().first()
LogCenter.log({ uploadRet.toString() }, Level.DEBUG)
val elem = when (chatType) { runCatching {
MsgConstant.KCHATTYPEGROUP -> Elem( fileInfo.uuid.toUInt()
customFace = CustomFace( }.onFailure {
filePath = uploadRet.fileName, NtV2RichMediaSvc.requestUploadNtPic(file, fileInfo.md5, fileInfo.sha, fileInfo.fileName, picWidth.toUInt(), picHeight.toUInt(), 5) {
fileId = uploadRet.uuid.toUInt(), when(chatType) {
serverIp = 0u, MsgConstant.KCHATTYPEGROUP -> {
serverPort = 0u, sceneType = 2u
fileType = FileUtils.getPicType(file).toUInt(), grp = GroupUserInfo(peerId.toULong())
useful = 1u, }
md5 = uploadRet.md5.hex2ByteArray(), MsgConstant.KCHATTYPEC2C -> {
bizType = data["subType"].asIntOrNull?.toUInt(), sceneType = 1u
imageType = FileUtils.getPicType(file).toUInt(), c2c = C2CUserInfo(
width = picWidth.toUInt(), accountType = 2u,
height = picHeight.toUInt(), uid = ContactHelper.getUidByUinAsync(peerId.toLong())
size = uploadRet.fileSize.toUInt(), )
origin = isOriginal, }
thumbWidth = 0u, else -> error("不支持的合并转发图片类型")
thumbHeight = 0u, }
pbReserve = CustomFace.Companion.PbReserve( }.onFailure {
field1 = 0, LogCenter.log("获取MultiMedia图片信息失败: $it", Level.ERROR)
field3 = 0, }.onSuccess {
field4 = 0, //LogCenter.log({ "获取MultiMedia图片信息成功: ${it.hashCode()}" }, Level.INFO)
field10 = 0, elems.add(Elem(
field21 = CustomFace.Companion.Object1( commonElem = CommonElem(
serviceType = 48,
businessType = 10,
elem = it.msgInfo!!.toByteArray()
)
))
}
}.onSuccess { uuid ->
elems.add(when (chatType) {
MsgConstant.KCHATTYPEGROUP -> Elem(
customFace = CustomFace(
filePath = fileInfo.fileName,
fileId = uuid,
serverIp = 0u,
serverPort = 0u,
fileType = FileUtils.getPicType(file).toUInt(),
useful = 1u,
md5 = fileInfo.md5.hex2ByteArray(),
bizType = data["subType"].asIntOrNull?.toUInt(),
imageType = FileUtils.getPicType(file).toUInt(),
width = picWidth.toUInt(),
height = picHeight.toUInt(),
size = fileInfo.fileSize.toUInt(),
origin = isOriginal,
thumbWidth = 0u,
thumbHeight = 0u,
pbReserve = CustomFace.Companion.PbReserve(
field1 = 0, field1 = 0,
field2 = "",
field3 = 0, field3 = 0,
field4 = 0, field4 = 0,
field5 = 0, field10 = 0,
md5Str = uploadRet.md5 field21 = CustomFace.Companion.Object1(
field1 = 0,
field2 = "",
field3 = 0,
field4 = 0,
field5 = 0,
md5Str = fileInfo.md5
)
) )
) )
) )
) MsgConstant.KCHATTYPEC2C -> Elem(
notOnlineImage = NotOnlineImage(
MsgConstant.KCHATTYPEC2C -> Elem( filePath = fileInfo.fileName,
notOnlineImage = NotOnlineImage( fileLen = fileInfo.fileSize.toUInt(),
filePath = uploadRet.fileName, downloadPath = fileInfo.uuid,
fileLen = uploadRet.fileSize.toUInt(), imgType = FileUtils.getPicType(file).toUInt(),
downloadPath = uploadRet.uuid, picMd5 = fileInfo.md5.hex2ByteArray(),
imgType = FileUtils.getPicType(file).toUInt(), picHeight = picWidth.toUInt(),
picMd5 = uploadRet.md5.hex2ByteArray(), picWidth = picHeight.toUInt(),
picHeight = picWidth.toUInt(), resId = fileInfo.uuid,
picWidth = picHeight.toUInt(), original = isOriginal, // true
resId = uploadRet.uuid, pbReserve = NotOnlineImage.Companion.PbReserve(
original = isOriginal, // true
pbReserve = NotOnlineImage.Companion.PbReserve(
field1 = 0,
field3 = 0,
field4 = 0,
field10 = 0,
field20 = NotOnlineImage.Companion.Object1(
field1 = 0, field1 = 0,
field2 = "",
field3 = 0, field3 = 0,
field4 = 0, field4 = 0,
field5 = 0, field10 = 0,
field7 = "", field20 = NotOnlineImage.Companion.Object1(
), field1 = 0,
md5Str = uploadRet.md5 field2 = "",
field3 = 0,
field4 = 0,
field5 = 0,
field7 = "",
),
md5Str = fileInfo.md5
)
) )
) )
) else -> throw LogicException("Not supported chatType($chatType) for PictureMsg")
})
else -> throw LogicException("Not supported chatType($chatType) for PictureMsg")
} }
elems.add(elem)
desc += "[图片]" summary.append("[图片]")
} }
private suspend fun createReplyElem( private suspend fun createReplyElem(
@ -402,7 +432,7 @@ internal class ElemMaker {
) )
} }
elems.add(elem) elems.add(elem)
desc += "[回复消息]" summary.append("[回复消息]")
} }
private suspend fun createJsonElem( private suspend fun createJsonElem(
@ -419,7 +449,7 @@ internal class ElemMaker {
) )
) )
elems.add(elem) elems.add(elem)
desc += "[Json消息]" summary .append( "[Json消息]" )
} }
private suspend fun createForwardStruct( private suspend fun createForwardStruct(
@ -485,7 +515,7 @@ internal class ElemMaker {
) )
) )
elems.add(elem) elems.add(elem)
desc += "[聊天记录]" this.summary .append( "[聊天记录]" )
} }
private suspend fun createWeatherElem( private suspend fun createWeatherElem(
@ -517,7 +547,7 @@ internal class ElemMaker {
) )
) )
elems.add(elem) elems.add(elem)
desc += "[天气卡片]" summary .append( "[天气卡片]" )
} else { } else {
throw LogicException("无法获取城市天气") throw LogicException("无法获取城市天气")
} }
@ -542,7 +572,7 @@ internal class ElemMaker {
) )
) )
elems.add(elem) elems.add(elem)
desc += "[戳一戳]" summary .append( "[戳一戳]" )
} }
private suspend fun createNewDiceElem( private suspend fun createNewDiceElem(
@ -568,7 +598,7 @@ internal class ElemMaker {
) )
) )
elems.add(elem) elems.add(elem)
desc += "[骰子]" summary .append( "[骰子]" )
} }
private suspend fun createNewRpsElem( private suspend fun createNewRpsElem(
@ -594,7 +624,7 @@ internal class ElemMaker {
) )
) )
elems.add(elem) elems.add(elem)
desc += "[包剪锤]" summary .append( "[包剪锤]" )
} }
private suspend fun createMarkdownElem( private suspend fun createMarkdownElem(
@ -612,7 +642,7 @@ internal class ElemMaker {
) )
) )
elems.add(elem) elems.add(elem)
desc += "[Markdown消息]" summary.append("[Markdown消息]")
} }
private suspend fun createButtonElem( private suspend fun createButtonElem(
@ -662,7 +692,7 @@ internal class ElemMaker {
) )
) )
elems.add(elem) elems.add(elem)
desc += "[Button消息]" summary.append("[Button消息]")
} }
private suspend fun createRecordElem( private suspend fun createRecordElem(
@ -675,7 +705,7 @@ internal class ElemMaker {
rich.ptt= Ptt( rich.ptt= Ptt(
) )
desc += "[语音消息]" summary .append( "[语音消息]" )
} }
private fun JsonObject.checkAndThrow(vararg key: String) { private fun JsonObject.checkAndThrow(vararg key: String) {

View File

@ -46,6 +46,7 @@ import protobuf.oidb.cmd0x11c5.NtV2RichMediaRsp
import protobuf.oidb.cmd0x11c5.SceneInfo import protobuf.oidb.cmd0x11c5.SceneInfo
import protobuf.oidb.cmd0x11c5.UploadInfo import protobuf.oidb.cmd0x11c5.UploadInfo
import protobuf.oidb.cmd0x11c5.UploadReq import protobuf.oidb.cmd0x11c5.UploadReq
import protobuf.oidb.cmd0x11c5.UploadRsp
import protobuf.oidb.cmd0x11c5.VideoDownloadExt import protobuf.oidb.cmd0x11c5.VideoDownloadExt
import protobuf.oidb.cmd0x388.Cmd0x388ReqBody import protobuf.oidb.cmd0x388.Cmd0x388ReqBody
import protobuf.oidb.cmd0x388.Cmd0x388RspBody import protobuf.oidb.cmd0x388.Cmd0x388RspBody
@ -281,7 +282,6 @@ internal object NtV2RichMediaSvc: BaseSvc() {
MsgConstant.KCHATTYPEC2C -> MessageHelper.generateContact(chatType, TicketSvc.getUin()) MsgConstant.KCHATTYPEC2C -> MessageHelper.generateContact(chatType, TicketSvc.getUin())
else -> Contact(chatType, fetchGroupResUploadTo(), null) else -> Contact(chatType, fetchGroupResUploadTo(), null)
} }
LogCenter.log(contact.toString())
val result = mutableListOf<CommonFileInfo>() val result = mutableListOf<CommonFileInfo>()
withTimeoutOrNull(timeout) { withTimeoutOrNull(timeout) {
suspendCancellableCoroutine { suspendCancellableCoroutine {
@ -313,6 +313,11 @@ internal object NtV2RichMediaSvc: BaseSvc() {
} }
} }
} }
if (result.isEmpty()) {
return Result.failure(Exception("upload failed"))
}
return Result.success(result) return Result.success(result)
} }
@ -392,9 +397,6 @@ internal object NtV2RichMediaSvc: BaseSvc() {
return Result.failure(Exception("unable to get c2c nt pic")) return Result.failure(Exception("unable to get c2c nt pic"))
} }
/**
* 请求上传Nt图片
*/
suspend fun requestUploadNtPic( suspend fun requestUploadNtPic(
file: File, file: File,
md5: String, md5: String,
@ -402,8 +404,27 @@ internal object NtV2RichMediaSvc: BaseSvc() {
name: String, name: String,
width: UInt, width: UInt,
height: UInt, height: UInt,
retryCnt: Int,
sceneBuilder: suspend SceneInfo.() -> Unit sceneBuilder: suspend SceneInfo.() -> Unit
) { ): Result<UploadRsp> {
return runCatching {
requestUploadNtPic(file, md5, sha, name, width, height, sceneBuilder).getOrThrow()
}.onFailure {
if (retryCnt > 0) {
return requestUploadNtPic(file, md5, sha, name, width, height, retryCnt - 1, sceneBuilder)
}
}
}
private suspend fun requestUploadNtPic(
file: File,
md5: String,
sha: String,
name: String,
width: UInt,
height: UInt,
sceneBuilder: suspend SceneInfo.() -> Unit
): Result<UploadRsp> {
val req = NtV2RichMediaReq( val req = NtV2RichMediaReq(
head = MultiMediaReqHead( head = MultiMediaReqHead(
commonHead = CommonHead( commonHead = CommonHead(
@ -443,12 +464,17 @@ internal object NtV2RichMediaSvc: BaseSvc() {
clientRandomId = Random.nextULong(), clientRandomId = Random.nextULong(),
compatQMsgSceneType = 1u, compatQMsgSceneType = 1u,
clientSeq = Random.nextUInt(), clientSeq = Random.nextUInt(),
noNeedCompatMsg = true noNeedCompatMsg = false
) )
).toByteArray() ).toByteArray()
val buffer = sendOidbAW("OidbSvcTrpcTcp.0x11c5_100", 4549, 100, req, true)?.slice(4) val buffer = sendOidbAW("OidbSvcTrpcTcp.0x11c5_100", 4549, 100, req, true, timeout = 3_000)?.slice(4)
val rsp = buffer?.decodeProtobuf<TrpcOidb>()?.buffer?.decodeProtobuf<NtV2RichMediaRsp>() ?: return Result.failure(Exception("no response: timeout"))
LogCenter.log("requestUploadPic => rsp: $rsp") val rspBuffer = buffer.decodeProtobuf<TrpcOidb>().buffer
val rsp = rspBuffer.decodeProtobuf<NtV2RichMediaRsp>()
if (rsp.upload == null) {
return Result.failure(Exception("unable to request upload nt pic: ${rsp.head}"))
}
return Result.success(rsp.upload!!)
} }
/** /**

View File

@ -33,31 +33,17 @@ internal object SendForwardMessage : IActionHandler() {
} }
} }
val peerId = when (chatType) { val peerId = when (chatType) {
MsgConstant.KCHATTYPEGROUP -> session.getLongOrNull("group_id") ?: return noParam( MsgConstant.KCHATTYPEGROUP -> session.getStringOrNull("group_id")
"group_id",
session.echo
)
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("user_id")
?: return noParam("user_id", session.echo)
else -> error("unknown chat type: $chatType")
}.toString()
val fromId = when (chatType) {
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("group_id")
?: return noParam("group_id", session.echo) ?: return noParam("group_id", session.echo)
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("user_id")
MsgConstant.KCHATTYPEC2C -> session.getLongOrNull("user_id") ?: return noParam( ?: return noParam("user_id", session.echo)
"user_id",
session.echo
)
else -> error("unknown chat type: $chatType") else -> error("unknown chat type: $chatType")
}.toString() }
val fromId = session.getStringOrNull("group_id")
val retryCnt = session.getIntOrNull("retry_cnt") ?: 5 val retryCnt = session.getIntOrNull("retry_cnt") ?: 5
return if (session.isArray("messages")) { return if (session.isArray("messages")) {
val messages = session.getArray("messages") val messages = session.getArray("messages")
invoke(chatType, peerId, fromId, messages, retryCnt, session.echo) invoke(chatType, peerId, fromId ?: peerId, messages, retryCnt, session.echo)
} else { } else {
logic("未知格式合并转发消息", session.echo) logic("未知格式合并转发消息", session.echo)
} }
@ -77,18 +63,16 @@ internal object SendForwardMessage : IActionHandler() {
echo: JsonElement = EmptyJsonString echo: JsonElement = EmptyJsonString
): String { ): String {
kotlin.runCatching { kotlin.runCatching {
val message = MsgSvc.uploadMultiMsg(chatType, peerId, fromId, messages, retryCnt) val message = MsgSvc.uploadMultiMsg(chatType, peerId, fromId, messages, retryCnt).onFailure {
.getOrElse { return logic(it.message ?: "", echo) } return error(it.message ?: it.stackTraceToString(), echo)
}.getOrThrow()
val result = MsgSvc.sendToAio(chatType, peerId, listOf(message).toJson(), fromId, retryCnt) val result = MsgSvc.sendToAio(chatType, peerId, listOf(message).toJson(), fromId, retryCnt).onFailure {
.getOrElse { return logic(it.message ?: "", echo) } return error(it.message ?: it.stackTraceToString(), echo)
}.getOrThrow()
return ok( return ok(SendForwardMessageResult(
SendForwardMessageResult( msgId = result.msgHashId,
msgId = result.msgHashId, resId = message.data["id"] as String
resId = message.data["id"] as String ), echo = echo)
), echo = echo
)
}.onFailure { }.onFailure {
return error("合并转发消息失败: $it", echo) return error("合并转发消息失败: $it", echo)
} }

View File

@ -32,31 +32,17 @@ internal object UploadMultiMessage : IActionHandler() {
} }
} }
val peerId = when (chatType) { val peerId = when (chatType) {
MsgConstant.KCHATTYPEGROUP -> session.getLongOrNull("group_id") ?: return noParam( MsgConstant.KCHATTYPEGROUP -> session.getStringOrNull("group_id")
"group_id",
session.echo
)
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("user_id")
?: return noParam("user_id", session.echo)
else -> error("unknown chat type: $chatType")
}.toString()
val fromId = when (chatType) {
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("group_id")
?: return noParam("group_id", session.echo) ?: return noParam("group_id", session.echo)
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("user_id")
MsgConstant.KCHATTYPEC2C -> session.getLongOrNull("user_id") ?: return noParam( ?: return noParam("user_id", session.echo)
"user_id",
session.echo
)
else -> error("unknown chat type: $chatType") else -> error("unknown chat type: $chatType")
}.toString() }
val fromId = session.getStringOrNull("group_id")
val retryCnt = session.getIntOrNull("retry_cnt") ?: 5 val retryCnt = session.getIntOrNull("retry_cnt") ?: 5
return if (session.isArray("messages")) { return if (session.isArray("messages")) {
val messages = session.getArray("messages") val messages = session.getArray("messages")
invoke(chatType, peerId, fromId, messages, retryCnt, echo = session.echo) invoke(chatType, peerId, fromId ?: peerId, messages, retryCnt, echo = session.echo)
} else { } else {
logic("未知格式合并转发消息", session.echo) logic("未知格式合并转发消息", session.echo)
} }
@ -76,9 +62,10 @@ internal object UploadMultiMessage : IActionHandler() {
echo: JsonElement = EmptyJsonString echo: JsonElement = EmptyJsonString
): String { ): String {
kotlin.runCatching { kotlin.runCatching {
val message = MsgSvc.uploadMultiMsg(chatType, peerId, fromId, messages, retryCnt) MsgSvc.uploadMultiMsg(chatType, peerId, fromId, messages, retryCnt).getOrThrow()
.getOrElse { return logic(it.message ?: "", echo) } }.onFailure {
return error("合并转发消息失败: ${it.stackTraceToString()}", echo)
}.onSuccess { message ->
return ok( return ok(
UploadForwardMessageResult( UploadForwardMessageResult(
resId = message.data["id"] as String, resId = message.data["id"] as String,
@ -87,8 +74,6 @@ internal object UploadMultiMessage : IActionHandler() {
desc = message.data["desc"] as String desc = message.data["desc"] as String
), echo = echo ), echo = echo
) )
}.onFailure {
return error("合并转发消息失败: $it", echo)
} }
return logic("合并转发消息失败(unknown error)", echo) return logic("合并转发消息失败(unknown error)", echo)
} }

View File

@ -312,16 +312,13 @@ fun Routing.messageAction() {
val userId = fetchPostOrNull("user_id") val userId = fetchPostOrNull("user_id")
val groupId = fetchPostOrNull("group_id") val groupId = fetchPostOrNull("group_id")
val messages = fetchPostJsonArray("messages") val messages = fetchPostJsonArray("messages")
call.respondText( call.respondText(UploadMultiMessage(
UploadMultiMessage( chatType = chatType,
chatType, peerId = if (chatType == MsgConstant.KCHATTYPEC2C) userId!! else groupId!!,
if (chatType == MsgConstant.KCHATTYPEC2C) userId!! else groupId!!, fromId = groupId ?: userId ?: "",
groupId ?: userId ?: "", messages = messages,
messages, retryCnt = retryCnt
retryCnt ), ContentType.Application.Json)
),
ContentType.Application.Json
)
} }
get { get {
respond(false, Status.InternalHandlerError, "Not support GET method") respond(false, Status.InternalHandlerError, "Not support GET method")