From 96610b30e33741ceee10ca95a68e8c756d1589b3 Mon Sep 17 00:00:00 2001 From: Him188 Date: Mon, 20 Apr 2020 21:26:16 +0800 Subject: [PATCH] Support CustomMessage --- .../mirai/qqandroid/message/convension.kt | 77 +++++-- .../kotlin/samples/CustomMessageSamples.kt | 34 +++ .../message/data/CombinedMessage.kt | 2 +- .../message/data/CustomMessage.kt | 196 ++++++++++++++++++ .../net.mamoe.mirai/message/data/Message.kt | 5 +- .../net.mamoe.mirai/message/data/impl.kt | 16 +- 6 files changed, 305 insertions(+), 25 deletions(-) create mode 100644 mirai-core-qqandroid/src/commonTest/kotlin/samples/CustomMessageSamples.kt create mode 100644 mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/CustomMessage.kt diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/convension.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/convension.kt index e5889e57a..8fbac2032 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/convension.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/convension.kt @@ -34,6 +34,7 @@ private val UNSUPPORTED_MERGED_MESSAGE_PLAIN = PlainText("你的QQ暂不支持 private val UNSUPPORTED_POKE_MESSAGE_PLAIN = PlainText("[戳一戳]请使用最新版手机QQ体验新功能。") private val UNSUPPORTED_FLASH_MESSAGE_PLAIN = PlainText("[闪照]请使用新版手机QQ查看闪照。") +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @OptIn(MiraiInternalAPI::class, MiraiExperimentalAPI::class) internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: Boolean): MutableList { val elements = mutableListOf() @@ -87,6 +88,15 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: B when (it) { is PlainText -> elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = it.stringValue))) + is CustomMessage -> { + @Suppress("UNCHECKED_CAST") + elements.add( + ImMsgBody.Elem(customElem = ImMsgBody.CustomElem( + enumType = MIRAI_CUSTOM_ELEM_TYPE, + data = CustomMessage.serialize(it.getFactory() as CustomMessage.Factory, it) + )) + ) + } is At -> { elements.add(ImMsgBody.Elem(text = it.toJceData())) elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " "))) @@ -243,20 +253,23 @@ internal inline fun Iterable<*>.firstIsInstanceOrNull(): R? { return null } +internal val MIRAI_CUSTOM_ELEM_TYPE = "mirai".hashCode() // 103904510 + @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") -@OptIn(MiraiInternalAPI::class, LowLevelAPI::class) -internal fun List.joinToMessageChain(groupIdOrZero: Long, bot: Bot, message: MessageChainBuilder) { +@OptIn(MiraiInternalAPI::class, LowLevelAPI::class, ExperimentalStdlibApi::class) +internal fun List.joinToMessageChain(groupIdOrZero: Long, bot: Bot, list: MessageChainBuilder) { // (this._miraiContentToString()) this.forEach { element -> when { - element.srcMsg != null -> - message.add(QuoteReply(OfflineMessageSourceImplBySourceMsg(element.srcMsg, bot, groupIdOrZero))) - element.notOnlineImage != null -> message.add(OnlineFriendImageImpl(element.notOnlineImage)) - element.customFace != null -> message.add(OnlineGroupImageImpl(element.customFace)) - element.face != null -> message.add(Face(element.face.index)) + element.srcMsg != null -> { + list.add(QuoteReply(OfflineMessageSourceImplBySourceMsg(element.srcMsg, bot, groupIdOrZero))) + } + element.notOnlineImage != null -> list.add(OnlineFriendImageImpl(element.notOnlineImage)) + element.customFace != null -> list.add(OnlineGroupImageImpl(element.customFace)) + element.face != null -> list.add(Face(element.face.index)) element.text != null -> { if (element.text.attr6Buf.isEmpty()) { - message.add(element.text.str.toMessage()) + list.add(element.text.str.toMessage()) } else { val id: Long element.text.attr6Buf.read { @@ -264,9 +277,9 @@ internal fun List.joinToMessageChain(groupIdOrZero: Long, bot: B id = readUInt().toLong() } if (id == 0L) { - message.add(AtAll) + list.add(AtAll) } else { - message.add(At._lowLevelConstructAtInstance(id, element.text.str)) + list.add(At._lowLevelConstructAtInstance(id, element.text.str)) } } } @@ -279,7 +292,7 @@ internal fun List.joinToMessageChain(groupIdOrZero: Long, bot: B else -> error("unknown compression flag=${element.lightApp.data[0]}") } } - message.add(LightApp(content)) + list.add(LightApp(content)) } element.richMsg != null -> { val content = runWithBugReport("解析 richMsg", { element.richMsg.template1.toUHexString() }) { @@ -301,7 +314,7 @@ internal fun List.joinToMessageChain(groupIdOrZero: Long, bot: B /** * [JsonMessage] */ - 1 -> message.add(JsonMessage(content)) + 1 -> list.add(JsonMessage(content)) /** * [LongMessage], [ForwardMessage] */ @@ -309,17 +322,17 @@ internal fun List.joinToMessageChain(groupIdOrZero: Long, bot: B val resId = this.firstIsInstanceOrNull()?.longTextResid if (resId != null) { - message.add(LongMessage(content, resId)) + list.add(LongMessage(content, resId)) } else { - message.add(ForwardMessage(content)) + list.add(ForwardMessage(content)) } } // 104 新群员入群的消息 else -> { if (element.richMsg.serviceId == 60 || content.startsWith(".joinToMessageChain(groupIdOrZero: Long, bot: B || element.generalFlags != null -> { } + element.customElem != null -> { + element.customElem.data.read { + kotlin.runCatching { + CustomMessage.deserialize(this) + }.fold( + onFailure = { + if (it is CustomMessage.Key.CustomMessageFullDataDeserializeInternalException) { + bot.logger.error("Internal error: " + + "exception while deserializing CustomMessage head data," + + " data=${element.customElem.data.toUHexString()}", it) + } else { + it as CustomMessage.Key.CustomMessageFullDataDeserializeUserException + bot.logger.error("User error: " + + "exception while deserializing CustomMessage body," + + " body=${it.body.toUHexString()}", it) + } + + }, + onSuccess = { + if (it != null) { + list.add(it) + } + } + ) + } + } element.commonElem != null -> { when (element.commonElem.serviceType) { 2 -> { val proto = element.commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype2.serializer()) - message.add(PokeMessage(proto.pokeType, proto.vaspokeId)) + list.add(PokeMessage(proto.pokeType, proto.vaspokeId)) } 3 -> { val proto = element.commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype3.serializer()) if (proto.flashTroopPic != null) { - message.add(GroupFlashImage(OnlineGroupImageImpl(proto.flashTroopPic))) + list.add(GroupFlashImage(OnlineGroupImageImpl(proto.flashTroopPic))) } if (proto.flashC2cPic != null) { - message.add(FriendFlashImage(OnlineFriendImageImpl(proto.flashC2cPic))) + list.add(FriendFlashImage(OnlineFriendImageImpl(proto.flashC2cPic))) } } } diff --git a/mirai-core-qqandroid/src/commonTest/kotlin/samples/CustomMessageSamples.kt b/mirai-core-qqandroid/src/commonTest/kotlin/samples/CustomMessageSamples.kt new file mode 100644 index 000000000..696f1706c --- /dev/null +++ b/mirai-core-qqandroid/src/commonTest/kotlin/samples/CustomMessageSamples.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_OVERRIDE") + +package samples + +import kotlinx.serialization.Serializable +import net.mamoe.mirai.message.data.CustomMessage +import net.mamoe.mirai.message.data.CustomMessageMetadata + + +/** + * 定义一个自定义消息类型. + * 在消息链中加入这个元素, 即可像普通元素一样发送和接收 (自动解析). + */ +@Serializable +data class CustomMessageIdentifier( + val identifier1: Long, + val custom: String +) : CustomMessageMetadata() { + // 可使用 JsonSerializerFactory 或 ProtoBufSerializerFactory + companion object Factory : CustomMessage.ProtoBufSerializerFactory( + "myMessage.CustomMessageIdentifier" + ) + + override fun getFactory(): Factory = Factory +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/CombinedMessage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/CombinedMessage.kt index 0008e48b3..9e7617d23 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/CombinedMessage.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/CombinedMessage.kt @@ -57,7 +57,7 @@ internal constructor( @OptIn(MiraiExperimentalAPI::class) override fun toString(): String { - return tail.toString() + left.toString() + return left.toString() + tail.toString() } override fun contentToString(): String { diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/CustomMessage.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/CustomMessage.kt new file mode 100644 index 000000000..3fee9e258 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/CustomMessage.kt @@ -0,0 +1,196 @@ +/* + * Copyright 2020 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.message.data + +import kotlinx.io.core.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.UnstableDefault +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonConfiguration +import kotlinx.serialization.protobuf.ProtoBuf +import kotlinx.serialization.protobuf.ProtoId +import net.mamoe.mirai.utils.* + + +/** + * 自定义消息 + * + * 它不会显示在消息文本中, 也不会被其他客户端识别. + * 只有 mirai 才能识别这些消息. + * + * 目前在回复时无法通过 [originalMessage] 获取自定义类型消息 + * + * **实现方法**: + * + * @sample samples.CustomMessageIdentifier 实现示例 + */ +@SinceMirai("0.38.0") +@MiraiExperimentalAPI +sealed class CustomMessage : SingleMessage { + /** + * 获取这个消息的工厂 + */ + abstract fun getFactory(): Factory + + /** + * 序列化和反序列化此消息的工厂, 将会自动注册. + * 应实现为 `object`. + * + * @see JsonSerializerFactory 使用 [Json] 作为序列模式的 [Factory] + * @see ProtoBufSerializerFactory 使用 [ProtoBuf] 作为序列模式的 [Factory] + */ + @MiraiExperimentalAPI + abstract class Factory( + /** + * 此类型消息的名称. + * 在发往服务器时使用此名称. + * 应确保唯一且不变. + */ + final override val typeName: String + ) : Message.Key { + + init { + @Suppress("LeakingThis") + register(this) + } + + /** + * 序列化此消息. + */ + @Throws(Exception::class) + abstract fun serialize(message: @UnsafeVariance M): ByteArray + + /** + * 从 [input] 读取此消息. + */ + @Throws(Exception::class) + abstract fun deserialize(input: ByteArray): @UnsafeVariance M + } + + /** + * 使用 [ProtoBuf] 作为序列模式的 [Factory]. + * 推荐使用此工厂 + */ + abstract class ProtoBufSerializerFactory(typeName: String) : + Factory(typeName) { + + /** + * 得到 [M] 的 [KSerializer]. + */ + abstract fun serializer(): KSerializer + + override fun serialize(message: M): ByteArray = ProtoBuf.dump(serializer(), message) + override fun deserialize(input: ByteArray): M = ProtoBuf.load(serializer(), input) + } + + /** + * 使用 [Json] 作为序列模式的 [Factory] + * 推荐在调试时使用此工厂 + */ + abstract class JsonSerializerFactory(typeName: String) : + Factory(typeName) { + + /** + * 得到 [M] 的 [KSerializer]. + */ + abstract fun serializer(): KSerializer + + @OptIn(UnstableDefault::class) + open val json = Json(JsonConfiguration.Default) + + override fun serialize(message: M): ByteArray = json.stringify(serializer(), message).toByteArray() + override fun deserialize(input: ByteArray): M = json.parse(serializer(), String(input)) + } + + companion object Key : Message.Key { + override val typeName: String get() = "CustomMessage" + private val factories: LockFreeLinkedList> = LockFreeLinkedList() + + internal fun register(factory: Factory) { + factories.removeIf { it::class == factory::class } + val exist = factories.asSequence().firstOrNull { it.typeName == factory.typeName } + if (exist != null) { + error("CustomMessage.Factory typeName ${factory.typeName} is already registered by ${exist::class.qualifiedName}") + } + factories.addLast(factory) + } + + @Serializable + class CustomMessageFullData( + @ProtoId(1) val miraiVersionFlag: Int, + @ProtoId(2) val typeName: String, + @ProtoId(3) val data: ByteArray + ) + + class CustomMessageFullDataDeserializeInternalException(cause: Throwable?) : RuntimeException(cause) + class CustomMessageFullDataDeserializeUserException(val body: ByteArray, cause: Throwable?) : + RuntimeException(cause) + + internal fun deserialize(fullData: ByteReadPacket): CustomMessage? { + val msg = kotlin.runCatching { + val length = fullData.readInt() + if (fullData.remaining != length.toLong()) { + return null + } + ProtoBuf.load(CustomMessageFullData.serializer(), fullData.readBytes(length)) + }.getOrElse { + throw CustomMessageFullDataDeserializeInternalException(it) + } + return kotlin.runCatching { + when (msg.miraiVersionFlag) { + 1 -> factories.asSequence().firstOrNull { it.typeName == msg.typeName }?.deserialize(msg.data) + else -> null + } + }.getOrElse { + throw CustomMessageFullDataDeserializeUserException(msg.data, it) + } + } + + internal fun serialize(factory: Factory, message: M): ByteArray = buildPacket { + ProtoBuf.dump(CustomMessageFullData.serializer(), CustomMessageFullData( + miraiVersionFlag = 1, + typeName = factory.typeName, + data = factory.serialize(message) + )).let { data -> + writeInt(data.size) + writeFully(data) + } + }.readBytes() + } +} + +/** + * 自定义消息元数据. + * + * @see CustomMessage 查看更多信息 + * @see ConstrainSingle 可实现此接口以保证消息链中只存在一个元素 + */ +@SinceMirai("0.38.0") +@MiraiExperimentalAPI +abstract class CustomMessageMetadata : CustomMessage(), MessageMetadata { + companion object Key : Message.Key { + override val typeName: String get() = "CustomMessageMetadata" + } + + open fun customToString(): ByteArray = customToStringImpl(this.getFactory()) + + final override fun toString(): String = + "[mirai:custom:${getFactory().typeName}:${String(customToString())}]" + + final override fun contentToString(): String = "" +} + + +@OptIn(MiraiExperimentalAPI::class) +internal fun T.customToStringImpl(factory: CustomMessage.Factory<*>): ByteArray { + @Suppress("UNCHECKED_CAST") + return (factory as CustomMessage.Factory).serialize(this) +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt index 6f31c9cfc..85e9ebe92 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Message.kt @@ -13,7 +13,6 @@ package net.mamoe.mirai.message.data import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.message.MessageReceipt -import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.PlannedRemoval import net.mamoe.mirai.utils.SinceMirai @@ -280,11 +279,9 @@ interface MessageMetadata : SingleMessage { /** * 约束一个 [MessageChain] 中只存在这一种类型的元素. 新元素将会替换旧元素, 保持原顺序. - * - * **MiraiExperimentalAPI**: 此 API 可能在将来版本修改 + * 实现此接口的元素将会在连接时自动处理替换. */ @SinceMirai("0.34.0") -@MiraiExperimentalAPI interface ConstrainSingle : MessageMetadata { val key: Message.Key } diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/impl.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/impl.kt index 230d3a2d3..e83658e35 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/impl.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/impl.kt @@ -202,7 +202,21 @@ internal fun MessageChain.firstOrNullImpl(key: Message.Key): M? FlashImage -> firstIsInstanceOrNull() GroupFlashImage -> firstIsInstanceOrNull() FriendFlashImage -> firstIsInstanceOrNull() - else -> null + CustomMessage -> firstIsInstanceOrNull() + CustomMessageMetadata -> firstIsInstanceOrNull() + else -> { + this.forEach { message -> + if (message is CustomMessage) { + @Suppress("UNCHECKED_CAST") + if (message.getFactory() == key) { + return message as? M + ?: error("cannot cast ${message::class.qualifiedName}. Make sure CustomMessage.getFactory returns a factory that has a generic type which is the same as the type of your CustomMessage") + } + } + } + + null + } } as M? /**