Completed CombinedMessage redesigning and constraining on concatenation

This commit is contained in:
Him188 2020-04-06 18:04:00 +08:00
parent eaa1e96ab5
commit b8b749bf65
20 changed files with 495 additions and 283 deletions

View File

@ -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))
}
}

View File

@ -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() {
}

View File

@ -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 <T> Iterator<T>.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 <T, A : Appendable> Iterator<T>.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 <T> 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())
}
}

View File

@ -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"))
}
}

View File

@ -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<QQ> = 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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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,

View File

@ -87,6 +87,7 @@ expect abstract class Bot() : CoroutineScope, LowLevelBotAPIAccessor {
/**
* 昵称
*/
@SinceMirai("0.33.1")
abstract val nick: String
/**

View File

@ -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<M : ContactMessage, out Ret, R : RR, RR>(
@MessageDsl
@SinceMirai("0.30.0")
inline fun <reified N : Message> 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]

View File

@ -357,8 +357,8 @@ suspend inline fun <reified M : Message> ContactMessage.nextMessageContaining(
): M {
return subscribingGet<ContactMessage, ContactMessage>(timeoutMillis) {
takeIf { this.isContextIdenticalWith(this@nextMessageContaining) }
.takeIf { this.message.any<M>() }
}.message.first()
.takeIf { this.message.anyIsInstance<M>() }
}.message.firstIsInstance()
}
@JvmSynthetic
@ -370,8 +370,8 @@ inline fun <reified M : Message> ContactMessage.nextMessageContainingAsync(
@Suppress("RemoveExplicitTypeArguments")
subscribingGet<ContactMessage, ContactMessage>(timeoutMillis) {
takeIf { this.isContextIdenticalWith(this@nextMessageContainingAsync) }
.takeIf { this.message.any<M>() }
}.message.first<M>()
.takeIf { this.message.anyIsInstance<M>() }
}.message.firstIsInstance<M>()
}
}
@ -391,8 +391,8 @@ suspend inline fun <reified M : Message> ContactMessage.nextMessageContainingOrN
): M? {
return subscribingGetOrNull<ContactMessage, ContactMessage>(timeoutMillis) {
takeIf { this.isContextIdenticalWith(this@nextMessageContainingOrNull) }
.takeIf { this.message.any<M>() }
}?.message?.first()
.takeIf { this.message.anyIsInstance<M>() }
}?.message?.firstIsInstance()
}
@JvmSynthetic
@ -403,8 +403,8 @@ inline fun <reified M : Message> ContactMessage.nextMessageContainingOrNullAsync
return this.bot.async(coroutineContext) {
subscribingGetOrNull<ContactMessage, ContactMessage>(timeoutMillis) {
takeIf { this.isContextIdenticalWith(this@nextMessageContainingOrNullAsync) }
.takeIf { this.message.any<M>() }
}?.message?.first<M>()
.takeIf { this.message.anyIsInstance<M>() }
}?.message?.firstIsInstance<M>()
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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<SingleMessage>, Message {
/*
// 不要把它用作 local function, 会编译错误
@OptIn(MiraiExperimentalAPI::class)
private suspend fun SequenceScope<Message>.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<Message>, " +
"whereas got ${it!!::class.simpleName}"
)
)
}
}
else -> {
check(message is SingleMessage) {
"unsupported Message type. " +
"A Message must be a CombinedMessage, a Iterable<Message> 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<SingleMessage> = sequence {
yield(left)
yield(tail)
yieldCombinedOrElementsFlatten(this@CombinedMessage)
}
override fun iterator(): Iterator<SingleMessage> {
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()
}
}
}
@JvmSynthetic
// 不要把它用作 local function, 会编译错误
@OptIn(MiraiExperimentalAPI::class, MiraiInternalAPI::class)
private suspend fun SequenceScope<SingleMessage>.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<Message> or a SingleMessage"
}
yield(message)
}
}
}

View File

@ -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<out M : Message> {
/**
* [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 <C : Contact> Message.sendTo(contact: C): MessageReceipt<C> {
return contact.sendMessage(this) as MessageReceipt<C>
}
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<String>
@Suppress("OverridingDeprecatedMember")
interface SingleMessage : Message, CharSequence, Comparable<String> {
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<M : Message> : SingleMessage {
interface ConstrainSingle<M : Message> : MessageMetadata {
val key: Message.Key<M>
}
@ -263,6 +357,7 @@ interface MessageContent : SingleMessage
/**
* [this] 发送给指定联系人
*/
@JvmSynthetic
@Suppress("UNCHECKED_CAST")
suspend inline fun <C : Contact> MessageChain.sendTo(contact: C): MessageReceipt<C> =
contact.sendMessage(this) as MessageReceipt<C>

View File

@ -40,10 +40,16 @@ import kotlin.reflect.KProperty
* @see flatten 扁平化
*/
interface MessageChain : Message, Iterable<SingleMessage> {
@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 <reified M : Message?> MessageChain.firstOrNull(): M? = this.firstOrNull { it is M } as M?
inline fun <reified M : Message?> MessageChain.firstIsInstanceOrNull(): M? = this.firstOrNull { it is M } as M?
/**
* 获取第一个 [M] 类型的 [Message] 实例
* @throws [NoSuchElementException] 如果找不到该类型的实例
*/
@JvmSynthetic
inline fun <reified M : Message> MessageChain.first(): M = this.first { it is M } as M
inline fun <reified M : Message> MessageChain.firstIsInstance(): M = this.first { it is M } as M
/**
* 获取第一个 [M] 类型的 [Message] 实例
*/
@JvmSynthetic
inline fun <reified M : Message> MessageChain.any(): Boolean = this.any { it is M }
inline fun <reified M : Message> MessageChain.anyIsInstance(): Boolean = this.any { it is M }
/**
@ -127,35 +133,35 @@ inline fun <reified M : Message> MessageChain.any(): Boolean = this.any { it is
@JvmSynthetic
@Suppress("UNCHECKED_CAST")
fun <M : Message> MessageChain.firstOrNull(key: Message.Key<M>): M? = when (key) {
At -> firstOrNull<At>()
AtAll -> firstOrNull<AtAll>()
PlainText -> firstOrNull<PlainText>()
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()

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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