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 package net.mamoe.mirai.message.code
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.MiraiExperimentalApi
/** /**
@ -36,5 +37,9 @@ public interface CodableMessage : Message {
* *
* @suppress 警告: API 可能在任何时刻被改变 * @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 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.code.internal.parseMiraiCodeImpl
import net.mamoe.mirai.message.data.Message import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.safeCast
/** /**
* 解析形如 "[mirai:]" mirai , [Message.toString] 返回的内容. * 解析形如 "[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 package net.mamoe.mirai.message.code.internal
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.MiraiInternalApi
@Suppress("RegExpRedundantEscape") // required on android internal fun String.parseMiraiCodeImpl(contact: Contact?): MessageChain = buildMessageChain {
internal val codeRegex = Regex("""(?:\[mirai:([^\]]*)?:(.*?)?\])|(?:\[mirai:([^:]+)\])""")
internal fun String.parseMiraiCodeImpl(): MessageChain = buildMessageChain {
forEachMiraiCode { origin, name, args -> forEachMiraiCode { origin, name, args ->
if (name == null) { if (name == null) {
add(PlainText(origin)) add(PlainText(origin.decodeMiraiCode()))
return@forEachMiraiCode return@forEachMiraiCode
} }
val parser = MiraiCodeParsers[name] ?: kotlin.run { val parser = MiraiCodeParsers[name] ?: kotlin.run {
add(PlainText(origin)) add(PlainText(origin.decodeMiraiCode()))
return@forEachMiraiCode return@forEachMiraiCode
} }
parser.argsRegex.matchEntire(args) parser.argsRegex.matchEntire(args)
?.destructured ?.destructured
?.let { ?.let {
parser.runCatching { parser.runCatching {
mapper(it) contact.mapper(it)
}.getOrNull() }.getOrNull()
} }
?.let(::add) ?.let(::add)
?: add(PlainText(origin)) ?: add(PlainText(origin.decodeMiraiCode()))
} }
} }
internal inline fun String.forEachMiraiCode(crossinline block: (origin: String, name: String?, args: String) -> Unit) { internal fun String.forEachMiraiCode(block: (origin: String, name: String?, args: String) -> Unit) {
var lastIndex = 0 var pos = 0
for (result in codeRegex.findAll(this)) { var lastPos = 0
if (result.range.first != lastIndex) { val len = length - 7 // [mirai:
// skipped string fun findEnding(start: Int): Int {
block(substring(lastIndex, result.range.first), null, "") 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 ?: "")
} }
if (lastIndex != this.length) { return -1
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( internal object MiraiCodeParsers : Map<String, MiraiCodeParser> by mapOf(
"at" to MiraiCodeParser(Regex("""(\d*)""")) { (target, _) -> "at" to MiraiCodeParser(Regex("""(\d*)""")) { (target) ->
At(target.toLong()) At(target.toLong())
}, },
"atall" to MiraiCodeParser(Regex("")) { "atall" to MiraiCodeParser(Regex("")) {
@ -79,40 +119,44 @@ internal object MiraiCodeParsers : Map<String, MiraiCodeParser> by mapOf(
}, },
"flash" to MiraiCodeParser(Regex("""(.*)""")) { (id) -> "flash" to MiraiCodeParser(Regex("""(.*)""")) { (id) ->
Image(id).flash() 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( internal class MiraiCodeParser(
val argsRegex: Regex, 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}" return "@${member.nameCardOrNick}"
} }
override fun appendMiraiCode(builder: StringBuilder) {
builder.append("[mirai:at:").append(target).append(']')
}
public companion object { public companion object {
/** /**
* 构造一个 [At], 仅供内部使用, 否则可能造成消息无法发出的问题. * 构造一个 [At], 仅供内部使用, 否则可能造成消息无法发出的问题.

View File

@ -40,6 +40,14 @@ public object AtAll :
return other === this return other === this
} }
override fun toMiraiCode(): String {
return toString()
}
override fun appendMiraiCode(builder: StringBuilder) {
builder.append(toString())
}
public override fun hashCode(): Int { public override fun hashCode(): Int {
return display.hashCode() 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 val name: String get() = contentToString().let { it.substring(1, it.length - 1) }
public override fun contentToString(): String = names.getOrElse(id) { "[表情]" } 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 equals(other: Any?): Boolean = other is Face && other.id == this.id
public override fun hashCode(): Int = 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.Contextual
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import net.mamoe.mirai.message.code.CodableMessage 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.message.data.VipFace.Kind
import net.mamoe.mirai.utils.MiraiInternalApi import net.mamoe.mirai.utils.MiraiInternalApi
import net.mamoe.mirai.utils.PlannedRemoval import net.mamoe.mirai.utils.PlannedRemoval
@ -173,6 +174,12 @@ public data class PokeMessage @MiraiInternalApi constructor(
private val stringValue = "[mirai:poke:$name,$pokeType,$id]" 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 toString(): String = stringValue
override fun contentToString(): String = "[戳一戳]" override fun contentToString(): String = "[戳一戳]"
//businessType=0x00000001(1) //businessType=0x00000001(1)
@ -290,6 +297,9 @@ public data class VipFace @MiraiInternalApi constructor(
private infix fun Int.to(name: String): Kind = Kind(this, name) 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]" 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}]" } 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 toString(): String = stringValue
public override fun contentToString(): String = "[闪照]" 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 toString(): String = _stringValue!!
final override fun contentToString(): String = "[图片]" 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.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import net.mamoe.mirai.event.events.MessageEvent 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.quote
import net.mamoe.mirai.message.data.MessageSource.Key.recall import net.mamoe.mirai.message.data.MessageSource.Key.recall
import net.mamoe.mirai.utils.safeCast
import kotlin.js.JsName import kotlin.js.JsName
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@ -56,7 +58,7 @@ import kotlin.reflect.KProperty
*/ */
@Serializable(MessageChain.Serializer::class) @Serializable(MessageChain.Serializer::class)
@Suppress("FunctionName", "DeprecatedCallableAddReplaceWith", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @Suppress("FunctionName", "DeprecatedCallableAddReplaceWith", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
public interface MessageChain : Message, List<SingleMessage>, RandomAccess { public interface MessageChain : Message, List<SingleMessage>, RandomAccess, CodableMessage {
/** /**
* 元素数量. [EmptyMessageChain] 不参加计数. * 元素数量. [EmptyMessageChain] 不参加计数.
*/ */
@ -98,6 +100,10 @@ public interface MessageChain : Message, List<SingleMessage>, RandomAccess {
override fun deserialize(decoder: Decoder): MessageChain = delegate.deserialize(decoder).asMessageChain() override fun deserialize(decoder: Decoder): MessageChain = delegate.deserialize(decoder).asMessageChain()
override fun serialize(encoder: Encoder, value: MessageChain): Unit = delegate.serialize(encoder, value) 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 // region accessors
@ -430,4 +436,6 @@ public object EmptyMessageChain : MessageChain, Iterator<SingleMessage>, List<Si
public override fun iterator(): Iterator<SingleMessage> = this public override fun iterator(): Iterator<SingleMessage> = this
public override fun hasNext(): Boolean = false public override fun hasNext(): Boolean = false
public override fun next(): SingleMessage = throw NoSuchElementException("EmptyMessageChain is empty.") 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 package net.mamoe.mirai.message.data
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import net.mamoe.mirai.message.code.CodableMessage
import net.mamoe.mirai.message.code.internal.appendAsMiraiCode
/** /**
* 纯文本. 可含 emoji 表情如 😊. * 纯文本. 可含 emoji 表情如 😊.
@ -23,13 +25,17 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
public data class PlainText( public data class PlainText(
public val content: String public val content: String
) : MessageContent { ) : MessageContent, CodableMessage {
@Suppress("unused") @Suppress("unused")
public constructor(charSequence: CharSequence) : this(charSequence.toString()) public constructor(charSequence: CharSequence) : this(charSequence.toString())
public override fun toString(): String = content public override fun toString(): String = content
public override fun contentToString(): String = content public override fun contentToString(): String = content
override fun appendMiraiCode(builder: StringBuilder) {
builder.appendAsMiraiCode(content)
}
public companion object public companion object
} }

View File

@ -14,6 +14,8 @@
package net.mamoe.mirai.message.data package net.mamoe.mirai.message.data
import kotlinx.serialization.Serializable 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.MiraiExperimentalApi
import net.mamoe.mirai.utils.PlannedRemoval import net.mamoe.mirai.utils.PlannedRemoval
import net.mamoe.mirai.utils.safeCast import net.mamoe.mirai.utils.safeCast
@ -94,10 +96,14 @@ public interface RichMessage : MessageContent, ConstrainSingle {
* @see ServiceMessage 服务消息 * @see ServiceMessage 服务消息
*/ */
@Serializable @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 companion object Key : AbstractMessageKey<LightApp>({ it.safeCast() })
public override fun toString(): String = "[mirai:app:$content]" 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() result = 31 * result + content.hashCode()
return result return result
} }
} }
@ -141,7 +148,7 @@ public class SimpleServiceMessage(
* @see LightApp 小程序类型消息 * @see LightApp 小程序类型消息
* @see SimpleServiceMessage * @see SimpleServiceMessage
*/ */
public interface ServiceMessage : RichMessage { public interface ServiceMessage : RichMessage, CodableMessage {
public companion object Key : public companion object Key :
AbstractPolymorphicMessageKey<RichMessage, ServiceMessage>(RichMessage, { it.safeCast() }) AbstractPolymorphicMessageKey<RichMessage, ServiceMessage>(RichMessage, { it.safeCast() })
@ -149,6 +156,10 @@ public interface ServiceMessage : RichMessage {
* 目前未知, XML 一般为 60, JSON 一般为 1 * 目前未知, XML 一般为 60, JSON 一般为 1
*/ */
public val serviceId: Int public val serviceId: Int
override fun appendMiraiCode(builder: StringBuilder) {
builder.append("[mirai:service:").append(serviceId).append(',').appendAsMiraiCode(content).append(']')
}
} }
@Suppress("FunctionName") @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.hexToBytes
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.internal.utils.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.Face
import net.mamoe.mirai.message.data.MarketFace 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() 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() override val name: String = delegate.faceName.decodeToString()
@Transient @Transient
override val id: Int = delegate.tabId 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]" 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 toString(): String = _stringValue!!
final override fun contentToString(): String = "[图片]" final override fun contentToString(): String = "[图片]"
override fun appendMiraiCode(builder: StringBuilder) {
builder.append("[mirai:image:").append(imageId).append("]")
}
} }
internal interface ConstOriginUrlAware : Image { internal interface ConstOriginUrlAware : Image {