refactor send_forward_msg

This commit is contained in:
Simplxs 2024-02-28 12:55:27 +08:00
parent 4dc83fdeba
commit ec56e32be1
No known key found for this signature in database
GPG Key ID: 8BD9E2834B275BD3
24 changed files with 891 additions and 534 deletions

View File

@ -7,9 +7,9 @@ import kotlinx.serialization.protobuf.ProtoNumber
data class Ptt(
@ProtoNumber(1) var fileType: UInt?=null,
@ProtoNumber(2) var srcUin: ULong?=null,
@ProtoNumber(3) var fileUuid: ByteArray?=null,
@ProtoNumber(3) var fileUuid: String?=null,
@ProtoNumber(4) var fileMd5: ByteArray?=null,
@ProtoNumber(5) var fileName: ByteArray?=null,
@ProtoNumber(5) var fileName: String?=null,
@ProtoNumber(6) var fileSize: UInt?=null,
@ProtoNumber(7) var reserve: ByteArray?=null,
@ProtoNumber(8) var fileId: UInt?=null,
@ -22,11 +22,19 @@ data class Ptt(
@ProtoNumber(15) var magicPttIndex: UInt?=null,
@ProtoNumber(16) var voiceSwitch: UInt?=null,
@ProtoNumber(17) var pttUrl: ByteArray?=null,
@ProtoNumber(18) var groupFileKey: ByteArray?=null,
@ProtoNumber(18) var groupFileKey: String?=null,
@ProtoNumber(19) var time: UInt?=null,
@ProtoNumber(20) var downPara: ByteArray?=null,
@ProtoNumber(29) var format: UInt?=null,
@ProtoNumber(30) var pbReserve: ByteArray?=null,
@ProtoNumber(30) var pbReserve: PbReserve?=null,
@ProtoNumber(31) var rptPttUrls: List<String>? = null,
@ProtoNumber(32) var downloadFlag: UInt?=null,
)
){
companion object{
@Serializable
data class PbReserve(
@ProtoNumber(2) var magic: Int?=null,
@ProtoNumber(7) var reserve: Int?=null,
)
}
}

View File

