From 92ebe0c6a889329ceca55da1cebed332bc73d736 Mon Sep 17 00:00:00 2001 From: Simplxs Date: Sun, 25 Feb 2024 04:09:51 +0800 Subject: [PATCH] send_forward_msg(support markdown, button...) --- .../moe/fuqiuluo/shamrock/ExampleUnitTest.kt | 6 +- .../main/java/protobuf/message/ContentHead.kt | 2 +- .../protobuf/message/element/CustomFace.kt | 2 +- .../java/protobuf/message/element/FaceMsg.kt | 4 +- .../protobuf/message/element/GeneralFlags.kt | 2 +- .../message/element/NotOnlineImage.kt | 2 +- .../protobuf/message/element/SourceMsg.kt | 2 +- .../java/protobuf/message/element/TextMsg.kt | 11 +- .../message/element/commelem/ButtonExtra.kt | 6 +- .../message/element/commelem/QFaceExtra.kt | 2 +- .../fuqiuluo/qqinterface/servlet/MsgSvc.kt | 2 +- .../qqinterface/servlet/msg/Converter.kt | 14 +- .../qqinterface/servlet/msg/MessageSegment.kt | 6 +- .../servlet/msg/converter/ElemConverter.kt | 621 ++++++++++++++++++ .../msg/converter/MessageElementConverter.kt | 568 ---------------- .../{MessageElementMaker.kt => ElemMaker.kt} | 166 +++-- .../servlet/msg/maker/NtMsgElementMaker.kt | 1 - .../fuqiuluo/shamrock/helper/MessageHelper.kt | 16 +- .../action/handlers/GetGroupRootFiles.kt | 10 +- .../remote/action/handlers/GetHistoryMsg.kt | 10 - .../action/handlers/SendForwardMessage.kt | 114 ++-- .../remote/action/handlers/SendMsgByResid.kt | 21 +- .../shamrock/remote/api/TestAction.kt | 80 ++- .../service/listener/PrimitiveListener.kt | 24 +- .../java/moe/fuqiuluo/shamrock/tools/Json.kt | 40 +- 25 files changed, 910 insertions(+), 822 deletions(-) create mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt delete mode 100644 xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/MessageElementConverter.kt rename xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/{MessageElementMaker.kt => ElemMaker.kt} (78%) diff --git a/app/src/test/java/moe/fuqiuluo/shamrock/ExampleUnitTest.kt b/app/src/test/java/moe/fuqiuluo/shamrock/ExampleUnitTest.kt index 3731c6d..b1126d4 100644 --- a/app/src/test/java/moe/fuqiuluo/shamrock/ExampleUnitTest.kt +++ b/app/src/test/java/moe/fuqiuluo/shamrock/ExampleUnitTest.kt @@ -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() { } } \ No newline at end of file diff --git a/protobuf/src/main/java/protobuf/message/ContentHead.kt b/protobuf/src/main/java/protobuf/message/ContentHead.kt index cfc065b..eb8dfc0 100644 --- a/protobuf/src/main/java/protobuf/message/ContentHead.kt +++ b/protobuf/src/main/java/protobuf/message/ContentHead.kt @@ -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 diff --git a/protobuf/src/main/java/protobuf/message/element/CustomFace.kt b/protobuf/src/main/java/protobuf/message/element/CustomFace.kt index 8c81f39..91bba1d 100644 --- a/protobuf/src/main/java/protobuf/message/element/CustomFace.kt +++ b/protobuf/src/main/java/protobuf/message/element/CustomFace.kt @@ -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, diff --git a/protobuf/src/main/java/protobuf/message/element/FaceMsg.kt b/protobuf/src/main/java/protobuf/message/element/FaceMsg.kt index c396b2f..fe7b303 100644 --- a/protobuf/src/main/java/protobuf/message/element/FaceMsg.kt +++ b/protobuf/src/main/java/protobuf/message/element/FaceMsg.kt @@ -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, -) \ No newline at end of file + ) \ No newline at end of file diff --git a/protobuf/src/main/java/protobuf/message/element/GeneralFlags.kt b/protobuf/src/main/java/protobuf/message/element/GeneralFlags.kt index 105aa59..3068460 100644 --- a/protobuf/src/main/java/protobuf/message/element/GeneralFlags.kt +++ b/protobuf/src/main/java/protobuf/message/element/GeneralFlags.kt @@ -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, diff --git a/protobuf/src/main/java/protobuf/message/element/NotOnlineImage.kt b/protobuf/src/main/java/protobuf/message/element/NotOnlineImage.kt index e9e772a..c3d5d79 100644 --- a/protobuf/src/main/java/protobuf/message/element/NotOnlineImage.kt +++ b/protobuf/src/main/java/protobuf/message/element/NotOnlineImage.kt @@ -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, diff --git a/protobuf/src/main/java/protobuf/message/element/SourceMsg.kt b/protobuf/src/main/java/protobuf/message/element/SourceMsg.kt index 53b4cd4..59c9be9 100644 --- a/protobuf/src/main/java/protobuf/message/element/SourceMsg.kt +++ b/protobuf/src/main/java/protobuf/message/element/SourceMsg.kt @@ -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, diff --git a/protobuf/src/main/java/protobuf/message/element/TextMsg.kt b/protobuf/src/main/java/protobuf/message/element/TextMsg.kt index b16405e..84fd6e5 100644 --- a/protobuf/src/main/java/protobuf/message/element/TextMsg.kt +++ b/protobuf/src/main/java/protobuf/message/element/TextMsg.kt @@ -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, -) \ No newline at end of file + @ProtoNumber(12) val pbReserve: PbReserve? = null, +){ + companion object { + @Serializable + data class PbReserve( + @ProtoNumber(1) val field1: String? = null, // [打 call]] 请使用最新版手机 QQ 体验新功能 + ) + } +} \ No newline at end of file diff --git a/protobuf/src/main/java/protobuf/message/element/commelem/ButtonExtra.kt b/protobuf/src/main/java/protobuf/message/element/commelem/ButtonExtra.kt index e9fce5c..19b56ad 100644 --- a/protobuf/src/main/java/protobuf/message/element/commelem/ButtonExtra.kt +++ b/protobuf/src/main/java/protobuf/message/element/commelem/ButtonExtra.kt @@ -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 diff --git a/protobuf/src/main/java/protobuf/message/element/commelem/QFaceExtra.kt b/protobuf/src/main/java/protobuf/message/element/commelem/QFaceExtra.kt index 15f992d..7f97523 100644 --- a/protobuf/src/main/java/protobuf/message/element/commelem/QFaceExtra.kt +++ b/protobuf/src/main/java/protobuf/message/element/commelem/QFaceExtra.kt @@ -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 diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt index 5b8b83d..9345154 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt @@ -222,7 +222,7 @@ internal object MsgSvc : BaseSvc() { } } - suspend fun sendMultiMsg( + suspend fun uploadMultiMsg( uid: String, groupUin: String?, messages: List, diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt index b25e997..bfbb846 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt @@ -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.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 { diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt index 8fd3c73..cea7d17 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt @@ -8,10 +8,10 @@ import moe.fuqiuluo.shamrock.tools.json internal data class MessageSegment( val type: String, - val data: Map = emptyMap() + val data: Map = emptyMap() ) { fun toJson(): JsonObject { - return hashMapOf( + return mapOf( "type" to type.json, "data" to data.json ).json @@ -26,7 +26,7 @@ internal fun List.toJson(): JsonArray { internal fun List.toListMap(): List> { return this.map { - hashMapOf( + mapOf( "type" to it.type.json, "data" to it.data.json ).json diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt new file mode 100644 index 0000000..be31b4d --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/ElemConverter.kt @@ -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() + 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() + MessageSegment( + type = "markdown", + data = mapOf( + "content" to markdownExtra.content!! + ) + ) + } + + 46 -> { + val buttonExtra = commonElem.elem!!.decodeProtobuf() + 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), +// ) +// ) +// } + + +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/MessageElementConverter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/MessageElementConverter.kt deleted file mode 100644 index c85abff..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/converter/MessageElementConverter.kt +++ /dev/null @@ -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() -// ) -// ) -// } -} diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/MessageElementMaker.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/ElemMaker.kt similarity index 78% rename from xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/MessageElementMaker.kt rename to xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/ElemMaker.kt index 2545cc0..2a2d855 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/MessageElementMaker.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/ElemMaker.kt @@ -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 -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 { 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 { + 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 { - 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 { - 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, diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt index fbf4e0f..abee890 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/maker/NtMsgElementMaker.kt @@ -333,7 +333,6 @@ internal object NtMsgElementMaker { LogCenter.log("无法发送天气分享", Level.ERROR) } } - return Result.failure(ActionMsgException) } diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt index 4ae626c..9ca5a75 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt @@ -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> { val msgList = arrayListOf() @@ -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> { val msgList = arrayListOf() 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 } diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupRootFiles.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupRootFiles.kt index c0e41c6..80b8a1b 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupRootFiles.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetGroupRootFiles.kt @@ -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 = arrayOf("group_id") diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt index fca7778..ba0027b 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/GetHistoryMsg.kt @@ -65,16 +65,6 @@ internal object GetHistoryMsg : IActionHandler() { val msgList = ArrayList().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), diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt index 774c6d9..7bb3597 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt @@ -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) } diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMsgByResid.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMsgByResid.kt index 5bec0a1..8207ee4 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMsgByResid.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendMsgByResid.kt @@ -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 ) ) ) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/TestAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/TestAction.kt index bae396c..360fde5 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/TestAction.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/TestAction.kt @@ -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?) { - 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") + } } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt index e23cf81..987b49b 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt @@ -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() - }?.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) { diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt index 6f51897..7e20275 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Json.kt @@ -42,35 +42,39 @@ val String.asJson: JsonElement val String.asJsonObject: JsonObject get() = Json.parseToJsonElement(this).asJsonObject -val Collection.json: JsonArray +val Collection.json: JsonArray get() { val arrayList = arrayListOf() 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).json) - is Collection<*> -> arrayList.add((it as Collection).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).json) + is Collection<*> -> arrayList.add((it as Collection).json) + else -> error("unknown array type: ${it::class.java}") + } } } return arrayList.jsonArray } -val Map.json: JsonObject +val Map.json: JsonObject get() { val map = hashMapOf() 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).json - is Collection<*> -> map[key] = (any as Collection).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).json + is Collection<*> -> map[key] = (any as Collection).json + else -> error("unknown object type: ${any::class.java}") + } } } return map.jsonObject