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.Test
import org.junit.Assert.*
/** /**
* Example local unit test, which will execute on the development machine (host). * Example local unit test, which will execute on the development machine (host).
* *
* See [testing documentation](http://d.android.com/tools/testing). * See [testing documentation](http://d.android.com/tools/testing).
*/ */
class ExampleUnitTest { class ExampleUnitTest {
@Test @Test
fun addition_isCorrect() { fun test() {
assertEquals(4, 2 + 2)
} }
} }

View File

@ -15,7 +15,7 @@ data class ContentHead(
@ProtoNumber(8) val u6: Int? = null, @ProtoNumber(8) val u6: Int? = null,
@ProtoNumber(9) val u7: Int? = null, @ProtoNumber(9) val u7: Int? = null,
@ProtoNumber(11) val msgSeq: Long? = 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(14) val u4: Long? = null,
@ProtoNumber(15) val forwardHead: ForwardHead? = null, @ProtoNumber(15) val forwardHead: ForwardHead? = null,
@ProtoNumber(28) val u5: Long? = null @ProtoNumber(28) val u5: Long? = null

View File

@ -30,7 +30,7 @@ data class CustomFace(
@ProtoNumber(23) var height: UInt? = null, @ProtoNumber(23) var height: UInt? = null,
@ProtoNumber(24) var source: UInt? = null, @ProtoNumber(24) var source: UInt? = null,
@ProtoNumber(25) var size: 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(27) var thumbWidth: UInt? = null,
@ProtoNumber(28) var thumbHeight: UInt? = null, @ProtoNumber(28) var thumbHeight: UInt? = null,
@ProtoNumber(29) var showLen: UInt? = null, @ProtoNumber(29) var showLen: UInt? = null,

View File

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

View File

@ -12,7 +12,7 @@ data class GeneralFlags(
@ProtoNumber(4) val rpId: ByteArray? = null, @ProtoNumber(4) val rpId: ByteArray? = null,
@ProtoNumber(5) val prpFold: UInt? = null, @ProtoNumber(5) val prpFold: UInt? = null,
@ProtoNumber(6) val longTextFlag: 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(8) val groupType: UInt? = null,
@ProtoNumber(9) val toUinFlag: UInt? = null, @ProtoNumber(9) val toUinFlag: UInt? = null,
@ProtoNumber(10) val glamourLevel: 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(10) val resId: ByteArray? = null, // md5 + ".jpg"
@ProtoNumber(11) val flag: ByteArray? = null, @ProtoNumber(11) val flag: ByteArray? = null,
@ProtoNumber(12) val thumbUrl: String? = 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(14) val bigUrl: String? = null,
@ProtoNumber(15) val origUrl: String? = null, @ProtoNumber(15) val origUrl: String? = null,
@ProtoNumber(16) val bizType: UInt? = null, @ProtoNumber(16) val bizType: UInt? = null,

View File

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

View File

@ -10,5 +10,12 @@ data class TextMsg(
@ProtoNumber(3) val attr6Buf: ByteArray? = null, @ProtoNumber(3) val attr6Buf: ByteArray? = null,
@ProtoNumber(4) val attr7Buf: ByteArray? = null, @ProtoNumber(4) val attr7Buf: ByteArray? = null,
@ProtoNumber(11) val buf: 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 @Serializable
data class Button( data class Button(
@ProtoNumber(1) val id: Int? = null, @ProtoNumber(1) val id: String? = null,
@ProtoNumber(2) val renderData: RenderData? = null, @ProtoNumber(2) val renderData: RenderData? = null,
@ProtoNumber(3) val action: Action? = null, @ProtoNumber(3) val action: Action? = null,
) )
@ -41,8 +41,8 @@ data class Action(
@ProtoNumber(2) val permission: Permission? = null, @ProtoNumber(2) val permission: Permission? = null,
@ProtoNumber(4) val unsupportTips: String? = null, @ProtoNumber(4) val unsupportTips: String? = null,
@ProtoNumber(5) val data: String? = null, @ProtoNumber(5) val data: String? = null,
@ProtoNumber(6) val reply: Boolean? = null, @ProtoNumber(7) val reply: Boolean? = null,
@ProtoNumber(7) val enter: Boolean? = null, @ProtoNumber(8) val enter: Boolean? = null,
) )
@Serializable @Serializable

View File

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

View File

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

View File

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

View File

@ -8,10 +8,10 @@ import moe.fuqiuluo.shamrock.tools.json
internal data class MessageSegment( internal data class MessageSegment(
val type: String, val type: String,
val data: Map<String, Any> = emptyMap() val data: Map<String, Any?> = emptyMap()
) { ) {
fun toJson(): JsonObject { fun toJson(): JsonObject {
return hashMapOf( return mapOf(
"type" to type.json, "type" to type.json,
"data" to data.json "data" to data.json
).json ).json
@ -26,7 +26,7 @@ internal fun List<MessageSegment>.toJson(): JsonArray {
internal fun List<MessageSegment>.toListMap(): List<Map<String, JsonElement>> { internal fun List<MessageSegment>.toListMap(): List<Map<String, JsonElement>> {
return this.map { return this.map {
hashMapOf( mapOf(
"type" to it.type.json, "type" to it.type.json,
"data" to it.data.json "data" to it.data.json
).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.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.MsgSvc import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc 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.*
import moe.fuqiuluo.qqinterface.servlet.transfile.PictureResource import moe.fuqiuluo.qqinterface.servlet.transfile.PictureResource
import moe.fuqiuluo.qqinterface.servlet.transfile.Private 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.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.LogicException import moe.fuqiuluo.shamrock.helper.LogicException
import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToMessageElements
import moe.fuqiuluo.shamrock.helper.ParamsException import moe.fuqiuluo.shamrock.helper.ParamsException
import moe.fuqiuluo.shamrock.tools.* import moe.fuqiuluo.shamrock.tools.*
import moe.fuqiuluo.shamrock.utils.DeflateTools 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 typealias IMessageElementMaker = suspend (Int, Long, String, JsonObject) -> Result<Elem>
internal object MessageElementMaker { internal object ElemMaker {
private val makerArray = hashMapOf( private val makerArray = hashMapOf(
"text" to MessageElementMaker::createTextElem, "text" to ElemMaker::createTextElem,
"at" to MessageElementMaker::createAtElem, "at" to ElemMaker::createAtElem,
"face" to MessageElementMaker::createFaceElem, "face" to ElemMaker::createFaceElem,
"pic" to MessageElementMaker::createImageElem, "pic" to ElemMaker::createImageElem,
"image" to MessageElementMaker::createImageElem, "image" to ElemMaker::createImageElem,
// "voice" to MessageElementMaker::createRecordElem, // "voice" to MessageElementMaker::createRecordElem,
// "record" to MessageElementMaker::createRecordElem, // "record" to MessageElementMaker::createRecordElem,
// "video" to MessageElementMaker::createVideoElem, // "video" to MessageElementMaker::createVideoElem,
"markdown" to MessageElementMaker::createMarkdownElem, "markdown" to ElemMaker::createMarkdownElem,
"button" to MessageElementMaker::createButtonElem, "button" to ElemMaker::createButtonElem,
"inline_keyboard" to MessageElementMaker::createButtonElem, "inline_keyboard" to ElemMaker::createButtonElem,
// "dice" to MessageElementMaker::createDiceElem, "dice" to ElemMaker::createNewDiceElem,
// "rps" to MessageElementMaker::createRpsElem, "rps" to ElemMaker::createNewRpsElem,
"basketball" to MessageElementMaker::createBasketballElem, "poke" to ElemMaker::createPokeElem,
"new_dice" to MessageElementMaker::createNewDiceElem,
"new_rps" to MessageElementMaker::createNewRpsElem,
"poke" to MessageElementMaker::createPokeElem,
// "anonymous" to MessageElementMaker::createAnonymousElem, // "anonymous" to MessageElementMaker::createAnonymousElem,
// "share" to MessageElementMaker::createShareElem, // "share" to MessageElementMaker::createShareElem,
// "contact" to MessageElementMaker::createContactElem, // "contact" to MessageElementMaker::createContactElem,
// "location" to MessageElementMaker::createLocationElem, // "location" to MessageElementMaker::createLocationElem,
// "music" to MessageElementMaker::createMusicElem, // "music" to MessageElementMaker::createMusicElem,
"reply" to MessageElementMaker::createReplyElem, "reply" to ElemMaker::createReplyElem,
// "touch" to MessageElementMaker::createTouchElem, // "touch" to MessageElementMaker::createTouchElem,
// "weather" to MessageElementMaker::createWeatherElem, "weather" to ElemMaker::createWeatherElem,
"json" to MessageElementMaker::createJsonElem, "json" to ElemMaker::createJsonElem,
//"node" to MessageMaker::createNodeElem, // "node" to MessageMaker::createNodeElem,
//"multi_msg" to MessageMaker::createLongMsgStruct, //"multi_msg" to MessageMaker::createLongMsgStruct,
//"bubble_face" to MessageElementMaker::createBubbleFaceElem, //"bubble_face" to MessageElementMaker::createBubbleFaceElem,
) )
@ -111,7 +112,11 @@ internal object MessageElementMaker {
else -> { else -> {
qq = qqStr.toLong() qq = qqStr.toLong()
type = 0 type = 0
"@" + (data["name"].asStringOrNull ?: GroupSvc.getTroopMemberInfoByUinV2(peerId.toLong(), qq, true) "@" + (data["name"].asStringOrNull ?: GroupSvc.getTroopMemberInfoByUinV2(
peerId.toLong(),
qq,
true
)
.let { .let {
val info = it.getOrNull() val info = it.getOrNull()
if (info == null) if (info == null)
@ -164,9 +169,31 @@ internal object MessageElementMaker {
data: JsonObject data: JsonObject
): Result<Elem> { ): Result<Elem> {
data.checkAndThrow("id") data.checkAndThrow("id")
val elem = Elem( val faceId = data["id"].asInt
face = FaceMsg(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) return Result.success(elem)
} }
@ -259,7 +286,7 @@ internal object MessageElementMaker {
width = picWidth.toUInt(), width = picWidth.toUInt(),
height = picHeight.toUInt(), height = picHeight.toUInt(),
size = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath).toUInt(), size = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath).toUInt(),
origin = if (isOriginal) 1u else 0u, origin = isOriginal,
thumbWidth = 0u, thumbWidth = 0u,
thumbHeight = 0u, thumbHeight = 0u,
pbReserve = CustomFace.Companion.PbReserve(field1 = 0) pbReserve = CustomFace.Companion.PbReserve(field1 = 0)
@ -279,7 +306,7 @@ internal object MessageElementMaker {
picHeight = picWidth.toUInt(), picHeight = picWidth.toUInt(),
picWidth = picHeight.toUInt(), picWidth = picHeight.toUInt(),
resId = "".toByteArray(), resId = "".toByteArray(),
original = if (isOriginal) 1u else 0u, // true original = isOriginal, // true
pbReserve = NotOnlineImage.Companion.PbReserve(field1 = 0) pbReserve = NotOnlineImage.Companion.PbReserve(field1 = 0)
) )
) )
@ -324,7 +351,7 @@ internal object MessageElementMaker {
), ),
type = 0u, type = 0u,
pbReserve = SourceMsg.Companion.PbReserve( pbReserve = SourceMsg.Companion.PbReserve(
field3 = Random.nextULong(), msgRand = Random.nextInt().toULong(),
field8 = Random.nextInt(0, 10000) field8 = Random.nextInt(0, 10000)
), ),
) )
@ -340,10 +367,19 @@ internal object MessageElementMaker {
senderUin = msg.senderUin.toULong(), senderUin = msg.senderUin.toULong(),
time = msg.msgTime.toULong(), time = msg.msgTime.toULong(),
flag = 1u, 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, type = 0u,
pbReserve = SourceMsg.Companion.PbReserve( pbReserve = SourceMsg.Companion.PbReserve(
field3 = Random.nextULong(), msgRand = Random.nextULong(),
senderUid = msg.senderUid, senderUid = msg.senderUid,
receiverUid = TicketSvc.getUid(), receiverUid = TicketSvc.getUid(),
field8 = Random.nextInt(0, 10000) field8 = Random.nextInt(0, 10000)
@ -370,6 +406,38 @@ internal object MessageElementMaker {
return Result.success(elem) 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( private suspend fun createPokeElem(
chatType: Int, chatType: Int,
msgId: Long, msgId: Long,
@ -391,31 +459,6 @@ internal object MessageElementMaker {
return Result.success(elem) 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( private suspend fun createNewDiceElem(
chatType: Int, chatType: Int,
msgId: Long, msgId: Long,
@ -431,7 +474,7 @@ internal object MessageElementMaker {
faceId = 358, faceId = 358,
field4 = 1, field4 = 1,
field5 = 2, field5 = 2,
field6 = "", result = "",
faceText = "/骰子", faceText = "/骰子",
field9 = 1 field9 = 1
).toByteArray(), ).toByteArray(),
@ -456,7 +499,7 @@ internal object MessageElementMaker {
faceId = 359, faceId = 359,
field4 = 1, field4 = 1,
field5 = 2, field5 = 2,
field6 = "", result = "",
faceText = "/包剪锤", faceText = "/包剪锤",
field9 = 1 field9 = 1
).toByteArray(), ).toByteArray(),
@ -489,19 +532,20 @@ internal object MessageElementMaker {
peerId: String, peerId: String,
data: JsonObject data: JsonObject
): Result<Elem> { ): Result<Elem> {
data.checkAndThrow("rows") data.checkAndThrow("buttons")
val elem = Elem( val elem = Elem(
commonElem = CommonElem( commonElem = CommonElem(
serviceType = 46, serviceType = 46,
elem = ButtonExtra( elem = ButtonExtra(
field1 = Object1( field1 = Object1(
rows = data["rows"].asJsonArray.map { row -> rows = data["buttons"].asJsonArray.map { row ->
Row(buttons = row.asJsonArray.map { Row(buttons = row.asJsonArray.map {
val button = it.asJsonObject val button = it.asJsonObject
val renderData = button["render_data"].asJsonObject val renderData = button["render_data"].asJsonObject
val action = button["action"].asJsonObject val action = button["action"].asJsonObject
val permission = action["permission"].asJsonObject
Button( Button(
id = button["id"].asIntOrNull, id = button["id"].asStringOrNull,
renderData = RenderData( renderData = RenderData(
label = renderData["label"].asString, label = renderData["label"].asString,
visitedLabel = renderData["visited_label"].asString, visitedLabel = renderData["visited_label"].asString,
@ -510,9 +554,9 @@ internal object MessageElementMaker {
action = Action( action = Action(
type = action["type"].asInt, type = action["type"].asInt,
permission = Permission( permission = Permission(
type = action["permission"].asJsonObject["type"].asInt, type = permission["type"].asInt,
specifyRoleIds = action["permission"].asJsonObject["specify_role_ids"].asJsonArrayOrNull?.map { id -> id.asString }, specifyRoleIds = permission["specify_role_ids"].asJsonArrayOrNull?.map { id -> id.asString },
specifyUserIds = action["permission"].asJsonObject["specify_user_ids"].asJsonArrayOrNull?.map { id -> id.asString } specifyUserIds = permission["specify_user_ids"].asJsonArrayOrNull?.map { id -> id.asString }
), ),
unsupportTips = action["unsupport_tips"].asString, unsupportTips = action["unsupport_tips"].asString,
data = action["data"].asString, data = action["data"].asString,

View File

@ -333,7 +333,6 @@ internal object NtMsgElementMaker {
LogCenter.log("无法发送天气分享", Level.ERROR) LogCenter.log("无法发送天气分享", Level.ERROR)
} }
} }
return Result.failure(ActionMsgException) 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 kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import moe.fuqiuluo.qqinterface.servlet.MsgSvc 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.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
@ -291,7 +291,7 @@ internal object MessageHelper {
suspend fun messageArrayToMsgElements( suspend fun messageArrayToMsgElements(
chatType: Int, chatType: Int,
msgId: Long, msgId: Long,
targetUin: String, peerId: String,
messageList: JsonArray messageList: JsonArray
): Pair<Boolean, ArrayList<MsgElement>> { ): Pair<Boolean, ArrayList<MsgElement>> {
val msgList = arrayListOf<MsgElement>() val msgList = arrayListOf<MsgElement>()
@ -302,7 +302,7 @@ internal object MessageHelper {
if (maker != null) { if (maker != null) {
try { try {
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
maker(chatType, msgId, targetUin, data).onSuccess { msgElem -> maker(chatType, msgId, peerId, data).onSuccess { msgElem ->
msgList.add(msgElem) msgList.add(msgElem)
}.onFailure { }.onFailure {
if (it.javaClass != ActionMsgException::class.java) { if (it.javaClass != ActionMsgException::class.java) {
@ -325,18 +325,18 @@ internal object MessageHelper {
suspend fun messageArrayToMessageElements( suspend fun messageArrayToMessageElements(
chatType: Int, chatType: Int,
msgId: Long, msgId: Long,
targetUin: String, peerId: String,
messageList: JsonArray messageList: JsonArray
): Pair<Boolean, ArrayList<Elem>> { ): Pair<Boolean, ArrayList<Elem>> {
val msgList = arrayListOf<Elem>() val msgList = arrayListOf<Elem>()
var hasActionMsg = false var hasActionMsg = false
messageList.forEach { messageList.forEach {
val msg = it.jsonObject val msg = it.jsonObject
val maker = MessageElementMaker[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, targetUin, data).onSuccess { msgElem -> maker(chatType, msgId, peerId, data).onSuccess { msgElem ->
msgList.add(msgElem) msgList.add(msgElem)
}.onFailure { }.onFailure {
if (it.javaClass != ActionMsgException::class.java) { if (it.javaClass != ActionMsgException::class.java) {
@ -431,11 +431,11 @@ internal object MessageHelper {
params[key] = value.json params[key] = value.json
} }
} }
val data = hashMapOf( val data = mapOf(
"type" to it["_type"]!!.json, "type" to it["_type"]!!.json,
"data" to JsonObject(params) "data" to JsonObject(params)
) )
arrayList.add(JsonObject(data)) arrayList.add(data.json)
} }
return arrayList.jsonArray return arrayList.jsonArray
} }

View File

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

View File

@ -65,16 +65,6 @@ internal object GetHistoryMsg : IActionHandler() {
val msgList = ArrayList<MessageDetail>().apply { val msgList = ArrayList<MessageDetail>().apply {
addAll(result.data!!.map { msg -> addAll(result.data!!.map { msg ->
val msgHash = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId) 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( MessageDetail(
time = msg.msgTime.toInt(), time = msg.msgTime.toInt(),
msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType), msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),

View File

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

View File

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

View File

@ -1,59 +1,55 @@
package moe.fuqiuluo.shamrock.remote.api 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.Level
import moe.fuqiuluo.shamrock.helper.LogCenter 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() { fun Routing.testAction() {
if(ShamrockVersion.contains("dev")) { if (ShamrockConfig.isDev()) {
LogCenter.log("testAction is enabled.", Level.WARN) LogCenter.log("testAction is enabled.", Level.WARN)
} else { } else {
return return
} }
/* getOrPost("/send_msg_by_resid") {
get("/test/createUidFromTinyId") { val resId = fetchOrThrow("res_id")
val selfId = fetchOrThrow("selfId").toLong() val peerId = fetchOrThrow("peer_Id")
val peerId = fetchOrThrow("peerId").toLong() val messageType = fetchOrThrow("message_type")
call.respondText(NTServiceFetcher.kernelService.wrapperSession.msgService.createUidFromTinyId(selfId, peerId)) 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 msgService = NTServiceFetcher.kernelService.wrapperSession.msgService
val msgId = msgService.getMsgUniqueId(System.currentTimeMillis()) val msgId = msgService.getMsgUniqueId(System.currentTimeMillis())
msgService.addSendMsg(msgId, MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin()), arrayListOf( msgService.addSendMsg(msgId,
MsgElement().apply { MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin()),
elementType = MsgConstant.KELEMTYPETEXT arrayListOf(
textElement = TextElement().apply { MsgElement().apply {
content = "测试消息" 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)
} }
}) }
} ),
if (msgs == null) { hashMapOf())
call.respondText("failed") call.respondText("ok")
return@get }
}
call.respondText("msg -> " + msgs.map { it.toCQCode() }.joinToString("\n"))
}.onFailure {
call.respondText("failed: ${it.stackTraceToString()}")
return@get
}
}*/
} }

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
import moe.fuqiuluo.qqinterface.servlet.GroupSvc.requestGroupSystemMsgNew import moe.fuqiuluo.qqinterface.servlet.GroupSvc.requestGroupSystemMsgNew
import moe.fuqiuluo.qqinterface.servlet.TicketSvc.getLongUin 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.ContactHelper
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
@ -30,11 +29,7 @@ import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.message.ContentHead import protobuf.message.ContentHead
import protobuf.message.MsgBody import protobuf.message.MsgBody
import protobuf.message.ResponseHead import protobuf.message.ResponseHead
import protobuf.message.multimedia.RichMediaForPicData
import protobuf.push.* import protobuf.push.*
import java.util.regex.Pattern
private val RKEY_PATTERN = Pattern.compile("rkey=([A-Za-z0-9_-]+)")
internal object PrimitiveListener { internal object PrimitiveListener {
fun registerListener() { fun registerListener() {
@ -94,22 +89,9 @@ internal object PrimitiveListener {
} }
private fun onGroupMessage(msgTime: Long, body: MsgBody) { private fun onGroupMessage(msgTime: Long, body: MsgBody) {
/*runCatching { 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
}
}
}
}*/
} }
private suspend fun onC2CPoke(msgTime: Long, body: MsgBody) { private suspend fun onC2CPoke(msgTime: Long, body: MsgBody) {

View File

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