@ -9,7 +9,7 @@ import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class RichText(
@ProtoNumber(1) val attr: Attr? = null,
@ProtoNumber(2) val elements: List<Elem>? = null,
@ProtoNumber(2) var elements: List<Elem>? = null,
@ProtoNumber(3) val not_online_file: NotOnlineFile? = null,
@ProtoNumber(4) val ptt: Ptt? = null,
@ProtoNumber(5) val tmp_ptt: TmpPtt? = null,

View File

@ -9,7 +9,9 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment
import moe.fuqiuluo.qqinterface.servlet.msg.toJson
import moe.fuqiuluo.qqinterface.servlet.msg.toListMap
import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
import moe.fuqiuluo.shamrock.helper.ContactHelper
@ -25,10 +27,12 @@ import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
import moe.fuqiuluo.shamrock.xposed.helper.msgService
import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.auto.toByteArray
import protobuf.message.PushMsgBody
import protobuf.message.*
import protobuf.message.longmsg.*
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.random.Random
internal object MsgSvc : BaseSvc() {
private suspend fun prepareTempChatFromGroup(
@ -207,45 +211,195 @@ internal object MsgSvc : BaseSvc() {
}
val result =
MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0))
if (result.isFailure) {
LogCenter.log("sendToAio: " + result.exceptionOrNull()?.stackTraceToString(), Level.ERROR)
return result
}
val sendResult = result.getOrThrow()
return if (sendResult.isTimeout) {
.getOrElse { return Result.failure(it) }
return if (result.isTimeout) {
// 发送失败,可能网络问题出现红色感叹号,重试
// 例如 rich media transfer failed
delay(100)
MessageHelper.resendMsg(chatType, peedId, fromId, sendResult.qqMsgId, retryCnt, sendResult.msgHashId)
MessageHelper.resendMsg(chatType, peedId, fromId, result.qqMsgId, retryCnt, result.msgHashId)
} else {
result
Result.success(result)
}
}
suspend fun uploadMultiMsg(
uid: String,
groupUin: String?,
messages: List<PushMsgBody>,
): Result<String> {
chatType: Int,
peerId: String,
fromId: String,
messages: JsonArray,
retryCnt: Int,
): Result<MessageSegment> {
var i = -1
val desc = MutableList(messages.size) { "" }
val forwardMsg = mutableMapOf<String, String>()
val msgs = messages.mapNotNull { msg ->
kotlin.runCatching {
val data = msg.asJsonObject["data"].asJsonObject
if (data.containsKey("id")) {
val record = getMsg(data["id"].asInt).getOrElse {
error("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it")
}
PushMsgBody(
msgHead = ResponseHead(
peerUid = record.senderUid,
receiverUid = record.peerUid,
forward = ResponseForward(
friendName = record.sendNickName
),
responseGrp = if (record.chatType == MsgConstant.KCHATTYPEGROUP) ResponseGrp(
groupCode = record.peerUin.toULong(),
memberCard = record.sendMemberName,
u1 = 2
) else null
),
contentHead = ContentHead(
msgType = when (record.chatType) {
MsgConstant.KCHATTYPEC2C -> 9
MsgConstant.KCHATTYPEGROUP -> 82
else -> throw UnsupportedOperationException(
"Unsupported chatType: $chatType"
)
},
msgSubType = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
divSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
msgViaRandom = record.msgId,
sequence = record.msgSeq, // idk what this is(i++)
msgTime = record.msgTime,
u2 = 1,
u6 = 0,
u7 = 0,
msgSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) record.msgSeq else null, // seq for dm
forwardHead = ForwardHead(
u1 = 0,
u2 = 0,
u3 = 0,
ub641 = "",
avatar = ""
)
),
body = MsgBody(
richText = MessageHelper.messageArrayToRichText(
record.chatType,
record.msgId,
record.peerUin.toString(),
record.elements.toSegments(
record.chatType,
record.peerUin.toString(),
"0"
).onEach { segment ->
if (segment.type == "forward")
forwardMsg[segment.data["filename"] as String] =
segment.data["id"] as String
}.toJson()
).getOrElse { throw Exception("消息合成失败: $it") }.let {
desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": " + it.first
it.second
}
)
)
} else if (data.containsKey("content")) {
PushMsgBody(
msgHead = ResponseHead(
peer = data["uin"]?.asLong ?: TicketSvc.getUin().toLong(),
peerUid = data["uid"]?.asString ?: TicketSvc.getUid(),
receiverUid = TicketSvc.getUid(),
forward = ResponseForward(
friendName = data["name"]?.asStringOrNull ?: TicketSvc.getNickname()
)
),
contentHead = ContentHead(
msgType = 9,
msgSubType = 175,
divSeq = 175,
msgViaRandom = Random.nextLong(),
sequence = data["seq"]?.asLong ?: Random.nextLong(),
msgTime = data["time"]?.asLong ?: (System.currentTimeMillis() / 1000),
u2 = 1,
u6 = 0,
u7 = 0,
msgSeq = data["seq"]?.asLong ?: Random.nextLong(),
forwardHead = ForwardHead(
u1 = 0,
u2 = 0,
u3 = 2,
ub641 = "",
avatar = ""
)
),
body = MsgBody(
richText = MessageHelper.messageArrayToRichText(
chatType = chatType,
msgId = Random.nextLong(),
peerId = data["uin"]?.asString ?: TicketSvc.getUin(),
messageList = when (data["content"]) {
is JsonObject -> listOf(data["content"] as JsonObject).json
is JsonArray -> data["content"] as JsonArray
else -> MessageHelper.decodeCQCode(data["content"].asString)
}.onEach { element ->
val elementData = element.asJsonObject["data"].asJsonObject
if (element.asJsonObject["type"].asString == "forward")
forwardMsg[elementData["filename"].asString] =
elementData["id"].asString
}
).getOrElse { throw Exception("消息合成失败: $it") }.let {
desc[++i] =
(data["name"].asStringOrNull ?: data["uin"].asStringOrNull
?: TicketSvc.getNickname()) + ": " + it.first
it.second
}
)
)
} else {
error("消息节点缺少id或content字段")
}
}.getOrElse {
LogCenter.log("消息节点解析失败:$it", Level.WARN)
null
}
}.ifEmpty { return Result.failure(Exception("消息节点为空")) }
val payload = LongMsgPayload(
action = listOf(
action = mutableListOf(
LongMsgAction(
command = "MultiMsg",
data = LongMsgContent(
body = messages
)
body = msgs
)
)
).apply {
forwardMsg.map { msg ->
addAll(getMultiMsg(msg.value).getOrElse { return Result.failure(Exception("无法获取嵌套转发消息: $it")) }
.map { action ->
if (action.command == "MultiMsg") LongMsgAction(
command = msg.key,
data = action.data
) else action
})
}
}
)
LogCenter.log(payload.toByteArray().toHexString(), Level.DEBUG)
val req = LongMsgReq(
sendInfo = SendLongMsgInfo(
type = if (groupUin == null) 1 else 3,
uid = LongMsgUid(groupUin ?: uid),
groupUin = groupUin?.toInt(),
sendInfo = when (chatType) {
MsgConstant.KCHATTYPEC2C -> SendLongMsgInfo(
type = 1,
uid = LongMsgUid(peerId),
payload = DeflateTools.gzip(payload.toByteArray())
),
)
MsgConstant.KCHATTYPEGROUP -> SendLongMsgInfo(
type = 3,
uid = LongMsgUid(fromId),
groupUin = fromId.toInt(),
payload = DeflateTools.gzip(payload.toByteArray())
)
else -> throw UnsupportedOperationException(
"Unsupported chatType: $chatType"
)
},
setting = LongMsgSettings(
field1 = 4,
field2 = 2,
@ -253,17 +407,29 @@ internal object MsgSvc : BaseSvc() {
field4 = 0
)
)
val buffer = sendBufferAW(
"trpc.group.long_msg_interface.MsgService.SsoSendLongMsg",
true,
req.toByteArray()
) ?: return Result.failure(Exception("unable to upload multi message"))
val rsp = buffer.slice(4).decodeProtobuf<LongMsgRsp>()
return rsp.sendResult?.resId?.let { Result.success(it) }
?: 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(
"forward",
mapOf(
"id" to resId,
"filename" to filename,
"summary" to "查看${desc.size}条转发消息",
"desc" to desc.slice(0..if (i < 3) i else 3).joinToString("\n")
)
)
)
}
suspend fun getMultiMsg(resId: String): Result<List<MessageDetail>> {
suspend fun getMultiMsg(resId: String): Result<List<LongMsgAction>> {
val req = LongMsgReq(
recvInfo = RecvLongMsgInfo(
uid = LongMsgUid(TicketSvc.getUid()),
@ -284,11 +450,18 @@ internal object MsgSvc : BaseSvc() {
) ?: return Result.failure(Exception("unable to get multi message"))
val rsp = buffer.slice(4).decodeProtobuf<LongMsgRsp>()
val zippedPayload = DeflateTools.ungzip(
rsp.recvResult?.payload ?: return Result.failure(Exception("unable to get multi message"))
rsp.recvResult?.payload ?: return Result.failure(Exception("payload is empty"))
)
LogCenter.log(zippedPayload.toHexString(), Level.DEBUG)
val payload = zippedPayload.decodeProtobuf<LongMsgPayload>()
payload.action?.forEach {
return Result.success(
zippedPayload.decodeProtobuf<LongMsgPayload>().action
?: return Result.failure(Exception("action is empty"))
)
}
suspend fun getForwardMsg(resId: String): Result<List<MessageDetail>> {
val result = getMultiMsg(resId).getOrElse { return Result.failure(it) }
result.forEach {
if (it.command == "MultiMsg") {
return Result.success(it.data?.body?.map { msg ->
val chatType =
@ -296,27 +469,29 @@ internal object MsgSvc : BaseSvc() {
MessageDetail(
time = msg.contentHead?.msgTime?.toInt() ?: 0,
msgType = MessageHelper.obtainDetailTypeByMsgType(chatType),
msgId = 0, // MessageHelper.generateMsgIdHash(chatType, msg.content!!.msgViaRandom), msgViaRandom 为空
msgId = 0, // msgViaRandom为空 tx不给
qqMsgId = 0,
msgSeq = msg.contentHead!!.msgSeq ?: 0,
realId = msg.contentHead!!.msgSeq ?: 0,
sender = MessageSender(
msg.msgHead?.peer ?: 0,
msg.msgHead?.responseGrp?.memberCard?.ifEmpty { msg.msgHead?.forward?.friendName }
?: msg.msgHead?.forward?.friendName ?: "",
msg.msgHead?.responseGrp?.memberCard ?: msg.msgHead?.forward?.friendName ?: "",
"unknown",
0,
msg.msgHead?.peerUid ?: "",
msg.msgHead?.peerUid ?: ""
),
message = msg.body?.richText?.elements?.toSegments(chatType, msg.msgHead?.peer.toString(), "0")
?.toListMap() ?: emptyList(),
message = msg.body?.richText?.toSegments(
chatType,
msg.msgHead?.peer.toString(),
"0"
)?.toListMap() ?: emptyList(),
peerId = msg.msgHead?.peer ?: 0,
groupId = if (chatType == MsgConstant.KCHATTYPEGROUP) msg.msgHead?.responseGrp?.groupCode?.toLong()
?: 0 else 0,
targetId = if (chatType != MsgConstant.KCHATTYPEGROUP) msg.msgHead?.peer ?: 0 else 0
)
}
?: return Result.failure(Exception("Msg is empty")))
} ?: return Result.failure(Exception("Msg is empty")))
}
}
return Result.failure(Exception("Can't find msg"))

View File

@ -10,24 +10,18 @@ import io.ktor.utils.io.core.writeInt
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.qqinterface.servlet.msg.MessageTempHandler
import moe.fuqiuluo.shamrock.remote.action.handlers.GetHistoryMsg
import moe.fuqiuluo.shamrock.remote.service.listener.AioListener
import moe.fuqiuluo.shamrock.tools.broadcast
import moe.fuqiuluo.shamrock.utils.DeflateTools
import protobuf.message.element.LightAppElem
import protobuf.message.PushMsgBody
import protobuf.message.ContentHead
import protobuf.message.Elem
import protobuf.message.RichText
import protobuf.message.ResponseHead
import protobuf.message.MsgBody
import protobuf.push.MessagePush
import mqq.app.MobileQQ
import protobuf.auto.toByteArray
import protobuf.message.*
import protobuf.message.element.LightAppElem
import protobuf.push.MessagePush
import kotlin.coroutines.resume
import kotlin.text.toByteArray
internal object PacketSvc: BaseSvc() {
internal object PacketSvc : BaseSvc() {
/**
* 伪造收到Json卡片消息
*/
@ -44,7 +38,12 @@ internal object PacketSvc: BaseSvc() {
private suspend fun fakeReceiveSelfMsg(msgService: IKernelMsgService, builder: () -> List<Elem>): Long {
val latestMsg = withTimeoutOrNull(3000) {
suspendCancellableCoroutine {
msgService.getMsgs(Contact(MsgConstant.KCHATTYPEC2C, app.currentUid, ""), 0L, 1, true) { code, why, msgs ->
msgService.getMsgs(
Contact(MsgConstant.KCHATTYPEC2C, app.currentUid, ""),
0L,
1,
true
) { code, why, msgs ->
it.resume(GetHistoryMsg.GetMsgResult(code, why, msgs))
}
}
@ -72,9 +71,11 @@ internal object PacketSvc: BaseSvc() {
u4 = msgSeq - 2,
u5 = msgSeq
),
body = MsgBody(RichText(
body = MsgBody(
RichText(
elements = builder()
))
)
)
)
)

View File

@ -1,21 +1,46 @@
package moe.fuqiuluo.qqinterface.servlet.msg
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import moe.fuqiuluo.qqinterface.servlet.msg.converter.ElemConverter
import moe.fuqiuluo.qqinterface.servlet.msg.converter.NtMsgElementConverter
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.tools.toHexString
import protobuf.message.Elem
import protobuf.message.RichText
@JvmName("elemListToSegments")
internal suspend fun List<Elem>.toSegments(
@JvmName("richTextToSegments")
internal suspend fun RichText.toSegments(
chatType: Int,
peerId: String,
subPeer: String
): List<MessageSegment> {
val messageData = arrayListOf<MessageSegment>()
this.forEach { msg ->
if (ptt != null) {
val md5 = ptt!!.fileMd5!!
messageData.add(
MessageSegment(
"record", mapOf(
"file" to md5.toHexString(),
"url" to when (chatType) {
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", ptt!!.fileUuid!!)
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
"0",
md5,
ptt!!.groupFileKey!!
)
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
},
"magic" to ptt!!.pbReserve?.magic,
)
)
)
}
elements?.forEach { msg ->
kotlin.runCatching {
val elementType = if (msg.text != null) {
1

View File

@ -12,8 +12,8 @@ internal data class MessageSegment(
) {
fun toJson(): JsonObject {
return mapOf(
"type" to type.json,
"data" to data.json
"type" to type,
"data" to data
).json
}
}
@ -29,6 +29,6 @@ internal fun List<MessageSegment>.toListMap(): List<Map<String, JsonElement>> {
mapOf(
"type" to it.type.json,
"data" to it.data.json
).json
)
}
}

View File

@ -13,11 +13,13 @@ import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.db.ImageDB
import moe.fuqiuluo.shamrock.helper.db.ImageMapping
import moe.fuqiuluo.shamrock.helper.db.MessageDB
import moe.fuqiuluo.shamrock.tools.asJsonArray
import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.symbols.decodeProtobuf
import moe.fuqiuluo.shamrock.tools.slice
import protobuf.message.Elem
import protobuf.message.element.commelem.ButtonExtra
import protobuf.message.element.commelem.MarkdownExtra
@ -335,10 +337,8 @@ internal object ElemConverter {
element: Elem
): MessageSegment {
val data = element.lightApp!!.data!!
val jsonStr =
(if (data[0].toInt() == 1) DeflateTools.uncompress(data.sliceArray(1 until data.size)) else data.sliceArray(
1 until data.size
)).toString()
val jsonStr = String(if (data[0].toInt() == 1) DeflateTools.uncompress(data.slice(1)) else data.slice(1))
LogCenter.log(jsonStr, Level.DEBUG)
val json = jsonStr.asJsonObject
return when (json["app"].asString) {
"com.tencent.multimsg" -> {
@ -346,7 +346,10 @@ internal object ElemConverter {
MessageSegment(
type = "forward",
data = mapOf(
"id" to info["resid"].asString
"id" to info["resid"].asString,
"filename" to info["uniseq"].asString,
"summary" to info["summary"].asString,
"desc" to info["news"].asJsonArray.joinToString("\n") { it.asJsonObject["text"].asString }
)
)
}

View File

@ -2,10 +2,6 @@ package moe.fuqiuluo.qqinterface.servlet.msg.converter
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
import moe.fuqiuluo.shamrock.helper.ContactHelper
@ -15,6 +11,7 @@ import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.db.ImageDB
import moe.fuqiuluo.shamrock.helper.db.ImageMapping
import moe.fuqiuluo.shamrock.helper.db.MessageDB
import moe.fuqiuluo.shamrock.tools.asJsonArray
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
@ -242,16 +239,10 @@ internal object NtMsgElementConverter {
data = hashMapOf(
"file" to md5,
"url" to when (chatType) {
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPttDownUrl(
"0",
record.md5HexStr,
record.fileUuid
)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
"0",
record.md5HexStr,
md5.hex2ByteArray(),
record.fileUuid
)
@ -343,7 +334,10 @@ internal object NtMsgElementConverter {
MessageSegment(
type = "forward",
data = mapOf(
"id" to info["resid"].asString
"id" to info["resid"].asString,
"filename" to info["uniseq"].asString,
"summary" to info["summary"].asString,
"desc" to info["news"].asJsonArray.joinToString("\n") { it.asJsonObject["text"].asString }
)
)
}

View File

@ -13,23 +13,27 @@ import moe.fuqiuluo.qqinterface.servlet.msg.toJson
import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc
import moe.fuqiuluo.shamrock.helper.*
import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToMessageElements
import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToRichText
import moe.fuqiuluo.shamrock.helper.MessageHelper.obtainMessageTypeByDetailType
import moe.fuqiuluo.shamrock.tools.*
import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.shamrock.utils.FileUtils
import protobuf.auto.toByteArray
import protobuf.message.Elem
import protobuf.message.RichText
import protobuf.message.element.*
import protobuf.message.element.commelem.*
import java.io.File
import java.nio.ByteBuffer
import java.util.*
import kotlin.random.Random
import kotlin.random.nextULong
import kotlin.time.Duration.Companion.seconds
internal typealias IElemMaker = suspend (Int, Long, String, JsonObject) -> Result<Elem>
internal typealias IElemMaker = suspend (ElemMaker, Int, Long, String, JsonObject) -> Unit
internal object ElemMaker {
internal class ElemMaker {
companion object {
private val makerArray = hashMapOf(
"text" to ElemMaker::createTextElem,
"at" to ElemMaker::createAtElem,
@ -39,11 +43,13 @@ internal object ElemMaker {
// "voice" to ElemMaker::createRecordElem,
// "record" to ElemMaker::createRecordElem,
// "video" to ElemMaker::createVideoElem,
"markdown" to ElemMaker::createMarkdownElem,
"button" to ElemMaker::createButtonElem,
"forward" to ElemMaker::createForwardStruct,
"json" to ElemMaker::createJsonElem,
"poke" to ElemMaker::createPokeElem,
"dice" to ElemMaker::createNewDiceElem,
"rps" to ElemMaker::createNewRpsElem,
"poke" to ElemMaker::createPokeElem,
"markdown" to ElemMaker::createMarkdownElem,
"button" to ElemMaker::createButtonElem,
// "anonymous" to ElemMaker::createAnonymousElem,
// "share" to ElemMaker::createShareElem,
// "contact" to ElemMaker::createContactElem,
@ -52,25 +58,39 @@ internal object ElemMaker {
"reply" to ElemMaker::createReplyElem,
// "touch" to ElemMaker::createTouchElem,
"weather" to ElemMaker::createWeatherElem,
"json" to ElemMaker::createJsonElem,
//"forward" to MessageMaker::createForwardElem,
//"multi_msg" to MessageMaker::createLongMsgStruct,
//"bubble_face" to ElemMaker::createBubbleFaceElem,
)
operator fun get(type: String): IElemMaker? = makerArray[type]
}
private var rich = RichText()
private val elems = mutableListOf<Elem>()
private var desc = ""
fun getRich(): RichText {
rich.elements = elems
return rich
}
fun getDesc(): String {
return desc
}
private suspend fun createTextElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
) {
data.checkAndThrow("text")
val elem = Elem(
text = TextMsg(data["text"].asString)
)
return Result.success(elem)
elems.add(elem)
desc += data["text"].asString
}
private suspend fun createAtElem(
@ -78,8 +98,8 @@ internal object ElemMaker {
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
return when (chatType) {
) {
when (chatType) {
MsgConstant.KCHATTYPEGROUP -> {
data.checkAndThrow("qq")
@ -105,13 +125,12 @@ internal object ElemMaker {
peerId.toLong(),
qq,
true
)
.let {
).let {
val info = it.getOrNull()
if (info == null)
LogCenter.log("无法获取群成员信息: $qqStr", Level.ERROR)
info?.troopnick
.ifNullOrEmpty(info?.friendnick)
else info.troopnick
.ifNullOrEmpty(info.friendnick)
.ifNullOrEmpty(qqStr)
})
}
@ -126,7 +145,8 @@ internal object ElemMaker {
val elem = Elem(
text = TextMsg(str = display, attr6Buf = attr6.array())
)
Result.success(elem)
elems.add(elem)
desc += display
}
MsgConstant.KCHATTYPEC2C -> {
@ -144,10 +164,11 @@ internal object ElemMaker {
val elem = Elem(
text = TextMsg(str = display)
)
Result.success(elem)
elems.add(elem)
desc += display
}
else -> Result.failure(ActionMsgException)
else -> throw UnsupportedOperationException("Unsupported chatType($chatType) for AtMsg")
}
}
@ -156,7 +177,7 @@ internal object ElemMaker {
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
) {
data.checkAndThrow("id")
val faceId = data["id"].asInt
val elem = if (data["big"].asBooleanOrNull == true) {
@ -183,7 +204,8 @@ internal object ElemMaker {
)
)
}
return Result.success(elem)
elems.add(elem)
desc += "[表情]"
}
private suspend fun createImageElem(
@ -191,7 +213,7 @@ internal object ElemMaker {
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
) {
val isOriginal = data["original"].asBooleanOrNull ?: true
val isFlash = data["flash"].asBooleanOrNull ?: false
val filePath = data["file"].asStringOrNull
@ -307,7 +329,8 @@ internal object ElemMaker {
else -> throw LogicException("Not supported chatType($chatType) for PictureMsg")
}
return Result.success(elem)
elems.add(elem)
desc += "[图片]"
}
private suspend fun createReplyElem(
@ -315,16 +338,15 @@ internal object ElemMaker {
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
) {
data.checkAndThrow("id")
val msgHash = data["id"].asInt
val mapping = MessageHelper.getMsgMappingByHash(msgHash)
?: return Result.failure(Exception("不存在该消息映射,无法回复消息"))
?: throw Exception("不存在该消息映射,无法回复消息")
if (mapping.qqMsgId == 0L) {
// 貌似获取失败了555
LogCenter.log("无法获取被回复消息", Level.ERROR)
return Result.failure(Exception("无法获取被回复消息"))
throw Exception("无法获取被回复消息")
}
val elem = if (data.containsKey("text")) {
@ -351,16 +373,15 @@ internal object ElemMaker {
)
} else {
val msg =
MsgSvc.getMsgByQMsgId(chatType, mapping.peerId, mapping.qqMsgId).getOrNull() ?: return Result.failure(
Exception("无法获取被回复消息")
)
MsgSvc.getMsgByQMsgId(chatType, mapping.peerId, mapping.qqMsgId).getOrNull()
?: throw Exception("无法获取被回复消息")
Elem(
srcMsg = SourceMsg(
origSeqs = listOf(msg.msgSeq.toInt()),
senderUin = msg.senderUin.toULong(),
time = msg.msgTime.toULong(),
flag = 1u,
elems = messageArrayToMessageElements(
elems = messageArrayToRichText(
msg.chatType,
msg.msgId,
msg.peerUin.toString(),
@ -369,7 +390,7 @@ internal object ElemMaker {
if (msg.chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(),
msg.channelId ?: msg.peerUin.toString()
).toJson()
).second,
).getOrElse { throw Exception("解析回复消息失败: $it") }.second.elements,
type = 0u,
pbReserve = SourceMsg.Companion.PbReserve(
msgRand = Random.nextULong(),
@ -380,7 +401,8 @@ internal object ElemMaker {
)
)
}
return Result.success(elem)
elems.add(elem)
desc += "[回复消息]"
}
private suspend fun createJsonElem(
@ -388,15 +410,82 @@ internal object ElemMaker {
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
) {
data.checkAndThrow("data")
val elem = Elem(
lightApp = LightAppElem(
data = DeflateTools.compress(data.toString().toByteArray())
data = byteArrayOf(1) + DeflateTools.compress(data.toString().toByteArray())
)
)
return Result.success(elem)
elems.add(elem)
desc += "[Json消息]"
}
private suspend fun createForwardStruct(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
) {
data.checkAndThrow("id")
val resId = data["id"].asString
val filename = data["filename"].asStringOrNull ?: UUID.randomUUID().toString().uppercase()
var summary = data["summary"].asStringOrNull
val descriptions = data["desc"].asStringOrNull
var news = descriptions?.split("\n")?.map { "text" to it }
if (news == null || summary == null) {
val forwardMsg = MsgSvc.getForwardMsg(resId).getOrThrow()
if (news == null) {
news = forwardMsg.map {
"text" to it.sender.nickName + ": " + messageArrayToRichText(
obtainMessageTypeByDetailType(it.msgType),
it.qqMsgId,
it.peerId.toString(),
it.message.json
).getOrThrow().first
}
}
if (summary == null) {
summary = "查看${forwardMsg.size}条转发消息"
}
}
val json = mapOf(
"app" to "com.tencent.multimsg",
"config" to mapOf(
"autosize" to 1,
"forward" to 1,
"round" to 1,
"type" to "normal",
"width" to 300
),
"desc" to "[聊天记录]",
"extra" to mapOf(
"filename" to filename,
"tsum" to 2
).json.toString(),
"meta" to mapOf(
"detail" to mapOf(
"news" to news,
"resid" to resId,
"source" to "群聊的聊天记录",
"summary" to summary,
"uniseq" to filename
)
),
"prompt" to "[聊天记录]",
"ver" to "0.0.0.5",
"view" to "contact"
)
val elem = Elem(
lightApp = LightAppElem(
data = byteArrayOf(1) + DeflateTools.compress(json.json.toString().toByteArray())
)
)
elems.add(elem)
desc += "[聊天记录]"
}
private suspend fun createWeatherElem(
@ -404,7 +493,7 @@ internal object ElemMaker {
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
) {
var code = data["code"].asIntOrNull
if (code == null) {
@ -416,27 +505,30 @@ internal object ElemMaker {
}
if (code != null) {
WeatherSvc.fetchWeatherCard(code).onSuccess {
val weatherCard = WeatherSvc.fetchWeatherCard(code).getOrThrow()
// OidbSvc.0xdc2_34
// 00 00 00 DF 08 C2 1B 10 22 22 C4 01 0A B7 01 08 A2 E0 F2 2F 10 01 18 00 2A 02 08 01 58 FB 91 F6 AE 02 62 A1 01 08 01 52 08 E5 8C 97 E4 BA AC 20 20 5A 19 2D 33 C2 B0 2F 33 C2 B0 0A E7 A9 BA E6 B0 94 E8 B4 A8 E9 87 8F 3A E8 89 AF 62 11 5B E5 88 86 E4 BA AB 5D 20 E5 8C 97 E4 BA AC 20 20 6A 25 68 74 74 70 73 3A 2F 2F 77 65 61 74 68 65 72 2E 6D 70 2E 71 71 2E 63 6F 6D 2F 3F 73 74 3D 30 26 5F 77 76 3D 31 72 3E 68 74 74 70 73 3A 2F 2F 69 6D 67 63 61 63 68 65 2E 71 71 2E 63 6F 6D 2F 61 63 2F 71 71 77 65 61 74 68 65 72 2F 69 6D 61 67 65 2F 73 68 61 72 65 5F 69 63 6F 6E 2F 66 69 6E 65 2E 70 6E 67 12 08 08 01 10 FB 91 F6 AE 02 32 0D 61 6E 64 72 6F 69 64 20 39 2E 30 2E 38
return createJsonElem(
chatType, msgId, peerId, it["weekStore"]
.asJsonObject["share"].asJsonObject
val elem = Elem(
lightApp = LightAppElem(
data = byteArrayOf(1) + DeflateTools.compress(
weatherCard["weekStore"]
.asJsonObject["share"].asString.toByteArray()
)
}.onFailure {
LogCenter.log("无法发送天气分享", Level.ERROR)
)
)
elems.add(elem)
desc += "[天气卡片]"
} else {
throw LogicException("无法获取城市天气")
}
}
return Result.failure(ActionMsgException)
}
private suspend fun createPokeElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
) {
data.checkAndThrow("type", "id")
val elem = Elem(
commonElem = CommonElem(
@ -449,7 +541,8 @@ internal object ElemMaker {
businessType = data["id"].asInt
)
)
return Result.success(elem)
elems.add(elem)
desc += "[戳一戳]"
}
private suspend fun createNewDiceElem(
@ -457,7 +550,7 @@ internal object ElemMaker {
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
) {
val elem = Elem(
commonElem = CommonElem(
serviceType = 37,
@ -474,7 +567,8 @@ internal object ElemMaker {
businessType = 2
)
)
return Result.success(elem)
elems.add(elem)
desc += "[骰子]"
}
private suspend fun createNewRpsElem(
@ -482,7 +576,7 @@ internal object ElemMaker {
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
) {
val elem = Elem(
commonElem = CommonElem(
serviceType = 37,
@ -499,7 +593,8 @@ internal object ElemMaker {
businessType = 1
)
)
return Result.success(elem)
elems.add(elem)
desc += "[包剪锤]"
}
private suspend fun createMarkdownElem(
@ -507,7 +602,7 @@ internal object ElemMaker {
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
) {
data.checkAndThrow("content")
val elem = Elem(
commonElem = CommonElem(
@ -516,7 +611,8 @@ internal object ElemMaker {
businessType = 1
)
)
return Result.success(elem)
elems.add(elem)
desc += "[Markdown消息]"
}
private suspend fun createButtonElem(
@ -524,7 +620,7 @@ internal object ElemMaker {
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
) {
data.checkAndThrow("buttons")
val elem = Elem(
commonElem = CommonElem(
@ -565,7 +661,8 @@ internal object ElemMaker {
businessType = 1
)
)
return Result.success(elem)
elems.add(elem)
desc += "[Button消息]"
}
private fun JsonObject.checkAndThrow(vararg key: String) {

View File

@ -16,6 +16,7 @@ import kotlinx.serialization.json.JsonPrimitive
import moe.fuqiuluo.qqinterface.servlet.CardSvc
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.LbsSvc
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc
import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc
@ -51,6 +52,8 @@ import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77
import tencent.im.oidb.cmd0xdc2.oidb_cmd0xdc2
import tencent.im.oidb.oidb_sso
import java.io.File
import java.util.*
import kotlin.collections.ArrayList
import kotlin.math.roundToInt
import kotlin.random.Random
import kotlin.random.nextInt
@ -80,10 +83,11 @@ internal object NtMsgElementMaker {
"touch" to NtMsgElementMaker::createTouchElem,
"weather" to NtMsgElementMaker::createWeatherElem,
"json" to NtMsgElementMaker::createJsonElem,
"forward" to NtMsgElementMaker::createForwardStruct,
"new_dice" to NtMsgElementMaker::createNewDiceElem,
"new_rps" to NtMsgElementMaker::createNewRpsElem,
"basketball" to NtMsgElementMaker::createBasketballElem,
//"multi_msg" to MessageMaker::createLongMsgStruct,
//"multi_msg" to NtMsgElementMaker::createLongMsgStruct,
"bubble_face" to NtMsgElementMaker::createBubbleFaceElem,
"button" to NtMsgElementMaker::createInlineKeywordElem,
"inline_keyboard" to NtMsgElementMaker::createInlineKeywordElem
@ -91,6 +95,70 @@ internal object NtMsgElementMaker {
operator fun get(type: String): IMsgElementMaker? = makerMap[type]
private suspend fun createForwardStruct(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
data.checkAndThrow("id")
val resId = data["id"].asString
val filename = data["filename"].asStringOrNull ?: UUID.randomUUID().toString().uppercase()
var summary = data["summary"].asStringOrNull
val descriptions = data["desc"].asStringOrNull
var news = descriptions?.split("\n")?.map { "text" to it }
if (news == null || summary == null) {
val forwardMsg = MsgSvc.getForwardMsg(resId).getOrElse { return Result.failure(it) }
if (news == null) {
news = forwardMsg.map {
"text" to it.sender.nickName + ": " + MessageHelper.messageArrayToRichText(
MessageHelper.obtainMessageTypeByDetailType(it.msgType),
it.qqMsgId,
it.peerId.toString(),
it.message.json
).getOrThrow().first
}
}
if (summary == null) {
summary = "查看${forwardMsg.size}条转发消息"
}
}
val json = mapOf(
"app" to "com.tencent.multimsg",
"config" to mapOf(
"autosize" to 1,
"forward" to 1,
"round" to 1,
"type" to "normal",
"width" to 300
),
"desc" to "[聊天记录]",
"extra" to mapOf(
"filename" to filename,
"tsum" to 2
).json.toString(),
"meta" to mapOf(
"detail" to mapOf(
"news" to news,
"resid" to resId,
"source" to "群聊的聊天记录",
"summary" to summary,
"uniseq" to filename
)
),
"prompt" to "[聊天记录]",
"ver" to "0.0.0.5",
"view" to "contact"
)
return createJsonElem(
chatType, msgId, peerId, mapOf(
"data" to json
).json
)
}
private suspend fun createInlineKeywordElem(
chatType: Int,
msgId: Long,
@ -262,19 +330,21 @@ internal object NtMsgElementMaker {
): Result<MsgElement> {
data.checkAndThrow("data")
val jsonStr = data["data"].let {
if (it is JsonObject) it.asJsonObject.toString() else {
val str = it.asStringOrNull ?: ""
if (it is JsonObject) {
it.asJsonObject.toString()
} else {
// 检查字符串是否是合法json不然qq会闪退
try {
val str = it.asString
val element = Json.decodeFromString<JsonElement>(str)
if (element !is JsonObject) {
return Result.failure(Exception("不合法的JSON字符串"))
}
str
} catch (err: Throwable) {
LogCenter.log(err.stackTraceToString(), Level.ERROR)
return Result.failure(Exception("不合法的JSON字符串"))
}
str
}
}
val element = MsgElement()

View File

@ -6,7 +6,6 @@ import com.tencent.mobileqq.transfile.FileMsg
import com.tencent.mobileqq.transfile.api.IProtoReqManager
import com.tencent.mobileqq.transfile.protohandler.RichProto
import com.tencent.mobileqq.transfile.protohandler.RichProtoProc
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.ExperimentalSerializationApi
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
@ -22,23 +21,9 @@ import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
import moe.fuqiuluo.symbols.decodeProtobuf
import mqq.app.MobileQQ
import protobuf.auto.toByteArray
import protobuf.oidb.TrpcOidb
import protobuf.oidb.cmd0x11c5.C2CUserInfo
import protobuf.oidb.cmd0x11c5.ChannelUserInfo
import protobuf.oidb.cmd0x11c5.ClientMeta
import protobuf.oidb.cmd0x11c5.CodecConfigReq
import protobuf.oidb.cmd0x11c5.CommonHead
import protobuf.oidb.cmd0x11c5.DownloadExt
import protobuf.oidb.cmd0x11c5.DownloadReq
import protobuf.oidb.cmd0x11c5.FileInfo
import protobuf.oidb.cmd0x11c5.FileType
import protobuf.oidb.cmd0x11c5.GroupUserInfo
import protobuf.oidb.cmd0x11c5.IndexNode
import protobuf.oidb.cmd0x11c5.MultiMediaReqHead
import protobuf.oidb.cmd0x11c5.NtV2RichMediaReq
import protobuf.oidb.cmd0x11c5.NtV2RichMediaRsp
import protobuf.oidb.cmd0x11c5.SceneInfo
import protobuf.oidb.cmd0x11c5.VideoDownloadExt
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo
import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody
@ -405,8 +390,8 @@ internal object RichProtoSvc: BaseSvc() {
suspend fun getGroupPttDownUrl(
peerId: String,
md5Hex: String,
fileUUId: String
md5: ByteArray,
groupFileKey: String
): String {
return suspendCancellableCoroutine {
val runtime = AppRuntimeFetcher.appRuntime
@ -417,8 +402,8 @@ internal object RichProtoSvc: BaseSvc() {
groupPttDownReq.secondUin = peerId
groupPttDownReq.uinType = FileMsg.UIN_TROOP
groupPttDownReq.groupFileID = 0
groupPttDownReq.groupFileKey = fileUUId
groupPttDownReq.md5 = md5Hex.hex2ByteArray()
groupPttDownReq.groupFileKey = groupFileKey
groupPttDownReq.md5 = md5
groupPttDownReq.voiceType = 1
groupPttDownReq.downType = 1
richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp ->

View File

@ -21,12 +21,9 @@ import moe.fuqiuluo.qqinterface.servlet.msg.maker.NtMsgElementMaker
import moe.fuqiuluo.shamrock.helper.db.MessageDB
import moe.fuqiuluo.shamrock.helper.db.MessageMapping
import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult
import moe.fuqiuluo.shamrock.tools.EmptyJsonObject
import moe.fuqiuluo.shamrock.tools.asJsonObjectOrNull
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.json
import moe.fuqiuluo.shamrock.tools.jsonArray
import moe.fuqiuluo.shamrock.tools.*
import protobuf.message.Elem
import protobuf.message.RichText
import kotlin.coroutines.resume
import kotlin.math.abs
import kotlin.time.Duration.Companion.seconds
@ -319,38 +316,30 @@ internal object MessageHelper {
return hasActionMsg to msgList
}
suspend fun messageArrayToMessageElements(
suspend fun messageArrayToRichText(
chatType: Int,
msgId: Long,
peerId: String,
messageList: JsonArray
): Pair<Boolean, ArrayList<Elem>> {
val msgList = arrayListOf<Elem>()
var hasActionMsg = false
messageList.forEach {
val msg = it.jsonObject
): Result<Pair<String, RichText>> {
val elemMaker = ElemMaker()
messageList.forEach { element ->
val msg = element.asJsonObject
val maker = ElemMaker[msg["type"].asString]
if (maker != null) {
try {
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
maker(chatType, msgId, peerId, data).onSuccess { msgElem ->
msgList.add(msgElem)
}.onFailure {
if (it.javaClass != ActionMsgException::class.java) {
throw it
} else {
hasActionMsg = true
}
}
maker(elemMaker, chatType, msgId, peerId, data)
} catch (e: Throwable) {
if (e.javaClass != ActionMsgException::class.java) {
LogCenter.log(e.stackTraceToString(), Level.ERROR)
}
}
} else {
LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR)
return false to arrayListOf()
}
}
return hasActionMsg to msgList
return Result.success(elemMaker.getDesc() to elemMaker.getRich())
}
fun generateMsgIdHash(chatType: Int, msgId: Long): Int {

View File

@ -1,12 +1,10 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.remote.service.data.MessageDetail
import moe.fuqiuluo.shamrock.remote.service.data.GetForwardMsgResult
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.symbols.OneBotHandler
@ -21,14 +19,12 @@ internal object GetForwardMsg : IActionHandler() {
resId: String,
echo: JsonElement = EmptyJsonString
): String {
val result = MsgSvc.getMultiMsg(resId).getOrElse { return logic(it.toString(), echo) }
return ok(data = GetForwardMsgResult(result), echo = echo)
}
@Serializable
data class GetForwardMsgResult(
@SerialName("messages") val msgs: List<MessageDetail>
return ok(
data = GetForwardMsgResult(
msgs = MsgSvc.getForwardMsg(resId).getOrElse { return logic(it.toString(), echo = echo) }),
echo = echo
)
}
override val requiredParams: Array<String> = arrayOf("id")
}

View File

@ -69,6 +69,7 @@ internal object GetHistoryMsg : IActionHandler() {
time = msg.msgTime.toInt(),
msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),
msgId = msgHash,
qqMsgId = msg.msgId,
msgSeq = msg.msgSeq,
realId = msg.msgSeq,
sender = MessageSender(
@ -92,6 +93,7 @@ internal object GetHistoryMsg : IActionHandler() {
time = msg.msgTime.toInt(),
msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),
msgId = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId),
qqMsgId = msg.msgId,
msgSeq = msg.msgSeq,
realId = msg.msgSeq,
sender = MessageSender(

View File

@ -29,6 +29,7 @@ internal object GetMsg: IActionHandler() {
time = msg.msgTime.toInt(),
msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),
msgId = msgHash,
qqMsgId = msg.msgId,
msgSeq = msg.msgSeq,
realId = msg.msgSeq,
sender = MessageSender(

View File

@ -3,20 +3,14 @@ package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import kotlinx.serialization.json.*
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.qqinterface.servlet.msg.toJson
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.ParamsException
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.remote.service.data.ForwardMessageResult
import moe.fuqiuluo.shamrock.remote.service.data.SendForwardMessageResult
import moe.fuqiuluo.shamrock.tools.*
import moe.fuqiuluo.symbols.OneBotHandler
import protobuf.message.*
import java.util.*
import kotlin.random.Random
@OneBotHandler("send_forward_msg")
internal object SendForwardMessage : IActionHandler() {
@ -60,9 +54,10 @@ internal object SendForwardMessage : IActionHandler() {
else -> error("unknown chat type: $chatType")
}.toString()
val retryCnt = session.getIntOrNull("retry_cnt") ?: 5
return if (session.isArray("messages")) {
val messages = session.getArray("messages")
invoke(chatType, peerId, messages, fromId, echo = session.echo)
invoke(chatType, peerId, fromId, messages, retryCnt, session.echo)
} else {
logic("未知格式合并转发消息", session.echo)
}
@ -76,222 +71,22 @@ internal object SendForwardMessage : IActionHandler() {
suspend operator fun invoke(
chatType: Int,
peerId: String,
messages: JsonArray,
fromId: String = peerId,
messages: JsonArray,
retryCnt: Int,
echo: JsonElement = EmptyJsonString
): String {
var uid: String? = null
var groupUin: String? = null
var i = -1
val desc = MutableList(messages.size) { "" }
val msgs = messages.map { msg ->
kotlin.runCatching {
val data = msg.asJsonObject["data"].asJsonObject
if (data.containsKey("id")) {
val record = MsgSvc.getMsg(data["id"].asInt).getOrElse {
error("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it")
}
if (record.chatType == MsgConstant.KCHATTYPEGROUP) groupUin = record.peerUin.toString()
if (record.chatType == MsgConstant.KCHATTYPEC2C) uid = record.peerUid
PushMsgBody(
msgHead = ResponseHead(
peerUid = record.senderUid,
receiverUid = record.peerUid,
forward = ResponseForward(
friendName = record.sendNickName
),
responseGrp = if (record.chatType == MsgConstant.KCHATTYPEGROUP) ResponseGrp(
groupCode = record.peerUin.toULong(),
memberCard = record.sendMemberName,
u1 = 2
) else null
),
contentHead = ContentHead(
msgType = when (record.chatType) {
MsgConstant.KCHATTYPEC2C -> 9
MsgConstant.KCHATTYPEGROUP -> 82
else -> throw UnsupportedOperationException(
"Unsupported chatType: $chatType"
)
},
msgSubType = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
divSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
msgViaRandom = record.msgId,
sequence = record.msgSeq, // idk what this is(i++)
msgTime = record.msgTime,
u2 = 1,
u6 = 0,
u7 = 0,
msgSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) record.msgSeq else null, // seq for dm
forwardHead = ForwardHead(
u1 = 0,
u2 = 0,
u3 = 0,
ub641 = "",
avatar = ""
)
),
body = MsgBody(
richText = RichText(
elements = MessageHelper.messageArrayToMessageElements(
record.chatType,
record.msgId,
record.peerUin.toString(),
record.elements.toSegments(
record.chatType,
record.peerUin.toString(),
"0"
).also {
desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": "
}.map {
desc[i] += when (it.type) {
"text" -> it.data["text"] as String
"at" -> "@${it.data["name"] as String? ?: it.data["qq"] as String}"
"face" -> "[表情]"
"pic", "image" -> "[图片]"
"voice", "record" -> "[语音]"
"video" -> "[视频]"
"node" -> "[合并转发消息]"
"markdown" -> "[Markdown消息]"
"button" -> "[Button类型]"
else -> "[未知消息类型]"
}
it.toJson()
}.json
).also {
if (it.second.isEmpty() && !it.first)
error("消息合成失败,请查看日志或者检查输入。")
}.second
)
)
)
} else if (data.containsKey("content")) {
PushMsgBody(
msgHead = ResponseHead(
peer = data["uin"]?.asLong ?: TicketSvc.getUin().toLong(),
peerUid = data["uid"]?.asString ?: TicketSvc.getUid(),
receiverUid = TicketSvc.getUid(),
forward = ResponseForward(
friendName = data["name"]?.asStringOrNull ?: TicketSvc.getNickname()
)
),
contentHead = ContentHead(
msgType = 9,
msgSubType = 175,
divSeq = 175,
msgViaRandom = Random.nextLong(),
sequence = data["seq"]?.asLong ?: Random.nextLong(),
msgTime = data["time"]?.asLong ?: (System.currentTimeMillis() / 1000),
u2 = 1,
u6 = 0,
u7 = 0,
msgSeq = data["seq"]?.asLong ?: Random.nextLong(),
forwardHead = ForwardHead(
u1 = 0,
u2 = 0,
u3 = 2,
ub641 = "",
avatar = ""
)
),
body = MsgBody(
richText = RichText(
elements = MessageHelper.messageArrayToMessageElements(
chatType = chatType,
msgId = Random.nextLong(),
peerId = data["uin"]?.asString ?: TicketSvc.getUin(),
messageList = when (data["content"]) {
is JsonObject -> listOf(data["content"] as JsonObject).json
is JsonArray -> data["content"] as JsonArray
else -> MessageHelper.decodeCQCode(data["content"].asString)
}.also {
desc[++i] =
(data["name"].asStringOrNull ?: data["uin"].asStringOrNull
?: TicketSvc.getNickname()) + ": "
}.onEach {
val type = it.asJsonObject["type"].asString
val itData = it.asJsonObject["data"].asJsonObject
desc[i] += when (type) {
"text" -> itData["text"].asString
"at" -> "@${itData["name"].asStringOrNull ?: itData["qq"].asString}"
"face" -> "[表情]"
"pic", "image" -> "[图片]"
"voice", "record" -> "[语音]"
"video" -> "[视频]"
"node" -> "[合并转发消息]"
"markdown" -> "[Markdown消息]"
"button" -> "[Button类型]"
else -> "[未知消息类型]"
}
}
).also {
if (it.second.isEmpty() && !it.first)
error("消息合成失败,请查看日志或者检查输入。")
}.second
)
)
)
} else {
error("消息节点缺少id或content字段")
}
}.getOrElse {
LogCenter.log("消息节点解析失败:$it", Level.WARN)
null
}
}.filterNotNull().ifEmpty { return logic("消息节点为空", echo) }
kotlin.runCatching {
val resid = MsgSvc.uploadMultiMsg(uid ?: TicketSvc.getUid(), groupUin, msgs)
val message = MsgSvc.uploadMultiMsg(chatType, peerId, fromId, messages, retryCnt)
.getOrElse { return logic(it.message ?: "", echo) }
val uniseq = UUID.randomUUID().toString().uppercase()
val result = MsgSvc.sendToAio(
chatType, peerId,
listOf(
mapOf(
"type" to "json",
"data" to mapOf(
"data" to mapOf(
"app" to "com.tencent.multimsg",
"config" to mapOf(
"autosize" to 1,
"forward" to 1,
"round" to 1,
"type" to "normal",
"width" to 300
),
"desc" to "[聊天记录]",
"extra" to mapOf(
"filename" to uniseq,
"tsum" to 2
).json.toString(),
"meta" to mapOf(
"detail" to mapOf(
"news" to desc.slice(0..if (i < 3) i else 3)
.map { mapOf("text" to it) },
"resid" to resid,
"source" to "群聊的聊天记录",
"summary" to "查看${msgs.size}条转发消息",
"uniseq" to uniseq
)
),
"prompt" to "[聊天记录]",
"ver" to "0.0.0.5",
"view" to "contact"
),
"resid" to resid
)
)
).json, fromId, 3
).getOrElse { return logic(it.message ?: "", echo) }
val result = MsgSvc.sendToAio(chatType, peerId, listOf(message).toJson(), fromId, retryCnt)
.getOrElse { return logic(it.message ?: "", echo) }
return ok(
ForwardMessageResult(
SendForwardMessageResult(
msgId = result.msgHashId,
forwardId = resid
resId = message.data["id"] as String
), echo = echo
)
}.onFailure {

View File

@ -6,12 +6,19 @@ import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.symbols.OneBotHandler
@OneBotHandler("send_group_forward_msg")
internal object SendGroupForwardMessage: IActionHandler() {
internal object SendGroupForwardMessage : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getLong("group_id")
val retryCnt = session.getIntOrNull("retry_cnt") ?: 5
return if (session.isArray("messages")) {
val messages = session.getArray("messages")
SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId.toString(), messages, echo = session.echo)
SendForwardMessage(
MsgConstant.KCHATTYPEGROUP,
groupId.toString(),
messages = messages,
retryCnt = retryCnt,
echo = session.echo
)
} else {
logic("未知格式合并转发消息", session.echo)
}

View File

@ -21,7 +21,7 @@ import moe.fuqiuluo.shamrock.tools.jsonArray
import moe.fuqiuluo.symbols.OneBotHandler
@OneBotHandler("send_msg", ["send_message"])
internal object SendMessage: IActionHandler() {
internal object SendMessage : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val detailType = session.getStringOrNull("detail_type") ?: session.getStringOrNull("message_type")
try {
@ -40,28 +40,61 @@ internal object SendMessage: IActionHandler() {
return noParam("detail_type/message_type", session.echo)
}
}
val peerId = when(chatType) {
MsgConstant.KCHATTYPEGROUP -> session.getLongOrNull("group_id") ?: return noParam("group_id", session.echo)
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("user_id") ?: return noParam("user_id", session.echo)
val peerId = when (chatType) {
MsgConstant.KCHATTYPEGROUP -> session.getLongOrNull("group_id") ?: return noParam(
"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)
val fromId = when (chatType) {
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getLongOrNull("group_id")
?: return noParam("group_id", session.echo)
MsgConstant.KCHATTYPEC2C -> session.getLongOrNull("user_id") ?: return noParam("user_id", session.echo)
else -> error("unknown chat type: $chatType")
}.toString()
val retryCnt = session.getIntOrNull("retry_cnt")
val retryCnt = session.getIntOrNull("retry_cnt") ?: 5
val recallDuration = session.getLongOrNull("recall_duration")
return if (session.isString("message")) {
val autoEscape = session.getBooleanOrDefault("auto_escape", false)
val message = session.getString("message")
invoke(chatType, peerId, message, autoEscape, echo = session.echo, fromId = fromId, retryCnt = retryCnt ?: 5, recallDuration = recallDuration)
invoke(
chatType,
peerId,
message,
autoEscape,
echo = session.echo,
fromId = fromId,
retryCnt = retryCnt,
recallDuration = recallDuration
)
} else if (session.isArray("message")) {
val message = session.getArray("message")
invoke(chatType, peerId, message, session.echo, fromId = fromId, retryCnt ?: 5, recallDuration = recallDuration)
invoke(
chatType,
peerId,
message,
session.echo,
fromId = fromId,
retryCnt = retryCnt,
recallDuration = recallDuration
)
} else {
val message = session.getObject("message")
invoke(chatType, peerId, listOf( message ).jsonArray, session.echo, fromId = fromId, retryCnt ?: 5, recallDuration = recallDuration)
invoke(
chatType,
peerId,
listOf(message).jsonArray,
session.echo,
fromId = fromId,
retryCnt = retryCnt,
recallDuration = recallDuration
)
}
} catch (e: ParamsException) {
return noParam(e.message!!, session.echo)
@ -82,14 +115,16 @@ internal object SendMessage: IActionHandler() {
echo: JsonElement = EmptyJsonString
): String {
val result = if (autoEscape) {
MsgSvc.sendToAio(chatType, peerId, listOf(
MsgSvc.sendToAio(
chatType, peerId, listOf(
mapOf(
"type" to "text",
"data" to mapOf(
"text" to message
)
)
).json, fromId = fromId, retryCnt)
).json, fromId = fromId, retryCnt
)
} else {
val msg = MessageHelper.decodeCQCode(message)
if (msg.isEmpty()) {
@ -98,44 +133,54 @@ internal object SendMessage: IActionHandler() {
} else {
MsgSvc.sendToAio(chatType, peerId, msg, fromId = fromId, retryCnt)
}
}
if (result.isFailure) {
return logic(result.exceptionOrNull()?.message ?: "", echo)
}
val sendMsgResult = result.getOrThrow()
if (sendMsgResult.msgHashId <= 0) {
}.getOrElse{ return logic(it.message ?: "", echo)}
if (result.msgHashId <= 0) {
return logic("send message failed", echo = echo)
}
recallDuration?.let { autoRecall(sendMsgResult.msgHashId, it) }
return ok(MessageResult(
msgId = sendMsgResult.msgHashId,
time = (sendMsgResult.msgTime * 0.001).toLong()
), echo = echo)
if (recallDuration != null) {
GlobalScope.launch(Dispatchers.Default) {
delay(recallDuration)
MsgSvc.recallMsg(result.msgHashId)
}
}
return ok(
MessageResult(
msgId = result.msgHashId,
time = (result.msgTime * 0.001).toLong()
), echo = echo
)
}
// 消息段格式消息
suspend operator fun invoke(
chatType: Int, peerId: String, message: JsonArray, echo: JsonElement = EmptyJsonString, fromId: String = peerId, retryCnt: Int, recallDuration: Long?,
chatType: Int,
peerId: String,
message: JsonArray,
echo: JsonElement = EmptyJsonString,
fromId: String = peerId,
retryCnt: Int,
recallDuration: Long?,
): String {
//if (!ContactHelper.checkContactAvailable(chatType, peerId)) {
// return logic("contact is not found", echo = echo)
//}
val result = MsgSvc.sendToAio(chatType, peerId, message, fromId = fromId, retryCnt).getOrElse { return logic(it.message ?: "", echo) }
val result = MsgSvc.sendToAio(chatType, peerId, message, fromId, retryCnt)
.getOrElse { return logic(it.message ?: "", echo) }
if (result.msgHashId <= 0) {
return logic("send message failed", echo = echo)
}
recallDuration?.let { autoRecall(result.msgHashId, it) }
return ok(MessageResult(
if (recallDuration != null) {
GlobalScope.launch(Dispatchers.Default) {
delay(recallDuration)
MsgSvc.recallMsg(result.msgHashId)
}
}
return ok(
MessageResult(
msgId = result.msgHashId,
time = (result.msgTime * 0.001).toLong()
), echo)
}
private fun autoRecall(msgHash: Int, duration: Long) {
GlobalScope.launch(Dispatchers.Default) {
delay(duration)
MsgSvc.recallMsg(msgHash)
}
), echo
)
}
override val requiredParams: Array<String> = arrayOf("message")

View File

@ -9,9 +9,18 @@ import moe.fuqiuluo.symbols.OneBotHandler
internal object SendPrivateForwardMessage : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val userId = session.getLong("user_id")
val groupId = session.getLongOrNull("group_id")
val retryCnt = session.getIntOrNull("retry_cnt") ?: 5
return if (session.isArray("messages")) {
val messages = session.getArray("messages")
SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId.toString(), messages, echo = session.echo)
SendForwardMessage(
MsgConstant.KCHATTYPEC2C,
userId.toString(),
groupId?.toString() ?: userId.toString(),
messages,
retryCnt,
echo = session.echo
)
} else {
logic("未知格式合并转发消息", session.echo)
}

View File

@ -0,0 +1,97 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import kotlinx.serialization.json.*
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.ParamsException
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.remote.service.data.UploadForwardMessageResult
import moe.fuqiuluo.shamrock.tools.*
import moe.fuqiuluo.symbols.OneBotHandler
@OneBotHandler("upload_multi_message")
internal object UploadMultiMessage : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val detailType = session.getStringOrNull("detail_type") ?: session.getStringOrNull("message_type")
try {
val chatType = detailType?.let {
MessageHelper.obtainMessageTypeByDetailType(it)
} ?: run {
if (session.has("user_id")) {
if (session.has("group_id")) {
MsgConstant.KCHATTYPETEMPC2CFROMGROUP
} else {
MsgConstant.KCHATTYPEC2C
}
} else if (session.has("group_id")) {
MsgConstant.KCHATTYPEGROUP
} else {
return noParam("detail_type/message_type", session.echo)
}
}
val peerId = when (chatType) {
MsgConstant.KCHATTYPEGROUP -> session.getLongOrNull("group_id") ?: return noParam(
"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)
MsgConstant.KCHATTYPEC2C -> session.getLongOrNull("user_id") ?: return noParam(
"user_id",
session.echo
)
else -> error("unknown chat type: $chatType")
}.toString()
val retryCnt = session.getIntOrNull("retry_cnt") ?: 5
return if (session.isArray("messages")) {
val messages = session.getArray("messages")
invoke(chatType, peerId, fromId, messages, retryCnt, echo = session.echo)
} else {
logic("未知格式合并转发消息", session.echo)
}
} catch (e: ParamsException) {
return noParam(e.message!!, session.echo)
} catch (e: Throwable) {
return logic(e.message ?: e.toString(), session.echo)
}
}
suspend operator fun invoke(
chatType: Int,
peerId: String,
fromId: String = peerId,
messages: JsonArray,
retryCnt: Int,
echo: JsonElement = EmptyJsonString
): String {
kotlin.runCatching {
val message = MsgSvc.uploadMultiMsg(chatType, peerId, fromId, messages, retryCnt)
.getOrElse { return logic(it.message ?: "", echo) }
return ok(
UploadForwardMessageResult(
resId = message.data["id"] as String,
filename = message.data["filename"] as String,
summary = message.data["summary"] as String,
desc = message.data["desc"] as String
), echo = echo
)
}.onFailure {
return error("合并转发消息失败: $it", echo)
}
return logic("合并转发消息失败(unknown error)", echo)
}
override val requiredParams: Array<String> = arrayOf("messages")
}

View File

@ -44,49 +44,6 @@ fun Routing.messageAction() {
}
}
route("/send_group_forward_(msg|message)".toRegex()) {
post {
val groupId = fetchPostOrNull("group_id")
val messages = fetchPostJsonArray("messages")
call.respondText(
SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId ?: "", messages),
ContentType.Application.Json
)
}
get {
respond(false, Status.InternalHandlerError, "Not support GET method")
}
}
route("/send_private_forward_(msg|message)".toRegex()) {
post {
val userId = fetchPostOrNull("user_id")
val messages = fetchPostJsonArray("messages")
call.respondText(
SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId ?: "", messages),
ContentType.Application.Json
)
}
get {
respond(false, Status.InternalHandlerError, "Not support GET method")
}
}
route("/send_forward_(msg|message)".toRegex()) {
post {
val userId = fetchPostOrNull("user_id")
val groupId = fetchPostOrNull("group_id")
val messages = fetchPostJsonArray("messages")
call.respondText(
SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId ?: groupId ?: "", messages),
ContentType.Application.Json
)
}
get {
respond(false, Status.InternalHandlerError, "Not support GET method")
}
}
getOrPost("/get_forward_msg") {
val id = fetchOrThrow("id")
call.respondText(GetForwardMsg(id), ContentType.Application.Json)
@ -144,7 +101,7 @@ fun Routing.messageAction() {
get {
val msgType = fetchGetOrThrow("message_type")
val message = fetchGetOrThrow("message")
val retryCnt = fetchGetOrNull("retry_cnt")?.toInt() ?: 3
val retryCnt = fetchGetOrNull("retry_cnt")?.toInt() ?: 5
val autoEscape = fetchGetOrNull("auto_escape")?.toBooleanStrict() ?: false
val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType)
@ -288,11 +245,11 @@ fun Routing.messageAction() {
post {
val userId = fetchPostOrThrow("user_id")
val groupId = fetchPostOrNull("group_id")
val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 3
val autoEscape = fetchPostOrNull("auto_escape")?.toBooleanStrict() ?: false
val chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP
val fromId = groupId ?: userId
val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 3
val autoEscape = fetchPostOrNull("auto_escape")?.toBooleanStrict() ?: false
val recallDuration = fetchPostOrNull("recall_duration")?.toLongOrNull()
val result = if (isJsonData()) {
@ -302,8 +259,9 @@ fun Routing.messageAction() {
peerId = userId,
message = fetchPostJsonString("message"),
autoEscape = autoEscape,
fromId = fromId,
retryCnt = retryCnt, recallDuration = recallDuration
fromId = groupId ?: userId,
retryCnt = retryCnt,
recallDuration = recallDuration
)
} else {
SendMessage(
@ -312,7 +270,7 @@ fun Routing.messageAction() {
message = if (isJsonObject("message")) listOf(fetchPostJsonObject("message")).jsonArray else fetchPostJsonArray(
"message"
),
fromId = groupId ?: userId ?: "",
fromId = groupId ?: userId,
retryCnt = retryCnt,
recallDuration = recallDuration
)
@ -323,12 +281,96 @@ fun Routing.messageAction() {
peerId = userId,
message = fetchPostOrThrow("message"),
autoEscape = autoEscape,
fromId = fromId,
retryCnt = retryCnt, recallDuration = recallDuration
fromId = groupId ?: userId,
retryCnt = retryCnt,
recallDuration = recallDuration
)
}
call.respondText(result, ContentType.Application.Json)
}
}
route("/upload_multi_(msg|message)".toRegex()) {
post {
val msgType = fetchPostOrThrow("message_type")
val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType)
val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5
val userId = fetchPostOrNull("user_id")
val groupId = fetchPostOrNull("group_id")
val messages = fetchPostJsonArray("messages")
call.respondText(
UploadMultiMessage(
chatType,
if (chatType == MsgConstant.KCHATTYPEC2C) userId!! else groupId!!,
groupId ?: userId ?: "",
messages,
retryCnt
),
ContentType.Application.Json
)
}
get {
respond(false, Status.InternalHandlerError, "Not support GET method")
}
}
route("/send_forward_(msg|message)".toRegex()) {
post {
val msgType = fetchPostOrThrow("message_type")
val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType)
val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5
val userId = fetchPostOrNull("user_id")
val groupId = fetchPostOrNull("group_id")
val messages = fetchPostJsonArray("messages")
call.respondText(
SendForwardMessage(
chatType,
if (chatType == MsgConstant.KCHATTYPEC2C) userId!! else groupId!!,
groupId ?: userId ?: "",
messages,
retryCnt
),
ContentType.Application.Json
)
}
get {
respond(false, Status.InternalHandlerError, "Not support GET method")
}
}
route("/send_private_forward_(msg|message)".toRegex()) {
post {
val userId = fetchPostOrThrow("user_id")
val groupId = fetchPostOrNull("group_id")
val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5
val messages = fetchPostJsonArray("messages")
call.respondText(
SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId, groupId ?: userId, messages, retryCnt),
ContentType.Application.Json
)
}
get {
respond(false, Status.InternalHandlerError, "Not support GET method")
}
}
route("/send_group_forward_(msg|message)".toRegex()) {
post {
val groupId = fetchPostOrThrow("group_id")
val retryCnt = fetchPostOrNull("retry_cnt")?.toInt() ?: 5
val messages = fetchPostJsonArray("messages")
call.respondText(
SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId, messages = messages, retryCnt = retryCnt),
ContentType.Application.Json
)
}
get {
respond(false, Status.InternalHandlerError, "Not support GET method")
}
}
}

View File

@ -9,10 +9,20 @@ internal data class MessageResult(
@SerialName("message_id") val msgId: Int,
@SerialName("time") val time: Long
)
@Serializable
internal data class ForwardMessageResult(
internal data class UploadForwardMessageResult(
@SerialName("res_id") val resId: String,
@SerialName("filename") val filename: String,
@SerialName("summary") val summary: String,
@SerialName("desc") val desc: String,
)
@Serializable
internal data class SendForwardMessageResult(
@SerialName("message_id") val msgId: Int,
@SerialName("forward_id") val forwardId: String
@SerialName("res_id") val resId: String,
@SerialName("forward_id") val forwardId: String = resId
)
@Serializable
@ -20,6 +30,7 @@ internal data class MessageDetail(
@SerialName("time") val time: Int,
@SerialName("message_type") val msgType: String,
@SerialName("message_id") val msgId: Int,
@SerialName("message_id_qq") val qqMsgId: Long,
@SerialName("message_seq") val msgSeq: Long,
@SerialName("real_id") val realId: Long,
@SerialName("sender") val sender: MessageSender,
@ -29,6 +40,11 @@ internal data class MessageDetail(
@SerialName("target_id") val targetId: Long = 0,
)
@Serializable
internal data class GetForwardMsgResult(
@SerialName("messages") val msgs: List<MessageDetail>
)
@Serializable
internal data class MessageSender(
@SerialName("user_id") val userId: Long,

View File

@ -69,7 +69,6 @@ internal object PrimitiveListener {
528 -> when (subType) {
35 -> onFriendApply(msgTime, push.clientInfo!!, body)
39 -> onCardChange(msgTime, body)
// invite
68 -> onGroupApply(msgTime, contentHead, body)
138 -> onC2CRecall(msgTime, body)
290 -> onC2CPoke(msgTime, body)
@ -79,7 +78,7 @@ internal object PrimitiveListener {
12 -> onGroupBan(msgTime, body)
16 -> onGroupUniqueTitleChange(msgTime, body)
17 -> onGroupRecall(msgTime, body)
20 -> onGroupPokeAndGroupSign(msgTime, body)
20 -> onGroupCommonTips(msgTime, body)
21 -> onEssenceMessage(msgTime, push.clientInfo, body)
}
}
@ -281,7 +280,7 @@ internal object PrimitiveListener {
}
private suspend fun onGroupPokeAndGroupSign(time: Long, body: MsgBody) {
private suspend fun onGroupCommonTips(time: Long, body: MsgBody) {
val event = runCatching {
body.msgContent!!.decodeProtobuf<GroupCommonTipsEvent>()
}.getOrElse {

View File

@ -52,6 +52,7 @@ val Collection<Any?>.json: JsonArray
is Number -> arrayList.add(it.json)
is String -> arrayList.add(it.json)
is Boolean -> arrayList.add(it.json)
is Pair<*, *> -> arrayList.add(mapOf(it as Pair<String, Any?>).json)
is Map<*, *> -> arrayList.add((it as Map<String, Any?>).json)
is Collection<*> -> arrayList.add((it as Collection<Any?>).json)
else -> error("unknown array type: ${it::class.java}")