Commit b8b749bf authored by Him188's avatar Him188

Completed CombinedMessage redesigning and constraining on concatenation

parent eaa1e96a
......@@ -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))
}
}
......
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package compatibility
fun main() {
}
\ No newline at end of file
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())
}
}
\ No newline at end of file
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package compatibility
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.PlainText
import org.junit.Test
internal class TestKotlinCompatibility {
@Test
fun testMessageChain() {
val x = PlainText("te") + PlainText("st")
println(Message::class.java.declaredMethods.joinToString("\n"))
println()
println(x::class.java.declaredMethods.joinToString("\n"))
}
}
\ No newline at end of file
......@@ -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)
......
......@@ -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
}
......
......@@ -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
}
......
......@@ -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,
......
......@@ -87,6 +87,7 @@ expect abstract class Bot() : CoroutineScope, LowLevelBotAPIAccessor {
/**
* 昵称
*/
@SinceMirai("0.33.1")
abstract val nick: String
/**
......
......@@ -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]
......
......@@ -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>()
}
}
......
......@@ -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)
}
......
......@@ -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)
}
......
......@@ -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)
}
}
}
\ No newline at end of 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
......
......@@ -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)
......
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
}
......
......@@ -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 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())
}
@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 testConstrainSingleInPlus() {
fun testChainPlusSingle() {
val new = TestConstrainSingleMessage()
val combined = (TestConstrainSingleMessage() + new) as CombinedMessage
assertEquals(combined.left, EmptyMessageChain)
assertSame(combined.tail, new)
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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment