Bladeren bron

Support CustomMessage

Him188 5 jaren geleden
bovenliggende
commit
96610b30e3

+ 58 - 19
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<ImMsgBody.Elem> {
     val elements = mutableListOf<ImMsgBody.Elem>()
@@ -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<CustomMessage>, 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 <reified R> 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<ImMsgBody.Elem>.joinToMessageChain(groupIdOrZero: Long, bot: Bot, message: MessageChainBuilder) {
+@OptIn(MiraiInternalAPI::class, LowLevelAPI::class, ExperimentalStdlibApi::class)
+internal fun List<ImMsgBody.Elem>.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<ImMsgBody.Elem>.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<ImMsgBody.Elem>.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<ImMsgBody.Elem>.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<ImMsgBody.Elem>.joinToMessageChain(groupIdOrZero: Long, bot: B
                         val resId = this.firstIsInstanceOrNull<ImMsgBody.GeneralFlags>()?.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("<?")) {
-                            message.add(XmlMessage(element.richMsg.serviceId, content))
-                        } else message.add(ServiceMessage(element.richMsg.serviceId, content))
+                            list.add(XmlMessage(element.richMsg.serviceId, content))
+                        } else list.add(ServiceMessage(element.richMsg.serviceId, content))
                     }
                 }
             }
@@ -328,19 +341,45 @@ internal fun List<ImMsgBody.Elem>.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)))
                         }
                     }
                 }

+ 34 - 0
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<CustomMessageIdentifier>(
+        "myMessage.CustomMessageIdentifier"
+    )
+
+    override fun getFactory(): Factory = Factory
+}

+ 1 - 1
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 {

+ 196 - 0
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<out CustomMessage>
+
+    /**
+     * 序列化和反序列化此消息的工厂, 将会自动注册.
+     * 应实现为 `object`.
+     *
+     * @see JsonSerializerFactory 使用 [Json] 作为序列模式的 [Factory]
+     * @see ProtoBufSerializerFactory 使用 [ProtoBuf] 作为序列模式的 [Factory]
+     */
+    @MiraiExperimentalAPI
+    abstract class Factory<M : CustomMessage>(
+        /**
+         * 此类型消息的名称.
+         * 在发往服务器时使用此名称.
+         * 应确保唯一且不变.
+         */
+        final override val typeName: String
+    ) : Message.Key<M> {
+
+        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<M : CustomMessage>(typeName: String) :
+        Factory<M>(typeName) {
+
+        /**
+         * 得到 [M] 的 [KSerializer].
+         */
+        abstract fun serializer(): KSerializer<M>
+
+        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<M : CustomMessage>(typeName: String) :
+        Factory<M>(typeName) {
+
+        /**
+         * 得到 [M] 的 [KSerializer].
+         */
+        abstract fun serializer(): KSerializer<M>
+
+        @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<CustomMessage> {
+        override val typeName: String get() = "CustomMessage"
+        private val factories: LockFreeLinkedList<Factory<*>> = LockFreeLinkedList()
+
+        internal fun register(factory: Factory<out CustomMessage>) {
+            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 <M : CustomMessage> serialize(factory: Factory<M>, 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<CustomMessageMetadata> {
+        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 : CustomMessageMetadata> T.customToStringImpl(factory: CustomMessage.Factory<*>): ByteArray {
+    @Suppress("UNCHECKED_CAST")
+    return (factory as CustomMessage.Factory<T>).serialize(this)
+}

+ 1 - 4
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<out M : Message> : MessageMetadata {
     val key: Message.Key<M>
 }

+ 15 - 1
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/impl.kt

@@ -202,7 +202,21 @@ internal fun <M : Message> MessageChain.firstOrNullImpl(key: Message.Key<M>): M?
     FlashImage -> firstIsInstanceOrNull<FlashImage>()
     GroupFlashImage -> firstIsInstanceOrNull<GroupFlashImage>()
     FriendFlashImage -> firstIsInstanceOrNull<FriendFlashImage>()
-    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?
 
 /**