send_forward_msg(support markdown, button...)

This commit is contained in:
Simplxs 2024-02-25 04:09:51 +08:00
parent 6b1147d065
commit 92ebe0c6a8
No known key found for this signature in database
GPG Key ID: E23537FF14DD6507
25 changed files with 910 additions and 822 deletions

View File

@ -2,16 +2,14 @@ package moe.fuqiuluo.shamrock
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
fun test() {
}
}

View File

@ -15,7 +15,7 @@ data class ContentHead(
@ProtoNumber(8) val u6: Int? = null,
@ProtoNumber(9) val u7: Int? = null,
@ProtoNumber(11) val msgSeq: Long? = null,
@ProtoNumber(12) val msgRandom: Long = Long.MIN_VALUE,
@ProtoNumber(12) val msgRandom: Long = Long.MIN_VALUE, // 0x0100000000000000L xor msgViaRandom
@ProtoNumber(14) val u4: Long? = null,
@ProtoNumber(15) val forwardHead: ForwardHead? = null,
@ProtoNumber(28) val u5: Long? = null

View File

@ -30,7 +30,7 @@ data class CustomFace(
@ProtoNumber(23) var height: UInt? = null,
@ProtoNumber(24) var source: UInt? = null,
@ProtoNumber(25) var size: UInt? = null,
@ProtoNumber(26) var origin: UInt? = null,
@ProtoNumber(26) var origin: Boolean? = null,
@ProtoNumber(27) var thumbWidth: UInt? = null,
@ProtoNumber(28) var thumbHeight: UInt? = null,
@ProtoNumber(29) var showLen: UInt? = null,

View File

@ -5,8 +5,8 @@ import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class FaceMsg(
@ProtoNumber(1) val id: Int? = null,
@ProtoNumber(1) val index: Int? = null,
@ProtoNumber(2) var old: ByteArray? = null,
@ProtoNumber(11) var buf: ByteArray? = null,
)
)

View File

@ -12,7 +12,7 @@ data class GeneralFlags(
@ProtoNumber(4) val rpId: ByteArray? = null,
@ProtoNumber(5) val prpFold: UInt? = null,
@ProtoNumber(6) val longTextFlag: UInt? = null,
@ProtoNumber(7) val longTextResid: ByteArray? = null,
@ProtoNumber(7) val longTextResid: String? = null,
@ProtoNumber(8) val groupType: UInt? = null,
@ProtoNumber(9) val toUinFlag: UInt? = null,
@ProtoNumber(10) val glamourLevel: UInt? = null,

View File

@ -17,7 +17,7 @@ data class NotOnlineImage(
@ProtoNumber(10) val resId: ByteArray? = null, // md5 + ".jpg"
@ProtoNumber(11) val flag: ByteArray? = null,
@ProtoNumber(12) val thumbUrl: String? = null,
@ProtoNumber(13) val original: UInt? = null,
@ProtoNumber(13) val original: Boolean? = null,
@ProtoNumber(14) val bigUrl: String? = null,
@ProtoNumber(15) val origUrl: String? = null,
@ProtoNumber(16) val bizType: UInt? = null,

View File

@ -21,7 +21,7 @@ data class SourceMsg(
companion object {
@Serializable
data class PbReserve(
@ProtoNumber(3) var field3: ULong? = null,
@ProtoNumber(3) var msgRand: ULong? = null,
@ProtoNumber(6) var senderUid: String? = null,
@ProtoNumber(7) var receiverUid: String? = null,
@ProtoNumber(8) var field8: Int? = null,

View File

@ -10,5 +10,12 @@ data class TextMsg(
@ProtoNumber(3) val attr6Buf: ByteArray? = null,
@ProtoNumber(4) val attr7Buf: ByteArray? = null,
@ProtoNumber(11) val buf: ByteArray? = null,
@ProtoNumber(12) val pbReserve: ByteArray? = null,
)
@ProtoNumber(12) val pbReserve: PbReserve? = null,
){
companion object {
@Serializable
data class PbReserve(
@ProtoNumber(1) val field1: String? = null, // [打 call]] 请使用最新版手机 QQ 体验新功能
)
}
}

View File

@ -23,7 +23,7 @@ data class Row(
@Serializable
data class Button(
@ProtoNumber(1) val id: Int? = null,
@ProtoNumber(1) val id: String? = null,
@ProtoNumber(2) val renderData: RenderData? = null,
@ProtoNumber(3) val action: Action? = null,
)
@ -41,8 +41,8 @@ data class Action(
@ProtoNumber(2) val permission: Permission? = null,
@ProtoNumber(4) val unsupportTips: String? = null,
@ProtoNumber(5) val data: String? = null,
@ProtoNumber(6) val reply: Boolean? = null,
@ProtoNumber(7) val enter: Boolean? = null,
@ProtoNumber(7) val reply: Boolean? = null,
@ProtoNumber(8) val enter: Boolean? = null,
)
@Serializable

View File

@ -11,7 +11,7 @@ data class QFaceExtra(
@ProtoNumber(3) val faceId: Int? = null,
@ProtoNumber(4) val field4: Int? = null,
@ProtoNumber(5) val field5: Int? = null,
@ProtoNumber(6) val field6: String? = null,
@ProtoNumber(6) val result: String? = null,
@ProtoNumber(7) val faceText: String? = null,
@ProtoNumber(9) val field9: Int? = null
) : Protobuf<QFaceExtra>

View File

@ -222,7 +222,7 @@ internal object MsgSvc : BaseSvc() {
}
}
suspend fun sendMultiMsg(
suspend fun uploadMultiMsg(
uid: String,
groupUin: String?,
messages: List<PushMsgBody>,

View File

@ -1,7 +1,7 @@
package moe.fuqiuluo.qqinterface.servlet.msg
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import moe.fuqiuluo.qqinterface.servlet.msg.converter.MessageElementConverter
import moe.fuqiuluo.qqinterface.servlet.msg.converter.ElemConverter
import moe.fuqiuluo.qqinterface.servlet.msg.converter.MsgElementConverter
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
@ -21,13 +21,21 @@ internal suspend fun List<Elem>.toSegments(
1
} else if (msg.face != null) {
2
} else if (msg.lightApp != null) {
} else if (msg.notOnlineImage != null) {
4
} else if (msg.customFace != null) {
8
} else if (msg.generalFlags != null) {
37
} else if (msg.srcMsg != null) {
45
} else if (msg.lightApp != null) {
51
} else if (msg.commonElem != null) {
53
} else
throw UnsupportedOperationException("不支持的消息element类型$msg")
val converter = MessageElementConverter[elementType]
val converter = ElemConverter[elementType]
converter?.invoke(chatType, peerId, subPeer, msg)
?: throw UnsupportedOperationException("不支持的消息element类型$elementType")
}.onSuccess {

View File

@ -8,10 +8,10 @@ import moe.fuqiuluo.shamrock.tools.json
internal data class MessageSegment(
val type: String,
val data: Map<String, Any> = emptyMap()
val data: Map<String, Any?> = emptyMap()
) {
fun toJson(): JsonObject {
return hashMapOf(
return mapOf(
"type" to type.json,
"data" to data.json
).json
@ -26,7 +26,7 @@ internal fun List<MessageSegment>.toJson(): JsonArray {
internal fun List<MessageSegment>.toListMap(): List<Map<String, JsonElement>> {
return this.map {
hashMapOf(
mapOf(
"type" to it.type.json,
"data" to it.data.json
).json

View File

@ -0,0 +1,621 @@
package moe.fuqiuluo.qqinterface.servlet.msg.converter
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import io.ktor.util.*
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readUInt
import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.Level
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.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 protobuf.message.Elem
import protobuf.message.element.commelem.ButtonExtra
import protobuf.message.element.commelem.MarkdownExtra
import protobuf.message.element.commelem.QFaceExtra
internal typealias IElemConverter = suspend (Int, String, String, Elem) -> MessageSegment
internal object ElemConverter {
private val convertMap = mapOf(
1 to ElemConverter::convertTextElem,
2 to ElemConverter::convertFaceElem,
4 to ElemConverter::convertNotOnlineImageElem,
8 to ElemConverter::convertCustomFaceElem,
// MsgConstant.KELEMTYPEPTT to ElemConverter::convertVoiceElem,
// MsgConstant.KELEMTYPEVIDEO to ElemConverter::convertVideoElem,
// MsgConstant.KELEMTYPEMARKETFACE to ElemConverter::convertMarketFaceElem,
37 to ElemConverter::convertGeneralFlagsElem,
45 to ElemConverter::convertReplyElem,
51 to ElemConverter::convertStructJsonElem,
53 to ElemConverter::convertCommonElem,
// MsgConstant.KELEMTYPEGRAYTIP to ElemConverter::convertGrayTipsElem,
// MsgConstant.KELEMTYPEFILE to ElemConverter::convertFileElem,
// //MsgConstant.KELEMTYPEMULTIFORWARD to ElemConverter::convertXmlMultiMsgElem,
// //MsgConstant.KELEMTYPESTRUCTLONGMSG to ElemConverter::convertXmlLongMsgElem,
// MsgConstant.KELEMTYPEFACEBUBBLE to ElemConverter::convertBubbleFaceElem,
)
operator fun get(type: Int): IElemConverter? = convertMap[type]
/**
* 文本 / 艾特 消息转换消息段
*/
private suspend fun convertTextElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val text = element.text!!
if (text.attr6Buf != null) {
val at = ByteReadPacket(text.attr6Buf!!)
at.discardExact(7)
val uin = at.readUInt()
return MessageSegment(
type = "at",
data = mapOf(
"qq" to uin
)
)
} else {
return MessageSegment(
type = "text",
data = mapOf(
"text" to text.str!!
)
)
}
}
/**
* 小表情 / 戳一戳 消息转换消息段
*/
private suspend fun convertFaceElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val face = element.face!!
return MessageSegment(
type = "face",
data = mapOf(
"id" to face.index!!
)
)
}
/**
* 图片消息转换消息段
*/
private suspend fun convertCustomFaceElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val customFace = element.customFace!!
val md5 = customFace.md5.toHexString()
ImageDB.getInstance().imageMappingDao().insert(
ImageMapping(md5.uppercase(), chatType, customFace.size!!.toLong())
)
val origUrl = customFace.origUrl!!
return MessageSegment(
type = "image",
data = mapOf(
"file" to md5,
"url" to when (chatType) {
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
origUrl,
md5
)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5)
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
},
"type" to if (customFace.origin == true) "original" else "show"
)
)
}
private suspend fun convertNotOnlineImageElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val notOnlineImage = element.notOnlineImage!!
val md5 = notOnlineImage.picMd5.toHexString()
ImageDB.getInstance().imageMappingDao().insert(
ImageMapping(md5.uppercase(), chatType, notOnlineImage.fileLen!!.toLong())
)
val origUrl = notOnlineImage.origUrl!!
return MessageSegment(
type = "image",
data = mapOf(
"file" to md5,
"url" to when (chatType) {
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
origUrl,
md5
)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5)
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
},
"type" to if (notOnlineImage.original == true) "original" else "show"
)
)
}
private suspend fun convertGeneralFlagsElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val generalFlags = element.generalFlags!!
if (generalFlags.longTextFlag == 1u) {
return MessageSegment(
type = "general_flags",
data = mapOf(
"res_id" to generalFlags.longTextResid
)
)
}
throw UnknownError("no segment")
}
//
// /**
// * 语音消息转换消息段
// */
// private suspend fun convertVoiceElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val record = element.pttElement
//
// val md5 = if (record.fileName.startsWith("silk"))
// record.fileName.substring(5)
// else record.md5HexStr
//
// return MessageSegment(
// type = "record",
// data = mapOf(
// "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(
// "0",
// record.md5HexStr,
// record.fileUuid
// )
//
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
// }
// ).also {
// if (record.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE) {
// it["magic"] = "1"
// }
// if ((it["url"] as String).isBlank()) {
// it.remove("url")
// }
// }
// )
// }
//
// /**
// * 视频消息转换消息段
// */
// private suspend fun convertVideoElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val video = element.videoElement
// val md5 = if (video.fileName.contains("/")) {
// video.videoMd5.takeIf {
// !it.isNullOrEmpty()
// }?.hex2ByteArray() ?: video.fileName.split("/").let {
// it[it.size - 2].hex2ByteArray()
// }
// } else video.fileName.split(".")[0].hex2ByteArray()
//
// //LogCenter.log({ "receive video msg: $video" }, Level.DEBUG)
//
// return MessageSegment(
// type = "video",
// data = mapOf(
// "file" to video.fileName,
// "url" to when (chatType) {
// MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
// }
// ).also {
// if ((it["url"] as String).isBlank())
// it.remove("url")
// }
// )
// }
//
// /**
// * 商城大表情消息转换消息段
// */
// private suspend fun convertMarketFaceElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val face = element.marketFaceElement
// return when (face.emojiId.lowercase()) {
// "4823d3adb15df08014ce5d6796b76ee1" -> MessageSegment("dice")
// "83c8a293ae65ca140f348120a77448ee" -> MessageSegment("rps")
// else -> MessageSegment(
// type = "mface",
// data = mapOf(
// "id" to face.emojiId
// )
// )
// }
// }
//
/**
* 回复消息转消息段
*/
private suspend fun convertReplyElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val srcMsg = element.srcMsg!!
val msgId = srcMsg.pbReserve?.msgRand?.toLong() ?: 0
val msgHash = if (msgId != 0L) {
MessageHelper.generateMsgIdHash(chatType, msgId)
} else {
val msgSeq = srcMsg.origSeqs?.first()?.toInt() ?: 0
MessageDB.getInstance().messageMappingDao()
.queryByMsgSeq(chatType, peerId, msgSeq)?.msgHashId
?: kotlin.run {
LogCenter.log("消息映射关系未找到: Message($msgSeq)", Level.WARN)
MessageHelper.generateMsgIdHash(chatType, msgId)
}
}
return MessageSegment(
type = "reply",
data = mapOf(
"id" to msgHash
)
)
}
/**
* JSON消息转消息段
*/
private suspend fun convertStructJsonElem(
chatType: Int,
peerId: String,
subPeer: String,
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 json = jsonStr.asJsonObject
return when (json["app"].asString) {
"com.tencent.multimsg" -> {
val info = json["meta"].asJsonObject["detail"].asJsonObject
MessageSegment(
type = "forward",
data = mapOf(
"id" to info["resid"].asString
)
)
}
"com.tencent.troopsharecard" -> {
val info = json["meta"].asJsonObject["contact"].asJsonObject
MessageSegment(
type = "contact",
data = mapOf(
"type" to "group",
"id" to info["jumpUrl"].asString.split("group_code=")[1]
)
)
}
"com.tencent.contact.lua" -> {
val info = json["meta"].asJsonObject["contact"].asJsonObject
MessageSegment(
type = "contact",
data = mapOf(
"type" to "private",
"id" to info["jumpUrl"].asString.split("uin=")[1]
)
)
}
"com.tencent.map" -> {
val info = json["meta"].asJsonObject["Location.Search"].asJsonObject
MessageSegment(
type = "location",
data = mapOf(
"lat" to info["lat"].asString,
"lon" to info["lng"].asString,
"content" to info["address"].asString,
"title" to info["name"].asString
)
)
}
else -> MessageSegment(
type = "json",
data = mapOf(
"data" to jsonStr
)
)
}
}
private suspend fun convertCommonElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val commonElem = element.commonElem!!
return when (commonElem.serviceType) {
37 -> {
val qFaceExtra = commonElem.elem!!.decodeProtobuf<QFaceExtra>()
when (qFaceExtra.faceId) {
358 -> MessageSegment(
type = "dice",
data = mapOf(
"result" to qFaceExtra.result!!
)
)
359 -> MessageSegment(
type = "rps",
data = mapOf(
"result" to qFaceExtra.result!!
)
)
else -> MessageSegment(
type = "face",
data = mapOf(
"id" to qFaceExtra.faceId!!,
"big" to true,
"result" to qFaceExtra.result!! // (1布 2剪 3锤) (骰子123456)
)
)
}
}
45 -> {
val markdownExtra = commonElem.elem!!.decodeProtobuf<MarkdownExtra>()
MessageSegment(
type = "markdown",
data = mapOf(
"content" to markdownExtra.content!!
)
)
}
46 -> {
val buttonExtra = commonElem.elem!!.decodeProtobuf<ButtonExtra>()
MessageSegment(
type = "button",
data = buttonExtra.field1!!.let {
mapOf(
"buttons" to it.rows!!.map { row ->
row.buttons!!.map { button ->
val renderData = button.renderData
val action = button.action
val permission = action?.permission
mapOf(
"id" to button.id,
"render_data" to mapOf(
"label" to (renderData?.label ?: ""),
"visited_label" to (renderData?.visitedLabel ?: ""),
"style" to (renderData?.style ?: 0)
),
"action" to mapOf(
"type" to (action?.type ?: 0),
"permission" to mapOf(
"type" to (permission?.type ?: 0),
"specify_role_ids" to permission?.specifyRoleIds,
"specify_user_ids" to permission?.specifyUserIds
),
"unsupport_tips" to (action?.unsupportTips ?: ""),
"data" to (action?.data ?: ""),
"reply" to action?.reply,
"enter" to action?.enter,
)
)
}
},
"appid" to it.appid
)
}
)
}
else -> MessageSegment(
type = "common",
data = mapOf(
"data" to commonElem.elem!!.encodeBase64()
)
)
}
}
//
// /**
// * 灰色提示条消息过滤
// */
// private suspend fun convertGrayTipsElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val tip = element.grayTipElement
// when (tip.subElementType) {
// MsgConstant.GRAYTIPELEMENTSUBTYPEJSON -> {
// val notify = tip.jsonGrayTipElement
// when (notify.busiId) {
// /* 新人入群 */ 17L, /* 群戳一戳 */1061L,
// /* 群撤回 */1014L, /* 群设精消息 */2401L,
// /* 群头衔 */2407L -> {
// }
//
// else -> LogCenter.log("不支持的灰条类型(JSON): ${notify.busiId}", Level.WARN)
// }
// }
//
// MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> {
// val notify = tip.xmlElement
// when (notify.busiId) {
// /* 群戳一戳 */1061L, /* 群打卡 */1068L -> {}
// else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN)
// }
// }
//
// else -> LogCenter.log("不支持的提示类型: ${tip.subElementType}", Level.WARN)
// }
// // 提示类消息这里提供的是一个xml不具备解析通用性
// // 在这里不推送
// throw UnknownError()
// }
//
// /**
// * 文件消息转换消息段
// */
// private suspend fun convertFileElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val fileMsg = element.fileElement
// val fileName = fileMsg.fileName
// val fileSize = fileMsg.fileSize
// val expireTime = fileMsg.expireTime ?: 0
// val fileId = fileMsg.fileUuid
// val bizId = fileMsg.fileBizId ?: 0
// val fileSubId = fileMsg.fileSubId ?: ""
// val url = when (chatType) {
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(peerId, subPeer, fileId, bizId)
// else -> RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId)
// }
//
// return MessageSegment(
// type = "file",
// data = mapOf(
// "name" to fileName,
// "size" to fileSize,
// "expire" to expireTime,
// "id" to fileId,
// "url" to url,
// "biz" to bizId,
// "sub" to fileSubId
// )
// )
// }
//
// /**
// * 老板QQ的合并转发信息
// */
// private suspend fun convertXmlMultiMsgElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val multiMsg = element.multiForwardElem
// return MessageSegment(
// type = "forward",
// data = mapOf(
// "id" to multiMsg.resId
// )
// )
// }
//
// private suspend fun convertXmlLongMsgElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val longMsg = element.structLongElem
// return MessageSegment(
// type = "forward",
// data = mapOf(
// "id" to longMsg.resId
// )
// )
// }
//
//
// private suspend fun convertBubbleFaceElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val bubbleElement = element.faceBubbleElement
// return MessageSegment(
// type = "bubble_face",
// data = mapOf(
// "id" to bubbleElement.yellowFaceInfo.index,
// "count" to (bubbleElement.faceCount ?: 1),
// )
// )
// }
}

View File

@ -1,568 +0,0 @@
package moe.fuqiuluo.qqinterface.servlet.msg.converter
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readUInt
import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment
import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString
import protobuf.message.Elem
internal typealias IMessageElementConverter = suspend (Int, String, String, Elem) -> MessageSegment
internal object MessageElementConverter {
private val convertMap = hashMapOf(
1 to MessageElementConverter::convertTextElem,
// MsgConstant.KELEMTYPEFACE to MessageElementConverter::convertFaceElem,
// MsgConstant.KELEMTYPEPIC to MessageElementConverter::convertImageElem,
// MsgConstant.KELEMTYPEPTT to MessageElementConverter::convertVoiceElem,
// MsgConstant.KELEMTYPEVIDEO to MessageElementConverter::convertVideoElem,
// MsgConstant.KELEMTYPEMARKETFACE to MessageElementConverter::convertMarketFaceElem,
51 to MessageElementConverter::convertStructJsonElem,
// MsgConstant.KELEMTYPEREPLY to MessageElementConverter::convertReplyElem,
// MsgConstant.KELEMTYPEGRAYTIP to MessageElementConverter::convertGrayTipsElem,
// MsgConstant.KELEMTYPEFILE to MessageElementConverter::convertFileElem,
// MsgConstant.KELEMTYPEMARKDOWN to MessageElementConverter::convertMarkdownElem,
// //MsgConstant.KELEMTYPEMULTIFORWARD to MessageElementConverter::convertXmlMultiMsgElem,
// //MsgConstant.KELEMTYPESTRUCTLONGMSG to MessageElementConverter::convertXmlLongMsgElem,
// MsgConstant.KELEMTYPEFACEBUBBLE to MessageElementConverter::convertBubbleFaceElem,
// MsgConstant.KELEMTYPEINLINEKEYBOARD to MessageElementConverter::convertInlineKeyboardElem,
)
operator fun get(type: Int): IMessageElementConverter? = convertMap[type]
/**
* 文本 / 艾特 消息转换消息段
*/
private suspend fun convertTextElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val text = element.text!!
if (text.attr6Buf != null) {
val at = ByteReadPacket(text.attr6Buf!!)
at.discardExact(7)
val uin = at.readUInt()
return MessageSegment(
type = "at",
data = hashMapOf(
"qq" to uin
)
)
} else {
return MessageSegment(
type = "text",
data = hashMapOf(
"text" to text.str!!
)
)
}
}
// /**
// * 小表情 / 戳一戳 消息转换消息段
// */
// private suspend fun convertFaceElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val face = element.faceElement
//
// if (face.faceType == 5) {
// return MessageSegment(
// type = "poke",
// data = hashMapOf(
// "type" to face.pokeType,
// "id" to face.vaspokeId,
// "strength" to face.pokeStrength
// )
// )
// }
// when (face.faceIndex) {
// 114 -> {
// return MessageSegment(
// type = "basketball",
// data = hashMapOf(
// "id" to face.resultId.ifEmpty { "0" }.toInt(),
// )
// )
// }
//
// 358 -> {
// if (face.sourceType == 1) return MessageSegment("new_dice")
// return MessageSegment(
// type = "new_dice",
// data = hashMapOf(
// "id" to face.resultId.ifEmpty { "0" }.toInt()
// )
// )
// }
//
// 359 -> {
// if (face.resultId.isEmpty()) return MessageSegment("new_rps")
// return MessageSegment(
// type = "new_rps",
// data = hashMapOf(
// "id" to face.resultId.ifEmpty { "0" }.toInt()
// )
// )
// }
//
// 394 -> {
// //LogCenter.log(face.toString())
// return MessageSegment(
// type = "face",
// data = hashMapOf(
// "id" to face.faceIndex,
// "big" to (face.faceType == 3),
// "result" to (face.resultId ?: "1")
// )
// )
// }
//
// else -> return MessageSegment(
// type = "face",
// data = hashMapOf(
// "id" to face.faceIndex,
// "big" to (face.faceType == 3)
// )
// )
// }
// }
//
// /**
// * 图片消息转换消息段
// */
// private suspend fun convertImageElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val image = element.picElement
// val md5 = image.md5HexStr ?: image.fileName
// .replace("{", "")
// .replace("}", "")
// .replace("-", "").split(".")[0]
//
// ImageDB.getInstance().imageMappingDao().insert(
// ImageMapping(md5.uppercase(), chatType, image.fileSize)
// )
//
// //LogCenter.log(image.toString())
//
// val originalUrl = image.originImageUrl ?: ""
// //LogCenter.log({ "receive image: $image" }, Level.DEBUG)
//
// return MessageSegment(
// type = "image",
// data = hashMapOf(
// "file" to md5,
// "url" to when (chatType) {
// MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
// originalUrl,
// md5
// )
//
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(originalUrl, md5)
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(originalUrl, md5)
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
// },
// "subType" to image.picSubType,
// "type" to if (image.isFlashPic == true) "flash" else if (image.original) "original" else "show"
// )
// )
// }
//
// /**
// * 语音消息转换消息段
// */
// private suspend fun convertVoiceElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val record = element.pttElement
//
// val md5 = if (record.fileName.startsWith("silk"))
// record.fileName.substring(5)
// else record.md5HexStr
//
// return MessageSegment(
// type = "record",
// 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(
// "0",
// record.md5HexStr,
// record.fileUuid
// )
//
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
// }
// ).also {
// if (record.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE) {
// it["magic"] = "1"
// }
// if ((it["url"] as String).isBlank()) {
// it.remove("url")
// }
// }
// )
// }
//
// /**
// * 视频消息转换消息段
// */
// private suspend fun convertVideoElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val video = element.videoElement
// val md5 = if (video.fileName.contains("/")) {
// video.videoMd5.takeIf {
// !it.isNullOrEmpty()
// }?.hex2ByteArray() ?: video.fileName.split("/").let {
// it[it.size - 2].hex2ByteArray()
// }
// } else video.fileName.split(".")[0].hex2ByteArray()
//
// //LogCenter.log({ "receive video msg: $video" }, Level.DEBUG)
//
// return MessageSegment(
// type = "video",
// data = hashMapOf(
// "file" to video.fileName,
// "url" to when (chatType) {
// MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
// }
// ).also {
// if ((it["url"] as String).isBlank())
// it.remove("url")
// }
// )
// }
//
// /**
// * 商城大表情消息转换消息段
// */
// private suspend fun convertMarketFaceElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val face = element.marketFaceElement
// return when (face.emojiId.lowercase()) {
// "4823d3adb15df08014ce5d6796b76ee1" -> MessageSegment("dice")
// "83c8a293ae65ca140f348120a77448ee" -> MessageSegment("rps")
// else -> MessageSegment(
// type = "mface",
// data = hashMapOf(
// "id" to face.emojiId
// )
// )
// }
// }
//
/**
* JSON消息转消息段
*/
private suspend fun convertStructJsonElem(
chatType: Int,
peerId: String,
subPeer: String,
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 json = jsonStr.asJsonObject
return when (json["app"].asString) {
"com.tencent.multimsg" -> {
val info = json["meta"].asJsonObject["detail"].asJsonObject
MessageSegment(
type = "forward",
data = mapOf(
"id" to info["resid"].asString
)
)
}
"com.tencent.troopsharecard" -> {
val info = json["meta"].asJsonObject["contact"].asJsonObject
MessageSegment(
type = "contact",
data = hashMapOf(
"type" to "group",
"id" to info["jumpUrl"].asString.split("group_code=")[1]
)
)
}
"com.tencent.contact.lua" -> {
val info = json["meta"].asJsonObject["contact"].asJsonObject
MessageSegment(
type = "contact",
data = hashMapOf(
"type" to "private",
"id" to info["jumpUrl"].asString.split("uin=")[1]
)
)
}
"com.tencent.map" -> {
val info = json["meta"].asJsonObject["Location.Search"].asJsonObject
MessageSegment(
type = "location",
data = hashMapOf(
"lat" to info["lat"].asString,
"lon" to info["lng"].asString,
"content" to info["address"].asString,
"title" to info["name"].asString
)
)
}
else -> MessageSegment(
type = "json",
data = mapOf(
"data" to jsonStr
)
)
}
}
// /**
// * 回复消息转消息段
// */
// private suspend fun convertReplyElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val reply = element.replyElement
// val msgId = reply.replayMsgId
// val msgHash = if (msgId != 0L) {
// MessageHelper.generateMsgIdHash(chatType, msgId)
// } else {
// MessageDB.getInstance().messageMappingDao()
// .queryByMsgSeq(chatType, peerId, reply.replayMsgSeq?.toInt() ?: 0)?.msgHashId
// ?: kotlin.run {
// LogCenter.log("消息映射关系未找到: Message($reply)", Level.WARN)
// MessageHelper.generateMsgIdHash(chatType, reply.sourceMsgIdInRecords)
// }
// }
//
// return MessageSegment(
// type = "reply",
// data = mapOf(
// "id" to msgHash
// )
// )
// }
//
// /**
// * 灰色提示条消息过滤
// */
// private suspend fun convertGrayTipsElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val tip = element.grayTipElement
// when (tip.subElementType) {
// MsgConstant.GRAYTIPELEMENTSUBTYPEJSON -> {
// val notify = tip.jsonGrayTipElement
// when (notify.busiId) {
// /* 新人入群 */ 17L, /* 群戳一戳 */1061L,
// /* 群撤回 */1014L, /* 群设精消息 */2401L,
// /* 群头衔 */2407L -> {
// }
//
// else -> LogCenter.log("不支持的灰条类型(JSON): ${notify.busiId}", Level.WARN)
// }
// }
//
// MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> {
// val notify = tip.xmlElement
// when (notify.busiId) {
// /* 群戳一戳 */1061L, /* 群打卡 */1068L -> {}
// else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN)
// }
// }
//
// else -> LogCenter.log("不支持的提示类型: ${tip.subElementType}", Level.WARN)
// }
// // 提示类消息这里提供的是一个xml不具备解析通用性
// // 在这里不推送
// throw UnknownError()
// }
//
// /**
// * 文件消息转换消息段
// */
// private suspend fun convertFileElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val fileMsg = element.fileElement
// val fileName = fileMsg.fileName
// val fileSize = fileMsg.fileSize
// val expireTime = fileMsg.expireTime ?: 0
// val fileId = fileMsg.fileUuid
// val bizId = fileMsg.fileBizId ?: 0
// val fileSubId = fileMsg.fileSubId ?: ""
// val url = when (chatType) {
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(peerId, subPeer, fileId, bizId)
// else -> RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId)
// }
//
// return MessageSegment(
// type = "file",
// data = mapOf(
// "name" to fileName,
// "size" to fileSize,
// "expire" to expireTime,
// "id" to fileId,
// "url" to url,
// "biz" to bizId,
// "sub" to fileSubId
// )
// )
// }
//
// /**
// * 老板QQ的合并转发信息
// */
// private suspend fun convertXmlMultiMsgElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val multiMsg = element.multiForwardMessageElement
// return MessageSegment(
// type = "forward",
// data = mapOf(
// "id" to multiMsg.resId
// )
// )
// }
//
// private suspend fun convertXmlLongMsgElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val longMsg = element.structLongMessageElement
// return MessageSegment(
// type = "forward",
// data = mapOf(
// "id" to longMsg.resId
// )
// )
// }
//
// private suspend fun convertMarkdownElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val markdown = element.markdownElement
// return MessageSegment(
// type = "markdown",
// data = mapOf(
// "content" to markdown.content
// )
// )
// }
//
// private suspend fun convertBubbleFaceElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val bubbleElement = element.faceBubbleElement
// return MessageSegment(
// type = "bubble_face",
// data = mapOf(
// "id" to bubbleElement.yellowFaceInfo.index,
// "count" to (bubbleElement.faceCount ?: 1),
// )
// )
// }
//
// private suspend fun convertInlineKeyboardElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val keyboard = element.inlineKeyboardElement
// return MessageSegment(
// type = "inline_keyboard",
// data = mapOf(
// "data" to buildJsonObject {
// putJsonArray("rows") {
// keyboard.rows.forEach { row ->
// add(buildJsonObject row@{
// putJsonArray("buttons") {
// row.buttons.forEach { button ->
// add(buildJsonObject {
// put("id", button.id ?: "")
// put("label", button.label ?: "")
// put("visited_label", button.visitedLabel ?: "")
// put("style", button.style)
// put("type", button.type)
// put("click_limit", button.clickLimit)
// put("unsupport_tips", button.unsupportTips ?: "")
// put("data", button.data)
// put("at_bot_show_channel_list", button.atBotShowChannelList)
// put("permission_type", button.permissionType)
// putJsonArray("specify_role_ids") {
// button.specifyRoleIds?.forEach { add(it) }
// }
// putJsonArray("specify_tinyids") {
// button.specifyTinyids?.forEach { add(it) }
// }
// })
// }
// }
// })
// }
// }
// put("bot_appid", keyboard.botAppid)
// }.toString()
// )
// )
// }
}

View File

@ -8,6 +8,9 @@ import moe.fuqiuluo.qqinterface.servlet.CardSvc
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc
import moe.fuqiuluo.qqinterface.servlet.msg.toJson
import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
import moe.fuqiuluo.qqinterface.servlet.transfile.*
import moe.fuqiuluo.qqinterface.servlet.transfile.PictureResource
import moe.fuqiuluo.qqinterface.servlet.transfile.Private
@ -18,6 +21,7 @@ import moe.fuqiuluo.shamrock.helper.ActionMsgException
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.LogicException
import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToMessageElements
import moe.fuqiuluo.shamrock.helper.ParamsException
import moe.fuqiuluo.shamrock.tools.*
import moe.fuqiuluo.shamrock.utils.DeflateTools
@ -35,35 +39,32 @@ import kotlin.random.nextULong
internal typealias IMessageElementMaker = suspend (Int, Long, String, JsonObject) -> Result<Elem>
internal object MessageElementMaker {
internal object ElemMaker {
private val makerArray = hashMapOf(
"text" to MessageElementMaker::createTextElem,
"at" to MessageElementMaker::createAtElem,
"face" to MessageElementMaker::createFaceElem,
"pic" to MessageElementMaker::createImageElem,
"image" to MessageElementMaker::createImageElem,
"text" to ElemMaker::createTextElem,
"at" to ElemMaker::createAtElem,
"face" to ElemMaker::createFaceElem,
"pic" to ElemMaker::createImageElem,
"image" to ElemMaker::createImageElem,
// "voice" to MessageElementMaker::createRecordElem,
// "record" to MessageElementMaker::createRecordElem,
// "video" to MessageElementMaker::createVideoElem,
"markdown" to MessageElementMaker::createMarkdownElem,
"button" to MessageElementMaker::createButtonElem,
"inline_keyboard" to MessageElementMaker::createButtonElem,
// "dice" to MessageElementMaker::createDiceElem,
// "rps" to MessageElementMaker::createRpsElem,
"basketball" to MessageElementMaker::createBasketballElem,
"new_dice" to MessageElementMaker::createNewDiceElem,
"new_rps" to MessageElementMaker::createNewRpsElem,
"poke" to MessageElementMaker::createPokeElem,
"markdown" to ElemMaker::createMarkdownElem,
"button" to ElemMaker::createButtonElem,
"inline_keyboard" to ElemMaker::createButtonElem,
"dice" to ElemMaker::createNewDiceElem,
"rps" to ElemMaker::createNewRpsElem,
"poke" to ElemMaker::createPokeElem,
// "anonymous" to MessageElementMaker::createAnonymousElem,
// "share" to MessageElementMaker::createShareElem,
// "contact" to MessageElementMaker::createContactElem,
// "location" to MessageElementMaker::createLocationElem,
// "music" to MessageElementMaker::createMusicElem,
"reply" to MessageElementMaker::createReplyElem,
"reply" to ElemMaker::createReplyElem,
// "touch" to MessageElementMaker::createTouchElem,
// "weather" to MessageElementMaker::createWeatherElem,
"json" to MessageElementMaker::createJsonElem,
//"node" to MessageMaker::createNodeElem,
"weather" to ElemMaker::createWeatherElem,
"json" to ElemMaker::createJsonElem,
// "node" to MessageMaker::createNodeElem,
//"multi_msg" to MessageMaker::createLongMsgStruct,
//"bubble_face" to MessageElementMaker::createBubbleFaceElem,
)
@ -111,7 +112,11 @@ internal object MessageElementMaker {
else -> {
qq = qqStr.toLong()
type = 0
"@" + (data["name"].asStringOrNull ?: GroupSvc.getTroopMemberInfoByUinV2(peerId.toLong(), qq, true)
"@" + (data["name"].asStringOrNull ?: GroupSvc.getTroopMemberInfoByUinV2(
peerId.toLong(),
qq,
true
)
.let {
val info = it.getOrNull()
if (info == null)
@ -164,9 +169,31 @@ internal object MessageElementMaker {
data: JsonObject
): Result<Elem> {
data.checkAndThrow("id")
val elem = Elem(
face = FaceMsg(data["id"].asInt)
)
val faceId = data["id"].asInt
val elem = if (data["big"].asBooleanOrNull == true) {
Elem(
commonElem = CommonElem(
serviceType = 37,
elem = QFaceExtra(
packId = "1",
stickerId = "1",
faceId = faceId,
field4 = 1,
field5 = 1,
result = "",
faceText = "", //todo 表情名字
field9 = 1
).toByteArray(),
businessType = 1
)
)
} else {
Elem(
face = FaceMsg(
index = faceId
)
)
}
return Result.success(elem)
}
@ -259,7 +286,7 @@ internal object MessageElementMaker {
width = picWidth.toUInt(),
height = picHeight.toUInt(),
size = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath).toUInt(),
origin = if (isOriginal) 1u else 0u,
origin = isOriginal,
thumbWidth = 0u,
thumbHeight = 0u,
pbReserve = CustomFace.Companion.PbReserve(field1 = 0)
@ -279,7 +306,7 @@ internal object MessageElementMaker {
picHeight = picWidth.toUInt(),
picWidth = picHeight.toUInt(),
resId = "".toByteArray(),
original = if (isOriginal) 1u else 0u, // true
original = isOriginal, // true
pbReserve = NotOnlineImage.Companion.PbReserve(field1 = 0)
)
)
@ -324,7 +351,7 @@ internal object MessageElementMaker {
),
type = 0u,
pbReserve = SourceMsg.Companion.PbReserve(
field3 = Random.nextULong(),
msgRand = Random.nextInt().toULong(),
field8 = Random.nextInt(0, 10000)
),
)
@ -340,10 +367,19 @@ internal object MessageElementMaker {
senderUin = msg.senderUin.toULong(),
time = msg.msgTime.toULong(),
flag = 1u,
// elems = msg.elements.toSegments(),
elems = messageArrayToMessageElements(
msg.chatType,
msg.msgId,
msg.peerUin.toString(),
msg.elements.toSegments(
msg.chatType,
if (msg.chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(),
msg.channelId ?: msg.peerUin.toString()
).toJson()
).second,
type = 0u,
pbReserve = SourceMsg.Companion.PbReserve(
field3 = Random.nextULong(),
msgRand = Random.nextULong(),
senderUid = msg.senderUid,
receiverUid = TicketSvc.getUid(),
field8 = Random.nextInt(0, 10000)
@ -370,6 +406,38 @@ internal object MessageElementMaker {
return Result.success(elem)
}
private suspend fun createWeatherElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
var code = data["code"].asIntOrNull
if (code == null) {
data.checkAndThrow("city")
val city = data["city"].asString
code = WeatherSvc.searchCity(city).onFailure {
LogCenter.log("无法获取城市天气: $city", Level.ERROR)
}.getOrNull()?.firstOrNull()?.adcode
}
if (code != null) {
WeatherSvc.fetchWeatherCard(code).onSuccess {
// 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
)
}.onFailure {
LogCenter.log("无法发送天气分享", Level.ERROR)
}
}
return Result.failure(ActionMsgException)
}
private suspend fun createPokeElem(
chatType: Int,
msgId: Long,
@ -391,31 +459,6 @@ internal object MessageElementMaker {
return Result.success(elem)
}
private suspend fun createBasketballElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
val elem = Elem(
commonElem = CommonElem(
serviceType = 37,
elem = QFaceExtra(
packId = "1",
stickerId = "13",
faceId = 114,
field4 = 1,
field5 = 2,
field6 = "",
faceText = "/篮球",
field9 = 1
).toByteArray(),
businessType = 2
)
)
return Result.success(elem)
}
private suspend fun createNewDiceElem(
chatType: Int,
msgId: Long,
@ -431,7 +474,7 @@ internal object MessageElementMaker {
faceId = 358,
field4 = 1,
field5 = 2,
field6 = "",
result = "",
faceText = "/骰子",
field9 = 1
).toByteArray(),
@ -456,7 +499,7 @@ internal object MessageElementMaker {
faceId = 359,
field4 = 1,
field5 = 2,
field6 = "",
result = "",
faceText = "/包剪锤",
field9 = 1
).toByteArray(),
@ -489,19 +532,20 @@ internal object MessageElementMaker {
peerId: String,
data: JsonObject
): Result<Elem> {
data.checkAndThrow("rows")
data.checkAndThrow("buttons")
val elem = Elem(
commonElem = CommonElem(
serviceType = 46,
elem = ButtonExtra(
field1 = Object1(
rows = data["rows"].asJsonArray.map { row ->
rows = data["buttons"].asJsonArray.map { row ->
Row(buttons = row.asJsonArray.map {
val button = it.asJsonObject
val renderData = button["render_data"].asJsonObject
val action = button["action"].asJsonObject
val permission = action["permission"].asJsonObject
Button(
id = button["id"].asIntOrNull,
id = button["id"].asStringOrNull,
renderData = RenderData(
label = renderData["label"].asString,
visitedLabel = renderData["visited_label"].asString,
@ -510,9 +554,9 @@ internal object MessageElementMaker {
action = Action(
type = action["type"].asInt,
permission = Permission(
type = action["permission"].asJsonObject["type"].asInt,
specifyRoleIds = action["permission"].asJsonObject["specify_role_ids"].asJsonArrayOrNull?.map { id -> id.asString },
specifyUserIds = action["permission"].asJsonObject["specify_user_ids"].asJsonArrayOrNull?.map { id -> id.asString }
type = permission["type"].asInt,
specifyRoleIds = permission["specify_role_ids"].asJsonArrayOrNull?.map { id -> id.asString },
specifyUserIds = permission["specify_user_ids"].asJsonArrayOrNull?.map { id -> id.asString }
),
unsupportTips = action["unsupport_tips"].asString,
data = action["data"].asString,

View File

@ -333,7 +333,6 @@ internal object NtMsgElementMaker {
LogCenter.log("无法发送天气分享", Level.ERROR)
}
}
return Result.failure(ActionMsgException)
}

View File

@ -16,7 +16,7 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.msg.maker.MessageElementMaker
import moe.fuqiuluo.qqinterface.servlet.msg.maker.ElemMaker
import moe.fuqiuluo.qqinterface.servlet.msg.maker.NtMsgElementMaker
import moe.fuqiuluo.shamrock.helper.db.MessageDB
import moe.fuqiuluo.shamrock.helper.db.MessageMapping
@ -291,7 +291,7 @@ internal object MessageHelper {
suspend fun messageArrayToMsgElements(
chatType: Int,
msgId: Long,
targetUin: String,
peerId: String,
messageList: JsonArray
): Pair<Boolean, ArrayList<MsgElement>> {
val msgList = arrayListOf<MsgElement>()
@ -302,7 +302,7 @@ internal object MessageHelper {
if (maker != null) {
try {
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
maker(chatType, msgId, targetUin, data).onSuccess { msgElem ->
maker(chatType, msgId, peerId, data).onSuccess { msgElem ->
msgList.add(msgElem)
}.onFailure {
if (it.javaClass != ActionMsgException::class.java) {
@ -325,18 +325,18 @@ internal object MessageHelper {
suspend fun messageArrayToMessageElements(
chatType: Int,
msgId: Long,
targetUin: String,
peerId: String,
messageList: JsonArray
): Pair<Boolean, ArrayList<Elem>> {
val msgList = arrayListOf<Elem>()
var hasActionMsg = false
messageList.forEach {
val msg = it.jsonObject
val maker = MessageElementMaker[msg["type"].asString]
val maker = ElemMaker[msg["type"].asString]
if (maker != null) {
try {
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
maker(chatType, msgId, targetUin, data).onSuccess { msgElem ->
maker(chatType, msgId, peerId, data).onSuccess { msgElem ->
msgList.add(msgElem)
}.onFailure {
if (it.javaClass != ActionMsgException::class.java) {
@ -431,11 +431,11 @@ internal object MessageHelper {
params[key] = value.json
}
}
val data = hashMapOf(
val data = mapOf(
"type" to it["_type"]!!.json,
"data" to JsonObject(params)
)
arrayList.add(JsonObject(data))
arrayList.add(data.json)
}
return arrayList.jsonArray
}

View File

@ -8,17 +8,17 @@ import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.symbols.OneBotHandler
@OneBotHandler("get_group_root_files")
internal object GetGroupRootFiles: IActionHandler() {
internal object GetGroupRootFiles : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getLong("group_id")
return invoke(groupId, session.echo)
}
suspend operator fun invoke(groupId: Long, echo: JsonElement = EmptyJsonString): String {
FileSvc.getGroupRootFiles(groupId).onSuccess {
return ok(it, echo = echo)
}.getOrNull()
return error(why = "获取失败", echo = echo)
return ok(
FileSvc.getGroupRootFiles(groupId).getOrElse { return error(why = "获取失败: $it", echo = echo) },
echo = echo
)
}
override val requiredParams: Array<String> = arrayOf("group_id")

View File

@ -65,16 +65,6 @@ internal object GetHistoryMsg : IActionHandler() {
val msgList = ArrayList<MessageDetail>().apply {
addAll(result.data!!.map { msg ->
val msgHash = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId)
MessageHelper.saveMsgMappingNotExist(
hash = msgHash,
qqMsgId = msg.msgId,
chatType = msg.chatType,
subChatType = msg.chatType,
peerId = msg.peerUin.toString(),
msgSeq = msg.msgSeq.toInt(),
time = msg.msgTime,
subPeerId = msg.channelId ?: msg.peerUin.toString()
)
MessageDetail(
time = msg.msgTime.toInt(),
msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),

View File

@ -80,19 +80,18 @@ internal object SendForwardMessage : IActionHandler() {
fromId: String = peerId,
echo: JsonElement = EmptyJsonString
): String {
kotlin.runCatching {
var uid: String? = null
var groupUin: String? = null
var uid: String? = null
var groupUin: String? = null
var i = -1
val desc = MutableList(messages.size) { "" }
var i = -1
val desc = MutableList(messages.size) { "" }
val msgs = messages.map { msg ->
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 {
LogCenter.log("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it", Level.WARN)
return@map null
error("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it")
}
if (record.chatType == MsgConstant.KCHATTYPEGROUP) groupUin = record.peerUin.toString()
if (record.chatType == MsgConstant.KCHATTYPEC2C) uid = record.peerUid
@ -147,21 +146,21 @@ internal object SendForwardMessage : IActionHandler() {
).also {
desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": "
}.map {
when (it.type) {
"text" -> desc[i] += it.data["text"] as String
"at" -> desc[i] += "@${it.data["name"] as String? ?: it.data["qq"] as String}"
"face" -> desc[i] += "[表情]"
"voice" -> desc[i] += "[语音]"
"node" -> desc[i] += "[合并转发消息]"
desc[++i] += when (it.type) {
"text" -> it.data["text"] as String
"at" -> "@${it.data["name"] as String? ?: it.data["qq"] as String}"
"face" -> "[表情]"
"voice" -> "[语音]"
"node" -> "[合并转发消息]"
"markdown" -> "[Markdown消息]"
"button" -> "[Button类型]"
else -> "[未知消息类型]"
}
it.toJson()
}.json
).also {
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
if (it.second.isEmpty() && !it.first)
error("消息合成失败,请查看日志或者检查输入。")
}.second
)
)
@ -198,84 +197,91 @@ internal object SendForwardMessage : IActionHandler() {
body = MsgBody(
richText = RichText(
elements = MessageHelper.messageArrayToMessageElements(
1,
Random.nextLong(),
data["uin"]?.asString ?: TicketSvc.getUin(),
when (data["content"]) {
chatType = MsgConstant.KCHATTYPEGROUP,
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()
}: "
desc[++i] =
(data["name"].asStringOrNull ?: data["uin"].asStringOrNull
?: TicketSvc.getNickname() )+ ": "
}.onEach {
val type = it.asJsonObject["type"].asString
val itData = it.asJsonObject["data"].asJsonObject
when (type) {
"text" -> desc[i] += itData["text"].asString
"at" -> desc[i] += "@${itData["name"].asStringOrNull ?: itData["qq"].asString}"
"face" -> desc[i] += "[表情]"
"image" -> desc[i] += "[图片]"
"voice" -> desc[i] += "[语音]"
"node" -> desc[i] += "[合并转发消息]"
desc[i] += when (type) {
"text" -> itData["text"].asString
"at" -> "@${itData["name"].asStringOrNull ?: itData["qq"].asString}"
"face" -> "[表情]"
"image" -> "[图片]"
"voice" -> "[语音]"
"node" -> "[合并转发消息]"
"markdown" -> "[Markdown消息]"
"button" -> "[Button类型]"
else -> "[未知消息类型]"
}
}
).also {
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
if (it.second.isEmpty() && !it.first)
error("消息合成失败,请查看日志或者检查输入。")
}.second
)
)
)
} else {
LogCenter.log("消息节点缺少id或content字段", Level.WARN)
null
error("消息节点缺少id或content字段")
}
}.filterNotNull().ifEmpty { return logic("消息节点为空", echo) }
}.getOrElse {
LogCenter.log("消息节点解析失败:$it", Level.WARN)
null
}
}.filterNotNull().ifEmpty { return logic("消息节点为空", echo) }
val resid = MsgSvc.sendMultiMsg(uid ?: TicketSvc.getUid(), groupUin, msgs)
kotlin.runCatching {
val resid = MsgSvc.uploadMultiMsg(uid ?: TicketSvc.getUid(), groupUin, msgs)
.getOrElse { return logic(it.message ?: "", echo) }
val uniseq = UUID.randomUUID().toString().uppercase()
val result = MsgSvc.sendToAio(
chatType, peerId,
listOf(
hashMapOf(
mapOf(
"type" to "json",
"data" to hashMapOf(
"data" to hashMapOf(
"data" to mapOf(
"data" to mapOf(
"app" to "com.tencent.multimsg",
"config" to hashMapOf(
"config" to mapOf(
"autosize" to 1,
"forward" to 1,
"round" to 1,
"type" to "normal",
"width" to 300
).json,
),
"desc" to "[聊天记录]",
"extra" to hashMapOf(
"extra" to mapOf(
"filename" to uniseq,
"tsum" to 2
).json.toString(),
"meta" to hashMapOf(
"detail" to hashMapOf(
"meta" to mapOf(
"detail" to mapOf(
"news" to desc.slice(0..if (i < 3) i else 3)
.map { hashMapOf("text" to it).json }.json,
.map { mapOf("text" to it) },
"resid" to resid,
"source" to "群聊的聊天记录",
"summary" to "查看${msgs.size}条转发消息",
"uniseq" to uniseq
).json
).json,
)
),
"prompt" to "[聊天记录]",
"ver" to "0.0.0.5",
"view" to "contact"
).json,
),
"resid" to resid
).json
).json
)
)
).json, fromId, 3
).getOrElse { return logic(it.message ?: "", echo) }
@ -286,7 +292,7 @@ internal object SendForwardMessage : IActionHandler() {
), echo = echo
)
}.onFailure {
return error("error: $it", echo)
return error("合并转发消息失败: $it", echo)
}
return logic("合并转发消息失败(unknown error)", echo)
}

View File

@ -21,18 +21,19 @@ internal object SendMsgByResid : IActionHandler() {
private val msgSeq = atomic(1000)
override suspend fun internalHandle(session: ActionSession): String {
val resid = session.getString("resid")
val peerId = session.getString("peer")
val msgType = session.getStringOrNull("message_type") ?: "group"
return invoke(peerId, resid, msgType, session.echo)
val resId = session.getString("res_id")
val peerId = session.getString("peer_id")
val messageType = session.getString("message_type")
invoke(resId, peerId, messageType)
return ok("ok", session.echo)
}
suspend operator fun invoke(peerId: String, resId: String, type: String, echo: JsonElement = EmptyJsonString): String {
suspend operator fun invoke(peerId: String, resId: String, messageType: String, echo: JsonElement = EmptyJsonString): String {
val req = PbSendMsgReq(
routingHead = when (type) {
"group" ->RoutingHead(grp = Grp(peerId.toUInt()))
"private" ->RoutingHead( c2c = C2C(peerId.toUInt()))
else ->RoutingHead( grp = Grp(peerId.toUInt()))
routingHead = when (messageType) {
"group" -> RoutingHead(grp = Grp(peerId.toUInt()))
"private" -> RoutingHead(c2c = C2C(peerId.toUInt()))
else -> RoutingHead(grp = Grp(peerId.toUInt()))
},
contentHead = ContentHead(1, 0, 0, 0),
msgBody = MsgBody(
@ -41,7 +42,7 @@ internal object SendMsgByResid : IActionHandler() {
Elem(
generalFlags = GeneralFlags(
longTextFlag = 1u,
longTextResid = resId.toByteArray()
longTextResid = resId
)
)
)

View File

@ -1,59 +1,55 @@
package moe.fuqiuluo.shamrock.remote.api
import io.ktor.server.routing.Routing
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.TextElement
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.remote.action.handlers.SendMsgByResid
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
import moe.fuqiuluo.shamrock.tools.getOrPost
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
fun Routing.testAction() {
if(ShamrockVersion.contains("dev")) {
if (ShamrockConfig.isDev()) {
LogCenter.log("testAction is enabled.", Level.WARN)
} else {
return
}
/*
get("/test/createUidFromTinyId") {
val selfId = fetchOrThrow("selfId").toLong()
val peerId = fetchOrThrow("peerId").toLong()
call.respondText(NTServiceFetcher.kernelService.wrapperSession.msgService.createUidFromTinyId(selfId, peerId))
getOrPost("/send_msg_by_resid") {
val resId = fetchOrThrow("res_id")
val peerId = fetchOrThrow("peer_Id")
val messageType = fetchOrThrow("message_type")
call.respondText(SendMsgByResid(resId, peerId, messageType))
}
get("/test/addSendMsg") {
getOrPost("/createUidFromTinyId") {
val selfId = fetchOrThrow("selfId").toLong()
val peerId = fetchOrThrow("peerId")
call.respondText(NTServiceFetcher.kernelService.wrapperSession.msgService.createUidFromTinyId(selfId, peerId.toLong()))
}
getOrPost("/addSendMsg") {
val msgService = NTServiceFetcher.kernelService.wrapperSession.msgService
val msgId = msgService.getMsgUniqueId(System.currentTimeMillis())
msgService.addSendMsg(msgId, MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin()), arrayListOf(
MsgElement().apply {
elementType = MsgConstant.KELEMTYPETEXT
textElement = TextElement().apply {
content = "测试消息"
}
}
), hashMapOf())
call.respondText("ok")
}*/
/*
get("/test/getMsgs") {
kotlin.runCatching {
val msgService = NTServiceFetcher.kernelService.wrapperSession.msgService
val msgs = suspendCoroutine {
msgService.getMsgs(Contact(MsgConstant.KCHATTYPEGROUP, "884587317", ""), 0L, 20, true, object: IMsgOperateCallback{
override fun onResult(code: Int, why: String?, msgs: ArrayList<MsgRecord>?) {
it.resume(msgs)
msgService.addSendMsg(msgId,
MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin()),
arrayListOf(
MsgElement().apply {
elementType = MsgConstant.KELEMTYPETEXT
textElement = TextElement().apply {
content = "测试消息"
}
})
}
if (msgs == null) {
call.respondText("failed")
return@get
}
call.respondText("msg -> " + msgs.map { it.toCQCode() }.joinToString("\n"))
}.onFailure {
call.respondText("failed: ${it.stackTraceToString()}")
return@get
}
}*/
}
),
hashMapOf())
call.respondText("ok")
}
}

View File

@ -12,7 +12,6 @@ import moe.fuqiuluo.qqinterface.servlet.FriendSvc.requestFriendSystemMsgNew
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.GroupSvc.requestGroupSystemMsgNew
import moe.fuqiuluo.qqinterface.servlet.TicketSvc.getLongUin
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
import moe.fuqiuluo.shamrock.helper.ContactHelper
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
@ -30,11 +29,7 @@ import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.message.ContentHead
import protobuf.message.MsgBody
import protobuf.message.ResponseHead
import protobuf.message.multimedia.RichMediaForPicData
import protobuf.push.*
import java.util.regex.Pattern
private val RKEY_PATTERN = Pattern.compile("rkey=([A-Za-z0-9_-]+)")
internal object PrimitiveListener {
fun registerListener() {
@ -94,22 +89,9 @@ internal object PrimitiveListener {
}
private fun onGroupMessage(msgTime: Long, body: MsgBody) {
/*runCatching {
body.richText?.elements?.filter {
it.commonElem != null && it.commonElem!!.serviceType == 48
}?.map {
it.commonElem!!.elem!!.decodeProtobuf<RichMediaForPicData>()
}?.forEach {
it.display?.show?.download?.url?.let {
RKEY_PATTERN.matcher(it).takeIf {
it.find()
}?.group(1)?.let { rkey ->
LogCenter.log("更新NT RKEY成功$rkey")
RichProtoSvc.multiMediaRKey = rkey
}
}
}
}*/
runCatching {
}
}
private suspend fun onC2CPoke(msgTime: Long, body: MsgBody) {

View File

@ -42,35 +42,39 @@ val String.asJson: JsonElement
val String.asJsonObject: JsonObject
get() = Json.parseToJsonElement(this).asJsonObject
val Collection<Any>.json: JsonArray
val Collection<Any?>.json: JsonArray
get() {
val arrayList = arrayListOf<JsonElement>()
forEach {
when (it) {
is JsonElement -> arrayList.add(it)
is Number -> arrayList.add(it.json)
is String -> arrayList.add(it.json)
is Boolean -> arrayList.add(it.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}")
if (it != null) {
when (it) {
is JsonElement -> arrayList.add(it)
is Number -> arrayList.add(it.json)
is String -> arrayList.add(it.json)
is Boolean -> arrayList.add(it.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}")
}
}
}
return arrayList.jsonArray
}
val Map<String, Any>.json: JsonObject
val Map<String, Any?>.json: JsonObject
get() {
val map = hashMapOf<String, JsonElement>()
forEach { (key, any) ->
when (any) {
is JsonElement -> map[key] = any
is Number -> map[key] = any.json
is String -> map[key] = any.json
is Boolean -> map[key] = any.json
is Map<*, *> -> map[key] = (any as Map<String, Any>).json
is Collection<*> -> map[key] = (any as Collection<Any>).json
else -> error("unknown object type: ${any::class.java}")
if (any != null) {
when (any) {
is JsonElement -> map[key] = any
is Number -> map[key] = any.json
is String -> map[key] = any.json
is Boolean -> map[key] = any.json
is Map<*, *> -> map[key] = (any as Map<String, Any?>).json
is Collection<*> -> map[key] = (any as Collection<Any?>).json
else -> error("unknown object type: ${any::class.java}")
}
}
}
return map.jsonObject