Redesign MessageChain hierarchy: Add LinearMessageChainImpl and CombinedMessage

This commit is contained in:
Him188 2022-01-20 23:10:50 +00:00
parent b40b681f81
commit 0c708c8197
20 changed files with 1458 additions and 246 deletions

View File

@ -3608,7 +3608,7 @@ public final class net/mamoe/mirai/message/data/Dice$Key : net/mamoe/mirai/messa
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/MessageChain {
public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/MessageChain, net/mamoe/mirai/message/data/MessageChainImpl {
public static final field INSTANCE Lnet/mamoe/mirai/message/data/EmptyMessageChain;
public synthetic fun add (ILjava/lang/Object;)V
public fun add (ILnet/mamoe/mirai/message/data/SingleMessage;)V
@ -3624,6 +3624,7 @@ public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/Li
public fun equals (Ljava/lang/Object;)Z
public synthetic fun get (I)Ljava/lang/Object;
public fun get (I)Lnet/mamoe/mirai/message/data/SingleMessage;
public fun getHasConstrainSingle ()Z
public fun getSize ()I
public fun hashCode ()I
public final fun indexOf (Ljava/lang/Object;)I

View File

@ -3608,7 +3608,7 @@ public final class net/mamoe/mirai/message/data/Dice$Key : net/mamoe/mirai/messa
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/MessageChain {
public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/MessageChain, net/mamoe/mirai/message/data/MessageChainImpl {
public static final field INSTANCE Lnet/mamoe/mirai/message/data/EmptyMessageChain;
public synthetic fun add (ILjava/lang/Object;)V
public fun add (ILnet/mamoe/mirai/message/data/SingleMessage;)V
@ -3624,6 +3624,7 @@ public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/Li
public fun equals (Ljava/lang/Object;)Z
public synthetic fun get (I)Ljava/lang/Object;
public fun get (I)Lnet/mamoe/mirai/message/data/SingleMessage;
public fun getHasConstrainSingle ()Z
public fun getSize ()I
public fun hashCode ()I
public final fun indexOf (Ljava/lang/Object;)I

View File

@ -92,7 +92,7 @@ private val builtInSerializersModule by lazy {
// contextual(SingleMessage::class, SingleMessage.Serializer)
contextual(MessageChain::class, MessageChain.Serializer)
contextual(MessageChainImpl::class, MessageChainImpl.serializer())
contextual(LinearMessageChainImpl::class, LinearMessageChainImpl.serializer())
contextual(ShowImageFlag::class, ShowImageFlag.Serializer)

View File

@ -0,0 +1,207 @@
/*
* Copyright 2019-2022 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/dev/LICENSE
*/
package net.mamoe.mirai.message.data
import net.mamoe.mirai.message.data.visitor.MessageVisitor
import net.mamoe.mirai.message.data.visitor.RecursiveMessageVisitor
import net.mamoe.mirai.message.data.visitor.accept
import net.mamoe.mirai.utils.MiraiInternalApi
/**
* One after one, hierarchically.
* @since 2.12
*/
@MiraiInternalApi
public class CombinedMessage @MessageChainConstructor constructor(
@MiraiInternalApi public val element: Message,
@MiraiInternalApi public val tail: Message,
@MiraiInternalApi public override val hasConstrainSingle: Boolean
) : MessageChainImpl, List<SingleMessage> {
override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R {
return visitor.visitCombinedMessage(this, data)
}
override fun <D> acceptChildren(visitor: MessageVisitor<D, *>, data: D) {
element.accept(visitor, data)
tail.accept(visitor, data)
}
override val size: Int by lazy {
if (slowList.isInitialized()) return@lazy slowList.value.size
var size = 0
val visitor = object : RecursiveMessageVisitor<Unit>() {
override fun visitMessageChain(messageChain: MessageChain, data: Unit) {
if (messageChain is DirectSizeAccess) {
size += messageChain.size
return
}
return super.visitMessageChain(messageChain, data)
}
override fun visitSingleMessage(message: SingleMessage, data: Unit) {
size++
}
}
element.accept(visitor)
tail.accept(visitor)
size
}
override fun isEmpty(): Boolean {
if (slowList.isInitialized()) return slowList.value.isEmpty()
return element is MessageChain && element.isEmpty() && tail is MessageChain && tail.isEmpty()
}
override fun contains(element: SingleMessage): Boolean {
if (slowList.isInitialized()) return slowList.value.contains(element)
if (this.element == element) return true
if (this.tail == element) return true
if (this.element is MessageChain && this.element.contains(element)) return true
if (this.tail is MessageChain && this.tail.contains(element)) return true
return false
}
private val toStringTemp: String by lazy {
if (slowList.isInitialized()) return@lazy slowList.value.toString()
buildString {
accept(object : RecursiveMessageVisitor<Unit>() {
override fun visitSingleMessage(message: SingleMessage, data: Unit) {
append(message.toString())
}
override fun visitMessageChain(messageChain: MessageChain, data: Unit) {
if (messageChain is DirectToStringAccess) {
append(messageChain.toString())
} else {
super.visitMessageChain(messageChain, data)
}
}
})
}
}
private val contentToStingTemp: String by lazy {
if (slowList.isInitialized()) return@lazy slowList.value.contentToString()
buildString {
accept(object : RecursiveMessageVisitor<Unit>() {
override fun visitSingleMessage(message: SingleMessage, data: Unit) {
append(message.contentToString())
}
override fun visitMessageChain(messageChain: MessageChain, data: Unit) {
if (messageChain is DirectToStringAccess) {
append(messageChain.contentToString())
} else {
super.visitMessageChain(messageChain, data)
}
}
})
}
}
override fun toString(): String = toStringTemp
override fun contentToString(): String = contentToStingTemp
override fun containsAll(elements: Collection<SingleMessage>): Boolean {
if (slowList.isInitialized()) return slowList.value.containsAll(elements)
if (elements.isEmpty()) return true
if (this.isEmpty()) return false
val remaining = elements.toMutableList()
accept(object : RecursiveMessageVisitor<Unit>() {
override fun isFinished(): Boolean = remaining.isEmpty()
override fun visitSingleMessage(message: SingleMessage, data: Unit) {
remaining.remove(message)
}
})
return remaining.isEmpty()
}
override fun iterator(): Iterator<SingleMessage> {
if (slowList.isInitialized()) return slowList.value.iterator()
suspend fun SequenceScope<SingleMessage>.yieldMessage(element: Message) {
when (element) {
is SingleMessage -> {
yield(element)
}
is MessageChain -> {
yieldAll(element.iterator())
}
}
}
return iterator {
yieldMessage(element)
yieldMessage(tail)
}
}
override fun subList(fromIndex: Int, toIndex: Int): List<SingleMessage> {
if (slowList.isInitialized()) return slowList.value.subList(fromIndex, toIndex)
if (fromIndex < 0 || fromIndex > toIndex) throw IndexOutOfBoundsException("fromIndex: $fromIndex, toIndex: $toIndex")
return buildList {
accept(object : RecursiveMessageVisitor<Unit>() {
private var currentIndex = 0
override fun isFinished(): Boolean = currentIndex >= toIndex
override fun visitSingleMessage(message: SingleMessage, data: Unit) {
if (isFinished()) return
if (currentIndex >= fromIndex) {
add(message)
}
currentIndex++
}
})
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CombinedMessage
if (element != other.element) return false
if (tail != other.tail) return false
if (hasConstrainSingle != other.hasConstrainSingle) return false
return true
}
override fun hashCode(): Int {
var result = element.hashCode()
result = 31 * result + tail.hashCode()
result = 31 * result + hasConstrainSingle.hashCode()
return result
}
///////////////////////////////////////////////////////////////////////////
// slow operations
///////////////////////////////////////////////////////////////////////////
internal val slowList: Lazy<MessageChain> = lazy {
sequenceOf(element, tail).toMessageChain()
}
// [MessageChain] implements [RandomAccess] so we should ensure that property here.
override fun get(index: Int): SingleMessage = slowList.value[index]
override fun indexOf(element: SingleMessage): Int = slowList.value.indexOf(element)
override fun lastIndexOf(element: SingleMessage): Int = slowList.value.lastIndexOf(element)
override fun listIterator(): ListIterator<SingleMessage> = slowList.value.listIterator()
override fun listIterator(index: Int): ListIterator<SingleMessage> = slowList.value.listIterator(index)
}
internal interface DirectSizeAccess : MessageChain
internal interface DirectToStringAccess : MessageChain

View File

@ -240,7 +240,20 @@ public interface Message {
* @see plus `+` 操作符重载
*/
@JvmSynthetic // in java they should use `plus` instead
public fun followedBy(tail: Message): MessageChain = followedByImpl(tail)
public fun followedBy(tail: Message): MessageChain {
var constrainSingleCount = 0
if (this.hasConstrainSingle) constrainSingleCount++
if (tail.hasConstrainSingle) constrainSingleCount++
return if (constrainSingleCount == 0) {
// Future optimize:
// When constrainSingleCount == 1, see if we can connect by CombinedMessage,
// this need some kind of replacement of `hasConstrainSingle` with more information about MessageKeys.
@OptIn(MessageChainConstructor::class)
CombinedMessage(this, tail, false)
} else {
LinearMessageChainImpl.combineCreate(this, tail)
}
}
/** 将 [another] 按顺序连接到这个消息的尾部. */
public operator fun plus(another: MessageChain): MessageChain = this + another as Message
@ -277,14 +290,14 @@ public interface Message {
/**
* @suppress 这是内部 API, 不要在任何情况下调用
* @since MESSAGE_VISITOR
* @since 2.12
*/
@MiraiInternalApi
public fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R = visitor.visitMessage(this, data)
/**
* @suppress 这是内部 API, 不要在任何情况下调用
* @since MESSAGE_VISITOR
* @since 2.12
*/
@MiraiInternalApi
public fun <D> acceptChildren(visitor: MessageVisitor<D, *>, data: D) {

View File

@ -143,6 +143,11 @@ import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.RESTRICTED_AB
* 例如用户发送了两条内容相同的消息, 但其中一条带有引用回复而另一条没有, 则两条消息的索引可能有变化 (当然内容顺序不会改变, 只是 [QuoteReply] 的位置有可能会变化).
* 因此在使用直接索引访问时要格外注意兼容性, 故不推荐这种访问方案.
*
* ### 避免索引访问以提高性能
*
* 2.12 , [MessageChain] 内部结构有性能优化. 该优化大幅降低元素数量多的 [MessageChain] 的连接的时间复杂度.
* 性能优化默认生效, 但若使用 [get], [subList] [List] [Collection] 之外的方法时则会让该优化失效 (相比 2.12 以前不会丢失性能, 只是没有优化).
*
* ## 撤回和引用
*
* 要撤回消息, 查看 [MessageSource]
@ -253,11 +258,6 @@ public sealed interface MessageChain :
@MiraiInternalApi
override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R = visitor.visitMessageChain(this, data)
@MiraiInternalApi
override fun <D> acceptChildren(visitor: MessageVisitor<D, *>, data: D) {
forEach { it.accept(visitor, data) }
}
/**
* [MessageChain] 作为 `List<SingleMessage>` 序列化. 使用 [多态序列化][Polymorphic].
*
@ -349,7 +349,7 @@ public sealed interface MessageChain :
/**
* 返回一个不含任何元素的 [MessageChain].
*
* @since 2.11
* @since 2.12
*/
// Java: MessageUtils.emptyMessageChain()
@Suppress("DEPRECATION")
@ -363,14 +363,18 @@ public fun emptyMessageChain(): MessageChain = EmptyMessageChain
"Please use emptyMessageChain()",
replaceWith = ReplaceWith("emptyMessageChain()", "net.mamoe.mirai.message.data.emptyMessageChain")
)
@DeprecatedSinceMirai(warningSince = "2.11")
public object EmptyMessageChain : MessageChain, List<SingleMessage> by emptyList() {
@DeprecatedSinceMirai(warningSince = "2.12")
public object EmptyMessageChain : MessageChain, List<SingleMessage> by emptyList(), MessageChainImpl {
override val size: Int get() = 0
override fun toString(): String = ""
override fun contentToString(): String = ""
override fun serializeToMiraiCode(): String = ""
@MiraiInternalApi
override val hasConstrainSingle: Boolean
get() = false
@MiraiExperimentalApi
override fun appendMiraiCodeTo(builder: StringBuilder) {
}
@ -486,7 +490,7 @@ public inline fun messageChainOf(vararg messages: Message): MessageChain = messa
*/
@JvmName("newChain")
public fun Sequence<Message>.toMessageChain(): MessageChain =
createMessageChainImplOptimized(this.constrainSingleMessages())
LinearMessageChainImpl.create(ConstrainSingleHelper.constrainSingleMessages(this))
/**
* 扁平化 [this] 并创建一个 [MessageChain].
@ -525,11 +529,8 @@ public inline fun Array<out Message>.toMessageChain(): MessageChain = this.asSeq
@JvmName("newChain")
public fun Message.toMessageChain(): MessageChain = when (this) {
is MessageChain -> (this as List<SingleMessage>).toMessageChain()
else -> MessageChainImpl(
listOf(
this as? SingleMessage ?: error("Message is either MessageChain nor SingleMessage: $this")
)
)
is SingleMessage -> LinearMessageChainImpl.create(listOf(this), this is ConstrainSingle)
else -> error("Message is either MessageChain nor SingleMessage: $this")
}

View File

@ -1,10 +1,10 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
* Copyright 2019-2022 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.
* 此源代码的使用受 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
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
@file:JvmMultifileClass
@ -176,7 +176,7 @@ public class MessageChainBuilder private constructor(
// avoid resolution to extensions
public fun asMessageChain(): MessageChain {
this.flushCache()
return createMessageChainImplOptimized(this.constrainSingleMessages())
return this.toMessageChain()
}
/** 同 [asMessageChain] */

View File

@ -17,8 +17,10 @@ import kotlinx.serialization.Serializable
import net.mamoe.mirai.message.data.Image.Key.IMAGE_ID_REGEX
import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_1
import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_2
import net.mamoe.mirai.utils.MiraiExperimentalApi
import net.mamoe.mirai.message.data.visitor.MessageVisitor
import net.mamoe.mirai.utils.MiraiInternalApi
import net.mamoe.mirai.utils.asImmutable
import net.mamoe.mirai.utils.castOrNull
import net.mamoe.mirai.utils.replaceAllKotlin
// region image
@ -27,18 +29,6 @@ import net.mamoe.mirai.utils.replaceAllKotlin
//// IMPLEMENTATIONS ////
/////////////////////////
@Suppress("unused", "UNUSED_PARAMETER")
private fun Message.hasDuplicationOfConstrain(key: MessageKey<*>): Boolean {
return true
/*
return when (this) {
is SingleMessage -> (this as? ConstrainSingle)?.key == key
is CombinedMessage -> return this.left.hasDuplicationOfConstrain(key) || this.tail.hasDuplicationOfConstrain(key)
is MessageChainImplByCollection -> this.delegate.any { (it as? ConstrainSingle)?.key == key }
else -> error("stub")
}*/
}
@JvmSynthetic
internal fun Message.contentEqualsStrictImpl(another: Message, ignoreCase: Boolean): Boolean {
if (!this.contentToString().equals(another.contentToString(), ignoreCase = ignoreCase)) return false
@ -65,117 +55,89 @@ internal fun Message.contentEqualsStrictImpl(another: Message, ignoreCase: Boole
}
}
@JvmSynthetic
internal fun Message.followedByImpl(tail: Message): MessageChain {
return MessageChainImplBySequence(this.toMessageChain().asSequence() + tail.toMessageChain().asSequence())
/*
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)) {
return MessageChainImplByCollection(constrainSingleMessagesImpl(this.asSequence() + tail))
}
return CombinedMessage(this, tail)
}
else -> { // both chain
this as MessageChain
tail as MessageChain
return MessageChainImplByCollection(
constrainSingleMessagesImpl(this.asSequence() + tail)
)
}
}*/
internal sealed interface MessageChainImpl : MessageChain {
/**
* 去重算法 v1 - 2.12:
* 在连接时若只有 0-1 方包含 [ConstrainSingle], 则使用 [CombinedMessage] 优化性能. 否则使用旧版复杂去重算法构造 [LinearMessageChainImpl].
*/
@MiraiInternalApi
val hasConstrainSingle: Boolean
}
@JvmSynthetic
internal fun Sequence<SingleMessage>.constrainSingleMessages(): List<SingleMessage> =
constrainSingleMessagesImpl(this)
/**
* - [Sequence.toMutableList]
* - Replace in-place with marker null
*/
@MiraiExperimentalApi
@JvmSynthetic
internal fun constrainSingleMessagesImpl(sequence: Sequence<SingleMessage>): List<SingleMessage> {
val list: MutableList<SingleMessage?> = sequence.toMutableList()
for (singleMessage in list.asReversed()) {
if (singleMessage is ConstrainSingle) {
val key = singleMessage.key.topmostKey
val firstOccurrence = list.first { it != null && key.isInstance(it) } // may be singleMessage itself
list.replaceAllKotlin {
when {
it == null -> null
it === firstOccurrence -> singleMessage
key.isInstance(it) -> null // remove duplicates
else -> it
}
}
}
internal val Message.hasConstrainSingle: Boolean
get() {
if (this is SingleMessage) return this is ConstrainSingle
// now `this` is MessageChain
return this.castOrNull<MessageChainImpl>()?.hasConstrainSingle ?: true // for external type, assume they do
}
/**
* @see ConstrainSingleHelper.constrainSingleMessages
*/
internal data class ConstrainSingleData(
val value: List<SingleMessage>,
val hasConstrainSingle: Boolean,
)
internal object ConstrainSingleHelper {
@JvmName("constrainSingleMessages_Sequence")
internal fun constrainSingleMessages(sequence: Sequence<Message>): ConstrainSingleData =
constrainSingleMessages(sequence.flatMap { it.toMessageChain() })
internal fun constrainSingleMessages(sequence: Sequence<SingleMessage>): ConstrainSingleData =
constrainSingleMessagesImpl(sequence)
/**
* - [Sequence.toMutableList]
* - Replace in-place with marker null
*/
private fun constrainSingleMessagesImpl(sequence: Sequence<SingleMessage>): ConstrainSingleData {
val list: MutableList<SingleMessage?> = sequence.toMutableList()
var hasConstrainSingle = false
for (singleMessage in list.asReversed()) {
if (singleMessage is ConstrainSingle) {
hasConstrainSingle = true
val key = singleMessage.key.topmostKey
val firstOccurrence = list.first { it != null && key.isInstance(it) } // may be singleMessage itself
list.replaceAllKotlin {
when {
it == null -> null
it === firstOccurrence -> singleMessage
key.isInstance(it) -> null // remove duplicates
else -> it
}
}
}
}
return ConstrainSingleData(list.filterNotNull().asImmutable(), hasConstrainSingle)
}
return list.filterNotNull().asImmutable()
}
@JvmSynthetic
internal fun Iterable<SingleMessage>.constrainSingleMessages(): List<SingleMessage> =
constrainSingleMessagesImpl(this.asSequence())
@JvmName("constrainSingleMessages_Sequence")
@JvmSynthetic
internal fun Sequence<Message>.constrainSingleMessages(): List<SingleMessage> =
this.flatMap { it.toMessageChain() }.constrainSingleMessages()
@JvmSynthetic
@Suppress("UNCHECKED_CAST", "DEPRECATION_ERROR", "DEPRECATION")
internal fun <M : SingleMessage> MessageChain.getImpl(key: MessageKey<M>): M? {
return this.asSequence().mapNotNull { key.safeCast.invoke(it) }.firstOrNull()
}
/**
* @return [EmptyMessageChain] if [delegate] is empty, otherwise [MessageChainImpl]
*/
internal fun createMessageChainImplOptimized(delegate: List<SingleMessage>): MessageChain {
return if (delegate.isEmpty()) emptyMessageChain()
else MessageChainImpl(delegate)
}
@RequiresOptIn
internal annotation class MessageChainConstructor
/**
* 使用 [Collection] 作为委托的 [MessageChain]
*/
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(MessageChain.Serializer::class)
internal data class MessageChainImpl constructor(
internal class LinearMessageChainImpl @MessageChainConstructor private constructor(
@JvmField
internal val delegate: List<SingleMessage> // 必须 constrainSingleMessages, 且为 immutable
) : Message, MessageChain, List<SingleMessage> by delegate {
internal val delegate: List<SingleMessage>,
override val hasConstrainSingle: Boolean
) : Message, MessageChain, List<SingleMessage> by delegate, MessageChainImpl,
DirectSizeAccess, DirectToStringAccess {
override val size: Int get() = delegate.size
override fun iterator(): Iterator<SingleMessage> = delegate.iterator()
@ -186,13 +148,81 @@ internal data class MessageChainImpl constructor(
override fun contentToString(): String = contentToStringTemp
override fun hashCode(): Int = delegate.hashCode()
override fun equals(other: Any?): Boolean = other is MessageChainImpl && other.delegate == this.delegate
}
override fun equals(other: Any?): Boolean = other is LinearMessageChainImpl && other.delegate == this.delegate
@Suppress("FunctionName") // source compatibility with 1.x
internal fun MessageChainImplBySequence(
delegate: Sequence<SingleMessage> // 可以有重复 ConstrainSingle
): MessageChain = createMessageChainImplOptimized(delegate.constrainSingleMessages())
override fun <D> acceptChildren(visitor: MessageVisitor<D, *>, data: D) {
for (singleMessage in delegate) {
singleMessage.accept(visitor, data)
}
}
companion object {
fun combineCreate(message: Message, tail: Message): MessageChain {
return create(
ConstrainSingleHelper.constrainSingleMessages(
message.toMessageChain().asSequence() + tail.toMessageChain().asSequence()
)
)
/*
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)) {
return MessageChainImplByCollection(constrainSingleMessagesImpl(this.asSequence() + tail))
}
return CombinedMessage(this, tail)
}
else -> { // both chain
this as MessageChain
tail as MessageChain
return MessageChainImplByCollection(
constrainSingleMessagesImpl(this.asSequence() + tail)
)
}
}*/
}
/**
* @param delegate must be immutable
*/
@OptIn(MessageChainConstructor::class)
fun create(delegate: List<SingleMessage>, hasConstrainSingle: Boolean): MessageChain {
return if (delegate.isEmpty()) {
emptyMessageChain()
} else {
LinearMessageChainImpl(delegate, hasConstrainSingle)
}
}
fun create(data: ConstrainSingleData): MessageChain {
return create(data.value, data.hasConstrainSingle)
}
}
}
//////////////////////

View File

@ -11,158 +11,232 @@ package net.mamoe.mirai.message.data.visitor
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.MiraiInternalApi
import net.mamoe.mirai.utils.NotStableForInheritance
/**
* @suppress 这是内部 API, 请不要调用
* @since MESSAGE_VISITOR
* @since 2.12
*/
@MiraiInternalApi
@NotStableForInheritance
public interface MessageVisitor<in D, out R> {
public fun visitMessage(message: Message, data: D): R
public fun visitSingleMessage(message: SingleMessage, data: D): R
public fun visitMessageChain(messageChain: MessageChain, data: D): R
public fun visitCombinedMessage(message: CombinedMessage, data: D): R
public fun visitSingleMessage(message: SingleMessage, data: D): R {
public fun visitMessageContent(message: MessageContent, data: D): R
public fun visitMessageMetadata(message: MessageMetadata, data: D): R
public fun visitMessageOrigin(message: MessageOrigin, data: D): R
public fun visitMessageSource(message: MessageSource, data: D): R
public fun visitQuoteReply(message: QuoteReply, data: D): R
public fun visitCustomMessageMetadata(message: CustomMessageMetadata, data: D): R
public fun visitShowImageFlag(message: ShowImageFlag, data: D): R
public fun visitPlainText(message: PlainText, data: D): R
public fun visitAt(message: At, data: D): R
public fun visitAtAll(message: AtAll, data: D): R
@Suppress("DEPRECATION_ERROR")
public fun visitVoice(message: net.mamoe.mirai.message.data.Voice, data: D): R
public fun visitAudio(message: Audio, data: D): R
public fun visitHummerMessage(message: HummerMessage, data: D): R
public fun visitFlashImage(message: FlashImage, data: D): R
public fun visitPokeMessage(message: PokeMessage, data: D): R
public fun visitVipFace(message: VipFace, data: D): R
public fun visitMarketFace(message: MarketFace, data: D): R
public fun visitDice(message: Dice, data: D): R
public fun visitFace(message: Face, data: D): R
public fun visitFileMessage(message: FileMessage, data: D): R
public fun visitImage(message: Image, data: D): R
public fun visitForwardMessage(message: ForwardMessage, data: D): R
public fun visitMusicShare(message: MusicShare, data: D): R
public fun visitRichMessage(message: RichMessage, data: D): R
public fun visitServiceMessage(message: ServiceMessage, data: D): R
public fun visitSimpleServiceMessage(message: SimpleServiceMessage, data: D): R
public fun visitLightApp(message: LightApp, data: D): R
public fun visitAbstractServiceMessage(message: AbstractServiceMessage, data: D): R
public fun visitUnsupportedMessage(message: UnsupportedMessage, data: D): R
}
/**
* @suppress 这是内部 API, 请不要调用
* @since 2.12
*/
@MiraiInternalApi
public abstract class AbstractMessageVisitor<in D, out R> : MessageVisitor<D, R> {
public override fun visitSingleMessage(message: SingleMessage, data: D): R {
return visitMessage(message, data)
}
public fun visitMessageChain(messageChain: MessageChain, data: D): R {
public override fun visitMessageChain(messageChain: MessageChain, data: D): R {
return visitMessage(messageChain, data)
}
public override fun visitCombinedMessage(message: CombinedMessage, data: D): R {
return visitMessageChain(message, data)
}
public fun visitMessageContent(message: MessageContent, data: D): R {
public override fun visitMessageContent(message: MessageContent, data: D): R {
return visitSingleMessage(message, data)
}
public fun visitMessageMetadata(message: MessageMetadata, data: D): R {
public override fun visitMessageMetadata(message: MessageMetadata, data: D): R {
return visitSingleMessage(message, data)
}
public fun visitMessageOrigin(message: MessageOrigin, data: D): R {
public override fun visitMessageOrigin(message: MessageOrigin, data: D): R {
return visitMessageMetadata(message, data)
}
public fun visitMessageSource(message: MessageSource, data: D): R {
public override fun visitMessageSource(message: MessageSource, data: D): R {
return visitMessageMetadata(message, data)
}
public fun visitQuoteReply(message: QuoteReply, data: D): R {
public override fun visitQuoteReply(message: QuoteReply, data: D): R {
return visitMessageMetadata(message, data)
}
public fun visitCustomMessageMetadata(message: CustomMessageMetadata, data: D): R {
public override fun visitCustomMessageMetadata(message: CustomMessageMetadata, data: D): R {
return visitMessageMetadata(message, data)
}
public fun visitShowImageFlag(message: ShowImageFlag, data: D): R {
public override fun visitShowImageFlag(message: ShowImageFlag, data: D): R {
return visitMessageMetadata(message, data)
}
public fun visitPlainText(message: PlainText, data: D): R {
public override fun visitPlainText(message: PlainText, data: D): R {
return visitMessageContent(message, data)
}
public fun visitAt(message: At, data: D): R {
public override fun visitAt(message: At, data: D): R {
return visitMessageContent(message, data)
}
public fun visitAtAll(message: AtAll, data: D): R {
public override fun visitAtAll(message: AtAll, data: D): R {
return visitMessageContent(message, data)
}
@Suppress("DEPRECATION_ERROR")
public fun visitVoice(message: Voice, data: D): R {
public override fun visitVoice(message: net.mamoe.mirai.message.data.Voice, data: D): R {
return visitMessageContent(message, data)
}
public fun visitAudio(message: Audio, data: D): R {
public override fun visitAudio(message: Audio, data: D): R {
return visitMessageContent(message, data)
}
public fun visitHummerMessage(message: HummerMessage, data: D): R {
public override fun visitHummerMessage(message: HummerMessage, data: D): R {
return visitMessageContent(message, data)
}
public fun visitFlashImage(message: FlashImage, data: D): R {
public override fun visitFlashImage(message: FlashImage, data: D): R {
return visitHummerMessage(message, data)
}
public fun visitPokeMessage(message: PokeMessage, data: D): R {
public override fun visitPokeMessage(message: PokeMessage, data: D): R {
return visitHummerMessage(message, data)
}
public fun visitVipFace(message: VipFace, data: D): R {
public override fun visitVipFace(message: VipFace, data: D): R {
return visitHummerMessage(message, data)
}
public fun visitMarketFace(message: MarketFace, data: D): R {
public override fun visitMarketFace(message: MarketFace, data: D): R {
return visitHummerMessage(message, data)
}
public fun visitDice(message: Dice, data: D): R {
public override fun visitDice(message: Dice, data: D): R {
return visitMarketFace(message, data)
}
public fun visitFace(message: Face, data: D): R {
public override fun visitFace(message: Face, data: D): R {
return visitMessageContent(message, data)
}
public fun visitFileMessage(message: FileMessage, data: D): R {
public override fun visitFileMessage(message: FileMessage, data: D): R {
return visitMessageContent(message, data)
}
public fun visitImage(message: Image, data: D): R {
public override fun visitImage(message: Image, data: D): R {
return visitMessageContent(message, data)
}
public fun visitForwardMessage(message: ForwardMessage, data: D): R {
public override fun visitForwardMessage(message: ForwardMessage, data: D): R {
return visitMessageContent(message, data)
}
public fun visitMusicShare(message: MusicShare, data: D): R {
public override fun visitMusicShare(message: MusicShare, data: D): R {
return visitMessageContent(message, data)
}
public fun visitUnsupportedMessage(message: UnsupportedMessage, data: D): R {
public override fun visitUnsupportedMessage(message: UnsupportedMessage, data: D): R {
return visitMessageContent(message, data)
}
public fun visitRichMessage(message: RichMessage, data: D): R {
public override fun visitRichMessage(message: RichMessage, data: D): R {
return visitMessageContent(message, data)
}
public fun visitServiceMessage(message: ServiceMessage, data: D): R {
public override fun visitServiceMessage(message: ServiceMessage, data: D): R {
return visitRichMessage(message, data)
}
public fun visitSimpleServiceMessage(message: SimpleServiceMessage, data: D): R {
public override fun visitSimpleServiceMessage(message: SimpleServiceMessage, data: D): R {
return visitServiceMessage(message, data)
}
public fun visitLightApp(message: LightApp, data: D): R {
public override fun visitLightApp(message: LightApp, data: D): R {
return visitRichMessage(message, data)
}
public fun visitAbstractServiceMessage(message: AbstractServiceMessage, data: D): R {
public override fun visitAbstractServiceMessage(message: AbstractServiceMessage, data: D): R {
return visitServiceMessage(message, data)
}
}
/**
* @suppress 这是内部 API, 请不要调用
* @since 2.11
* @since 2.12
*/
@MiraiInternalApi
public interface MessageVisitorUnit<in D> : MessageVisitor<D, Unit> {
public abstract class RecursiveMessageVisitor<D> : MessageVisitorUnit<D>() {
protected open fun isFinished(): Boolean = false
override fun visitMessage(message: Message, data: D) {
if (isFinished()) return
message.acceptChildren(this, data)
}
}
/**
* @suppress 这是内部 API, 请不要调用
* @since 2.12
*/
@MiraiInternalApi
public abstract class MessageVisitorUnit<in D> : AbstractMessageVisitor<D, Unit>() {
override fun visitMessage(message: Message, data: D): Unit = Unit
}
/**
* @suppress 这是内部 API, 请不要调用
* @since 2.11
* @since 2.12
*/
@MiraiInternalApi
public fun <R> Message.accept(visitor: MessageVisitor<Unit, R>): R = this.accept(visitor, Unit)
public fun <R> Message.accept(visitor: MessageVisitor<Unit, R>): R = this.accept(visitor, Unit)
/**
* @suppress 这是内部 API, 请不要调用
* @since 2.12
*/
@MiraiInternalApi
public fun Message.acceptChildren(visitor: MessageVisitor<Unit, *>): Unit = this.acceptChildren(visitor, Unit)

View File

@ -1,85 +1,220 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
* Copyright 2019-2022 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.
* 此源代码的使用受 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
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.message.data
package net.mamoe.mirai.message.data
import net.mamoe.mirai.message.data.visitor.MessageVisitorUnit
import net.mamoe.mirai.message.data.visitor.RecursiveMessageVisitor
import net.mamoe.mirai.message.data.visitor.accept
import net.mamoe.mirai.message.data.visitor.acceptChildren
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertIs
@OptIn(MessageChainConstructor::class)
internal class CombinedMessageTest {
private fun linearMessageChainOf(vararg values: SingleMessage) =
LinearMessageChainImpl.create(values.toList(), values.any { it.hasConstrainSingle })
@Test
fun testAsSequence() {
var message: Message = PlainText("Hello ")
message += "World"
fun singlePlusSingleCreatesCombinedMessage() {
kotlin.run {
val chain = PlainText("fo").plus(PlainText("o"))
assertIs<CombinedMessage>(chain)
assertEquals(PlainText("fo"), chain.element)
assertEquals(PlainText("o"), chain.tail)
assertEquals(false, chain.hasConstrainSingle)
}
kotlin.run {
val chain = PlainText("fo").plus(LightApp("c"))
assertIs<LinearMessageChainImpl>(chain)
assertEquals(LightApp("c"), chain.single())
assertEquals(true, chain.hasConstrainSingle)
}
}
@Test
fun singlePlusChainCreatesCombinedMessage() {
val chain = PlainText("fo").plus(linearMessageChainOf(PlainText("o")))
assertIs<CombinedMessage>(chain)
assertEquals(PlainText("fo"), chain.element)
assertEquals(linearMessageChainOf(PlainText("o")), chain.tail)
assertEquals(false, chain.hasConstrainSingle)
}
@Test
fun chainPlusSingleCreatesCombinedMessage() {
val chain = linearMessageChainOf(PlainText("o")).plus(PlainText("fo"))
assertIs<CombinedMessage>(chain)
assertEquals(linearMessageChainOf(PlainText("o")), chain.element)
assertEquals(PlainText("fo"), chain.tail)
assertEquals(false, chain.hasConstrainSingle)
}
private fun createTestInstance(): CombinedMessage {
return CombinedMessage(
buildMessageChain {
+PlainText("bar")
+emptyMessageChain()
+buildMessageChain {
+AtAll
+PlainText("zoo")
}
},
buildMessageChain {
+buildMessageChain {
+At(2)
}
+At(1)
},
false
)
}
private fun createComplexCombined() = CombinedMessage(
PlainText("foo"),
createTestInstance(),
false
)
private val complexCombined = createComplexCombined()
///////////////////////////////////////////////////////////////////////////
// properties
///////////////////////////////////////////////////////////////////////////
@Test
fun sizeTest() {
assertEquals(0, CombinedMessage(messageChainOf(), messageChainOf(), false).size)
assertEquals(
"Hello World",
message.toMessageChain().asSequence().joinToString(separator = "")
0,
CombinedMessage(messageChainOf(), CombinedMessage(messageChainOf(), messageChainOf(), false), false).size
)
createTestInstance().run {
assertEquals(5, size)
assertFalse { slowList.isInitialized() }
}
}
@Test
fun slowListTest() {
assertEquals(messageChainOf(), CombinedMessage(messageChainOf(), messageChainOf(), false).slowList.value)
assertEquals(
listOf(
PlainText("foo"),
PlainText("bar"),
AtAll,
PlainText("zoo"),
At(2),
At(1),
),
complexCombined.slowList.value.toList()
)
}
@Test
fun testAsSequence2() {
var message: Message = PlainText("Hello ")
message += listOf(
PlainText("W"),
PlainText("o"),
PlainText("r") + PlainText("ld")
).toMessageChain()
fun consistencyTest() {
val first = createTestInstance()
val second = createTestInstance()
assertEquals(first, second)
assertEquals(first.hashCode(), second.hashCode())
assertEquals(first.toString(), second.toString())
assertEquals(first.contentToString(), second.contentToString())
assertEquals(first.size, second.size)
}
///////////////////////////////////////////////////////////////////////////
// functions
///////////////////////////////////////////////////////////////////////////
@Test
fun subList() {
assertEquals(
"Hello World",
message.toMessageChain().asSequence().joinToString(separator = "")
createComplexCombined().slowList.value.subList(2, 4),
complexCombined.subList(2, 4)
)
assertFalse {
complexCombined.slowList.isInitialized()
}
complexCombined.slowList.value // initialize
assertEquals(
createComplexCombined().slowList.value.subList(2, 4),
complexCombined.subList(2, 4)
)
}
}
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()
}
///////////////////////////////////////////////////////////////////////////
// visiting
///////////////////////////////////////////////////////////////////////////
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
@Test
fun acceptChildrenTest() {
val list = buildList {
complexCombined.acceptChildren(object : MessageVisitorUnit<Unit>() {
override fun visitMessage(message: Message, data: Unit) {
add(message)
super.visitMessage(message, data)
}
})
}
assertEquals(
listOf(
PlainText("foo"),
createTestInstance()
),
list
)
}
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())
@OptIn(MessageChainConstructor::class)
@Test
fun acceptChildrenRecursiveTest() {
val list = buildList {
complexCombined.accept(object : RecursiveMessageVisitor<Unit>() {
override fun visitMessage(message: Message, data: Unit) {
println("visitMessage: (${message::class.simpleName}) $message")
super.visitMessage(message, data)
}
override fun visitSingleMessage(message: SingleMessage, data: Unit) {
add(message)
super.visitSingleMessage(message, data)
}
})
}
assertEquals(
listOf(
PlainText("foo"),
PlainText("bar"),
AtAll,
PlainText("zoo"),
At(2),
At(1),
),
list
)
}
@Test
fun hierarchicalIterator() {
assertEquals(
listOf(
PlainText("foo"),
PlainText("bar"),
AtAll,
PlainText("zoo"),
At(2),
At(1),
),
complexCombined.iterator().asSequence().toList()
)
assertFalse { complexCombined.slowList.isInitialized() }
}
}

View File

@ -0,0 +1,177 @@
/*
* Copyright 2019-2022 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/dev/LICENSE
*/
package net.mamoe.mirai.message.data
import net.mamoe.mirai.utils.safeCast
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
internal class ConstrainSingleHelperTest {
@Test
fun linearPlains() {
val list = listOf(PlainText("1"), PlainText("2"))
ConstrainSingleHelper.constrainSingleMessages(list.asSequence()).run {
assertEquals(list, value)
assertFalse { hasConstrainSingle }
}
}
@Test
fun linearNonConstrains() {
val list = listOf(PlainText("1"), At(2))
ConstrainSingleHelper.constrainSingleMessages(list.asSequence()).run {
assertEquals(list, value)
assertFalse { hasConstrainSingle }
}
}
@Test
fun hierarchicalNonConstrains() {
val list = listOf(PlainText("1"), (PlainText("1") + At(2)))
ConstrainSingleHelper.constrainSingleMessages(list.asSequence()).run {
assertEquals(listOf(PlainText("1"), PlainText("1"), At(2)), value)
assertFalse { hasConstrainSingle }
}
}
@Test
fun singleConstrain() {
val list = listOf(Dice(2))
ConstrainSingleHelper.constrainSingleMessages(list.asSequence()).run {
assertEquals(list, value)
assertTrue { hasConstrainSingle }
}
}
@Test
fun linearDuplicatedConstrains() {
val list = listOf(Dice(2), Dice(3))
ConstrainSingleHelper.constrainSingleMessages(list.asSequence()).run {
assertEquals(1, value.size)
assertEquals(Dice(3), value.first())
assertTrue { hasConstrainSingle }
}
}
@Test
fun linearMultipleDuplicatedConstrains() {
val list = listOf(PlainText("a"), LightApp("aa"), Dice(2), Dice(3), LightApp("bb"), PlainText("c"))
ConstrainSingleHelper.constrainSingleMessages(list.asSequence()).run {
assertEquals(1, value.size)
assertEquals(listOf(LightApp("bb")), value)
assertTrue { hasConstrainSingle }
}
}
@Test
fun hierarchicalDuplicatedConstrains() {
val list = listOf(
MySingle(1), // before first MessageContent that can be replaced by the last ConstrainSingle
PlainText("a"),
LightApp("aa"),
LinearMessageChainImpl.create( // without processing ConstrainSingle
listOf(
PlainText("ab"),
LightApp("aab"),
Dice(5),
Dice(3),
LightApp("bbb"),
PlainText("cb")
), true
),
Dice(3),
LightApp("bb"), // last ConstrainSingle
PlainText("c")
)
ConstrainSingleHelper.constrainSingleMessages(list.asSequence()).run {
assertEquals(2, value.size)
assertEquals(listOf(MySingle(1), LightApp("bb")), value)
assertTrue { hasConstrainSingle }
}
}
@Test
fun hierarchicalDuplicatedConstrains3() {
val list = listOf(
PlainText("a"),
LightApp("aa"),
LinearMessageChainImpl.create( // without processing ConstrainSingle
listOf(
PlainText("ab"),
LightApp("aab"),
Dice(5),
Dice(3),
LightApp("bbb"),
PlainText("cb")
), true
),
Dice(3),
MySingle(1), // after first MessageContent that can be replaced by the last ConstrainSingle
LightApp("bb"), // last ConstrainSingle
PlainText("c")
)
ConstrainSingleHelper.constrainSingleMessages(list.asSequence()).run {
assertEquals(2, value.size)
assertEquals(listOf(LightApp("bb"), MySingle(1)), value)
assertTrue { hasConstrainSingle }
}
}
@Test
fun hierarchicalDuplicatedConstrains2() {
val list = listOf(
PlainText("a"),
LightApp("aa"),
Dice(3),
LightApp("bb"),
PlainText("c"),
LinearMessageChainImpl.create( // without processing ConstrainSingle
listOf(
PlainText("a"),
LightApp("aa"),
Dice(2),
MySingle(1), // after first MessageContent that can be replaced by the last ConstrainSingle
Dice(3),
LightApp("foo"), // last ConstrainSingle
PlainText("c")
), true
),
)
ConstrainSingleHelper.constrainSingleMessages(list.asSequence()).run {
assertEquals(2, value.size)
assertEquals(listOf(LightApp("foo"), MySingle(1)), value)
assertTrue { hasConstrainSingle }
}
}
}
private class MySingle(
val value: Int
) : MessageMetadata, ConstrainSingle {
override val key: MessageKey<MySingle>
get() = Key
override fun toString(): String {
return "MySingle@${this.hashCode()}"
}
override fun equals(other: Any?): Boolean {
return other is MySingle && other.value == this.value
}
override fun hashCode(): Int {
return value
}
private object Key : AbstractMessageKey<MySingle>({ it.safeCast() })
}

View File

@ -1,10 +1,10 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
* Copyright 2019-2022 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.
* 此源代码的使用受 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
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.message.data
@ -72,7 +72,7 @@ internal class ConstrainSingleTest {
assertEquals(7, result.size)
assertEquals(" [OK]ss p test", result.contentToString())
result as MessageChainImpl
result as LinearMessageChainImpl
assertSame(new, result.delegate.toTypedArray()[2])
}
@ -85,7 +85,8 @@ internal class ConstrainSingleTest {
last
)
val result = sequence.constrainSingleMessages()
val (result, has) = ConstrainSingleHelper.constrainSingleMessages(sequence)
assertTrue { has }
assertEquals(result.count(), 1)
assertSame(result.single(), last)
}
@ -100,7 +101,8 @@ internal class ConstrainSingleTest {
last
)
val result = sequence.constrainSingleMessages()
val (result, has) = ConstrainSingleHelper.constrainSingleMessages(sequence)
assertTrue { has }
assertEquals(result.count(), 2)
assertSame(result.first(), last)
}

View File

@ -0,0 +1,87 @@
/*
* Copyright 2019-2022 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/dev/LICENSE
*/
package net.mamoe.mirai.message.data
import net.mamoe.mirai.message.data.visitor.MessageVisitorUnit
import net.mamoe.mirai.message.data.visitor.acceptChildren
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
internal class LinearMessageChainImplTest {
private val complexLinearChain = buildMessageChain {
+PlainText("foo")
+buildMessageChain {
+PlainText("bar")
+emptyMessageChain()
+buildMessageChain {
+AtAll
+PlainText("zoo")
}
}
+buildMessageChain {
+buildMessageChain {
+At(2)
}
+At(1)
}
}
@Test
fun buildMessageChainCreatesLinearMessageChainImpl() {
assertIs<LinearMessageChainImpl>(complexLinearChain)
}
@Test
fun hierarchicalIterator() {
assertEquals(
listOf(
PlainText("foo"),
PlainText("bar"),
AtAll,
PlainText("zoo"),
At(2),
At(1),
),
complexLinearChain.iterator().asSequence().toList()
)
}
@Test
fun sizeTest() {
assertEquals(0, messageChainOf().size)
assertEquals(6, complexLinearChain.size)
assertEquals(6, complexLinearChain.count())
}
@Test
fun acceptChildrenTest() {
val list = buildList {
complexLinearChain.acceptChildren(object : MessageVisitorUnit<Unit>() {
override fun visitMessage(message: Message, data: Unit) {
add(message)
super.visitMessage(message, data)
}
})
}
assertEquals(
listOf(
PlainText("foo"),
PlainText("bar"),
AtAll,
PlainText("zoo"),
At(2),
At(1),
),
list
)
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2019-2022 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/dev/LICENSE
*/
package net.mamoe.mirai.message.data
import org.junit.jupiter.api.Test
import kotlin.test.assertIs
internal class MessageChainImplTest {
@OptIn(MessageChainConstructor::class)
@Test
fun allInternalImplementationsOfMessageChainAreMessageChainImpl() {
assertIs<MessageChainImpl>(CombinedMessage(AtAll, AtAll, false))
assertIs<MessageChainImpl>(emptyMessageChain())
val linear = LinearMessageChainImpl.create(listOf(AtAll), true)
assertIs<LinearMessageChainImpl>(linear)
assertIs<MessageChainImpl>(linear)
}
}

View File

@ -0,0 +1,461 @@
/*
* Copyright 2019-2022 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/dev/LICENSE
*/
package net.mamoe.mirai.message.data
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.message.data.visitor.AbstractMessageVisitor
import net.mamoe.mirai.message.data.visitor.accept
import net.mamoe.mirai.utils.MiraiExperimentalApi
import org.junit.jupiter.api.Test
import kotlin.test.assertContentEquals
internal class MessageVisitorTest {
object GetCalledMethodNames : AbstractMessageVisitor<Unit, Array<String>>() {
override fun visitMessage(message: Message, data: Unit): Array<String> {
return arrayOf("visitMessage")
}
override fun visitSingleMessage(message: SingleMessage, data: Unit): Array<String> {
return arrayOf("visitSingleMessage") + super.visitSingleMessage(message, data)
}
override fun visitMessageChain(messageChain: MessageChain, data: Unit): Array<String> {
return arrayOf("visitMessageChain") + super.visitMessageChain(messageChain, data)
}
override fun visitCombinedMessage(message: CombinedMessage, data: Unit): Array<String> {
return arrayOf("visitCombinedMessage") + super.visitCombinedMessage(message, data)
}
override fun visitMessageContent(message: MessageContent, data: Unit): Array<String> {
return arrayOf("visitMessageContent") + super.visitMessageContent(message, data)
}
override fun visitMessageMetadata(message: MessageMetadata, data: Unit): Array<String> {
return arrayOf("visitMessageMetadata") + super.visitMessageMetadata(message, data)
}
override fun visitMessageOrigin(message: MessageOrigin, data: Unit): Array<String> {
return arrayOf("visitMessageOrigin") + super.visitMessageOrigin(message, data)
}
override fun visitMessageSource(message: MessageSource, data: Unit): Array<String> {
return arrayOf("visitMessageSource") + super.visitMessageSource(message, data)
}
override fun visitQuoteReply(message: QuoteReply, data: Unit): Array<String> {
return arrayOf("visitQuoteReply") + super.visitQuoteReply(message, data)
}
override fun visitCustomMessageMetadata(message: CustomMessageMetadata, data: Unit): Array<String> {
return arrayOf("visitCustomMessageMetadata") + super.visitCustomMessageMetadata(message, data)
}
override fun visitShowImageFlag(message: ShowImageFlag, data: Unit): Array<String> {
return arrayOf("visitShowImageFlag") + super.visitShowImageFlag(message, data)
}
override fun visitPlainText(message: PlainText, data: Unit): Array<String> {
return arrayOf("visitPlainText") + super.visitPlainText(message, data)
}
override fun visitAt(message: At, data: Unit): Array<String> {
return arrayOf("visitAt") + super.visitAt(message, data)
}
override fun visitAtAll(message: AtAll, data: Unit): Array<String> {
return arrayOf("visitAtAll") + super.visitAtAll(message, data)
}
@Suppress("DEPRECATION_ERROR")
override fun visitVoice(message: Voice, data: Unit): Array<String> {
return arrayOf("visitVoice") + super.visitVoice(message, data)
}
override fun visitAudio(message: Audio, data: Unit): Array<String> {
return arrayOf("visitAudio") + super.visitAudio(message, data)
}
override fun visitHummerMessage(message: HummerMessage, data: Unit): Array<String> {
return arrayOf("visitHummerMessage") + super.visitHummerMessage(message, data)
}
override fun visitFlashImage(message: FlashImage, data: Unit): Array<String> {
return arrayOf("visitFlashImage") + super.visitFlashImage(message, data)
}
override fun visitPokeMessage(message: PokeMessage, data: Unit): Array<String> {
return arrayOf("visitPokeMessage") + super.visitPokeMessage(message, data)
}
override fun visitVipFace(message: VipFace, data: Unit): Array<String> {
return arrayOf("visitVipFace") + super.visitVipFace(message, data)
}
override fun visitMarketFace(message: MarketFace, data: Unit): Array<String> {
return arrayOf("visitMarketFace") + super.visitMarketFace(message, data)
}
override fun visitDice(message: Dice, data: Unit): Array<String> {
return arrayOf("visitDice") + super.visitDice(message, data)
}
override fun visitFace(message: Face, data: Unit): Array<String> {
return arrayOf("visitFace") + super.visitFace(message, data)
}
override fun visitFileMessage(message: FileMessage, data: Unit): Array<String> {
return arrayOf("visitFileMessage") + super.visitFileMessage(message, data)
}
override fun visitImage(message: Image, data: Unit): Array<String> {
return arrayOf("visitImage") + super.visitImage(message, data)
}
override fun visitForwardMessage(message: ForwardMessage, data: Unit): Array<String> {
return arrayOf("visitForwardMessage") + super.visitForwardMessage(message, data)
}
override fun visitMusicShare(message: MusicShare, data: Unit): Array<String> {
return arrayOf("visitMusicShare") + super.visitMusicShare(message, data)
}
override fun visitUnsupportedMessage(message: UnsupportedMessage, data: Unit): Array<String> {
return arrayOf("visitUnsupportedMessage") + super.visitUnsupportedMessage(message, data)
}
override fun visitRichMessage(message: RichMessage, data: Unit): Array<String> {
return arrayOf("visitRichMessage") + super.visitRichMessage(message, data)
}
override fun visitServiceMessage(message: ServiceMessage, data: Unit): Array<String> {
return arrayOf("visitServiceMessage") + super.visitServiceMessage(message, data)
}
override fun visitSimpleServiceMessage(message: SimpleServiceMessage, data: Unit): Array<String> {
return arrayOf("visitSimpleServiceMessage") + super.visitSimpleServiceMessage(message, data)
}
override fun visitLightApp(message: LightApp, data: Unit): Array<String> {
return arrayOf("visitLightApp") + super.visitLightApp(message, data)
}
override fun visitAbstractServiceMessage(message: AbstractServiceMessage, data: Unit): Array<String> {
return arrayOf("visitAbstractServiceMessage") + super.visitAbstractServiceMessage(message, data)
}
}
@OptIn(MessageChainConstructor::class)
@Test
fun visitMessageChain() {
assertContentEquals(
arrayOf(
"visitMessageChain",
"visitMessage",
),
messageChainOf(PlainText("1")).accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitMessageChain",
"visitMessage",
),
emptyMessageChain().accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitCombinedMessage",
"visitMessageChain",
"visitMessage",
),
CombinedMessage(AtAll, AtAll, false).accept(GetCalledMethodNames)
)
}
@Test
fun visitSingleMessageContent() {
assertContentEquals(
arrayOf(
"visitMessage",
),
object : Message {
@Suppress("RedundantOverride") // false positive
override fun toString(): String = super.toString()
override fun contentToString(): String = super.toString()
}.accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitSingleMessage",
"visitMessage",
),
object : SingleMessage {
@Suppress("RedundantOverride") // false positive
override fun toString(): String = super.toString()
override fun contentToString(): String = super.toString()
}.accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
object : MessageContent {
@Suppress("RedundantOverride") // false positive
override fun toString(): String = super.toString()
override fun contentToString(): String = super.toString()
}.accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitPlainText",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
PlainText("1").accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitAt",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
At(1).accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitAtAll",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
AtAll.accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitVoice",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
@Suppress("DEPRECATION_ERROR")
Voice("", byteArrayOf(), 1, 1, "").accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitAudio",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
object : OfflineAudio {
override val filename: String get() = ""
override val fileMd5: ByteArray get() = byteArrayOf()
override val fileSize: Long get() = 1
override val codec: AudioCodec get() = AudioCodec.AMR
override val extraData: ByteArray? get() = null
override fun toString(): String = "test"
}.accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitFlashImage",
"visitHummerMessage",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
FlashImage(createImage()).accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitPokeMessage",
"visitHummerMessage",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
PokeMessage.BiXin.accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitVipFace",
"visitHummerMessage",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
VipFace(VipFace.AiXin, 1).accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitMarketFace",
"visitHummerMessage",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
object : MarketFace {
override val name: String get() = "f"
override val id: Int get() = 1
override fun toString(): String = "ok"
}.accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitDice",
"visitMarketFace",
"visitHummerMessage",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
Dice(1).accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitFace",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
Face(Face.AI_NI).accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitFileMessage",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
object : FileMessage {
override val id: String get() = ""
override val internalId: Int get() = 1
override val name: String get() = ""
override val size: Long get() = 1
override suspend fun toAbsoluteFile(contact: FileSupported): Nothing =
throw UnsupportedOperationException()
override fun toString(): String = ""
}.accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitImage",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
createImage().accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitForwardMessage",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
ForwardMessage(listOf(), "", "", "", "", listOf()).accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitMusicShare",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
MusicShare(
kind = MusicKind.NeteaseCloudMusic,
title = "ファッション",
summary = "rinahamu/Yunomi",
brief = "",
jumpUrl = "https://music.163.com/song/1338728297/?userid=324076307",
pictureUrl = "https://p2.music.126.net/y19E5SadGUmSR8SZxkrNtw==/109951163785855539.jpg",
musicUrl = "https://music.163.com/song/media/outer/url?id=1338728297&userid=324076307"
).accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitUnsupportedMessage",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
object : UnsupportedMessage {
override val struct: ByteArray get() = byteArrayOf()
override fun toString(): String = "test"
}.accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitSimpleServiceMessage",
"visitServiceMessage",
"visitRichMessage",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
SimpleServiceMessage(1, "str").accept(GetCalledMethodNames)
)
assertContentEquals(
arrayOf(
"visitLightApp",
"visitRichMessage",
"visitMessageContent",
"visitSingleMessage",
"visitMessage",
),
LightApp("str").accept(GetCalledMethodNames)
)
}
}
private fun createImage(): Image {
return object : Image {
override val imageId: String get() = "{88914B32-B758-74ED-B00D-CAA6D2A5D7F6}.jpg"
override val width: Int get() = 1
override val height: Int get() = 1
override val size: Long get() = 1
override val imageType: ImageType get() = ImageType.APNG
override fun toString(): String = "test"
override fun contentToString(): String = "test"
@MiraiExperimentalApi
override fun appendMiraiCodeTo(builder: StringBuilder) {
}
}
}

View File

@ -319,9 +319,7 @@ internal suspend fun <C : Contact> SendMessageHandler<C>.transformSpecialMessage
)
forward.nodeList.forEach { tmp.addAll(it.messageChain) }
// toMessageChain will lose some element
@Suppress("INVISIBLE_MEMBER")
createMessageChainImplOptimized(tmp).verifyLength(forward, contact)
tmp.verifyLength(forward, contact)
}
val resId = getMiraiImpl().uploadMessageHighway(

View File

@ -30,7 +30,7 @@ internal fun Contact.logMessageSent(message: Message) {
}
}
internal fun MessageChain.countImages(): Int = this.count { it is Image }
internal fun Iterable<SingleMessage>.countImages(): Int = this.count { it is Image }
private val logger by lazy { MiraiLogger.Factory.create(SendMessageHandler::class) }
@ -55,14 +55,14 @@ internal fun Message.verifySendingValid() {
}
}
internal fun MessageChain.verifyLength(
internal fun Iterable<SingleMessage>.verifyLength(
originalMessage: Message, target: Contact,
): Int {
val chain = this
val length = estimateLength(target, 15001)
if (length > 15000 || countImages() > 50) {
throw MessageTooLargeException(
target, originalMessage, this,
target, originalMessage, this.toMessageChain(),
"message(${
chain.joinToString("", limit = 10).let { rsp ->
if (rsp.length > 100) {

View File

@ -156,8 +156,9 @@ internal object LightMessageRefiner : MessageRefiner() {
val source = this.sourceOrNull?.safeCast<IncomingMessageSourceInternal>() ?: return this
val originalMessage = this
source.originalMessageLazy = lazy {
@Suppress("INVISIBLE_MEMBER")
createMessageChainImplOptimized(originalMessage.filterNot { it is MessageSource })
originalMessage.filterNot { it is MessageSource }.toMessageChain()
// @Suppress("INVISIBLE_MEMBER")
// createMessageChainImplOptimized(originalMessage.filterNot { it is MessageSource })
}
return this
}

View File

@ -44,7 +44,7 @@ internal fun String.toIpV4Long(): Long {
}
}
internal fun MessageChain.estimateLength(target: ContactOrBot, upTo: Int): Int =
internal fun Iterable<SingleMessage>.estimateLength(target: ContactOrBot, upTo: Int): Int =
sumUpTo(upTo) { it, up ->
it.estimateLength(target, up)
}

View File

@ -151,8 +151,7 @@ internal class CleanupRubbishMessageElementsTest {
}
private fun noMessageSource(c: MessageChain): MessageChain {
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
return createMessageChainImplOptimized(c.filterNot { it is MessageSource })
return c.filterNot { it is MessageSource }.toMessageChain()
}
//endregion