Mirai code (#768)

* Mirai Code

* cleanup code

* mirai code of MarketFace
This commit is contained in:
Karlatemp 2020-12-26 15:55:13 +08:00 committed by GitHub
parent da7abc5bf6
commit 2f903cef9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 237 additions and 61 deletions

View File

@ -10,6 +10,7 @@
package net.mamoe.mirai.message.code
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.MiraiExperimentalApi
/**
@ -36,5 +37,9 @@ public interface CodableMessage : Message {
*
* @suppress 警告: API 可能在任何时刻被改变
*/
public fun toMiraiCode(): String = this.toString()
public fun toMiraiCode(): String = buildString { appendMiraiCode(this) }
// Using StringBuilder faster than direct plus objects
@MiraiExperimentalApi
public fun appendMiraiCode(builder: StringBuilder)
}

View File

@ -12,11 +12,21 @@
package net.mamoe.mirai.message.code
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.message.code.internal.parseMiraiCodeImpl
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.safeCast
/**
* 解析形如 "[mirai:]" mirai , [Message.toString] 返回的内容.
*/
public fun String.parseMiraiCode(): MessageChain = parseMiraiCodeImpl()
@JvmOverloads
public fun String.parseMiraiCode(contact: Contact? = null): MessageChain = parseMiraiCodeImpl(contact)
public fun <T : Message> Iterable<T>.toMiraiCode(): String = iterator().toMiraiCode()
public fun <T : Message> Iterator<T>.toMiraiCode(): String = buildString {
this@toMiraiCode.forEach {
it.safeCast<CodableMessage>()?.appendMiraiCode(this)
}
}

View File

