From b8b749bf65f19d219560773b442d500a08523853 Mon Sep 17 00:00:00 2001 From: Him188 Date: Mon, 6 Apr 2020 18:04:00 +0800 Subject: [PATCH] Completed CombinedMessage redesigning and constraining on concatenation --- compatibility-validator/build.gradle.kts | 19 +- .../compatibility/testKotlinCompatibility.kt | 14 ++ .../compatibility/CombinedMessageTest.kt | 79 ++++++++ .../compatibility/TestKotlinCompatibility.kt | 27 +++ .../mirai/qqandroid/QQAndroidBot.common.kt | 6 +- .../mamoe/mirai/qqandroid/contact/QQImpl.kt | 2 +- .../network/QQAndroidBotNetworkHandler.kt | 4 +- .../network/protocol/data/jce/FriendList.kt | 2 +- .../commonMain/kotlin/net.mamoe.mirai/Bot.kt | 1 + .../event/subscribeMessages.kt | 4 +- .../net.mamoe.mirai/message/MessagePacket.kt | 16 +- .../kotlin/net.mamoe.mirai/message/data/At.kt | 4 +- .../net.mamoe.mirai/message/data/AtAll.kt | 4 +- .../message/data/CombinedMessage.kt | 98 ++++----- .../net.mamoe.mirai/message/data/Message.kt | 191 +++++++++++++----- .../message/data/MessageChain.kt | 122 +++++------ .../net.mamoe.mirai/message/data/PlainText.kt | 1 - .../net.mamoe.mirai/utils/BotConfiguration.kt | 24 --- .../mirai/message.data/CombinedMessageTest.kt | 85 +------- .../mirai/message.data/ConstrainSingleTest.kt | 75 ++++++- 20 files changed, 495 insertions(+), 283 deletions(-) create mode 100644 compatibility-validator/src/main/kotlin/compatibility/testKotlinCompatibility.kt create mode 100644 compatibility-validator/src/test/kotlin/compatibility/CombinedMessageTest.kt create mode 100644 compatibility-validator/src/test/kotlin/compatibility/TestKotlinCompatibility.kt diff --git a/compatibility-validator/build.gradle.kts b/compatibility-validator/build.gradle.kts index 0cfb04292..e577836da 100644 --- a/compatibility-validator/build.gradle.kts +++ b/compatibility-validator/build.gradle.kts @@ -7,6 +7,11 @@ plugins { description = "Binary and source compatibility validator for mirai-core and mirai-core-qqandroid" +repositories { + mavenCentral() + jcenter() +} + kotlin { sourceSets { all { @@ -17,7 +22,19 @@ kotlin { main { dependencies { api(kotlin("stdlib")) - api(project(":mirai-core-qqandroid")) + runtimeOnly(project(":mirai-core-qqandroid")) + compileOnly("net.mamoe:mirai-core-qqandroid-jvm:0.33.0") + api(kotlinx("coroutines-core", Versions.Kotlin.coroutines)) + } + } + + test { + dependencies { + api(kotlin("stdlib")) + api(kotlin("test")) + api(kotlin("test-junit")) + runtimeOnly(project(":mirai-core-qqandroid")) + compileOnly("net.mamoe:mirai-core-qqandroid-jvm:0.33.0") api(kotlinx("coroutines-core", Versions.Kotlin.coroutines)) } } diff --git a/compatibility-validator/src/main/kotlin/compatibility/testKotlinCompatibility.kt b/compatibility-validator/src/main/kotlin/compatibility/testKotlinCompatibility.kt new file mode 100644 index 000000000..cb481e337 --- /dev/null +++ b/compatibility-validator/src/main/kotlin/compatibility/testKotlinCompatibility.kt @@ -0,0 +1,14 @@ +/* + * 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 compatibility + +fun main() { + +} \ No newline at end of file diff --git a/compatibility-validator/src/test/kotlin/compatibility/CombinedMessageTest.kt b/compatibility-validator/src/test/kotlin/compatibility/CombinedMessageTest.kt new file mode 100644 index 000000000..a3aafeaef --- /dev/null +++ b/compatibility-validator/src/test/kotlin/compatibility/CombinedMessageTest.kt @@ -0,0 +1,79 @@ +package compatibility + +import net.mamoe.mirai.message.data.* +import net.mamoe.mirai.utils.MiraiInternalAPI +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(MiraiInternalAPI::class) +internal class CombinedMessageTest { + + + @Test + fun testAsSequence() { + var message: Message = "Hello ".toMessage() + message += "World" + + assertEquals( + "Hello World", + (message as CombinedMessage).asSequence().joinToString(separator = "") + ) + } + + @Test + fun testAsSequence2() { + var message: Message = "Hello ".toMessage() + message += listOf( + PlainText("W"), + PlainText("o"), + PlainText("r") + PlainText("ld") + ).asMessageChain() + + assertEquals( + "Hello World", + (message as CombinedMessage).asSequence().joinToString(separator = "") + ) + } +} + +fun Iterator.joinToString( + separator: CharSequence = ", ", + prefix: CharSequence = "", + postfix: CharSequence = "", + limit: Int = -1, + truncated: CharSequence = "...", + transform: ((T) -> CharSequence)? = null +): String { + return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString() +} + +fun Iterator.joinTo( + buffer: A, + separator: CharSequence = ", ", + prefix: CharSequence = "", + postfix: CharSequence = "", + limit: Int = -1, + truncated: CharSequence = "...", + transform: ((T) -> CharSequence)? = null +): A { + buffer.append(prefix) + var count = 0 + for (element in this) { + if (++count > 1) buffer.append(separator) + if (limit < 0 || count <= limit) { + buffer.appendElement(element, transform) + } else break + } + if (limit in 0 until count) buffer.append(truncated) + buffer.append(postfix) + return buffer +} + +internal fun Appendable.appendElement(element: T, transform: ((T) -> CharSequence)?) { + when { + transform != null -> append(transform(element)) + element is CharSequence? -> append(element) + element is Char -> append(element) + else -> append(element.toString()) + } +} \ No newline at end of file diff --git a/compatibility-validator/src/test/kotlin/compatibility/TestKotlinCompatibility.kt b/compatibility-validator/src/test/kotlin/compatibility/TestKotlinCompatibility.kt new file mode 100644 index 000000000..e298ceab6 --- /dev/null +++ b/compatibility-validator/src/test/kotlin/compatibility/TestKotlinCompatibility.kt @@ -0,0 +1,27 @@ +/* + * 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 compatibility + +import net.mamoe.mirai.message.data.Message +import net.mamoe.mirai.message.data.PlainText +import org.junit.Test + + +internal class TestKotlinCompatibility { + + @Test + fun testMessageChain() { + val x = PlainText("te") + PlainText("st") + + println(Message::class.java.declaredMethods.joinToString("\n")) + println() + println(x::class.java.declaredMethods.joinToString("\n")) + } +} \ No newline at end of file diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.common.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.common.kt index 5c60d54e4..4ba33b2d5 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.common.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.common.kt @@ -56,6 +56,7 @@ import kotlin.coroutines.CoroutineContext import kotlin.jvm.JvmSynthetic import kotlin.math.absoluteValue import kotlin.random.Random +import net.mamoe.mirai.qqandroid.network.protocol.data.jce.FriendInfo as JceFriendInfo @OptIn(ExperimentalContracts::class) internal fun Bot.asQQAndroidBot(): QQAndroidBot { @@ -98,8 +99,9 @@ internal abstract class QQAndroidBotBase constructor( override val friends: ContactList = ContactList(LockFreeLinkedList()) - override lateinit var nick: String - internal set + override val nick: String get() = selfInfo.nick + + internal lateinit var selfInfo: JceFriendInfo override val selfQQ: QQ by lazy { @OptIn(LowLevelAPI::class) diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/QQImpl.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/QQImpl.kt index bef3dcb51..17978552a 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/QQImpl.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/QQImpl.kt @@ -51,7 +51,7 @@ import kotlin.jvm.JvmSynthetic internal inline class FriendInfoImpl( private val jceFriendInfo: net.mamoe.mirai.qqandroid.network.protocol.data.jce.FriendInfo ) : FriendInfo { - override val nick: String get() = jceFriendInfo.nick ?: "" + override val nick: String get() = jceFriendInfo.nick override val uin: Long get() = jceFriendInfo.friendUin } diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt index 52d109dcc..f628fbe87 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt @@ -223,8 +223,8 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler } // self info - data.selfInfo?.apply { - bot.nick = nick ?: "" + data.selfInfo?.run { + bot.selfInfo = this // bot.remark = remark ?: "" // bot.sex = sex } diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/jce/FriendList.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/jce/FriendList.kt index 1961ca9c5..ae071e353 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/jce/FriendList.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/jce/FriendList.kt @@ -109,7 +109,7 @@ internal class FriendInfo( @JceId(11) val sqqOnLineStateV2: Byte? = null, @JceId(12) val sShowName: String? = "", @JceId(13) val isRemark: Byte? = null, - @JceId(14) val nick: String? = "", + @JceId(14) val nick: String = "", @JceId(15) val specialFlag: Byte? = null, @JceId(16) val vecIMGroupID: ByteArray? = null, @JceId(17) val vecMSFGroupID: ByteArray? = null, diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt index 034c9311c..eb734c7f8 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt @@ -87,6 +87,7 @@ expect abstract class Bot() : CoroutineScope, LowLevelBotAPIAccessor { /** * 昵称 */ + @SinceMirai("0.33.1") abstract val nick: String /** diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/subscribeMessages.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/subscribeMessages.kt index 774254add..c1db25247 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/subscribeMessages.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/subscribeMessages.kt @@ -24,7 +24,7 @@ import net.mamoe.mirai.message.ContactMessage import net.mamoe.mirai.message.FriendMessage import net.mamoe.mirai.message.GroupMessage import net.mamoe.mirai.message.data.Message -import net.mamoe.mirai.message.data.first +import net.mamoe.mirai.message.data.firstIsInstance import net.mamoe.mirai.utils.SinceMirai import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind @@ -700,7 +700,7 @@ open class MessageSubscribersBuilder( @MessageDsl @SinceMirai("0.30.0") inline fun has(noinline onEvent: @MessageDsl suspend M.(N) -> R): Ret = - content({ message.any { it is N } }, { onEvent.invoke(this, message.first()) }) + content({ message.any { it is N } }, { onEvent.invoke(this, message.firstIsInstance()) }) /** * 如果 [mapper] 返回值非空, 就执行 [onEvent] diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessagePacket.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessagePacket.kt index e0ccaf11a..add4cfcef 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessagePacket.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessagePacket.kt @@ -357,8 +357,8 @@ suspend inline fun ContactMessage.nextMessageContaining( ): M { return subscribingGet(timeoutMillis) { takeIf { this.isContextIdenticalWith(this@nextMessageContaining) } - .takeIf { this.message.any() } - }.message.first() + .takeIf { this.message.anyIsInstance() } + }.message.firstIsInstance() } @JvmSynthetic @@ -370,8 +370,8 @@ inline fun ContactMessage.nextMessageContainingAsync( @Suppress("RemoveExplicitTypeArguments") subscribingGet(timeoutMillis) { takeIf { this.isContextIdenticalWith(this@nextMessageContainingAsync) } - .takeIf { this.message.any() } - }.message.first() + .takeIf { this.message.anyIsInstance() } + }.message.firstIsInstance() } } @@ -391,8 +391,8 @@ suspend inline fun ContactMessage.nextMessageContainingOrN ): M? { return subscribingGetOrNull(timeoutMillis) { takeIf { this.isContextIdenticalWith(this@nextMessageContainingOrNull) } - .takeIf { this.message.any() } - }?.message?.first() + .takeIf { this.message.anyIsInstance() } + }?.message?.firstIsInstance() } @JvmSynthetic @@ -403,8 +403,8 @@ inline fun ContactMessage.nextMessageContainingOrNullAsync return this.bot.async(coroutineContext) { subscribingGetOrNull(timeoutMillis) { takeIf { this.isContextIdenticalWith(this@nextMessageContainingOrNullAsync) } - .takeIf { this.message.any() } - }?.message?.first() + .takeIf { this.message.anyIsInstance() } + }?.message?.firstIsInstance() } } diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/At.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/At.kt index b044ae4c4..f485dab1a 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/At.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/At.kt @@ -17,6 +17,7 @@ package net.mamoe.mirai.message.data import net.mamoe.mirai.LowLevelAPI import net.mamoe.mirai.contact.Member import net.mamoe.mirai.contact.nameCardOrNick +import net.mamoe.mirai.utils.MiraiInternalAPI import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName import kotlin.jvm.JvmStatic @@ -56,6 +57,7 @@ private constructor(val target: Long, val display: String) : } // 自动为消息补充 " " + @OptIn(MiraiInternalAPI::class) @Suppress("INAPPLICABLE_JVM_NAME") @Deprecated("for binary compatibility", level = DeprecationLevel.HIDDEN) @JvmName("followedBy") @@ -67,7 +69,7 @@ private constructor(val target: Long, val display: String) : return followedByInternalForBinaryCompatibility(PlainText(" ") + tail) } - override fun followedBy(tail: Message): Message { + override fun followedBy(tail: Message): MessageChain { if (tail is PlainText && tail.stringValue.startsWith(' ')) { return super.followedBy(tail) } diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/AtAll.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/AtAll.kt index 74b4403ca..7fcfa1335 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/AtAll.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/AtAll.kt @@ -12,6 +12,7 @@ package net.mamoe.mirai.message.data +import net.mamoe.mirai.utils.MiraiInternalAPI import net.mamoe.mirai.utils.SinceMirai import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName @@ -42,6 +43,7 @@ object AtAll : override fun contentToString(): String = display // 自动为消息补充 " " + @OptIn(MiraiInternalAPI::class) @Deprecated("for binary compatibility", level = DeprecationLevel.HIDDEN) @Suppress("INAPPLICABLE_JVM_NAME") @JvmName("followedBy") @@ -53,7 +55,7 @@ object AtAll : return followedByInternalForBinaryCompatibility(PlainText(" ") + tail) } - override fun followedBy(tail: Message): Message { + override fun followedBy(tail: Message): MessageChain { if (tail is PlainText && tail.stringValue.startsWith(' ')) { return super.followedBy(tail) } 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 e4b96601f..a2eb493b1 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 @@ -14,11 +14,13 @@ package net.mamoe.mirai.message.data import net.mamoe.mirai.utils.MiraiExperimentalAPI import net.mamoe.mirai.utils.MiraiInternalAPI +import net.mamoe.mirai.utils.PlannedRemoval import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName +import kotlin.jvm.JvmSynthetic /** - * 链接的两个消息. + * 快速链接的两个消息 (避免构造新的 list). * * 不要直接构造 [CombinedMessage], 使用 [Message.plus] * 要连接多个 [Message], 使用 [buildMessageChain] @@ -27,64 +29,68 @@ import kotlin.jvm.JvmName * * Left-biased list */ +@MiraiInternalAPI("this API is going to be internal") class CombinedMessage -@Deprecated(message = "use Message.plus", level = DeprecationLevel.ERROR) -@MiraiInternalAPI("CombinedMessage 构造器可能会在将来被改动") constructor( - @MiraiExperimentalAPI("CombinedMessage.left 可能会在将来被改动") - val left: SingleMessage, - @MiraiExperimentalAPI("CombinedMessage.tail 可能会在将来被改动") - val tail: SingleMessage -) : Iterable, Message { - - /* - // 不要把它用作 local function, 会编译错误 - @OptIn(MiraiExperimentalAPI::class) - private suspend fun SequenceScope.yieldCombinedOrElements(message: Message) { - when (message) { - is CombinedMessage -> { - // fast path, 避免创建新的 iterator, 也不会挂起协程 - yieldCombinedOrElements(message.left) - yieldCombinedOrElements(message.tail) - } - is Iterable<*> -> { - // 更好的性能, 因为协程不会挂起. - // 这可能会导致爆栈 (十万个元素), 但作为消息序列足够了. - message.forEach { - yieldCombinedOrElements( - it as? Message ?: error( - "A Message implementing Iterable must implement Iterable, " + - "whereas got ${it!!::class.simpleName}" - ) - ) - } - } - else -> { - check(message is SingleMessage) { - "unsupported Message type. " + - "A Message must be a CombinedMessage, a Iterable or a SingleMessage" - } - yield(message) - } - } - } - */ - +internal constructor( + internal val left: Message, // 必须已经完成 constrain single + internal val tail: Message +) : Message, MessageChain { @OptIn(MiraiExperimentalAPI::class) fun asSequence(): Sequence = sequence { - yield(left) - yield(tail) + yieldCombinedOrElementsFlatten(this@CombinedMessage) } override fun iterator(): Iterator { return asSequence().iterator() } + @PlannedRemoval("1.0.0") + @Deprecated( + "有歧义, 自行使用 contentToString() 比较", + ReplaceWith("this.contentToString() == other"), + DeprecationLevel.HIDDEN + ) + override fun contains(sub: String): Boolean { + return contentToString().contains(sub) + } + + override val size: Int = when { + left === EmptyMessageChain && tail !== EmptyMessageChain -> 1 + left === EmptyMessageChain && tail === EmptyMessageChain -> 0 + left !== EmptyMessageChain && tail === EmptyMessageChain -> 1 + left !== EmptyMessageChain && tail !== EmptyMessageChain -> 2 + else -> error("stub") + } + @OptIn(MiraiExperimentalAPI::class) override fun toString(): String { return tail.toString() + left.toString() } override fun contentToString(): String { - return toString() + return left.contentToString() + tail.contentToString() } -} \ No newline at end of file +} + +@JvmSynthetic +// 不要把它用作 local function, 会编译错误 +@OptIn(MiraiExperimentalAPI::class, MiraiInternalAPI::class) +private suspend fun SequenceScope.yieldCombinedOrElementsFlatten(message: Message) { + when (message) { + is CombinedMessage -> { + // fast path, 避免创建新的 iterator, 也不会挂起协程 + yieldCombinedOrElementsFlatten(message.left) + yieldCombinedOrElementsFlatten(message.tail) + } + is MessageChain -> { + yieldAll(message) + } + else -> { + check(message is SingleMessage) { + "unsupported Message type: ${message::class}" + + "A Message must be a CombinedMessage, a Iterable or a SingleMessage" + } + yield(message) + } + } +} 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 d27aa10a2..f0f33704d 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 @@ -15,6 +15,7 @@ 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 import kotlin.jvm.JvmName import kotlin.jvm.JvmSynthetic @@ -24,16 +25,16 @@ import kotlin.jvm.JvmSynthetic * 采用这样的消息模式是因为 QQ 的消息多元化, 一条消息中可包含 [纯文本][PlainText], [图片][Image] 等. * * [消息][Message] 分为 - * - [MessageMetadata] 消息元数据, 包括: [消息来源][MessageSource] - * - [MessageContent] 单个消息, 包括: [纯文本][PlainText], [@群员][At], [@全体成员][AtAll] 等. - * - [CombinedMessage] 通过 [plus] 连接的两个消息. 可通过 [asMessageChain] 转换为 [MessageChain] - * - [MessageChain] 不可变消息链, 链表形式链接的多个 [SingleMessage] 实例. + * - [SingleMessage]: + * - [MessageMetadata] 消息元数据, 包括: [消息来源][MessageSource], [引用回复][QuoteReply]. + * - [MessageContent] 含内容的消息, 包括: [纯文本][PlainText], [@群员][At], [@全体成员][AtAll] 等. + * - [MessageChain]: 不可变消息链, 链表形式链接的多个 [SingleMessage] 实例. * * #### 在 Kotlin 使用 [Message]: * 这与使用 [String] 的使用非常类似. * - * 比较 [Message] 与 [String] (使用 infix [Message.eq]): - * `if(event eq "你好") qq.sendMessage(event)` + * 比较 [SingleMessage] 与 [String]: + * `if(message.contentToString() == "你好") qq.sendMessage(event)` * * 连接 [Message] 与 [Message], [String], (使用 operator [Message.plus]): * ```kotlin @@ -65,6 +66,7 @@ import kotlin.jvm.JvmSynthetic * * @see Contact.sendMessage 发送消息 */ +@OptIn(MiraiInternalAPI::class) interface Message { /** * 类型 Key. @@ -75,25 +77,16 @@ interface Message { */ interface Key { /** - * 此 [Key] 指代的 [Message] 类型名. 一般为 `class.simpleName` + * 此 [Key] 指代的 [Message] 类型名. 一般为 `class.simpleName`, 如 "QuoteReply", "PlainText" */ @SinceMirai("0.34.0") val typeName: String } - infix fun eq(other: Message): Boolean = this.toString() == other.toString() - - /** - * 将 [toString] 与 [other] 比较 - */ - infix fun eq(other: String): Boolean = this.toString() == other - - operator fun contains(sub: String): Boolean = false - /** * 把 `this` 连接到 [tail] 的头部. 类似于字符串相加. * - * 连接后无法保证 [ConstrainSingle] 的元素单独存在. 需在 + * 连接后可以保证 [ConstrainSingle] 的元素单独存在. * * 例: * ```kotlin @@ -111,17 +104,67 @@ interface Message { @Suppress("DEPRECATION_ERROR") @OptIn(MiraiInternalAPI::class) @JvmSynthetic // in java they should use `plus` instead - fun followedBy(tail: Message): Message { - TODO() - if (this is SingleMessage && tail is SingleMessage) { - if (this is ConstrainSingle<*> && tail is ConstrainSingle<*>) { - return if (this.key == tail.key) { - tail - } else { - CombinedMessage(this, tail) + fun followedBy(tail: Message): MessageChain { + when { + this is SingleMessage && tail is SingleMessage -> { + if (this is ConstrainSingle<*> && tail is ConstrainSingle<*>) { + if (this.key == tail.key) + return SingleMessageChainImpl(tail) } + return CombinedMessage(this, tail) } + this is SingleMessage -> { // tail is not + tail as MessageChain + + if (this is ConstrainSingle<*>) { + val key = this.key + if (tail.any { (it as? ConstrainSingle<*>)?.key == key }) { + return tail + } + } + return CombinedMessage(this, tail) + } + + tail is SingleMessage -> { + this as MessageChain + + if (tail is ConstrainSingle<*> && this.hasDuplicationOfConstrain(tail.key)) { + val iterator = this.iterator() + var tailUsed = false + return MessageChainImplByCollection( + constrainSingleMessagesImpl { + if (iterator.hasNext()) { + iterator.next() + } else if (!tailUsed) { + tailUsed = true + tail + } else null + } + ) + } + + return CombinedMessage(this, tail) + } + + else -> { // both chain + this as MessageChain + tail as MessageChain + + var iterator = this.iterator() + var tailUsed = false + return MessageChainImplByCollection( + constrainSingleMessagesImpl { + if (iterator.hasNext()) { + iterator.next() + } else if (!tailUsed) { + tailUsed = true + iterator = tail.iterator() + iterator.next() + } else null + } + ) + } } } @@ -147,43 +190,79 @@ interface Message { @SinceMirai("0.34.0") fun contentToString(): String - operator fun plus(another: Message): Message = this.followedBy(another) + operator fun plus(another: Message): MessageChain = this.followedBy(another) - // avoid resolution ambiguity - operator fun plus(another: SingleMessage): Message = this.followedBy(another) + // don't remove! avoid resolution ambiguity between `CharSequence` and `Message` + operator fun plus(another: SingleMessage): MessageChain = this.followedBy(another) - operator fun plus(another: String): Message = this.followedBy(another.toMessage()) + operator fun plus(another: String): MessageChain = this.followedBy(another.toMessage()) // `+ ""` will be resolved to `plus(String)` instead of `plus(CharSeq)` - operator fun plus(another: CharSequence): Message = this.followedBy(another.toString().toMessage()) - + operator fun plus(another: CharSequence): MessageChain = this.followedBy(another.toString().toMessage()) + ////////////////////////////////////// // FOR BINARY COMPATIBILITY UNTIL 1.0.0 + ////////////////////////////////////// + @PlannedRemoval("1.0.0") + @JvmSynthetic + @Deprecated( + "有歧义, 自行使用 contentToString() 比较", + ReplaceWith("this.contentToString() == other"), + DeprecationLevel.HIDDEN + ) + infix fun eq(other: Message): Boolean = this.toString() == other.toString() + + /** + * 将 [contentToString] 与 [other] 比较 + */ + @PlannedRemoval("1.0.0") + @JvmSynthetic + @Deprecated( + "有歧义, 自行使用 contentToString() 比较", + ReplaceWith("this.contentToString() == other"), + DeprecationLevel.HIDDEN + ) + infix fun eq(other: String): Boolean = this.contentToString() == other + + @PlannedRemoval("1.0.0") + @JvmSynthetic + @Deprecated( + "有歧义, 自行使用 contentToString() 比较", + ReplaceWith("this.contentToString() == other"), + DeprecationLevel.HIDDEN + ) + operator fun contains(sub: String): Boolean = false + + @PlannedRemoval("1.0.0") @Suppress("INAPPLICABLE_JVM_NAME") @JvmName("followedBy") @Deprecated("for binary compatibility", level = DeprecationLevel.HIDDEN) @JvmSynthetic fun followedBy1(tail: Message): CombinedMessage = this.followedByInternalForBinaryCompatibility(tail) + @PlannedRemoval("1.0.0") @Suppress("INAPPLICABLE_JVM_NAME") @JvmName("plus") @Deprecated("for binary compatibility", level = DeprecationLevel.HIDDEN) @JvmSynthetic fun plus1(another: Message): CombinedMessage = this.followedByInternalForBinaryCompatibility(another) + @PlannedRemoval("1.0.0") @Suppress("INAPPLICABLE_JVM_NAME") @JvmName("plus") @Deprecated("for binary compatibility", level = DeprecationLevel.HIDDEN) @JvmSynthetic fun plus1(another: SingleMessage): CombinedMessage = this.followedByInternalForBinaryCompatibility(another) + @PlannedRemoval("1.0.0") @Suppress("INAPPLICABLE_JVM_NAME") @JvmName("plus") @Deprecated("for binary compatibility", level = DeprecationLevel.HIDDEN) @JvmSynthetic fun plus1(another: String): CombinedMessage = this.followedByInternalForBinaryCompatibility(another.toMessage()) + @PlannedRemoval("1.0.0") @Suppress("INAPPLICABLE_JVM_NAME") @JvmName("plus") @Deprecated("for binary compatibility", level = DeprecationLevel.HIDDEN) @@ -192,24 +271,24 @@ interface Message { this.followedByInternalForBinaryCompatibility(another.toString().toMessage()) } + +@OptIn(MiraiInternalAPI::class) +private fun Message.hasDuplicationOfConstrain(key: Message.Key<*>): Boolean { + return when (this) { + is SingleMessage -> (this as? ConstrainSingle<*>)?.key == key + is CombinedMessage -> return this.left.hasDuplicationOfConstrain(key) || this.tail.hasDuplicationOfConstrain(key) + is SingleMessageChainImpl -> (this.delegate as? ConstrainSingle<*>)?.key == key + is MessageChainImplByCollection -> this.delegate.any { (it as? ConstrainSingle<*>)?.key == key } + is MessageChainImplBySequence -> this.any { (it as? ConstrainSingle<*>)?.key == key } + else -> error("stub") + } +} + @OptIn(MiraiInternalAPI::class) @JvmSynthetic @Suppress("DEPRECATION_ERROR") internal fun Message.followedByInternalForBinaryCompatibility(tail: Message): CombinedMessage { - TODO() - if (this is ConstrainSingle<*>) { - - } - - if (this is SingleMessage && tail is SingleMessage) { - if (this is ConstrainSingle<*> && tail is ConstrainSingle<*> - && this.key == tail.key - ) return CombinedMessage(EmptyMessageChain, tail) - return CombinedMessage(left = this, tail = tail) - } else { - - // return CombinedMessage(left = this.constrain) - } + return CombinedMessage(EmptyMessageChain, this.followedBy(tail)) } @JvmSynthetic @@ -218,7 +297,8 @@ suspend inline fun Message.sendTo(contact: C): MessageReceipt { return contact.sendMessage(this) as MessageReceipt } -fun Message.repeat(count: Int): MessageChain { +// inline: for future removal +inline fun Message.repeat(count: Int): MessageChain { if (this is ConstrainSingle<*>) { // fast-path return SingleMessageChainImpl(this) @@ -231,7 +311,21 @@ fun Message.repeat(count: Int): MessageChain { @JvmSynthetic inline operator fun Message.times(count: Int): MessageChain = this.repeat(count) -interface SingleMessage : Message, CharSequence, Comparable +@Suppress("OverridingDeprecatedMember") +interface SingleMessage : Message, CharSequence, Comparable { + override operator fun contains(sub: String): Boolean = sub in this.contentToString() + + /** + * 比较两个消息的 [contentToString] + */ + override infix fun eq(other: Message): Boolean = this.contentToString() == other.contentToString() + + /** + * 将 [contentToString] 与 [other] 比较 + */ + override infix fun eq(other: String): Boolean = this.contentToString() == other + +} /** * 消息元数据, 即不含内容的元素. @@ -251,7 +345,7 @@ interface MessageMetadata : SingleMessage { */ @SinceMirai("0.34.0") @MiraiExperimentalAPI -interface ConstrainSingle : SingleMessage { +interface ConstrainSingle : MessageMetadata { val key: Message.Key } @@ -263,6 +357,7 @@ interface MessageContent : SingleMessage /** * 将 [this] 发送给指定联系人 */ +@JvmSynthetic @Suppress("UNCHECKED_CAST") suspend inline fun MessageChain.sendTo(contact: C): MessageReceipt = contact.sendMessage(this) as MessageReceipt \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt index 518d4a100..12d44e021 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt @@ -40,10 +40,16 @@ import kotlin.reflect.KProperty * @see flatten 扁平化 */ interface MessageChain : Message, Iterable { + @PlannedRemoval("1.0.0") + @Deprecated( + "有歧义, 自行使用 contentToString() 比较", + ReplaceWith("this.contentToString() == other"), + DeprecationLevel.HIDDEN + ) override operator fun contains(sub: String): Boolean /** - * 元素数量 + * 元素数量. [EmptyMessageChain] 不参加计数. */ @SinceMirai("0.31.1") val size: Int @@ -104,20 +110,20 @@ inline fun MessageChain.foreachContent(block: (Message) -> Unit) { * 获取第一个 [M] 类型的 [Message] 实例 */ @JvmSynthetic -inline fun MessageChain.firstOrNull(): M? = this.firstOrNull { it is M } as M? +inline fun MessageChain.firstIsInstanceOrNull(): M? = this.firstOrNull { it is M } as M? /** * 获取第一个 [M] 类型的 [Message] 实例 * @throws [NoSuchElementException] 如果找不到该类型的实例 */ @JvmSynthetic -inline fun MessageChain.first(): M = this.first { it is M } as M +inline fun MessageChain.firstIsInstance(): M = this.first { it is M } as M /** * 获取第一个 [M] 类型的 [Message] 实例 */ @JvmSynthetic -inline fun MessageChain.any(): Boolean = this.any { it is M } +inline fun MessageChain.anyIsInstance(): Boolean = this.any { it is M } /** @@ -127,35 +133,35 @@ inline fun MessageChain.any(): Boolean = this.any { it is @JvmSynthetic @Suppress("UNCHECKED_CAST") fun MessageChain.firstOrNull(key: Message.Key): M? = when (key) { - At -> firstOrNull() - AtAll -> firstOrNull() - PlainText -> firstOrNull() - Image -> firstOrNull<Image>() - OnlineImage -> firstOrNull<OnlineImage>() - OfflineImage -> firstOrNull<OfflineImage>() - GroupImage -> firstOrNull<GroupImage>() - FriendImage -> firstOrNull<FriendImage>() - Face -> firstOrNull<Face>() - QuoteReply -> firstOrNull<QuoteReply>() - MessageSource -> firstOrNull<MessageSource>() - OnlineMessageSource -> firstOrNull<OnlineMessageSource>() - OfflineMessageSource -> firstOrNull<OfflineMessageSource>() - OnlineMessageSource.Outgoing -> firstOrNull<OnlineMessageSource.Outgoing>() - OnlineMessageSource.Outgoing.ToGroup -> firstOrNull<OnlineMessageSource.Outgoing.ToGroup>() - OnlineMessageSource.Outgoing.ToFriend -> firstOrNull<OnlineMessageSource.Outgoing.ToFriend>() - OnlineMessageSource.Incoming -> firstOrNull<OnlineMessageSource.Incoming>() - OnlineMessageSource.Incoming.FromGroup -> firstOrNull<OnlineMessageSource.Incoming.FromGroup>() - OnlineMessageSource.Incoming.FromFriend -> firstOrNull<OnlineMessageSource.Incoming.FromFriend>() - OnlineMessageSource -> firstOrNull<OnlineMessageSource>() - XmlMessage -> firstOrNull<XmlMessage>() - JsonMessage -> firstOrNull<JsonMessage>() - RichMessage -> firstOrNull<RichMessage>() - LightApp -> firstOrNull<LightApp>() - PokeMessage -> firstOrNull<PokeMessage>() - HummerMessage -> firstOrNull<HummerMessage>() - FlashImage -> firstOrNull<FlashImage>() - GroupFlashImage -> firstOrNull<GroupFlashImage>() - FriendFlashImage -> firstOrNull<FriendFlashImage>() + At -> firstIsInstanceOrNull<At>() + AtAll -> firstIsInstanceOrNull<AtAll>() + PlainText -> firstIsInstanceOrNull<PlainText>() + Image -> firstIsInstanceOrNull<Image>() + OnlineImage -> firstIsInstanceOrNull<OnlineImage>() + OfflineImage -> firstIsInstanceOrNull<OfflineImage>() + GroupImage -> firstIsInstanceOrNull<GroupImage>() + FriendImage -> firstIsInstanceOrNull<FriendImage>() + Face -> firstIsInstanceOrNull<Face>() + QuoteReply -> firstIsInstanceOrNull<QuoteReply>() + MessageSource -> firstIsInstanceOrNull<MessageSource>() + OnlineMessageSource -> firstIsInstanceOrNull<OnlineMessageSource>() + OfflineMessageSource -> firstIsInstanceOrNull<OfflineMessageSource>() + OnlineMessageSource.Outgoing -> firstIsInstanceOrNull<OnlineMessageSource.Outgoing>() + OnlineMessageSource.Outgoing.ToGroup -> firstIsInstanceOrNull<OnlineMessageSource.Outgoing.ToGroup>() + OnlineMessageSource.Outgoing.ToFriend -> firstIsInstanceOrNull<OnlineMessageSource.Outgoing.ToFriend>() + OnlineMessageSource.Incoming -> firstIsInstanceOrNull<OnlineMessageSource.Incoming>() + OnlineMessageSource.Incoming.FromGroup -> firstIsInstanceOrNull<OnlineMessageSource.Incoming.FromGroup>() + OnlineMessageSource.Incoming.FromFriend -> firstIsInstanceOrNull<OnlineMessageSource.Incoming.FromFriend>() + OnlineMessageSource -> firstIsInstanceOrNull<OnlineMessageSource>() + XmlMessage -> firstIsInstanceOrNull<XmlMessage>() + JsonMessage -> firstIsInstanceOrNull<JsonMessage>() + RichMessage -> firstIsInstanceOrNull<RichMessage>() + LightApp -> firstIsInstanceOrNull<LightApp>() + PokeMessage -> firstIsInstanceOrNull<PokeMessage>() + HummerMessage -> firstIsInstanceOrNull<HummerMessage>() + FlashImage -> firstIsInstanceOrNull<FlashImage>() + GroupFlashImage -> firstIsInstanceOrNull<GroupFlashImage>() + FriendFlashImage -> firstIsInstanceOrNull<FriendFlashImage>() else -> null } as M? @@ -191,7 +197,8 @@ inline fun <M : Message> MessageChain.any(key: Message.Key<M>): Boolean = firstO * val image: Image by message */ @JvmSynthetic -inline operator fun <reified T : Message> MessageChain.getValue(thisRef: Any?, property: KProperty<*>): T = this.first() +inline operator fun <reified T : Message> MessageChain.getValue(thisRef: Any?, property: KProperty<*>): T = + this.firstIsInstance() /** * 可空的委托 @@ -215,7 +222,8 @@ inline class OrNullDelegate<out R : Message?>(private val value: Any?) { * @see orElse 提供一个不存在则使用默认值的委托 */ @JvmSynthetic -inline fun <reified T : Message> MessageChain.orNull(): OrNullDelegate<T?> = OrNullDelegate(this.firstOrNull<T>()) +inline fun <reified T : Message> MessageChain.orNull(): OrNullDelegate<T?> = + OrNullDelegate(this.firstIsInstanceOrNull<T>()) /** * 提供一个类型的 [Message] 的委托, 若不存在这个类型的 [Message] 则委托会提供 `null` @@ -234,7 +242,7 @@ inline fun <reified T : Message> MessageChain.orNull(): OrNullDelegate<T?> = OrN inline fun <reified T : Message?> MessageChain.orElse( lazyDefault: () -> T ): OrNullDelegate<T> = - OrNullDelegate<T>(this.firstOrNull<T>() ?: lazyDefault()) + OrNullDelegate<T>(this.firstIsInstanceOrNull<T>() ?: lazyDefault()) // endregion delegate @@ -250,6 +258,7 @@ inline fun <reified T : Message?> MessageChain.orElse( @JvmName("newChain") @JsName("newChain") @Suppress("UNCHECKED_CAST") +@OptIn(MiraiInternalAPI::class) fun Message.asMessageChain(): MessageChain = when (this) { is MessageChain -> this is CombinedMessage -> (this as Iterable<Message>).asMessageChain() @@ -281,6 +290,7 @@ fun Iterable<SingleMessage>.asMessageChain(): MessageChain = inline fun MessageChain.asMessageChain(): MessageChain = this // 避免套娃 @JvmSynthetic +@OptIn(MiraiInternalAPI::class) fun CombinedMessage.asMessageChain(): MessageChain { @OptIn(MiraiExperimentalAPI::class) if (left is SingleMessage && this.tail is SingleMessage) { @@ -377,6 +387,7 @@ inline fun Sequence<SingleMessage>.flatten(): Sequence<SingleMessage> = this // * - 其他: 返回 `sequenceOf(this)` */ fun Message.flatten(): Sequence<SingleMessage> { + @OptIn(MiraiInternalAPI::class) return when (this) { is MessageChain -> this.asSequence() is CombinedMessage -> this.flatten() // already constrained single. @@ -385,6 +396,7 @@ fun Message.flatten(): Sequence<SingleMessage> { } @JvmSynthetic // make Java user happier with less methods +@OptIn(MiraiInternalAPI::class) fun CombinedMessage.flatten(): Sequence<SingleMessage> { // already constrained single. @Suppress("UNCHECKED_CAST") @@ -400,17 +412,12 @@ inline fun MessageChain.flatten(): Sequence<SingleMessage> = this.asSequence() / /** * 不含任何元素的 [MessageChain] */ -object EmptyMessageChain : MessageChain, Iterator<SingleMessage>, @MiraiExperimentalAPI SingleMessage { +object EmptyMessageChain : MessageChain, Iterator<SingleMessage> { override fun contains(sub: String): Boolean = sub.isEmpty() override val size: Int get() = 0 override fun toString(): String = "" override fun contentToString(): String = "" - override val length: Int get() = 0 - override fun get(index: Int): Char = ""[index] - override fun subSequence(startIndex: Int, endIndex: Int): CharSequence = "".subSequence(startIndex, endIndex) - override fun compareTo(other: String): Int = "".compareTo(other) - override fun iterator(): Iterator<SingleMessage> = this override fun hasNext(): Boolean = false override fun next(): SingleMessage = throw NoSuchElementException("EmptyMessageChain is empty.") @@ -495,20 +502,21 @@ internal inline fun <T> List<T>.indexOfFirst(offset: Int, predicate: (T) -> Bool */ @PublishedApi internal class MessageChainImplByCollection constructor( - private val delegate: Collection<SingleMessage> // 必须 constrainSingleMessages, 且为 immutable + internal val delegate: Collection<SingleMessage> // 必须 constrainSingleMessages, 且为 immutable ) : Message, Iterable<SingleMessage>, MessageChain { override val size: Int get() = delegate.size override fun iterator(): Iterator<SingleMessage> = delegate.iterator() + private var toStringTemp: String? = null - override fun toString(): String = - toStringTemp ?: this.delegate.joinToString("") { it.toString() }.also { toStringTemp = it } + get() = field ?: this.delegate.joinToString("") { it.toString() }.also { field = it } + + override fun toString(): String = toStringTemp!! private var contentToStringTemp: String? = null - override fun contentToString(): String = - contentToStringTemp ?: this.delegate.joinToString("") { it.contentToString() }.also { contentToStringTemp = it } + get() = field ?: this.delegate.joinToString("") { it.contentToString() }.also { field = it } - - override operator fun contains(sub: String): Boolean = delegate.any { it.contains(sub) } + override fun contentToString(): String = contentToStringTemp!! + override operator fun contains(sub: String): Boolean = sub in contentToStringTemp!! } /** @@ -525,17 +533,17 @@ internal class MessageChainImplBySequence constructor( */ private val collected: List<SingleMessage> by lazy { delegate.constrainSingleMessages() } override fun iterator(): Iterator<SingleMessage> = collected.iterator() + private var toStringTemp: String? = null - override fun toString(): String = - toStringTemp ?: this.collected.joinToString("") { it.toString() }.also { toStringTemp = it } + get() = field ?: this.joinToString("") { it.toString() }.also { field = it } + + override fun toString(): String = toStringTemp!! private var contentToStringTemp: String? = null - override fun contentToString(): String = - contentToStringTemp ?: this.collected.joinToString("") { it.contentToString() } - .also { contentToStringTemp = it } + get() = field ?: this.joinToString("") { it.contentToString() }.also { field = it } - - override operator fun contains(sub: String): Boolean = collected.any { it.contains(sub) } + override fun contentToString(): String = contentToStringTemp!! + override operator fun contains(sub: String): Boolean = sub in contentToStringTemp!! } /** @@ -543,7 +551,7 @@ internal class MessageChainImplBySequence constructor( */ @PublishedApi internal class SingleMessageChainImpl constructor( - private val delegate: SingleMessage + internal val delegate: SingleMessage ) : Message, Iterable<SingleMessage>, MessageChain { override val size: Int get() = 1 override fun toString(): String = this.delegate.toString() diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/PlainText.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/PlainText.kt index 11ec8289c..daa902b45 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/PlainText.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/PlainText.kt @@ -30,7 +30,6 @@ class PlainText(val stringValue: String) : @Suppress("unused") constructor(charSequence: CharSequence) : this(charSequence.toString()) - override operator fun contains(sub: String): Boolean = sub in stringValue override fun toString(): String = stringValue override fun contentToString(): String = stringValue diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt index f4e1b49bc..89a0945f1 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/BotConfiguration.kt @@ -97,30 +97,6 @@ open class BotConfiguration { fun fileBasedDeviceInfo(filename: String = "device.json") { deviceInfo = getFileBasedDeviceInfoSupplier(filename) } - - - @PlannedRemoval("0.34.0") - @Deprecated( - "use fileBasedDeviceInfo(filepath", level = DeprecationLevel.ERROR, - replaceWith = ReplaceWith("fileBasedDeviceInfo") - ) - operator fun FileBasedDeviceInfo.unaryPlus() { - fileBasedDeviceInfo(this.filepath) - } -} - -/** - * 使用文件系统存储设备信息. - */ -@PlannedRemoval("0.34.0") -@Deprecated( - "use fileBasedDeviceInfo(filepath", level = DeprecationLevel.ERROR -) -inline class FileBasedDeviceInfo(val filepath: String) { - /** - * 使用 "device.json" 存储设备信息 - */ - companion object ByDeviceDotJson } @OptIn(ExperimentalMultiplatform::class) diff --git a/mirai-core/src/commonTest/kotlin/net/mamoe/mirai/message.data/CombinedMessageTest.kt b/mirai-core/src/commonTest/kotlin/net/mamoe/mirai/message.data/CombinedMessageTest.kt index 254042942..92ffc6ae3 100644 --- a/mirai-core/src/commonTest/kotlin/net/mamoe/mirai/message.data/CombinedMessageTest.kt +++ b/mirai-core/src/commonTest/kotlin/net/mamoe/mirai/message.data/CombinedMessageTest.kt @@ -1,12 +1,13 @@ package net.mamoe.mirai.message.data +import net.mamoe.mirai.utils.MiraiInternalAPI import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.time.ExperimentalTime -import kotlin.time.measureTime +@OptIn(MiraiInternalAPI::class) internal class CombinedMessageTest { + @Test fun testAsSequence() { var message: Message = "Hello ".toMessage() @@ -32,84 +33,6 @@ internal class CombinedMessageTest { (message as CombinedMessage).asSequence().joinToString(separator = "") ) } - - private val toAdd = "1".toMessage() - - @OptIn(ExperimentalTime::class) - @Test - fun speedTest() = repeat(100) { - var count = 1L - - repeat(Int.MAX_VALUE) { - count++ - } - - var combineMessage: Message = toAdd - - println( - "init combine ok " + measureTime { - repeat(1000) { - combineMessage += toAdd - } - }.inMilliseconds - ) - - val list = mutableListOf<Message>() - println( - "init messageChain ok " + measureTime { - repeat(1000) { - list += toAdd - } - }.inMilliseconds - ) - - measureTime { - list.joinToString(separator = "") - }.let { time -> - println("list foreach: ${time.inMilliseconds} ms") - } - - measureTime { - (combineMessage as CombinedMessage).iterator().joinToString(separator = "") - }.let { time -> - println("combined iterate: ${time.inMilliseconds} ms") - } - - measureTime { - (combineMessage as CombinedMessage).asSequence().joinToString(separator = "") - }.let { time -> - println("combined sequence: ${time.inMilliseconds} ms") - } - - repeat(5) { - println() - } - } - - @OptIn(ExperimentalTime::class) - @Test - fun testFastIteration() { - println("start!") - println("start!") - println("start!") - println("start!") - - var combineMessage: Message = toAdd - - println( - "init combine ok " + measureTime { - repeat(1000) { - combineMessage += toAdd - } - }.inMilliseconds - ) - - measureTime { - (combineMessage as CombinedMessage).iterator().joinToString(separator = "") - }.let { time -> - println("combine: ${time.inMilliseconds} ms") - } - } } fun <T> Iterator<T>.joinToString( @@ -140,7 +63,7 @@ fun <T, A : Appendable> Iterator<T>.joinTo( buffer.appendElement(element, transform) } else break } - if (limit >= 0 && count > limit) buffer.append(truncated) + if (limit in 0 until count) buffer.append(truncated) buffer.append(postfix) return buffer } diff --git a/mirai-core/src/commonTest/kotlin/net/mamoe/mirai/message.data/ConstrainSingleTest.kt b/mirai-core/src/commonTest/kotlin/net/mamoe/mirai/message.data/ConstrainSingleTest.kt index 0d88c51f0..c13a9bce6 100644 --- a/mirai-core/src/commonTest/kotlin/net/mamoe/mirai/message.data/ConstrainSingleTest.kt +++ b/mirai-core/src/commonTest/kotlin/net/mamoe/mirai/message.data/ConstrainSingleTest.kt @@ -10,9 +10,11 @@ package net.mamoe.mirai.message.data import net.mamoe.mirai.utils.MiraiExperimentalAPI +import net.mamoe.mirai.utils.MiraiInternalAPI import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertSame +import kotlin.test.assertTrue @OptIn(MiraiExperimentalAPI::class) @@ -25,7 +27,7 @@ internal class TestConstrainSingleMessage : ConstrainSingle<TestConstrainSingleM override fun toString(): String = "<TestConstrainSingleMessage#${super.hashCode()}>" override fun contentToString(): String { - TODO("Not yet implemented") + return "" } override val key: Message.Key<TestConstrainSingleMessage> @@ -46,16 +48,75 @@ internal class TestConstrainSingleMessage : ConstrainSingle<TestConstrainSingleM } } +@OptIn(MiraiExperimentalAPI::class) internal class ConstrainSingleTest { - @OptIn(MiraiExperimentalAPI::class) + @OptIn(MiraiInternalAPI::class) @Test - fun testConstrainSingleInPlus() { - val new = TestConstrainSingleMessage() - val combined = (TestConstrainSingleMessage() + new) as CombinedMessage + fun testCombine() { + val result = PlainText("te") + PlainText("st") + assertTrue(result is CombinedMessage) + assertEquals("te", result.left.contentToString()) + assertEquals("st", result.tail.contentToString()) + assertEquals(2, result.size) + assertEquals("test", result.contentToString()) + } - assertEquals(combined.left, EmptyMessageChain) - assertSame(combined.tail, new) + @Test + fun testSinglePlusChain() { + val result = PlainText("te") + buildMessageChain { + add(TestConstrainSingleMessage()) + add("st") + } + assertTrue(result is MessageChainImplByCollection) + assertEquals(3, result.size) + assertEquals(result.contentToString(), "test") + } + + @Test + fun testSinglePlusChainConstrain() { + val chain = buildMessageChain { + add(TestConstrainSingleMessage()) + add("st") + } + val result = TestConstrainSingleMessage() + chain + assertSame(chain, result) + assertEquals(2, result.size) + assertEquals(result.contentToString(), "st") + assertTrue { result.first() is TestConstrainSingleMessage } + } + + @Test + fun testSinglePlusSingle() { + val new = TestConstrainSingleMessage() + val combined = (TestConstrainSingleMessage() + new) + + assertTrue(combined is SingleMessageChainImpl) + assertSame(new, combined.delegate) + } + + @Test + fun testChainPlusSingle() { + val new = TestConstrainSingleMessage() + + val result = buildMessageChain { + add(" ") + add(Face(Face.hao)) + add(TestConstrainSingleMessage()) + add( + PlainText("ss") + + " " + ) + } + buildMessageChain { + add(PlainText("p ")) + add(new) + add(PlainText("test")) + } + + assertEquals(7, result.size) + assertEquals(" [表情]ss p test", result.contentToString()) + result as MessageChainImplByCollection + assertSame(new, result.delegate.toTypedArray()[2]) } @Test // net.mamoe.mirai/message/data/MessageChain.kt:441