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( data class Ptt(
@ProtoNumber(1) var fileType: UInt?=null, @ProtoNumber(1) var fileType: UInt?=null,
@ProtoNumber(2) var srcUin: ULong?=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(4) var fileMd5: ByteArray?=null,
@ProtoNumber(5) var fileName: ByteArray?=null, @ProtoNumber(5) var fileName: String?=null,
@ProtoNumber(6) var fileSize: UInt?=null, @ProtoNumber(6) var fileSize: UInt?=null,
@ProtoNumber(7) var reserve: ByteArray?=null, @ProtoNumber(7) var reserve: ByteArray?=null,
@ProtoNumber(8) var fileId: UInt?=null, @ProtoNumber(8) var fileId: UInt?=null,
@ -22,11 +22,19 @@ data class Ptt(
@ProtoNumber(15) var magicPttIndex: UInt?=null, @ProtoNumber(15) var magicPttIndex: UInt?=null,
@ProtoNumber(16) var voiceSwitch: UInt?=null, @ProtoNumber(16) var voiceSwitch: UInt?=null,
@ProtoNumber(17) var pttUrl: ByteArray?=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(19) var time: UInt?=null,
@ProtoNumber(20) var downPara: ByteArray?=null, @ProtoNumber(20) var downPara: ByteArray?=null,
@ProtoNumber(29) var format: UInt?=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(31) var rptPttUrls: List<String>? = null,
@ProtoNumber(32) var downloadFlag: UInt?=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 @Serializable
data class RichText( data class RichText(
@ProtoNumber(1) val attr: Attr? = null, @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(3) val not_online_file: NotOnlineFile? = null,
@ProtoNumber(4) val ptt: Ptt? = null, @ProtoNumber(4) val ptt: Ptt? = null,
@ProtoNumber(5) val tmp_ptt: TmpPtt? = 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.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.JsonArray 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.toListMap
import moe.fuqiuluo.qqinterface.servlet.msg.toSegments import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
import moe.fuqiuluo.shamrock.helper.ContactHelper 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.shamrock.xposed.helper.msgService
import moe.fuqiuluo.symbols.decodeProtobuf import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.auto.toByteArray import protobuf.auto.toByteArray
import protobuf.message.PushMsgBody import protobuf.message.*
import protobuf.message.longmsg.* import protobuf.message.longmsg.*
import java.util.*
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import kotlin.random.Random
internal object MsgSvc : BaseSvc() { internal object MsgSvc : BaseSvc() {
private suspend fun prepareTempChatFromGroup( private suspend fun prepareTempChatFromGroup(
@ -207,45 +211,195 @@ internal object MsgSvc : BaseSvc() {
} }
val result = val result =
MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0)) MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0))
if (result.isFailure) { .getOrElse { return Result.failure(it) }
LogCenter.log("sendToAio: " + result.exceptionOrNull()?.stackTraceToString(), Level.ERROR) return if (result.isTimeout) {
return result
}
val sendResult = result.getOrThrow()
return if (sendResult.isTimeout) {
// 发送失败,可能网络问题出现红色感叹号,重试 // 发送失败,可能网络问题出现红色感叹号,重试
// 例如 rich media transfer failed // 例如 rich media transfer failed
delay(100) delay(100)
MessageHelper.resendMsg(chatType, peedId, fromId, sendResult.qqMsgId, retryCnt, sendResult.msgHashId) MessageHelper.resendMsg(chatType, peedId, fromId, result.qqMsgId, retryCnt, result.msgHashId)
} else { } else {
result Result.success(result)
} }
} }
suspend fun uploadMultiMsg( suspend fun uploadMultiMsg(
uid: String, chatType: Int,
groupUin: String?, peerId: String,
messages: List<PushMsgBody>, fromId: String,
): Result<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( val payload = LongMsgPayload(
action = listOf( action = mutableListOf(
LongMsgAction( LongMsgAction(
command = "MultiMsg", command = "MultiMsg",
data = LongMsgContent( 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) LogCenter.log(payload.toByteArray().toHexString(), Level.DEBUG)
val req = LongMsgReq( val req = LongMsgReq(
sendInfo = SendLongMsgInfo( sendInfo = when (chatType) {
type = if (groupUin == null) 1 else 3, MsgConstant.KCHATTYPEC2C -> SendLongMsgInfo(
uid = LongMsgUid(groupUin ?: uid), type = 1,
groupUin = groupUin?.toInt(), uid = LongMsgUid(peerId),
payload = DeflateTools.gzip(payload.toByteArray()) 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( setting = LongMsgSettings(
field1 = 4, field1 = 4,
field2 = 2, field2 = 2,
@ -253,17 +407,29 @@ internal object MsgSvc : BaseSvc() {
field4 = 0 field4 = 0
) )
) )
val buffer = sendBufferAW( val buffer = sendBufferAW(
"trpc.group.long_msg_interface.MsgService.SsoSendLongMsg", "trpc.group.long_msg_interface.MsgService.SsoSendLongMsg",
true, true,
req.toByteArray() req.toByteArray()
) ?: return Result.failure(Exception("unable to upload multi message")) ) ?: return Result.failure(Exception("unable to upload multi message"))
val rsp = buffer.slice(4).decodeProtobuf<LongMsgRsp>() val rsp = buffer.slice(4).decodeProtobuf<LongMsgRsp>()
return rsp.sendResult?.resId?.let { Result.success(it) } val resId = rsp.sendResult?.resId ?: return Result.failure(Exception("unable to upload multi message"))
?: 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( val req = LongMsgReq(
recvInfo = RecvLongMsgInfo( recvInfo = RecvLongMsgInfo(
uid = LongMsgUid(TicketSvc.getUid()), uid = LongMsgUid(TicketSvc.getUid()),
@ -284,11 +450,18 @@ internal object MsgSvc : BaseSvc() {
) ?: return Result.failure(Exception("unable to get multi message")) ) ?: return Result.failure(Exception("unable to get multi message"))
val rsp = buffer.slice(4).decodeProtobuf<LongMsgRsp>() val rsp = buffer.slice(4).decodeProtobuf<LongMsgRsp>()
val zippedPayload = DeflateTools.ungzip( 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) LogCenter.log(zippedPayload.toHexString(), Level.DEBUG)
val payload = zippedPayload.decodeProtobuf<LongMsgPayload>() return Result.success(
payload.action?.forEach { 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") { if (it.command == "MultiMsg") {
return Result.success(it.data?.body?.map { msg -> return Result.success(it.data?.body?.map { msg ->
val chatType = val chatType =
@ -296,27 +469,29 @@ internal object MsgSvc : BaseSvc() {
MessageDetail( MessageDetail(
time = msg.contentHead?.msgTime?.toInt() ?: 0, time = msg.contentHead?.msgTime?.toInt() ?: 0,
msgType = MessageHelper.obtainDetailTypeByMsgType(chatType), msgType = MessageHelper.obtainDetailTypeByMsgType(chatType),
msgId = 0, // MessageHelper.generateMsgIdHash(chatType, msg.content!!.msgViaRandom), msgViaRandom 为空 msgId = 0, // msgViaRandom为空 tx不给
qqMsgId = 0,
msgSeq = msg.contentHead!!.msgSeq ?: 0, msgSeq = msg.contentHead!!.msgSeq ?: 0,
realId = msg.contentHead!!.msgSeq ?: 0, realId = msg.contentHead!!.msgSeq ?: 0,
sender = MessageSender( sender = MessageSender(
msg.msgHead?.peer ?: 0, msg.msgHead?.peer ?: 0,
msg.msgHead?.responseGrp?.memberCard?.ifEmpty { msg.msgHead?.forward?.friendName } msg.msgHead?.responseGrp?.memberCard ?: msg.msgHead?.forward?.friendName ?: "",
?: msg.msgHead?.forward?.friendName ?: "",
"unknown", "unknown",
0, 0,
msg.msgHead?.peerUid ?: "", msg.msgHead?.peerUid ?: "",
msg.msgHead?.peerUid ?: "" msg.msgHead?.peerUid ?: ""
), ),
message = msg.body?.richText?.elements?.toSegments(chatType, msg.msgHead?.peer.toString(), "0") message = msg.body?.richText?.toSegments(
?.toListMap() ?: emptyList(), chatType,
msg.msgHead?.peer.toString(),
"0"
)?.toListMap() ?: emptyList(),
peerId = msg.msgHead?.peer ?: 0, peerId = msg.msgHead?.peer ?: 0,
groupId = if (chatType == MsgConstant.KCHATTYPEGROUP) msg.msgHead?.responseGrp?.groupCode?.toLong() groupId = if (chatType == MsgConstant.KCHATTYPEGROUP) msg.msgHead?.responseGrp?.groupCode?.toLong()
?: 0 else 0, ?: 0 else 0,
targetId = if (chatType != MsgConstant.KCHATTYPEGROUP) msg.msgHead?.peer ?: 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")) 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.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.qqinterface.servlet.msg.MessageTempHandler import moe.fuqiuluo.qqinterface.servlet.msg.MessageTempHandler
import moe.fuqiuluo.shamrock.remote.action.handlers.GetHistoryMsg 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.tools.broadcast
import moe.fuqiuluo.shamrock.utils.DeflateTools 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 mqq.app.MobileQQ
import protobuf.auto.toByteArray import protobuf.auto.toByteArray
import protobuf.message.*
import protobuf.message.element.LightAppElem
import protobuf.push.MessagePush
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.text.toByteArray
internal object PacketSvc: BaseSvc() { internal object PacketSvc : BaseSvc() {
/** /**
* 伪造收到Json卡片消息 * 伪造收到Json卡片消息
*/ */
@ -36,7 +30,7 @@ internal object PacketSvc: BaseSvc() {
listOf( listOf(
Elem( Elem(
lightApp = LightAppElem((byteArrayOf(1) + DeflateTools.compress(content.toByteArray()))) lightApp = LightAppElem((byteArrayOf(1) + DeflateTools.compress(content.toByteArray())))
) )
) )
} }
} }
@ -44,7 +38,12 @@ internal object PacketSvc: BaseSvc() {
private suspend fun fakeReceiveSelfMsg(msgService: IKernelMsgService, builder: () -> List<Elem>): Long { private suspend fun fakeReceiveSelfMsg(msgService: IKernelMsgService, builder: () -> List<Elem>): Long {
val latestMsg = withTimeoutOrNull(3000) { val latestMsg = withTimeoutOrNull(3000) {
suspendCancellableCoroutine { 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)) it.resume(GetHistoryMsg.GetMsgResult(code, why, msgs))
} }
} }
@ -72,9 +71,11 @@ internal object PacketSvc: BaseSvc() {
u4 = msgSeq - 2, u4 = msgSeq - 2,
u5 = msgSeq u5 = msgSeq
), ),
body = MsgBody(RichText( body = MsgBody(
elements = builder() RichText(
)) elements = builder()
)
)
) )
) )

View File

@ -1,21 +1,46 @@
package moe.fuqiuluo.qqinterface.servlet.msg package moe.fuqiuluo.qqinterface.servlet.msg
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import moe.fuqiuluo.qqinterface.servlet.msg.converter.ElemConverter import moe.fuqiuluo.qqinterface.servlet.msg.converter.ElemConverter
import moe.fuqiuluo.qqinterface.servlet.msg.converter.NtMsgElementConverter 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.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.tools.toHexString
import protobuf.message.Elem import protobuf.message.Elem
import protobuf.message.RichText
@JvmName("elemListToSegments") @JvmName("richTextToSegments")
internal suspend fun List<Elem>.toSegments( internal suspend fun RichText.toSegments(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
subPeer: String subPeer: String
): List<MessageSegment> { ): List<MessageSegment> {
val messageData = arrayListOf<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 { kotlin.runCatching {
val elementType = if (msg.text != null) { val elementType = if (msg.text != null) {
1 1
@ -29,7 +54,7 @@ internal suspend fun List<Elem>.toSegments(
37 37
} else if (msg.srcMsg != null) { } else if (msg.srcMsg != null) {
45 45
} else if (msg.lightApp != null) { } else if (msg.lightApp != null) {
51 51
} else if (msg.commonElem != null) { } else if (msg.commonElem != null) {
53 53

View File

@ -12,8 +12,8 @@ internal data class MessageSegment(
) { ) {
fun toJson(): JsonObject { fun toJson(): JsonObject {
return mapOf( return mapOf(
"type" to type.json, "type" to type,
"data" to data.json "data" to data
).json ).json
} }
} }
@ -29,6 +29,6 @@ internal fun List<MessageSegment>.toListMap(): List<Map<String, JsonElement>> {
mapOf( mapOf(
"type" to it.type.json, "type" to it.type.json,
"data" to it.data.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.ImageDB
import moe.fuqiuluo.shamrock.helper.db.ImageMapping import moe.fuqiuluo.shamrock.helper.db.ImageMapping
import moe.fuqiuluo.shamrock.helper.db.MessageDB import moe.fuqiuluo.shamrock.helper.db.MessageDB
import moe.fuqiuluo.shamrock.tools.asJsonArray
import moe.fuqiuluo.shamrock.utils.DeflateTools import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.shamrock.tools.asJsonObject import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.toHexString import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.symbols.decodeProtobuf import moe.fuqiuluo.symbols.decodeProtobuf
import moe.fuqiuluo.shamrock.tools.slice
import protobuf.message.Elem import protobuf.message.Elem
import protobuf.message.element.commelem.ButtonExtra import protobuf.message.element.commelem.ButtonExtra
import protobuf.message.element.commelem.MarkdownExtra import protobuf.message.element.commelem.MarkdownExtra
@ -335,10 +337,8 @@ internal object ElemConverter {
element: Elem element: Elem
): MessageSegment { ): MessageSegment {
val data = element.lightApp!!.data!! val data = element.lightApp!!.data!!
val jsonStr = val jsonStr = String(if (data[0].toInt() == 1) DeflateTools.uncompress(data.slice(1)) else data.slice(1))
(if (data[0].toInt() == 1) DeflateTools.uncompress(data.sliceArray(1 until data.size)) else data.sliceArray( LogCenter.log(jsonStr, Level.DEBUG)
1 until data.size
)).toString()
val json = jsonStr.asJsonObject val json = jsonStr.asJsonObject
return when (json["app"].asString) { return when (json["app"].asString) {
"com.tencent.multimsg" -> { "com.tencent.multimsg" -> {
@ -346,7 +346,10 @@ internal object ElemConverter {
MessageSegment( MessageSegment(
type = "forward", type = "forward",
data = mapOf( 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.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement 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.msg.MessageSegment
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
import moe.fuqiuluo.shamrock.helper.ContactHelper 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.ImageDB
import moe.fuqiuluo.shamrock.helper.db.ImageMapping import moe.fuqiuluo.shamrock.helper.db.ImageMapping
import moe.fuqiuluo.shamrock.helper.db.MessageDB import moe.fuqiuluo.shamrock.helper.db.MessageDB
import moe.fuqiuluo.shamrock.tools.asJsonArray
import moe.fuqiuluo.shamrock.tools.asJsonObject import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.hex2ByteArray import moe.fuqiuluo.shamrock.tools.hex2ByteArray
@ -242,16 +239,10 @@ internal object NtMsgElementConverter {
data = hashMapOf( data = hashMapOf(
"file" to md5, "file" to md5,
"url" to when (chatType) { "url" to when (chatType) {
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPttDownUrl(
"0",
record.md5HexStr,
record.fileUuid
)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid) MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl( MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
"0", "0",
record.md5HexStr, md5.hex2ByteArray(),
record.fileUuid record.fileUuid
) )
@ -343,7 +334,10 @@ internal object NtMsgElementConverter {
MessageSegment( MessageSegment(
type = "forward", type = "forward",
data = mapOf( 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,64 +13,84 @@ import moe.fuqiuluo.qqinterface.servlet.msg.toJson
import moe.fuqiuluo.qqinterface.servlet.msg.toSegments import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc
import moe.fuqiuluo.shamrock.helper.* 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.tools.*
import moe.fuqiuluo.shamrock.utils.DeflateTools import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.shamrock.utils.FileUtils import moe.fuqiuluo.shamrock.utils.FileUtils
import protobuf.auto.toByteArray import protobuf.auto.toByteArray
import protobuf.message.Elem import protobuf.message.Elem
import protobuf.message.RichText
import protobuf.message.element.* import protobuf.message.element.*
import protobuf.message.element.commelem.* import protobuf.message.element.commelem.*
import java.io.File import java.io.File
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.*
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.nextULong import kotlin.random.nextULong
import kotlin.time.Duration.Companion.seconds 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 {
private val makerArray = hashMapOf( companion object {
"text" to ElemMaker::createTextElem, private val makerArray = hashMapOf(
"at" to ElemMaker::createAtElem, "text" to ElemMaker::createTextElem,
"face" to ElemMaker::createFaceElem, "at" to ElemMaker::createAtElem,
"pic" to ElemMaker::createImageElem, "face" to ElemMaker::createFaceElem,
"image" to ElemMaker::createImageElem, "pic" to ElemMaker::createImageElem,
"image" to ElemMaker::createImageElem,
// "voice" to ElemMaker::createRecordElem, // "voice" to ElemMaker::createRecordElem,
// "record" to ElemMaker::createRecordElem, // "record" to ElemMaker::createRecordElem,
// "video" to ElemMaker::createVideoElem, // "video" to ElemMaker::createVideoElem,
"markdown" to ElemMaker::createMarkdownElem, "forward" to ElemMaker::createForwardStruct,
"button" to ElemMaker::createButtonElem, "json" to ElemMaker::createJsonElem,
"dice" to ElemMaker::createNewDiceElem, "poke" to ElemMaker::createPokeElem,
"rps" to ElemMaker::createNewRpsElem, "dice" to ElemMaker::createNewDiceElem,
"poke" to ElemMaker::createPokeElem, "rps" to ElemMaker::createNewRpsElem,
"markdown" to ElemMaker::createMarkdownElem,
"button" to ElemMaker::createButtonElem,
// "anonymous" to ElemMaker::createAnonymousElem, // "anonymous" to ElemMaker::createAnonymousElem,
// "share" to ElemMaker::createShareElem, // "share" to ElemMaker::createShareElem,
// "contact" to ElemMaker::createContactElem, // "contact" to ElemMaker::createContactElem,
// "location" to ElemMaker::createLocationElem, // "location" to ElemMaker::createLocationElem,
// "music" to ElemMaker::createMusicElem, // "music" to ElemMaker::createMusicElem,
"reply" to ElemMaker::createReplyElem, "reply" to ElemMaker::createReplyElem,
// "touch" to ElemMaker::createTouchElem, // "touch" to ElemMaker::createTouchElem,
"weather" to ElemMaker::createWeatherElem, "weather" to ElemMaker::createWeatherElem,
"json" to ElemMaker::createJsonElem, //"forward" to MessageMaker::createForwardElem,
//"forward" to MessageMaker::createForwardElem, //"multi_msg" to MessageMaker::createLongMsgStruct,
//"multi_msg" to MessageMaker::createLongMsgStruct, //"bubble_face" to ElemMaker::createBubbleFaceElem,
//"bubble_face" to ElemMaker::createBubbleFaceElem, )
)
operator fun get(type: String): IElemMaker? = makerArray[type] 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( private suspend fun createTextElem(
chatType: Int, chatType: Int,
msgId: Long, msgId: Long,
peerId: String, peerId: String,
data: JsonObject data: JsonObject
): Result<Elem> { ) {
data.checkAndThrow("text") data.checkAndThrow("text")
val elem = Elem( val elem = Elem(
text = TextMsg(data["text"].asString) text = TextMsg(data["text"].asString)
) )
return Result.success(elem) elems.add(elem)
desc += data["text"].asString
} }
private suspend fun createAtElem( private suspend fun createAtElem(
@ -78,8 +98,8 @@ internal object ElemMaker {
msgId: Long, msgId: Long,
peerId: String, peerId: String,
data: JsonObject data: JsonObject
): Result<Elem> { ) {
return when (chatType) { when (chatType) {
MsgConstant.KCHATTYPEGROUP -> { MsgConstant.KCHATTYPEGROUP -> {
data.checkAndThrow("qq") data.checkAndThrow("qq")
@ -105,15 +125,14 @@ internal object ElemMaker {
peerId.toLong(), peerId.toLong(),
qq, qq,
true true
) ).let {
.let { val info = it.getOrNull()
val info = it.getOrNull() if (info == null)
if (info == null) LogCenter.log("无法获取群成员信息: $qqStr", Level.ERROR)
LogCenter.log("无法获取群成员信息: $qqStr", Level.ERROR) else info.troopnick
info?.troopnick .ifNullOrEmpty(info.friendnick)
.ifNullOrEmpty(info?.friendnick) .ifNullOrEmpty(qqStr)
.ifNullOrEmpty(qqStr) })
})
} }
} }
@ -126,7 +145,8 @@ internal object ElemMaker {
val elem = Elem( val elem = Elem(
text = TextMsg(str = display, attr6Buf = attr6.array()) text = TextMsg(str = display, attr6Buf = attr6.array())
) )
Result.success(elem) elems.add(elem)
desc += display
} }
MsgConstant.KCHATTYPEC2C -> { MsgConstant.KCHATTYPEC2C -> {
@ -144,10 +164,11 @@ internal object ElemMaker {
val elem = Elem( val elem = Elem(
text = TextMsg(str = display) 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, msgId: Long,
peerId: String, peerId: String,
data: JsonObject data: JsonObject
): Result<Elem> { ) {
data.checkAndThrow("id") data.checkAndThrow("id")
val faceId = data["id"].asInt val faceId = data["id"].asInt
val elem = if (data["big"].asBooleanOrNull == true) { 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( private suspend fun createImageElem(
@ -191,7 +213,7 @@ internal object ElemMaker {
msgId: Long, msgId: Long,
peerId: String, peerId: String,
data: JsonObject data: JsonObject
): Result<Elem> { ) {
val isOriginal = data["original"].asBooleanOrNull ?: true val isOriginal = data["original"].asBooleanOrNull ?: true
val isFlash = data["flash"].asBooleanOrNull ?: false val isFlash = data["flash"].asBooleanOrNull ?: false
val filePath = data["file"].asStringOrNull val filePath = data["file"].asStringOrNull
@ -307,7 +329,8 @@ internal object ElemMaker {
else -> throw LogicException("Not supported chatType($chatType) for PictureMsg") else -> throw LogicException("Not supported chatType($chatType) for PictureMsg")
} }
return Result.success(elem) elems.add(elem)
desc += "[图片]"
} }
private suspend fun createReplyElem( private suspend fun createReplyElem(
@ -315,16 +338,15 @@ internal object ElemMaker {
msgId: Long, msgId: Long,
peerId: String, peerId: String,
data: JsonObject data: JsonObject
): Result<Elem> { ) {
data.checkAndThrow("id") data.checkAndThrow("id")
val msgHash = data["id"].asInt val msgHash = data["id"].asInt
val mapping = MessageHelper.getMsgMappingByHash(msgHash) val mapping = MessageHelper.getMsgMappingByHash(msgHash)
?: return Result.failure(Exception("不存在该消息映射,无法回复消息")) ?: throw Exception("不存在该消息映射,无法回复消息")
if (mapping.qqMsgId == 0L) { if (mapping.qqMsgId == 0L) {
// 貌似获取失败了555 // 貌似获取失败了555
LogCenter.log("无法获取被回复消息", Level.ERROR) throw Exception("无法获取被回复消息")
return Result.failure(Exception("无法获取被回复消息"))
} }
val elem = if (data.containsKey("text")) { val elem = if (data.containsKey("text")) {
@ -351,16 +373,15 @@ internal object ElemMaker {
) )
} else { } else {
val msg = val msg =
MsgSvc.getMsgByQMsgId(chatType, mapping.peerId, mapping.qqMsgId).getOrNull() ?: return Result.failure( MsgSvc.getMsgByQMsgId(chatType, mapping.peerId, mapping.qqMsgId).getOrNull()
Exception("无法获取被回复消息") ?: throw Exception("无法获取被回复消息")
)
Elem( Elem(
srcMsg = SourceMsg( srcMsg = SourceMsg(
origSeqs = listOf(msg.msgSeq.toInt()), origSeqs = listOf(msg.msgSeq.toInt()),
senderUin = msg.senderUin.toULong(), senderUin = msg.senderUin.toULong(),
time = msg.msgTime.toULong(), time = msg.msgTime.toULong(),
flag = 1u, flag = 1u,
elems = messageArrayToMessageElements( elems = messageArrayToRichText(
msg.chatType, msg.chatType,
msg.msgId, msg.msgId,
msg.peerUin.toString(), msg.peerUin.toString(),
@ -369,7 +390,7 @@ internal object ElemMaker {
if (msg.chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(), if (msg.chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(),
msg.channelId ?: msg.peerUin.toString() msg.channelId ?: msg.peerUin.toString()
).toJson() ).toJson()
).second, ).getOrElse { throw Exception("解析回复消息失败: $it") }.second.elements,
type = 0u, type = 0u,
pbReserve = SourceMsg.Companion.PbReserve( pbReserve = SourceMsg.Companion.PbReserve(
msgRand = Random.nextULong(), msgRand = Random.nextULong(),
@ -380,7 +401,8 @@ internal object ElemMaker {
) )
) )
} }
return Result.success(elem) elems.add(elem)
desc += "[回复消息]"
} }
private suspend fun createJsonElem( private suspend fun createJsonElem(
@ -388,15 +410,82 @@ internal object ElemMaker {
msgId: Long, msgId: Long,
peerId: String, peerId: String,
data: JsonObject data: JsonObject
): Result<Elem> { ) {
data.checkAndThrow("data") data.checkAndThrow("data")
val elem = Elem( val elem = Elem(
lightApp = LightAppElem( 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( private suspend fun createWeatherElem(
@ -404,7 +493,7 @@ internal object ElemMaker {
msgId: Long, msgId: Long,
peerId: String, peerId: String,
data: JsonObject data: JsonObject
): Result<Elem> { ) {
var code = data["code"].asIntOrNull var code = data["code"].asIntOrNull
if (code == null) { if (code == null) {
@ -416,19 +505,22 @@ internal object ElemMaker {
} }
if (code != null) { if (code != null) {
WeatherSvc.fetchWeatherCard(code).onSuccess { val weatherCard = WeatherSvc.fetchWeatherCard(code).getOrThrow()
// OidbSvc.0xdc2_34 // 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 // 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( val elem = Elem(
chatType, msgId, peerId, it["weekStore"] lightApp = LightAppElem(
.asJsonObject["share"].asJsonObject 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( private suspend fun createPokeElem(
@ -436,7 +528,7 @@ internal object ElemMaker {
msgId: Long, msgId: Long,
peerId: String, peerId: String,
data: JsonObject data: JsonObject
): Result<Elem> { ) {
data.checkAndThrow("type", "id") data.checkAndThrow("type", "id")
val elem = Elem( val elem = Elem(
commonElem = CommonElem( commonElem = CommonElem(
@ -449,7 +541,8 @@ internal object ElemMaker {
businessType = data["id"].asInt businessType = data["id"].asInt
) )
) )
return Result.success(elem) elems.add(elem)
desc += "[戳一戳]"
} }
private suspend fun createNewDiceElem( private suspend fun createNewDiceElem(
@ -457,7 +550,7 @@ internal object ElemMaker {
msgId: Long, msgId: Long,
peerId: String, peerId: String,
data: JsonObject data: JsonObject
): Result<Elem> { ) {
val elem = Elem( val elem = Elem(
commonElem = CommonElem( commonElem = CommonElem(
serviceType = 37, serviceType = 37,
@ -474,7 +567,8 @@ internal object ElemMaker {
businessType = 2 businessType = 2
) )
) )
return Result.success(elem) elems.add(elem)
desc += "[骰子]"
} }
private suspend fun createNewRpsElem( private suspend fun createNewRpsElem(
@ -482,7 +576,7 @@ internal object ElemMaker {
msgId: Long, msgId: Long,
peerId: String, peerId: String,
data: JsonObject data: JsonObject
): Result<Elem> { ) {
val elem = Elem( val elem = Elem(
commonElem = CommonElem( commonElem = CommonElem(
serviceType = 37, serviceType = 37,
@ -499,7 +593,8 @@ internal object ElemMaker {
businessType = 1 businessType = 1
) )
) )
return Result.success(elem) elems.add(elem)
desc += "[包剪锤]"
} }
private suspend fun createMarkdownElem( private suspend fun createMarkdownElem(
@ -507,7 +602,7 @@ internal object ElemMaker {
msgId: Long, msgId: Long,
peerId: String, peerId: String,
data: JsonObject data: JsonObject
): Result<Elem> { ) {
data.checkAndThrow("content") data.checkAndThrow("content")
val elem = Elem( val elem = Elem(
commonElem = CommonElem( commonElem = CommonElem(
@ -516,7 +611,8 @@ internal object ElemMaker {
businessType = 1 businessType = 1
) )
) )
return Result.success(elem) elems.add(elem)
desc += "[Markdown消息]"
} }
private suspend fun createButtonElem( private suspend fun createButtonElem(
@ -524,7 +620,7 @@ internal object ElemMaker {
msgId: Long, msgId: Long,
peerId: String, peerId: String,
data: JsonObject data: JsonObject
): Result<Elem> { ) {
data.checkAndThrow("buttons") data.checkAndThrow("buttons")
val elem = Elem( val elem = Elem(
commonElem = CommonElem( commonElem = CommonElem(
@ -565,7 +661,8 @@ internal object ElemMaker {
businessType = 1 businessType = 1
) )
) )
return Result.success(elem) elems.add(elem)
desc += "[Button消息]"
} }
private fun JsonObject.checkAndThrow(vararg key: String) { 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.CardSvc
import moe.fuqiuluo.qqinterface.servlet.GroupSvc import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.LbsSvc 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.data.ArkAppInfo
import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc
import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc 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.cmd0xdc2.oidb_cmd0xdc2
import tencent.im.oidb.oidb_sso import tencent.im.oidb.oidb_sso
import java.io.File import java.io.File
import java.util.*
import kotlin.collections.ArrayList
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.nextInt import kotlin.random.nextInt
@ -80,10 +83,11 @@ internal object NtMsgElementMaker {
"touch" to NtMsgElementMaker::createTouchElem, "touch" to NtMsgElementMaker::createTouchElem,
"weather" to NtMsgElementMaker::createWeatherElem, "weather" to NtMsgElementMaker::createWeatherElem,
"json" to NtMsgElementMaker::createJsonElem, "json" to NtMsgElementMaker::createJsonElem,
"forward" to NtMsgElementMaker::createForwardStruct,
"new_dice" to NtMsgElementMaker::createNewDiceElem, "new_dice" to NtMsgElementMaker::createNewDiceElem,
"new_rps" to NtMsgElementMaker::createNewRpsElem, "new_rps" to NtMsgElementMaker::createNewRpsElem,
"basketball" to NtMsgElementMaker::createBasketballElem, "basketball" to NtMsgElementMaker::createBasketballElem,
//"multi_msg" to MessageMaker::createLongMsgStruct, //"multi_msg" to NtMsgElementMaker::createLongMsgStruct,
"bubble_face" to NtMsgElementMaker::createBubbleFaceElem, "bubble_face" to NtMsgElementMaker::createBubbleFaceElem,
"button" to NtMsgElementMaker::createInlineKeywordElem, "button" to NtMsgElementMaker::createInlineKeywordElem,
"inline_keyboard" to NtMsgElementMaker::createInlineKeywordElem "inline_keyboard" to NtMsgElementMaker::createInlineKeywordElem
@ -91,6 +95,70 @@ internal object NtMsgElementMaker {
operator fun get(type: String): IMsgElementMaker? = makerMap[type] 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( private suspend fun createInlineKeywordElem(
chatType: Int, chatType: Int,
msgId: Long, msgId: Long,
@ -262,19 +330,21 @@ internal object NtMsgElementMaker {
): Result<MsgElement> { ): Result<MsgElement> {
data.checkAndThrow("data") data.checkAndThrow("data")
val jsonStr = data["data"].let { val jsonStr = data["data"].let {
if (it is JsonObject) it.asJsonObject.toString() else { if (it is JsonObject) {
val str = it.asStringOrNull ?: "" it.asJsonObject.toString()
} else {
// 检查字符串是否是合法json不然qq会闪退 // 检查字符串是否是合法json不然qq会闪退
try { try {
val str = it.asString
val element = Json.decodeFromString<JsonElement>(str) val element = Json.decodeFromString<JsonElement>(str)
if (element !is JsonObject) { if (element !is JsonObject) {
return Result.failure(Exception("不合法的JSON字符串")) return Result.failure(Exception("不合法的JSON字符串"))
} }
str
} catch (err: Throwable) { } catch (err: Throwable) {
LogCenter.log(err.stackTraceToString(), Level.ERROR) LogCenter.log(err.stackTraceToString(), Level.ERROR)
return Result.failure(Exception("不合法的JSON字符串")) return Result.failure(Exception("不合法的JSON字符串"))
} }
str
} }
} }
val element = MsgElement() 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.api.IProtoReqManager
import com.tencent.mobileqq.transfile.protohandler.RichProto import com.tencent.mobileqq.transfile.protohandler.RichProto
import com.tencent.mobileqq.transfile.protohandler.RichProtoProc import com.tencent.mobileqq.transfile.protohandler.RichProtoProc
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import moe.fuqiuluo.qqinterface.servlet.BaseSvc import moe.fuqiuluo.qqinterface.servlet.BaseSvc
@ -22,23 +21,9 @@ import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
import moe.fuqiuluo.symbols.decodeProtobuf import moe.fuqiuluo.symbols.decodeProtobuf
import mqq.app.MobileQQ import mqq.app.MobileQQ
import protobuf.auto.toByteArray import protobuf.auto.toByteArray
import protobuf.oidb.TrpcOidb
import protobuf.oidb.cmd0x11c5.C2CUserInfo import protobuf.oidb.cmd0x11c5.C2CUserInfo
import protobuf.oidb.cmd0x11c5.ChannelUserInfo 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.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.Oidb0xfc2ChannelInfo
import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody
@ -405,8 +390,8 @@ internal object RichProtoSvc: BaseSvc() {
suspend fun getGroupPttDownUrl( suspend fun getGroupPttDownUrl(
peerId: String, peerId: String,
md5Hex: String, md5: ByteArray,
fileUUId: String groupFileKey: String
): String { ): String {
return suspendCancellableCoroutine { return suspendCancellableCoroutine {
val runtime = AppRuntimeFetcher.appRuntime val runtime = AppRuntimeFetcher.appRuntime
@ -417,8 +402,8 @@ internal object RichProtoSvc: BaseSvc() {
groupPttDownReq.secondUin = peerId groupPttDownReq.secondUin = peerId
groupPttDownReq.uinType = FileMsg.UIN_TROOP groupPttDownReq.uinType = FileMsg.UIN_TROOP
groupPttDownReq.groupFileID = 0 groupPttDownReq.groupFileID = 0
groupPttDownReq.groupFileKey = fileUUId groupPttDownReq.groupFileKey = groupFileKey
groupPttDownReq.md5 = md5Hex.hex2ByteArray() groupPttDownReq.md5 = md5
groupPttDownReq.voiceType = 1 groupPttDownReq.voiceType = 1
groupPttDownReq.downType = 1 groupPttDownReq.downType = 1
richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp -> 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.MessageDB
import moe.fuqiuluo.shamrock.helper.db.MessageMapping import moe.fuqiuluo.shamrock.helper.db.MessageMapping
import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult
import moe.fuqiuluo.shamrock.tools.EmptyJsonObject import moe.fuqiuluo.shamrock.tools.*
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 protobuf.message.Elem import protobuf.message.Elem
import protobuf.message.RichText
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.math.abs import kotlin.math.abs
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -75,7 +72,7 @@ internal object MessageHelper {
} }
if (resendRet != 0 && if (resendRet != 0 &&
resendRet != 4 // 使用OldBDH 100%触发 resendRet != 4 // 使用OldBDH 100%触发
) { ) {
resendMsg(contact, msgId, retryCnt - 1, msgHashId) resendMsg(contact, msgId, retryCnt - 1, msgHashId)
} else { } else {
Result.success(SendMsgResult(msgHashId, msgId, System.currentTimeMillis())) Result.success(SendMsgResult(msgHashId, msgId, System.currentTimeMillis()))
@ -319,38 +316,30 @@ internal object MessageHelper {
return hasActionMsg to msgList return hasActionMsg to msgList
} }
suspend fun messageArrayToMessageElements( suspend fun messageArrayToRichText(
chatType: Int, chatType: Int,
msgId: Long, msgId: Long,
peerId: String, peerId: String,
messageList: JsonArray messageList: JsonArray
): Pair<Boolean, ArrayList<Elem>> { ): Result<Pair<String, RichText>> {
val msgList = arrayListOf<Elem>() val elemMaker = ElemMaker()
var hasActionMsg = false messageList.forEach { element ->
messageList.forEach { val msg = element.asJsonObject
val msg = it.jsonObject
val maker = ElemMaker[msg["type"].asString] val maker = ElemMaker[msg["type"].asString]
if (maker != null) { if (maker != null) {
try { try {
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
maker(chatType, msgId, peerId, data).onSuccess { msgElem -> maker(elemMaker, chatType, msgId, peerId, data)
msgList.add(msgElem)
}.onFailure {
if (it.javaClass != ActionMsgException::class.java) {
throw it
} else {
hasActionMsg = true
}
}
} catch (e: Throwable) { } catch (e: Throwable) {
LogCenter.log(e.stackTraceToString(), Level.ERROR) if (e.javaClass != ActionMsgException::class.java) {
LogCenter.log(e.stackTraceToString(), Level.ERROR)
}
} }
} else { } else {
LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR) 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 { fun generateMsgIdHash(chatType: Int, msgId: Long): Int {

View File

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

View File

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

View File

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

View File

@ -3,20 +3,14 @@ package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import moe.fuqiuluo.qqinterface.servlet.MsgSvc import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc import moe.fuqiuluo.qqinterface.servlet.msg.toJson
import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.ParamsException import moe.fuqiuluo.shamrock.helper.ParamsException
import moe.fuqiuluo.shamrock.remote.action.ActionSession import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler 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.shamrock.tools.*
import moe.fuqiuluo.symbols.OneBotHandler import moe.fuqiuluo.symbols.OneBotHandler
import protobuf.message.*
import java.util.*
import kotlin.random.Random
@OneBotHandler("send_forward_msg") @OneBotHandler("send_forward_msg")
internal object SendForwardMessage : IActionHandler() { internal object SendForwardMessage : IActionHandler() {
@ -60,9 +54,10 @@ internal object SendForwardMessage : IActionHandler() {
else -> error("unknown chat type: $chatType") else -> error("unknown chat type: $chatType")
}.toString() }.toString()
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, messages, fromId, echo = session.echo) invoke(chatType, peerId, fromId, messages, retryCnt, session.echo)
} else { } else {
logic("未知格式合并转发消息", session.echo) logic("未知格式合并转发消息", session.echo)
} }
@ -76,222 +71,22 @@ internal object SendForwardMessage : IActionHandler() {
suspend operator fun invoke( suspend operator fun invoke(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
messages: JsonArray,
fromId: String = peerId, fromId: String = peerId,
messages: JsonArray,
retryCnt: Int,
echo: JsonElement = EmptyJsonString echo: JsonElement = EmptyJsonString
): String { ): 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 { 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) } .getOrElse { return logic(it.message ?: "", echo) }
val uniseq = UUID.randomUUID().toString().uppercase()
val result = MsgSvc.sendToAio( val result = MsgSvc.sendToAio(chatType, peerId, listOf(message).toJson(), fromId, retryCnt)
chatType, peerId, .getOrElse { return logic(it.message ?: "", echo) }
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) }
return ok( return ok(
ForwardMessageResult( SendForwardMessageResult(
msgId = result.msgHashId, msgId = result.msgHashId,
forwardId = resid resId = message.data["id"] as String
), echo = echo ), echo = echo
) )
}.onFailure { }.onFailure {

View File

@ -6,12 +6,19 @@ import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.symbols.OneBotHandler import moe.fuqiuluo.symbols.OneBotHandler
@OneBotHandler("send_group_forward_msg") @OneBotHandler("send_group_forward_msg")
internal object SendGroupForwardMessage: IActionHandler() { internal object SendGroupForwardMessage : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String { override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getLong("group_id") val groupId = session.getLong("group_id")
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")
SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId.toString(), messages, echo = session.echo) SendForwardMessage(
MsgConstant.KCHATTYPEGROUP,
groupId.toString(),
messages = messages,
retryCnt = retryCnt,
echo = session.echo
)
} else { } else {
logic("未知格式合并转发消息", session.echo) logic("未知格式合并转发消息", session.echo)
} }

View File

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

View File

@ -9,9 +9,18 @@ import moe.fuqiuluo.symbols.OneBotHandler
internal object SendPrivateForwardMessage : IActionHandler() { internal object SendPrivateForwardMessage : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String { override suspend fun internalHandle(session: ActionSession): String {
val userId = session.getLong("user_id") val userId = session.getLong("user_id")
val groupId = session.getLongOrNull("group_id")
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")
SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId.toString(), messages, echo = session.echo) SendForwardMessage(
MsgConstant.KCHATTYPEC2C,
userId.toString(),
groupId?.toString() ?: userId.toString(),
messages,
retryCnt,
echo = session.echo
)
} else { } else {
logic("未知格式合并转发消息", session.echo) 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") { getOrPost("/get_forward_msg") {
val id = fetchOrThrow("id") val id = fetchOrThrow("id")
call.respondText(GetForwardMsg(id), ContentType.Application.Json) call.respondText(GetForwardMsg(id), ContentType.Application.Json)
@ -144,7 +101,7 @@ fun Routing.messageAction() {
get { get {
val msgType = fetchGetOrThrow("message_type") val msgType = fetchGetOrThrow("message_type")
val message = fetchGetOrThrow("message") 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 autoEscape = fetchGetOrNull("auto_escape")?.toBooleanStrict() ?: false
val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType) val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType)
@ -288,11 +245,11 @@ fun Routing.messageAction() {
post { post {
val userId = fetchPostOrThrow("user_id") val userId = fetchPostOrThrow("user_id")
val groupId = fetchPostOrNull("group_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 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 recallDuration = fetchPostOrNull("recall_duration")?.toLongOrNull()
val result = if (isJsonData()) { val result = if (isJsonData()) {
@ -302,8 +259,9 @@ fun Routing.messageAction() {
peerId = userId, peerId = userId,
message = fetchPostJsonString("message"), message = fetchPostJsonString("message"),
autoEscape = autoEscape, autoEscape = autoEscape,
fromId = fromId, fromId = groupId ?: userId,
retryCnt = retryCnt, recallDuration = recallDuration retryCnt = retryCnt,
recallDuration = recallDuration
) )
} else { } else {
SendMessage( SendMessage(
@ -312,7 +270,7 @@ fun Routing.messageAction() {
message = if (isJsonObject("message")) listOf(fetchPostJsonObject("message")).jsonArray else fetchPostJsonArray( message = if (isJsonObject("message")) listOf(fetchPostJsonObject("message")).jsonArray else fetchPostJsonArray(
"message" "message"
), ),
fromId = groupId ?: userId ?: "", fromId = groupId ?: userId,
retryCnt = retryCnt, retryCnt = retryCnt,
recallDuration = recallDuration recallDuration = recallDuration
) )
@ -323,12 +281,96 @@ fun Routing.messageAction() {
peerId = userId, peerId = userId,
message = fetchPostOrThrow("message"), message = fetchPostOrThrow("message"),
autoEscape = autoEscape, autoEscape = autoEscape,
fromId = fromId, fromId = groupId ?: userId,
retryCnt = retryCnt, recallDuration = recallDuration retryCnt = retryCnt,
recallDuration = recallDuration
) )
} }
call.respondText(result, ContentType.Application.Json) 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("message_id") val msgId: Int,
@SerialName("time") val time: Long @SerialName("time") val time: Long
) )
@Serializable @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("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 @Serializable
@ -20,6 +30,7 @@ internal data class MessageDetail(
@SerialName("time") val time: Int, @SerialName("time") val time: Int,
@SerialName("message_type") val msgType: String, @SerialName("message_type") val msgType: String,
@SerialName("message_id") val msgId: Int, @SerialName("message_id") val msgId: Int,
@SerialName("message_id_qq") val qqMsgId: Long,
@SerialName("message_seq") val msgSeq: Long, @SerialName("message_seq") val msgSeq: Long,
@SerialName("real_id") val realId: Long, @SerialName("real_id") val realId: Long,
@SerialName("sender") val sender: MessageSender, @SerialName("sender") val sender: MessageSender,
@ -29,6 +40,11 @@ internal data class MessageDetail(
@SerialName("target_id") val targetId: Long = 0, @SerialName("target_id") val targetId: Long = 0,
) )
@Serializable
internal data class GetForwardMsgResult(
@SerialName("messages") val msgs: List<MessageDetail>
)
@Serializable @Serializable
internal data class MessageSender( internal data class MessageSender(
@SerialName("user_id") val userId: Long, @SerialName("user_id") val userId: Long,

View File

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

View File

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