@ -11,55 +11,95 @@
package net.mamoe.mirai.message.code.internal
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.MiraiInternalApi
@Suppress("RegExpRedundantEscape") // required on android
internal val codeRegex = Regex("""(?:\[mirai:([^\]]*)?:(.*?)?\])|(?:\[mirai:([^:]+)\])""")
internal fun String.parseMiraiCodeImpl(): MessageChain = buildMessageChain {
internal fun String.parseMiraiCodeImpl(contact: Contact?): MessageChain = buildMessageChain {
forEachMiraiCode { origin, name, args ->
if (name == null) {
add(PlainText(origin))
add(PlainText(origin.decodeMiraiCode()))
return@forEachMiraiCode
}
val parser = MiraiCodeParsers[name] ?: kotlin.run {
add(PlainText(origin))
add(PlainText(origin.decodeMiraiCode()))
return@forEachMiraiCode
}
parser.argsRegex.matchEntire(args)
?.destructured
?.let {
parser.runCatching {
mapper(it)
contact.mapper(it)
}.getOrNull()
}
?.let(::add)
?: add(PlainText(origin))
?: add(PlainText(origin.decodeMiraiCode()))
}
}
internal inline fun String.forEachMiraiCode(crossinline block: (origin: String, name: String?, args: String) -> Unit) {
var lastIndex = 0
for (result in codeRegex.findAll(this)) {
if (result.range.first != lastIndex) {
// skipped string
block(substring(lastIndex, result.range.first), null, "")
internal fun String.forEachMiraiCode(block: (origin: String, name: String?, args: String) -> Unit) {
var pos = 0
var lastPos = 0
val len = length - 7 // [mirai:
fun findEnding(start: Int): Int {
var pos0 = start
while (pos0 < length) {
when (get(pos0)) {
'\\' -> pos0 += 2
']' -> return pos0
else -> pos0++
}
}
lastIndex = result.range.last + 1
if (result.groups[3] != null) {
// no param
block(result.value, result.groups[3]!!.value, "")
} else block(result.value, result.groups[1]!!.value, result.groups[2]?.value ?: "")
return -1
}
if (lastIndex != this.length) {
block(substring(lastIndex, this.length), null, "")
while (pos < len) {
when (get(pos)) {
'\\' -> {
pos += 2
}
'[' -> {
if (get(pos + 1) == 'm' && get(pos + 2) == 'i' &&
get(pos + 3) == 'r' && get(pos + 4) == 'a' &&
get(pos + 5) == 'i' && get(pos + 6) == ':'
) {
val begin = pos
pos += 7
val ending = findEnding(pos)
if (ending == -1) {
block(substring(lastPos), null, "")
return
} else {
if (lastPos < begin) {
block(substring(lastPos, begin), null, "")
}
val v = substring(begin, ending + 1)
val splitter = v.indexOf(':', 7)
block(
v, if (splitter == -1)
v.substring(7, v.length - 1)
else v.substring(7, splitter),
if (splitter == -1) {
""
} else v.substring(splitter + 1, v.length - 1)
)
lastPos = ending + 1
pos = lastPos
}
} else pos++
}
else -> {
pos++
}
}
}
if (lastPos < length) {
block(substring(lastPos), null, "")
}
}
internal object MiraiCodeParsers : Map<String, MiraiCodeParser> by mapOf(
"at" to MiraiCodeParser(Regex("""(\d*)""")) { (target, _) ->
"at" to MiraiCodeParser(Regex("""(\d*)""")) { (target) ->
At(target.toLong())
},
"atall" to MiraiCodeParser(Regex("")) {
@ -79,40 +119,44 @@ internal object MiraiCodeParsers : Map<String, MiraiCodeParser> by mapOf(
},
"flash" to MiraiCodeParser(Regex("""(.*)""")) { (id) ->
Image(id).flash()
},
"service" to MiraiCodeParser(Regex("""(\d*),(.*)""")) { (id, content) ->
SimpleServiceMessage(id.toInt(), content.decodeMiraiCode())
},
"app" to MiraiCodeParser(Regex("""(.*)""")) { (content) ->
LightApp(content.decodeMiraiCode())
}
)
/*
internal object MiraiCodeParsers2 : Map<String, (args: String) -> Message?> by mapOf(
"at" to l@{ args ->
val group = args.split(',')
if (group.size != 2) return@l null
val target = group[0].toLongOrNull() ?: return@l null
@Suppress("INVISIBLE_MEMBER")
At(target, group[1])
},
"atall" to l@{
AtAll
},
"poke" to l@{ args ->
val group = args.split(',')
if (group.size != 2) return@l null
val type = group[1].toIntOrNull() ?: return@l null
val id = group[2].toIntOrNull() ?: return@l null
@Suppress("INVISIBLE_MEMBER")
PokeMessage(group[0], type, id)
},
"vipface" to l@{ args ->
val group = args.split(',')
if (group.size != 2) return@l null
val type = group[1].toIntOrNull() ?: return@l null
val id = group[2].toIntOrNull() ?: return@l null
@Suppress("INVISIBLE_MEMBER")
PokeMessage(group[0], type, id)
}
)*/
internal class MiraiCodeParser(
val argsRegex: Regex,
val mapper: MiraiCodeParser.(MatchResult.Destructured) -> Message?
)
val mapper: Contact?.(MatchResult.Destructured) -> Message?
)
@MiraiInternalApi
public fun StringBuilder.appendAsMiraiCode(value: String): StringBuilder = apply {
value.forEach { char ->
when (char) {
'[', ']',
':', ',',
'\\',
-> append("\\").append(char)
'\n' -> append("\\n")
'\r' -> append("\\r")
else -> append(char)
}
}
}
@Suppress("RegExpRedundantEscape")
internal val DECODE_MIRAI_CODE_REGEX = """\\.""".toRegex()
internal val DECODE_MIRAI_CODE_TRANSLATOR: (MatchResult) -> String = {
when (it.value[1]) {
'n' -> "\n"
'r' -> "\r"
'\n' -> ""
else -> it.value.substring(1)
}
}
internal fun String.decodeMiraiCode() = replace(DECODE_MIRAI_CODE_REGEX, DECODE_MIRAI_CODE_TRANSLATOR)

View File

@ -55,6 +55,10 @@ public data class At(
return "@${member.nameCardOrNick}"
}
override fun appendMiraiCode(builder: StringBuilder) {
builder.append("[mirai:at:").append(target).append(']')
}
public companion object {
/**
* 构造一个 [At], 仅供内部使用, 否则可能造成消息无法发出的问题.

View File

@ -40,6 +40,14 @@ public object AtAll :
return other === this
}
override fun toMiraiCode(): String {
return toString()
}
override fun appendMiraiCode(builder: StringBuilder) {
builder.append(toString())
}
public override fun hashCode(): Int {
return display.hashCode()
}

View File

@ -29,6 +29,10 @@ public data class Face(public val id: Int) : // used in delegation
public val name: String get() = contentToString().let { it.substring(1, it.length - 1) }
public override fun contentToString(): String = names.getOrElse(id) { "[表情]" }
override fun appendMiraiCode(builder: StringBuilder) {
builder.append("[mirai:face:").append(id).append(']')
}
public override fun equals(other: Any?): Boolean = other is Face && other.id == this.id
public override fun hashCode(): Int = id

View File

@ -16,6 +16,7 @@ package net.mamoe.mirai.message.data
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import net.mamoe.mirai.message.code.CodableMessage
import net.mamoe.mirai.message.code.internal.appendAsMiraiCode
import net.mamoe.mirai.message.data.VipFace.Kind
import net.mamoe.mirai.utils.MiraiInternalApi
import net.mamoe.mirai.utils.PlannedRemoval
@ -173,6 +174,12 @@ public data class PokeMessage @MiraiInternalApi constructor(
private val stringValue = "[mirai:poke:$name,$pokeType,$id]"
override fun appendMiraiCode(builder: StringBuilder) {
builder.append("[mirai:poke:").appendAsMiraiCode(name)
.append(',').append(pokeType).append(',').append(id)
.append(']')
}
override fun toString(): String = stringValue
override fun contentToString(): String = "[戳一戳]"
//businessType=0x00000001(1)
@ -290,6 +297,9 @@ public data class VipFace @MiraiInternalApi constructor(
private infix fun Int.to(name: String): Kind = Kind(this, name)
}
override fun appendMiraiCode(builder: StringBuilder) {
builder.append(stringValue) // TODO:
}
private val stringValue = "[mirai:vipface:$kind,$count]"
@ -339,6 +349,11 @@ public data class FlashImage(
private val stringValue: String by lazy(LazyThreadSafetyMode.NONE) { "[mirai:flash:${image.imageId}]" }
override fun appendMiraiCode(builder: StringBuilder) {
builder.append(stringValue)
}
override fun toMiraiCode(): String = stringValue
public override fun toString(): String = stringValue
public override fun contentToString(): String = "[闪照]"
}

View File

@ -229,6 +229,9 @@ public sealed class AbstractImage : Image {
final override fun toString(): String = _stringValue!!
final override fun contentToString(): String = "[图片]"
override fun appendMiraiCode(builder: StringBuilder) {
builder.append("[mirai:image:").append(imageId).append("]")
}
}

View File

@ -20,8 +20,10 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.message.code.CodableMessage
import net.mamoe.mirai.message.data.MessageSource.Key.quote
import net.mamoe.mirai.message.data.MessageSource.Key.recall
import net.mamoe.mirai.utils.safeCast
import kotlin.js.JsName
import kotlin.reflect.KProperty
@ -56,7 +58,7 @@ import kotlin.reflect.KProperty
*/
@Serializable(MessageChain.Serializer::class)
@Suppress("FunctionName", "DeprecatedCallableAddReplaceWith", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
public interface MessageChain : Message, List<SingleMessage>, RandomAccess {
public interface MessageChain : Message, List<SingleMessage>, RandomAccess, CodableMessage {
/**
* 元素数量. [EmptyMessageChain] 不参加计数.
*/
@ -98,6 +100,10 @@ public interface MessageChain : Message, List<SingleMessage>, RandomAccess {
override fun deserialize(decoder: Decoder): MessageChain = delegate.deserialize(decoder).asMessageChain()
override fun serialize(encoder: Encoder, value: MessageChain): Unit = delegate.serialize(encoder, value)
}
override fun appendMiraiCode(builder: StringBuilder) {
forEach { it.safeCast<CodableMessage>()?.appendMiraiCode(builder) }
}
}
// region accessors
@ -430,4 +436,6 @@ public object EmptyMessageChain : MessageChain, Iterator<SingleMessage>, List<Si
public override fun iterator(): Iterator<SingleMessage> = this
public override fun hasNext(): Boolean = false
public override fun next(): SingleMessage = throw NoSuchElementException("EmptyMessageChain is empty.")
override fun toMiraiCode(): String = ""
override fun appendMiraiCode(builder: StringBuilder) {}
}

View File

@ -14,6 +14,8 @@
package net.mamoe.mirai.message.data
import kotlinx.serialization.Serializable
import net.mamoe.mirai.message.code.CodableMessage
import net.mamoe.mirai.message.code.internal.appendAsMiraiCode
/**
* 纯文本. 可含 emoji 表情如 😊.
@ -23,13 +25,17 @@ import kotlinx.serialization.Serializable
@Serializable
public data class PlainText(
public val content: String
) : MessageContent {
) : MessageContent, CodableMessage {
@Suppress("unused")
public constructor(charSequence: CharSequence) : this(charSequence.toString())
public override fun toString(): String = content
public override fun contentToString(): String = content
override fun appendMiraiCode(builder: StringBuilder) {
builder.appendAsMiraiCode(content)
}
public companion object
}

View File

@ -14,6 +14,8 @@
package net.mamoe.mirai.message.data
import kotlinx.serialization.Serializable
import net.mamoe.mirai.message.code.CodableMessage
import net.mamoe.mirai.message.code.internal.appendAsMiraiCode
import net.mamoe.mirai.utils.MiraiExperimentalApi
import net.mamoe.mirai.utils.PlannedRemoval
import net.mamoe.mirai.utils.safeCast
@ -94,10 +96,14 @@ public interface RichMessage : MessageContent, ConstrainSingle {
* @see ServiceMessage 服务消息
*/
@Serializable
public data class LightApp(override val content: String) : RichMessage {
public data class LightApp(override val content: String) : RichMessage, CodableMessage {
public companion object Key : AbstractMessageKey<LightApp>({ it.safeCast() })
public override fun toString(): String = "[mirai:app:$content]"
override fun appendMiraiCode(builder: StringBuilder) {
builder.append("[mirai:app:").appendAsMiraiCode(content).append(']')
}
}
/**
@ -130,6 +136,7 @@ public class SimpleServiceMessage(
result = 31 * result + content.hashCode()
return result
}
}
@ -141,7 +148,7 @@ public class SimpleServiceMessage(
* @see LightApp 小程序类型消息
* @see SimpleServiceMessage
*/
public interface ServiceMessage : RichMessage {
public interface ServiceMessage : RichMessage, CodableMessage {
public companion object Key :
AbstractPolymorphicMessageKey<RichMessage, ServiceMessage>(RichMessage, { it.safeCast() })
@ -149,6 +156,10 @@ public interface ServiceMessage : RichMessage {
* 目前未知, XML 一般为 60, JSON 一般为 1
*/
public val serviceId: Int
override fun appendMiraiCode(builder: StringBuilder) {
builder.append("[mirai:service:").append(serviceId).append(',').appendAsMiraiCode(content).append(']')
}
}
@Suppress("FunctionName")

View File

@ -0,0 +1,46 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.message.code
import net.mamoe.mirai.message.data.*
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class TestMiraiCode {
@Test
fun testCodes() {
assertEquals(AtAll.asMessageChain(), "[mirai:atall]".parseMiraiCode())
assertEquals(PlainText("[Hello").asMessageChain(), "\\[Hello".parseMiraiCode())
assertEquals(buildMessageChain {
+PlainText("1")
+AtAll
+PlainText("2345")
+AtAll
}, "1[mirai:atall]2345[mirai:atall]".parseMiraiCode())
assertEquals(buildMessageChain {
+PlainText("1")
+AtAll
+PlainText("2345[mirai:atall")
}, "1[mirai:atall]2345[mirai:atall".parseMiraiCode())
assertEquals(buildMessageChain {
+PlainText("[mirai:atall]")
}, "\\[mirai:atall]".parseMiraiCode())
assertEquals(buildMessageChain {
+PlainText("[mirai:atall]")
}, "[mirai:atall\\]".parseMiraiCode())
assertEquals(buildMessageChain {
+PlainText("[mirai:atall]")
}, "[mirai\\:atall]".parseMiraiCode())
assertEquals(buildMessageChain {
+SimpleServiceMessage(1, "[HiHi!!!\\]")
+PlainText(" XE")
}, "[mirai:service:1,\\[HiHi!!!\\\\\\]] XE".parseMiraiCode())
}
}

View File

@ -17,8 +17,10 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.internal.utils.hexToBytes
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.internal.utils.toByteArray
import net.mamoe.mirai.message.code.internal.appendAsMiraiCode
import net.mamoe.mirai.message.data.Face
import net.mamoe.mirai.message.data.MarketFace
import net.mamoe.mirai.utils.MiraiExperimentalApi
internal val FACE_BUF = "00 01 00 04 52 CC F5 D0".hexToBytes()
@ -51,5 +53,11 @@ internal data class MarketFaceImpl internal constructor(
override val name: String = delegate.faceName.decodeToString()
@Transient
override val id: Int = delegate.tabId
@MiraiExperimentalApi
override fun appendMiraiCode(builder: StringBuilder) {
builder.append("[mirai:marketface:").append(id).append(",").appendAsMiraiCode(name).append(']')
}
override fun toString() = "[mirai:marketface:$id,$name]"
}

View File

@ -121,6 +121,10 @@ internal abstract class AbstractImage : Image { // make sealed in 1.3.0 ?
final override fun toString(): String = _stringValue!!
final override fun contentToString(): String = "[图片]"
override fun appendMiraiCode(builder: StringBuilder) {
builder.append("[mirai:image:").append(imageId).append("]")
}
}
internal interface ConstOriginUrlAware : Image {