mirror of
https://github.com/mamoe/mirai.git
synced 2025-02-09 06:57:16 +08:00
MessageProtocol implementations
This commit is contained in:
parent
d6343870b8
commit
c47779c726
File diff suppressed because one or more lines are too long
@ -9,28 +9,19 @@
|
|||||||
|
|
||||||
package net.mamoe.mirai.internal.message
|
package net.mamoe.mirai.internal.message
|
||||||
|
|
||||||
import kotlinx.io.core.discardExact
|
|
||||||
import kotlinx.io.core.readUInt
|
|
||||||
import kotlinx.io.core.readUShort
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import net.mamoe.mirai.Bot
|
import net.mamoe.mirai.Bot
|
||||||
import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep
|
import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep
|
||||||
import net.mamoe.mirai.internal.message.LightMessageRefiner.refineLight
|
import net.mamoe.mirai.internal.message.LightMessageRefiner.refineLight
|
||||||
import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.cleanupRubbishMessageElements
|
import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.cleanupRubbishMessageElements
|
||||||
import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.joinToMessageChain
|
import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.joinToMessageChain
|
||||||
import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.toAudio
|
import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.toAudio
|
||||||
import net.mamoe.mirai.internal.message.data.*
|
import net.mamoe.mirai.internal.message.data.LongMessageInternal
|
||||||
import net.mamoe.mirai.internal.message.image.OnlineFriendImageImpl
|
import net.mamoe.mirai.internal.message.data.OnlineAudioImpl
|
||||||
import net.mamoe.mirai.internal.message.image.OnlineGroupImageImpl
|
|
||||||
import net.mamoe.mirai.internal.message.source.*
|
import net.mamoe.mirai.internal.message.source.*
|
||||||
import net.mamoe.mirai.internal.network.protocol.data.proto.*
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm
|
||||||
import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
|
|
||||||
import net.mamoe.mirai.message.data.*
|
import net.mamoe.mirai.message.data.*
|
||||||
import net.mamoe.mirai.utils.read
|
|
||||||
import net.mamoe.mirai.utils.toLongUnsigned
|
import net.mamoe.mirai.utils.toLongUnsigned
|
||||||
import net.mamoe.mirai.utils.toUHexString
|
|
||||||
import net.mamoe.mirai.utils.unzip
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 只在手动构造 [OfflineMessageSource] 时调用
|
* 只在手动构造 [OfflineMessageSource] 时调用
|
||||||
@ -113,6 +104,7 @@ private fun List<MsgComm.Msg>.toMessageChain(
|
|||||||
builder.add(ReceiveMessageTransformer.createMessageSource(bot, onlineSource, messageSourceKind, messageList))
|
builder.add(ReceiveMessageTransformer.createMessageSource(bot, onlineSource, messageSourceKind, messageList))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
joinToMessageChain(elements, groupIdOrZero, messageSourceKind, bot, builder)
|
joinToMessageChain(elements, groupIdOrZero, messageSourceKind, bot, builder)
|
||||||
|
|
||||||
for (msg in messageList) {
|
for (msg in messageList) {
|
||||||
@ -160,7 +152,9 @@ internal object ReceiveMessageTransformer {
|
|||||||
for (element in elements) {
|
for (element in elements) {
|
||||||
transformElement(element, groupIdOrZero, messageSourceKind, bot, builder)
|
transformElement(element, groupIdOrZero, messageSourceKind, bot, builder)
|
||||||
when {
|
when {
|
||||||
element.richMsg != null -> decodeRichMessage(element.richMsg, builder)
|
element.richMsg != null -> {
|
||||||
|
// removed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -173,16 +167,36 @@ internal object ReceiveMessageTransformer {
|
|||||||
builder: MessageChainBuilder,
|
builder: MessageChainBuilder,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
element.srcMsg != null -> decodeSrcMsg(element.srcMsg, builder, bot, messageSourceKind, groupIdOrZero)
|
element.srcMsg != null -> {
|
||||||
element.notOnlineImage != null -> builder.add(OnlineFriendImageImpl(element.notOnlineImage))
|
// removed
|
||||||
element.customFace != null -> decodeCustomFace(element.customFace, builder)
|
}
|
||||||
element.face != null -> builder.add(Face(element.face.index))
|
element.notOnlineImage != null -> {
|
||||||
element.text != null -> decodeText(element.text, builder)
|
// removed
|
||||||
element.marketFace != null -> builder.add(MarketFaceInternal(element.marketFace))
|
}
|
||||||
element.lightApp != null -> decodeLightApp(element.lightApp, builder)
|
element.customFace != null -> {
|
||||||
element.customElem != null -> decodeCustomElem(element.customElem, builder)
|
// removed
|
||||||
element.commonElem != null -> decodeCommonElem(element.commonElem, builder)
|
}
|
||||||
element.transElemInfo != null -> decodeTransElem(element.transElemInfo, builder)
|
element.face != null -> {
|
||||||
|
// removed
|
||||||
|
}
|
||||||
|
element.text != null -> {
|
||||||
|
// removed
|
||||||
|
}
|
||||||
|
element.marketFace != null -> {
|
||||||
|
// removed
|
||||||
|
}
|
||||||
|
element.lightApp != null -> {
|
||||||
|
// removed
|
||||||
|
}
|
||||||
|
element.customElem != null -> {
|
||||||
|
// removed
|
||||||
|
}
|
||||||
|
element.commonElem != null -> {
|
||||||
|
// removed
|
||||||
|
}
|
||||||
|
element.transElemInfo != null -> {
|
||||||
|
// removed
|
||||||
|
}
|
||||||
|
|
||||||
element.elemFlags2 != null
|
element.elemFlags2 != null
|
||||||
|| element.extraInfo != null
|
|| element.extraInfo != null
|
||||||
@ -192,9 +206,7 @@ internal object ReceiveMessageTransformer {
|
|||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
UnsupportedMessageImpl(element).takeIf {
|
// removed
|
||||||
it.struct.isNotEmpty()
|
|
||||||
}?.let(builder::add)
|
|
||||||
// println(it._miraiContentToString())
|
// println(it._miraiContentToString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -304,20 +316,7 @@ internal object ReceiveMessageTransformer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeText(text: ImMsgBody.Text, list: MessageChainBuilder) {
|
private fun decodeText(text: ImMsgBody.Text, list: MessageChainBuilder) {
|
||||||
if (text.attr6Buf.isEmpty()) {
|
// removed
|
||||||
list.add(PlainText(text.str))
|
|
||||||
} else {
|
|
||||||
val id: Long
|
|
||||||
text.attr6Buf.read {
|
|
||||||
discardExact(7)
|
|
||||||
id = readUInt().toLong()
|
|
||||||
}
|
|
||||||
if (id == 0L) {
|
|
||||||
list.add(AtAll)
|
|
||||||
} else {
|
|
||||||
list.add(At(id)) // element.text.str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeSrcMsg(
|
private fun decodeSrcMsg(
|
||||||
@ -327,238 +326,14 @@ internal object ReceiveMessageTransformer {
|
|||||||
messageSourceKind: MessageSourceKind,
|
messageSourceKind: MessageSourceKind,
|
||||||
groupIdOrZero: Long,
|
groupIdOrZero: Long,
|
||||||
) {
|
) {
|
||||||
list.add(QuoteReply(OfflineMessageSourceImplData(srcMsg, bot, messageSourceKind, groupIdOrZero)))
|
// removed
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeCustomFace(
|
|
||||||
customFace: ImMsgBody.CustomFace,
|
|
||||||
builder: MessageChainBuilder,
|
|
||||||
) {
|
|
||||||
builder.add(OnlineGroupImageImpl(customFace))
|
|
||||||
customFace.pbReserve.let {
|
|
||||||
if (it.isNotEmpty() && it.loadAs(CustomFace.ResvAttr.serializer()).msgImageShow != null) {
|
|
||||||
builder.add(ShowImageFlag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeLightApp(
|
private fun decodeLightApp(
|
||||||
lightApp: ImMsgBody.LightAppElem,
|
lightApp: ImMsgBody.LightAppElem,
|
||||||
list: MessageChainBuilder,
|
list: MessageChainBuilder,
|
||||||
) {
|
) {
|
||||||
val content = runWithBugReport("解析 lightApp",
|
// removed
|
||||||
{ "resId=" + lightApp.msgResid + "data=" + lightApp.data.toUHexString() }) {
|
|
||||||
when (lightApp.data[0].toInt()) {
|
|
||||||
0 -> lightApp.data.decodeToString(startIndex = 1)
|
|
||||||
1 -> lightApp.data.unzip(1).decodeToString()
|
|
||||||
else -> error("unknown compression flag=${lightApp.data[0]}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
list.add(LightAppInternal(content))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeCustomElem(
|
|
||||||
customElem: ImMsgBody.CustomElem,
|
|
||||||
list: MessageChainBuilder,
|
|
||||||
) {
|
|
||||||
customElem.data.read {
|
|
||||||
kotlin.runCatching {
|
|
||||||
CustomMessage.load(this)
|
|
||||||
}.fold(
|
|
||||||
onFailure = {
|
|
||||||
if (it is CustomMessage.Companion.CustomMessageFullDataDeserializeInternalException) {
|
|
||||||
throw IllegalStateException(
|
|
||||||
"Internal error: " +
|
|
||||||
"exception while deserializing CustomMessage head data," +
|
|
||||||
" data=${customElem.data.toUHexString()}", it
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
it as CustomMessage.Companion.CustomMessageFullDataDeserializeUserException
|
|
||||||
throw IllegalStateException(
|
|
||||||
"User error: " +
|
|
||||||
"exception while deserializing CustomMessage body," +
|
|
||||||
" body=${it.body.toUHexString()}", it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
onSuccess = {
|
|
||||||
if (it != null) {
|
|
||||||
list.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeTransElem(
|
|
||||||
transElement: ImMsgBody.TransElem,
|
|
||||||
list: MessageChainBuilder,
|
|
||||||
) {
|
|
||||||
// file
|
|
||||||
// type=24
|
|
||||||
when (transElement.elemType) {
|
|
||||||
24 -> transElement.elemValue.read {
|
|
||||||
// group file feed
|
|
||||||
// 01 00 77 08 06 12 0A 61 61 61 61 61 61 2E 74 78 74 1A 06 31 35 42 79 74 65 3A 5F 12 5D 08 66 12 25 2F 64 37 34 62 62 66 33 61 2D 37 62 32 35 2D 31 31 65 62 2D 38 34 66 38 2D 35 34 35 32 30 30 37 62 35 64 39 66 18 0F 22 0A 61 61 61 61 61 61 2E 74 78 74 28 00 3A 00 42 20 61 33 32 35 66 36 33 34 33 30 65 37 61 30 31 31 66 37 64 30 38 37 66 63 33 32 34 37 35 34 39 63
|
|
||||||
// fun getFileRsrvAttr(file: ObjMsg.MsgContentInfo.MsgFile): HummerResv21.ResvAttr? {
|
|
||||||
// if (file.ext.isEmpty()) return null
|
|
||||||
// val element = kotlin.runCatching {
|
|
||||||
// jsonForFileDecode.parseToJsonElement(file.ext) as? JsonObject
|
|
||||||
// }.getOrNull() ?: return null
|
|
||||||
// val extInfo = element["ExtInfo"]?.toString()?.decodeBase64() ?: return null
|
|
||||||
// return extInfo.loadAs(HummerResv21.ResvAttr.serializer())
|
|
||||||
// }
|
|
||||||
|
|
||||||
val var7 = readByte()
|
|
||||||
if (var7 == 1.toByte()) {
|
|
||||||
while (remaining > 2) {
|
|
||||||
val proto = readProtoBuf(ObjMsg.ObjMsg.serializer(), readUShort().toInt())
|
|
||||||
// proto.msgType=6
|
|
||||||
|
|
||||||
val file = proto.msgContentInfo.firstOrNull()?.msgFile ?: continue // officially get(0) only.
|
|
||||||
// val attr = getFileRsrvAttr(file) ?: continue
|
|
||||||
// val info = attr.forwardExtFileInfo ?: continue
|
|
||||||
|
|
||||||
list.add(
|
|
||||||
FileMessageImpl(
|
|
||||||
id = file.filePath,
|
|
||||||
busId = file.busId, // path i.e. /a99e95fa-7b2d-11eb-adae-5452007b698a
|
|
||||||
name = file.fileName,
|
|
||||||
size = file.fileSize
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private val jsonForFileDecode = Json {
|
|
||||||
isLenient = true
|
|
||||||
coerceInputValues = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeCommonElem(
|
|
||||||
commonElem: ImMsgBody.CommonElem,
|
|
||||||
list: MessageChainBuilder,
|
|
||||||
) {
|
|
||||||
when (commonElem.serviceType) {
|
|
||||||
23 -> {
|
|
||||||
val proto =
|
|
||||||
commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype23.serializer())
|
|
||||||
list.add(VipFace(VipFace.Kind(proto.faceType, proto.faceSummary), proto.faceBubbleCount))
|
|
||||||
}
|
|
||||||
2 -> {
|
|
||||||
val proto =
|
|
||||||
commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype2.serializer())
|
|
||||||
list.add(PokeMessage(
|
|
||||||
proto.vaspokeName.takeIf { it.isNotEmpty() }
|
|
||||||
?: PokeMessage.values.firstOrNull { it.id == proto.vaspokeId && it.pokeType == proto.pokeType }?.name
|
|
||||||
.orEmpty(),
|
|
||||||
proto.pokeType,
|
|
||||||
proto.vaspokeId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
3 -> {
|
|
||||||
val proto =
|
|
||||||
commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype3.serializer())
|
|
||||||
if (proto.flashTroopPic != null) {
|
|
||||||
list.add(FlashImage(OnlineGroupImageImpl(proto.flashTroopPic)))
|
|
||||||
}
|
|
||||||
if (proto.flashC2cPic != null) {
|
|
||||||
list.add(FlashImage(OnlineFriendImageImpl(proto.flashC2cPic)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
33 -> {
|
|
||||||
val proto =
|
|
||||||
commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype33.serializer())
|
|
||||||
list.add(Face(proto.index))
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun decodeRichMessage(
|
|
||||||
richMsg: ImMsgBody.RichMsg,
|
|
||||||
builder: MessageChainBuilder,
|
|
||||||
) {
|
|
||||||
val content = runWithBugReport("解析 richMsg", { richMsg.template1.toUHexString() }) {
|
|
||||||
when (richMsg.template1[0].toInt()) {
|
|
||||||
0 -> richMsg.template1.decodeToString(startIndex = 1)
|
|
||||||
1 -> richMsg.template1.unzip(1).decodeToString()
|
|
||||||
else -> error("unknown compression flag=${richMsg.template1[0]}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findStringProperty(name: String): String {
|
|
||||||
return content.substringAfter("$name=\"", "").substringBefore("\"", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
val serviceId = when (val sid = richMsg.serviceId) {
|
|
||||||
0 -> {
|
|
||||||
val serviceIdStr = findStringProperty("serviceID")
|
|
||||||
if (serviceIdStr.isEmpty() || serviceIdStr.isBlank()) {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
serviceIdStr.toIntOrNull() ?: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> sid
|
|
||||||
}
|
|
||||||
when (serviceId) {
|
|
||||||
// 5: 使用微博长图转换功能分享到QQ群
|
|
||||||
/*
|
|
||||||
<?xml version="1.0" encoding="utf-8"?><msg serviceID="5" templateID="12345" brief="[分享]想要沐浴阳光,就别钻进
|
|
||||||
阴影。 ???" ><item layout="0"><image uuid="{E5F68BD5-05F8-148B-9DA7-FECD026D30AD}.jpg" md5="E5F68BD505F8148B9DA7FECD026D
|
|
||||||
30AD" GroupFiledid="2167263882" minWidth="120" minHeight="120" maxWidth="180" maxHeight="180" /></item><source name="新
|
|
||||||
浪微博" icon="http://i.gtimg.cn/open/app_icon/00/73/69/03//100736903_100_m.png" appid="100736903" action="" i_actionData
|
|
||||||
="" a_actionData="" url=""/></msg>
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* json?
|
|
||||||
*/
|
|
||||||
1 -> @Suppress("DEPRECATION_ERROR")
|
|
||||||
builder.add(SimpleServiceMessage(1, content))
|
|
||||||
/**
|
|
||||||
* [LongMessageInternal], [ForwardMessage]
|
|
||||||
*/
|
|
||||||
35 -> {
|
|
||||||
|
|
||||||
val resId = findStringProperty("m_resid")
|
|
||||||
val fileName = findStringProperty("m_fileName").takeIf { it.isNotEmpty() }
|
|
||||||
|
|
||||||
val msg = if (resId.isEmpty()) {
|
|
||||||
// Nested ForwardMessage
|
|
||||||
if (fileName != null && findStringProperty("action") == "viewMultiMsg") {
|
|
||||||
ForwardMessageInternal(content, null, fileName)
|
|
||||||
} else {
|
|
||||||
SimpleServiceMessage(35, content)
|
|
||||||
}
|
|
||||||
} else when (findStringProperty("multiMsgFlag").toIntOrNull()) {
|
|
||||||
1 -> LongMessageInternal(content, resId)
|
|
||||||
0 -> ForwardMessageInternal(content, resId, fileName)
|
|
||||||
else -> {
|
|
||||||
// from PC QQ
|
|
||||||
if (findStringProperty("action") == "viewMultiMsg") {
|
|
||||||
ForwardMessageInternal(content, resId, fileName)
|
|
||||||
} else {
|
|
||||||
SimpleServiceMessage(35, content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.add(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 104 新群员入群的消息
|
|
||||||
else -> {
|
|
||||||
builder.add(SimpleServiceMessage(serviceId, content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ImMsgBody.Ptt.toAudio() = OnlineAudioImpl(
|
fun ImMsgBody.Ptt.toAudio() = OnlineAudioImpl(
|
||||||
|
@ -13,17 +13,10 @@ package net.mamoe.mirai.internal.message.data
|
|||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.serialization.Transient
|
||||||
import net.mamoe.mirai.Bot
|
|
||||||
import net.mamoe.mirai.internal.message.RefinableMessage
|
|
||||||
import net.mamoe.mirai.internal.message.RefineContext
|
|
||||||
import net.mamoe.mirai.internal.message.visitor.ex
|
import net.mamoe.mirai.internal.message.visitor.ex
|
||||||
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
import net.mamoe.mirai.message.data.Dice
|
|
||||||
import net.mamoe.mirai.message.data.MarketFace
|
import net.mamoe.mirai.message.data.MarketFace
|
||||||
import net.mamoe.mirai.message.data.Message
|
|
||||||
import net.mamoe.mirai.message.data.MessageChain
|
|
||||||
import net.mamoe.mirai.message.data.visitor.MessageVisitor
|
import net.mamoe.mirai.message.data.visitor.MessageVisitor
|
||||||
import net.mamoe.mirai.utils.hexToBytes
|
|
||||||
|
|
||||||
@SerialName(MarketFace.SERIAL_NAME)
|
@SerialName(MarketFace.SERIAL_NAME)
|
||||||
@Serializable
|
@Serializable
|
||||||
@ -41,82 +34,4 @@ internal data class MarketFaceImpl internal constructor(
|
|||||||
override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R {
|
override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R {
|
||||||
return visitor.ex()?.visitMarketFaceImpl(this, data) ?: super.accept(visitor, data)
|
return visitor.ex()?.visitMarketFaceImpl(this, data) ?: super.accept(visitor, data)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For refinement
|
|
||||||
*/
|
|
||||||
internal class MarketFaceInternal(
|
|
||||||
private val delegate: ImMsgBody.MarketFace,
|
|
||||||
) : MarketFace, RefinableMessage {
|
|
||||||
override val name: String get() = delegate.faceName.decodeToString()
|
|
||||||
override val id: Int get() = delegate.tabId
|
|
||||||
|
|
||||||
override fun tryRefine(bot: Bot, context: MessageChain, refineContext: RefineContext): Message {
|
|
||||||
delegate.toDiceOrNull()?.let { return it } // TODO: 2021/2/12 add dice origin, maybe rename MessageOrigin
|
|
||||||
return MarketFaceImpl(delegate)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String = "[mirai:marketface:$id,$name]"
|
|
||||||
|
|
||||||
override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R {
|
|
||||||
return visitor.ex()?.visitMarketFaceInternal(this, data) ?: super<MarketFace>.accept(visitor, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// From https://github.com/mamoe/mirai/issues/1012
|
|
||||||
internal fun Dice.toJceStruct(): ImMsgBody.MarketFace {
|
|
||||||
return ImMsgBody.MarketFace(
|
|
||||||
faceName = byteArrayOf(91, -23, -86, -80, -27, -83, -112, 93),
|
|
||||||
itemType = 6,
|
|
||||||
faceInfo = 1,
|
|
||||||
faceId = byteArrayOf(
|
|
||||||
72, 35, -45, -83, -79, 93,
|
|
||||||
-16, -128, 20, -50, 93, 103,
|
|
||||||
-106, -73, 110, -31
|
|
||||||
),
|
|
||||||
tabId = 11464,
|
|
||||||
subType = 3,
|
|
||||||
key = byteArrayOf(52, 48, 57, 101, 50, 97, 54, 57, 98, 49, 54, 57, 49, 56, 102, 57),
|
|
||||||
mediaType = 0,
|
|
||||||
imageWidth = 200,
|
|
||||||
imageHeight = 200,
|
|
||||||
mobileParam = byteArrayOf(
|
|
||||||
114, 115, 99, 84, 121, 112, 101,
|
|
||||||
63, 49, 59, 118, 97, 108, 117,
|
|
||||||
101, 61,
|
|
||||||
(47 + value).toByte()
|
|
||||||
),
|
|
||||||
pbReserve = byteArrayOf(
|
|
||||||
10, 6, 8, -56, 1, 16, -56, 1, 64,
|
|
||||||
1, 88, 0, 98, 9, 35, 48, 48, 48,
|
|
||||||
48, 48, 48, 48, 48, 106, 9, 35,
|
|
||||||
48, 48, 48, 48, 48, 48, 48, 48
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PC 客户端没有 [ImMsgBody.MarketFace.mobileParam], 是按 [ImMsgBody.MarketFace.faceId] 发的...
|
|
||||||
*/
|
|
||||||
@Suppress("SpellCheckingInspection")
|
|
||||||
private val DICE_PC_FACE_IDS = mapOf(
|
|
||||||
1 to "E6EEDE15CDFBEB4DF0242448535354F1".hexToBytes(),
|
|
||||||
2 to "C5A95816FB5AFE34A58AF0E837A3B5A0".hexToBytes(),
|
|
||||||
3 to "382131D722EEA4624F087C5B8035AF5F".hexToBytes(),
|
|
||||||
4 to "FA90E956DCAD76742F2DB87723D3B669".hexToBytes(),
|
|
||||||
5 to "D51FA892017647431BB243920EC9FB8E".hexToBytes(),
|
|
||||||
6 to "7A2303AD80755FCB6BBFAC38327E0C01".hexToBytes(),
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun ImMsgBody.MarketFace.toDiceOrNull(): Dice? {
|
|
||||||
if (this.tabId != 11464) return null
|
|
||||||
val value = when {
|
|
||||||
mobileParam.isNotEmpty() -> mobileParam.lastOrNull()?.toInt()?.and(0xff)?.minus(47) ?: return null
|
|
||||||
else -> DICE_PC_FACE_IDS.entries.find { it.value.contentEquals(faceId) }?.key ?: return null
|
|
||||||
}
|
|
||||||
if (value in 1..6) {
|
|
||||||
return Dice(value)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
}
|
@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.internal.message.data
|
|
||||||
|
|
||||||
import kotlinx.io.core.buildPacket
|
|
||||||
import kotlinx.io.core.readBytes
|
|
||||||
import net.mamoe.mirai.contact.Group
|
|
||||||
import net.mamoe.mirai.contact.nameCardOrNick
|
|
||||||
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
|
||||||
import net.mamoe.mirai.message.data.*
|
|
||||||
import net.mamoe.mirai.utils.dropEmoji
|
|
||||||
import net.mamoe.mirai.utils.safeCast
|
|
||||||
|
|
||||||
|
|
||||||
internal fun At.toJceData(
|
|
||||||
group: Group?,
|
|
||||||
source: MessageSource?,
|
|
||||||
isForward: Boolean,
|
|
||||||
): ImMsgBody.Text {
|
|
||||||
fun findFromGroup(g: Group?): String? {
|
|
||||||
return g?.members?.get(this.target)?.nameCardOrNick
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findFromSource(): String? {
|
|
||||||
return when (source) {
|
|
||||||
is OnlineMessageSource -> {
|
|
||||||
return findFromGroup(source.target.safeCast())
|
|
||||||
}
|
|
||||||
is OfflineMessageSource -> {
|
|
||||||
if (source.kind == MessageSourceKind.GROUP) {
|
|
||||||
return findFromGroup(group?.bot?.getGroup(source.targetId))
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val text = "@${
|
|
||||||
if (isForward) {
|
|
||||||
findFromSource() ?: findFromGroup(group)
|
|
||||||
} else {
|
|
||||||
findFromGroup(group) ?: findFromSource()
|
|
||||||
} ?: target
|
|
||||||
}".dropEmoji()
|
|
||||||
return ImMsgBody.Text(
|
|
||||||
str = text,
|
|
||||||
attr6Buf = buildPacket {
|
|
||||||
// MessageForText$AtTroopMemberInfo
|
|
||||||
writeShort(1) // const
|
|
||||||
writeShort(0) // startPos
|
|
||||||
writeShort(text.length.toShort()) // textLen
|
|
||||||
writeByte(0) // flag, may=1
|
|
||||||
writeInt(target.toInt()) // uin
|
|
||||||
writeShort(0) // const
|
|
||||||
}.readBytes()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Suppress("unused") // limit scope
|
|
||||||
internal val AtAll.jceData
|
|
||||||
get() = atAllData
|
|
||||||
|
|
||||||
private val atAllData by lazy {
|
|
||||||
ImMsgBody.Elem(
|
|
||||||
text = ImMsgBody.Text(
|
|
||||||
str = AtAll.display,
|
|
||||||
attr6Buf = buildPacket {
|
|
||||||
// MessageForText$AtTroopMemberInfo
|
|
||||||
writeShort(1) // const
|
|
||||||
writeShort(0) // startPos
|
|
||||||
writeShort(AtAll.display.length.toShort()) // textLen
|
|
||||||
writeByte(1) // flag, may=1
|
|
||||||
writeInt(0) // uin
|
|
||||||
writeShort(0) // const
|
|
||||||
}.readBytes()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.internal.message.data
|
|
||||||
|
|
||||||
import kotlinx.io.core.toByteArray
|
|
||||||
import net.mamoe.mirai.internal.network.protocol.data.proto.HummerCommelem
|
|
||||||
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
|
||||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
|
||||||
import net.mamoe.mirai.message.data.Face
|
|
||||||
import net.mamoe.mirai.utils.hexToBytes
|
|
||||||
import net.mamoe.mirai.utils.toByteArray
|
|
||||||
|
|
||||||
private val FACE_BUF = "00 01 00 04 52 CC F5 D0".hexToBytes()
|
|
||||||
|
|
||||||
internal fun Face.toJceData(): ImMsgBody.Face {
|
|
||||||
return ImMsgBody.Face(
|
|
||||||
index = this.id,
|
|
||||||
old = (0x1445 - 4 + this.id).toShort().toByteArray(),
|
|
||||||
buf = FACE_BUF
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun Face.toCommData(): ImMsgBody.CommonElem {
|
|
||||||
return ImMsgBody.CommonElem(
|
|
||||||
serviceType = 33,
|
|
||||||
pbElem = HummerCommelem.MsgElemInfoServtype33(
|
|
||||||
index = this.id,
|
|
||||||
name = "/${this.name}".toByteArray(),
|
|
||||||
compat = "/${this.name}".toByteArray()
|
|
||||||
).toByteArray(HummerCommelem.MsgElemInfoServtype33.serializer()),
|
|
||||||
businessType = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
@ -9,15 +9,7 @@
|
|||||||
|
|
||||||
package net.mamoe.mirai.internal.message.image
|
package net.mamoe.mirai.internal.message.image
|
||||||
|
|
||||||
import net.mamoe.mirai.contact.ContactOrBot
|
|
||||||
import net.mamoe.mirai.contact.User
|
|
||||||
import net.mamoe.mirai.internal.network.protocol.data.proto.HummerCommelem
|
|
||||||
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
|
||||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
|
||||||
import net.mamoe.mirai.message.data.FlashImage
|
|
||||||
import net.mamoe.mirai.message.data.Image
|
import net.mamoe.mirai.message.data.Image
|
||||||
import net.mamoe.mirai.message.data.ImageType
|
|
||||||
import net.mamoe.mirai.utils.generateImageId
|
|
||||||
import net.mamoe.mirai.utils.toUHexString
|
import net.mamoe.mirai.utils.toUHexString
|
||||||
|
|
||||||
|
|
||||||
@ -26,149 +18,3 @@ internal val Image.friendImageId: String
|
|||||||
// /1234567890-3666252994-EFF4427CE3D27DB6B1D9A8AB72E7A29C
|
// /1234567890-3666252994-EFF4427CE3D27DB6B1D9A8AB72E7A29C
|
||||||
return "/000000000-000000000-${md5.toUHexString("")}"
|
return "/000000000-000000000-${md5.toUHexString("")}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal fun ImMsgBody.NotOnlineImage.toCustomFace(): ImMsgBody.CustomFace {
|
|
||||||
|
|
||||||
return ImMsgBody.CustomFace(
|
|
||||||
filePath = generateImageId(picMd5, getImageType(imgType)),
|
|
||||||
picMd5 = picMd5,
|
|
||||||
bizType = 5,
|
|
||||||
fileType = 66,
|
|
||||||
useful = 1,
|
|
||||||
flag = ByteArray(4),
|
|
||||||
bigUrl = bigUrl,
|
|
||||||
origUrl = origUrl,
|
|
||||||
width = picWidth.coerceAtLeast(1),
|
|
||||||
height = picHeight.coerceAtLeast(1),
|
|
||||||
imageType = imgType,
|
|
||||||
//_400Height = 235,
|
|
||||||
//_400Url = "/gchatpic_new/000000000/1041235568-2195821338-01E9451B70EDEAE3B37C101F1EEBF5B5/400?term=2",
|
|
||||||
//_400Width = 351,
|
|
||||||
origin = original,
|
|
||||||
size = fileLen.toInt()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// aka friend image id
|
|
||||||
internal fun ImMsgBody.NotOnlineImageOrCustomFace.calculateResId(): String {
|
|
||||||
val url = origUrl.takeIf { it.isNotBlank() }
|
|
||||||
?: thumbUrl.takeIf { it.isNotBlank() }
|
|
||||||
?: _400Url.takeIf { it.isNotBlank() }
|
|
||||||
?: ""
|
|
||||||
|
|
||||||
// gchatpic_new
|
|
||||||
// offpic_new
|
|
||||||
val picSenderId = url.substringAfter("pic_new/").substringBefore("/")
|
|
||||||
.takeIf { it.isNotBlank() } ?: "000000000"
|
|
||||||
val unknownInt = url.substringAfter("-").substringBefore("-")
|
|
||||||
.takeIf { it.isNotBlank() } ?: "000000000"
|
|
||||||
|
|
||||||
return "/$picSenderId-$unknownInt-${picMd5.toUHexString("")}"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
internal fun OfflineFriendImage.toJceData(): ImMsgBody.NotOnlineImage {
|
|
||||||
val friendImageId = this.friendImageId
|
|
||||||
return ImMsgBody.NotOnlineImage(
|
|
||||||
filePath = friendImageId,
|
|
||||||
resId = friendImageId,
|
|
||||||
oldPicMd5 = false,
|
|
||||||
picMd5 = this.md5,
|
|
||||||
fileLen = size,
|
|
||||||
downloadPath = friendImageId,
|
|
||||||
original = if (imageType == ImageType.GIF) {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
},
|
|
||||||
picWidth = width,
|
|
||||||
picHeight = height,
|
|
||||||
imgType = getIdByImageType(imageType),
|
|
||||||
pbReserve = byteArrayOf(0x78, 0x02)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
internal fun OfflineGroupImage.toJceData(): ImMsgBody.CustomFace {
|
|
||||||
return ImMsgBody.CustomFace(
|
|
||||||
fileId = this.fileId ?: 0,
|
|
||||||
filePath = this.imageId,
|
|
||||||
picMd5 = this.md5,
|
|
||||||
flag = ByteArray(4),
|
|
||||||
size = size.toInt(),
|
|
||||||
width = width.coerceAtLeast(1),
|
|
||||||
height = height.coerceAtLeast(1),
|
|
||||||
imageType = getIdByImageType(imageType),
|
|
||||||
origin = if (imageType == ImageType.GIF) {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
},
|
|
||||||
//_400Height = 235,
|
|
||||||
//_400Url = "/gchatpic_new/000000000/1041235568-2195821338-01E9451B70EDEAE3B37C101F1EEBF5B5/400?term=2",
|
|
||||||
//_400Width = 351,
|
|
||||||
// pbReserve = "08 00 10 00 32 00 50 00 78 08".autoHexToBytes(),
|
|
||||||
bizType = 5,
|
|
||||||
fileType = 66,
|
|
||||||
useful = 1,
|
|
||||||
// pbReserve = CustomFaceExtPb.ResvAttr().toByteArray(CustomFaceExtPb.ResvAttr.serializer())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun ImMsgBody.CustomFace.toNotOnlineImage(): ImMsgBody.NotOnlineImage {
|
|
||||||
val resId = calculateResId()
|
|
||||||
|
|
||||||
return ImMsgBody.NotOnlineImage(
|
|
||||||
filePath = filePath,
|
|
||||||
resId = resId,
|
|
||||||
oldPicMd5 = false,
|
|
||||||
picWidth = width,
|
|
||||||
picHeight = height,
|
|
||||||
imgType = imageType,
|
|
||||||
picMd5 = picMd5,
|
|
||||||
fileLen = size.toLong(),
|
|
||||||
oldVerSendFile = oldData,
|
|
||||||
downloadPath = resId,
|
|
||||||
original = origin,
|
|
||||||
bizType = bizType,
|
|
||||||
pbReserve = byteArrayOf(0x78, 0x02),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
internal fun FlashImage.toJceData(messageTarget: ContactOrBot?): ImMsgBody.Elem {
|
|
||||||
return if (messageTarget is User) {
|
|
||||||
ImMsgBody.Elem(
|
|
||||||
commonElem = ImMsgBody.CommonElem(
|
|
||||||
serviceType = 3,
|
|
||||||
businessType = 0,
|
|
||||||
pbElem = HummerCommelem.MsgElemInfoServtype3(
|
|
||||||
flashC2cPic = ImMsgBody.NotOnlineImage(
|
|
||||||
filePath = image.friendImageId,
|
|
||||||
resId = image.friendImageId,
|
|
||||||
picMd5 = image.md5,
|
|
||||||
oldPicMd5 = false,
|
|
||||||
pbReserve = byteArrayOf(0x78, 0x06)
|
|
||||||
)
|
|
||||||
).toByteArray(HummerCommelem.MsgElemInfoServtype3.serializer())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ImMsgBody.Elem(
|
|
||||||
commonElem = ImMsgBody.CommonElem(
|
|
||||||
serviceType = 3,
|
|
||||||
businessType = 0,
|
|
||||||
pbElem = HummerCommelem.MsgElemInfoServtype3(
|
|
||||||
flashTroopPic = ImMsgBody.CustomFace(
|
|
||||||
filePath = image.imageId,
|
|
||||||
picMd5 = image.md5,
|
|
||||||
pbReserve = byteArrayOf(0x78, 0x06)
|
|
||||||
)
|
|
||||||
).toByteArray(HummerCommelem.MsgElemInfoServtype3.serializer())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,22 +9,20 @@
|
|||||||
|
|
||||||
package net.mamoe.mirai.internal.message
|
package net.mamoe.mirai.internal.message
|
||||||
|
|
||||||
import kotlinx.io.core.toByteArray
|
|
||||||
import net.mamoe.mirai.contact.AnonymousMember
|
import net.mamoe.mirai.contact.AnonymousMember
|
||||||
import net.mamoe.mirai.contact.ContactOrBot
|
import net.mamoe.mirai.contact.ContactOrBot
|
||||||
import net.mamoe.mirai.contact.Group
|
import net.mamoe.mirai.contact.Group
|
||||||
import net.mamoe.mirai.contact.User
|
import net.mamoe.mirai.internal.message.data.MarketFaceImpl
|
||||||
import net.mamoe.mirai.internal.message.data.*
|
import net.mamoe.mirai.internal.message.data.UnsupportedMessageImpl
|
||||||
import net.mamoe.mirai.internal.message.flags.InternalFlagOnlyMessage
|
import net.mamoe.mirai.internal.message.flags.InternalFlagOnlyMessage
|
||||||
import net.mamoe.mirai.internal.message.image.*
|
import net.mamoe.mirai.internal.message.image.OfflineFriendImage
|
||||||
|
import net.mamoe.mirai.internal.message.image.OfflineGroupImage
|
||||||
|
import net.mamoe.mirai.internal.message.image.OnlineFriendImageImpl
|
||||||
|
import net.mamoe.mirai.internal.message.image.OnlineGroupImageImpl
|
||||||
import net.mamoe.mirai.internal.message.source.MessageSourceInternal
|
import net.mamoe.mirai.internal.message.source.MessageSourceInternal
|
||||||
import net.mamoe.mirai.internal.network.protocol.data.proto.HummerCommelem
|
|
||||||
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
|
||||||
import net.mamoe.mirai.message.data.*
|
import net.mamoe.mirai.message.data.*
|
||||||
import net.mamoe.mirai.utils.hexToBytes
|
import net.mamoe.mirai.utils.hexToBytes
|
||||||
import net.mamoe.mirai.utils.safeCast
|
|
||||||
import net.mamoe.mirai.utils.zip
|
|
||||||
|
|
||||||
internal val MIRAI_CUSTOM_ELEM_TYPE = "mirai".hashCode() // 103904510
|
internal val MIRAI_CUSTOM_ELEM_TYPE = "mirai".hashCode() // 103904510
|
||||||
|
|
||||||
@ -34,7 +32,6 @@ internal val UNSUPPORTED_POKE_MESSAGE_PLAIN = PlainText("[戳一戳]请使用最
|
|||||||
internal val UNSUPPORTED_FLASH_MESSAGE_PLAIN = PlainText("[闪照]请使用新版手机QQ查看闪照。")
|
internal val UNSUPPORTED_FLASH_MESSAGE_PLAIN = PlainText("[闪照]请使用新版手机QQ查看闪照。")
|
||||||
internal val UNSUPPORTED_VOICE_MESSAGE_PLAIN = PlainText("收到语音消息,你需要升级到最新版QQ才能接收,升级地址https://im.qq.com")
|
internal val UNSUPPORTED_VOICE_MESSAGE_PLAIN = PlainText("收到语音消息,你需要升级到最新版QQ才能接收,升级地址https://im.qq.com")
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
|
||||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||||
internal fun MessageChain.toRichTextElems(
|
internal fun MessageChain.toRichTextElems(
|
||||||
messageTarget: ContactOrBot?,
|
messageTarget: ContactOrBot?,
|
||||||
@ -48,180 +45,64 @@ internal fun MessageChain.toRichTextElems(
|
|||||||
|
|
||||||
fun transformOneMessage(currentMessage: Message) {
|
fun transformOneMessage(currentMessage: Message) {
|
||||||
if (currentMessage is RichMessage) {
|
if (currentMessage is RichMessage) {
|
||||||
val content = currentMessage.content.toByteArray().zip()
|
// removed
|
||||||
when (currentMessage) {
|
|
||||||
is ForwardMessageInternal -> {
|
|
||||||
elements.add(
|
|
||||||
ImMsgBody.Elem(
|
|
||||||
richMsg = ImMsgBody.RichMsg(
|
|
||||||
serviceId = currentMessage.serviceId, // ok
|
|
||||||
template1 = byteArrayOf(1) + content
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN)
|
|
||||||
}
|
|
||||||
is LongMessageInternal -> {
|
|
||||||
check(longTextResId == null) { "There must be no more than one LongMessage element in the message chain" }
|
|
||||||
elements.add(
|
|
||||||
ImMsgBody.Elem(
|
|
||||||
richMsg = ImMsgBody.RichMsg(
|
|
||||||
serviceId = currentMessage.serviceId, // ok
|
|
||||||
template1 = byteArrayOf(1) + content
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN)
|
|
||||||
longTextResId = currentMessage.resId
|
|
||||||
}
|
|
||||||
is LightApp -> elements.add(
|
|
||||||
ImMsgBody.Elem(
|
|
||||||
lightApp = ImMsgBody.LightAppElem(
|
|
||||||
data = byteArrayOf(1) + content
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else -> elements.add(
|
|
||||||
ImMsgBody.Elem(
|
|
||||||
richMsg = ImMsgBody.RichMsg(
|
|
||||||
serviceId = when (currentMessage) {
|
|
||||||
is ServiceMessage -> currentMessage.serviceId
|
|
||||||
else -> error("unsupported RichMessage: ${currentMessage::class.simpleName}")
|
|
||||||
},
|
|
||||||
template1 = byteArrayOf(1) + content
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
when (currentMessage) {
|
when (currentMessage) {
|
||||||
is PlainText -> elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = currentMessage.content)))
|
is PlainText -> {
|
||||||
|
// removed
|
||||||
|
}
|
||||||
is CustomMessage -> {
|
is CustomMessage -> {
|
||||||
@Suppress("UNCHECKED_CAST")
|
// removed
|
||||||
elements.add(
|
|
||||||
ImMsgBody.Elem(
|
|
||||||
customElem = ImMsgBody.CustomElem(
|
|
||||||
enumType = MIRAI_CUSTOM_ELEM_TYPE,
|
|
||||||
data = CustomMessage.dump(
|
|
||||||
currentMessage.getFactory() as CustomMessage.Factory<CustomMessage>,
|
|
||||||
currentMessage
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
is At -> {
|
is At -> {
|
||||||
elements.add(
|
// removed
|
||||||
ImMsgBody.Elem(
|
|
||||||
text = currentMessage.toJceData(
|
|
||||||
messageTarget.safeCast(),
|
|
||||||
this[MessageSource],
|
|
||||||
isForward,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " ")))
|
|
||||||
// removed by https://github.com/mamoe/mirai/issues/524
|
|
||||||
// 发送 QuoteReply 消息时无可避免的产生多余空格 #524
|
|
||||||
}
|
}
|
||||||
is PokeMessage -> {
|
is PokeMessage -> {
|
||||||
elements.add(
|
// removed
|
||||||
ImMsgBody.Elem(
|
|
||||||
commonElem = ImMsgBody.CommonElem(
|
|
||||||
serviceType = 2,
|
|
||||||
businessType = currentMessage.pokeType,
|
|
||||||
pbElem = HummerCommelem.MsgElemInfoServtype2(
|
|
||||||
pokeType = currentMessage.pokeType,
|
|
||||||
vaspokeId = currentMessage.id,
|
|
||||||
vaspokeMinver = "7.2.0",
|
|
||||||
vaspokeName = currentMessage.name
|
|
||||||
).toByteArray(HummerCommelem.MsgElemInfoServtype2.serializer())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
transformOneMessage(UNSUPPORTED_POKE_MESSAGE_PLAIN)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
is OfflineGroupImage -> {
|
is OfflineGroupImage -> {
|
||||||
if (messageTarget is User) {
|
// removed
|
||||||
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.toJceData().toNotOnlineImage()))
|
|
||||||
} else {
|
|
||||||
elements.add(ImMsgBody.Elem(customFace = currentMessage.toJceData()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is OnlineGroupImageImpl -> {
|
is OnlineGroupImageImpl -> {
|
||||||
if (messageTarget is User) {
|
// removed
|
||||||
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.delegate.toNotOnlineImage()))
|
|
||||||
} else {
|
|
||||||
elements.add(ImMsgBody.Elem(customFace = currentMessage.delegate))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is OnlineFriendImageImpl -> {
|
is OnlineFriendImageImpl -> {
|
||||||
if (messageTarget is User) {
|
// removed
|
||||||
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.delegate))
|
|
||||||
} else {
|
|
||||||
elements.add(ImMsgBody.Elem(customFace = currentMessage.delegate.toCustomFace()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is OfflineFriendImage -> {
|
is OfflineFriendImage -> {
|
||||||
if (messageTarget is User) {
|
// removed
|
||||||
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.toJceData()))
|
|
||||||
} else {
|
|
||||||
elements.add(ImMsgBody.Elem(customFace = currentMessage.toJceData().toCustomFace()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
is FlashImage -> elements.add(currentMessage.toJceData(messageTarget))
|
is FlashImage -> {
|
||||||
.also { transformOneMessage(UNSUPPORTED_FLASH_MESSAGE_PLAIN) }
|
// removed
|
||||||
|
}
|
||||||
|
|
||||||
|
is AtAll -> {
|
||||||
is AtAll -> elements.add(AtAll.jceData)
|
// removed
|
||||||
is Face -> elements.add(
|
}
|
||||||
if (currentMessage.id >= 260) {
|
is Face -> {
|
||||||
ImMsgBody.Elem(commonElem = currentMessage.toCommData())
|
// removed
|
||||||
} else {
|
}
|
||||||
ImMsgBody.Elem(face = currentMessage.toJceData())
|
|
||||||
}
|
|
||||||
)
|
|
||||||
is QuoteReply -> { // transformed
|
is QuoteReply -> { // transformed
|
||||||
}
|
}
|
||||||
is Dice -> transformOneMessage(MarketFaceImpl(currentMessage.toJceStruct()))
|
is Dice -> {
|
||||||
is MarketFace -> {
|
// removed
|
||||||
if (currentMessage is MarketFaceImpl) {
|
}
|
||||||
elements.add(ImMsgBody.Elem(marketFace = currentMessage.delegate))
|
is MarketFace -> {
|
||||||
}
|
// removed
|
||||||
//兼容信息
|
}
|
||||||
transformOneMessage(PlainText(currentMessage.name))
|
is VipFace -> {
|
||||||
if (currentMessage is MarketFaceImpl) {
|
// removed
|
||||||
elements.add(
|
|
||||||
ImMsgBody.Elem(
|
|
||||||
extraInfo = ImMsgBody.ExtraInfo(flags = 8, groupMask = 1)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is VipFace -> transformOneMessage(PlainText(currentMessage.contentToString()))
|
|
||||||
is PttMessage -> {
|
is PttMessage -> {
|
||||||
elements.add(
|
// removed
|
||||||
ImMsgBody.Elem(
|
|
||||||
extraInfo = ImMsgBody.ExtraInfo(flags = 16, groupMask = 1)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elements.add(
|
|
||||||
ImMsgBody.Elem(
|
|
||||||
elemFlags2 = ImMsgBody.ElemFlags2(
|
|
||||||
vipStatus = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
is MusicShare -> {
|
is MusicShare -> {
|
||||||
// 只有在 QuoteReply 的 source 里才会进行 MusicShare 转换, 因此可以转 PT.
|
// removed
|
||||||
// 发送消息时会被特殊处理
|
|
||||||
transformOneMessage(PlainText(currentMessage.content))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is ForwardMessage,
|
is ForwardMessage,
|
||||||
@ -233,7 +114,9 @@ internal fun MessageChain.toRichTextElems(
|
|||||||
is InternalFlagOnlyMessage, is ShowImageFlag -> {
|
is InternalFlagOnlyMessage, is ShowImageFlag -> {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
is UnsupportedMessageImpl -> elements.add(currentMessage.structElem)
|
is UnsupportedMessageImpl -> {
|
||||||
|
// removed
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// unrecognized types are ignored
|
// unrecognized types are ignored
|
||||||
// error("unsupported message type: ${currentMessage::class.simpleName}")
|
// error("unsupported message type: ${currentMessage::class.simpleName}")
|
||||||
@ -265,44 +148,28 @@ internal fun MessageChain.toRichTextElems(
|
|||||||
if (withGeneralFlags) {
|
if (withGeneralFlags) {
|
||||||
when {
|
when {
|
||||||
longTextResId != null -> {
|
longTextResId != null -> {
|
||||||
elements.add(
|
// removed
|
||||||
ImMsgBody.Elem(
|
|
||||||
generalFlags = ImMsgBody.GeneralFlags(
|
|
||||||
longTextFlag = 1,
|
|
||||||
longTextResid = longTextResId!!,
|
|
||||||
pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
this.anyIsInstance<MarketFaceImpl>() -> {
|
this.anyIsInstance<MarketFaceImpl>() -> {
|
||||||
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_MARKET_FACE)))
|
// removed
|
||||||
}
|
}
|
||||||
this.anyIsInstance<RichMessage>() -> {
|
this.anyIsInstance<RichMessage>() -> {
|
||||||
// 08 09 78 00 A0 01 81 DC 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00
|
// removed
|
||||||
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_RICH_MESSAGE)))
|
|
||||||
}
|
}
|
||||||
this.anyIsInstance<FlashImage>() -> {
|
this.anyIsInstance<FlashImage>() -> {
|
||||||
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_DOUTU)))
|
// removed
|
||||||
}
|
}
|
||||||
this.anyIsInstance<PttMessage>() -> {
|
this.anyIsInstance<PttMessage>() -> {
|
||||||
elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_PTT)))
|
// removed
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// removed
|
||||||
}
|
}
|
||||||
else -> elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_ELSE)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return elements
|
return elements
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val PB_RESERVE_FOR_RICH_MESSAGE =
|
|
||||||
"08 09 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00".hexToBytes()
|
|
||||||
|
|
||||||
internal val PB_RESERVE_FOR_PTT =
|
|
||||||
"78 00 F8 01 00 C8 02 00 AA 03 26 08 22 12 22 41 20 41 3B 25 3E 16 45 3F 43 2F 29 3E 44 24 14 18 46 3D 2B 4A 44 3A 18 2E 19 29 1B 26 32 31 31 29 43".hexToBytes()
|
|
||||||
|
|
||||||
@Suppress("SpellCheckingInspection")
|
@Suppress("SpellCheckingInspection")
|
||||||
internal val PB_RESERVE_FOR_DOUTU = "78 00 90 01 01 F8 01 00 A0 02 00 C8 02 00".hexToBytes()
|
|
||||||
internal val PB_RESERVE_FOR_MARKET_FACE =
|
|
||||||
"02 78 80 80 04 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 00 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 04 08 02 10 3B 90 04 80 C0 80 80 04 B8 04 00 C0 04 00 CA 04 00 F8 04 80 80 04 88 05 00".hexToBytes()
|
|
||||||
internal val PB_RESERVE_FOR_ELSE = "78 00 F8 01 00 C8 02 00".hexToBytes()
|
internal val PB_RESERVE_FOR_ELSE = "78 00 F8 01 00 C8 02 00".hexToBytes()
|
||||||
|
@ -9,13 +9,11 @@
|
|||||||
|
|
||||||
package net.mamoe.mirai.internal.message.protocol
|
package net.mamoe.mirai.internal.message.protocol
|
||||||
|
|
||||||
|
import net.mamoe.mirai.internal.message.PB_RESERVE_FOR_ELSE
|
||||||
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
import net.mamoe.mirai.internal.pipeline.AbstractProcessorPipeline
|
import net.mamoe.mirai.internal.pipeline.AbstractProcessorPipeline
|
||||||
import net.mamoe.mirai.message.data.SingleMessage
|
import net.mamoe.mirai.message.data.SingleMessage
|
||||||
import net.mamoe.mirai.utils.MiraiLogger
|
import net.mamoe.mirai.utils.*
|
||||||
import net.mamoe.mirai.utils.TypeSafeMap
|
|
||||||
import net.mamoe.mirai.utils.systemProp
|
|
||||||
import net.mamoe.mirai.utils.withSwitch
|
|
||||||
|
|
||||||
private val defaultTraceLogging: MiraiLogger by lazy {
|
private val defaultTraceLogging: MiraiLogger by lazy {
|
||||||
MiraiLogger.Factory.create(MessageEncoderPipelineImpl::class, "MessageEncoderPipeline")
|
MiraiLogger.Factory.create(MessageEncoderPipelineImpl::class, "MessageEncoderPipeline")
|
||||||
@ -28,7 +26,12 @@ internal open class MessageEncoderPipelineImpl :
|
|||||||
),
|
),
|
||||||
MessageEncoderPipeline {
|
MessageEncoderPipeline {
|
||||||
|
|
||||||
inner class MessageEncoderContextImpl(attributes: TypeSafeMap) : MessageEncoderContext, BaseContextImpl(attributes)
|
inner class MessageEncoderContextImpl(attributes: TypeSafeMap) : MessageEncoderContext,
|
||||||
|
BaseContextImpl(attributes) {
|
||||||
|
override var generalFlags: ImMsgBody.Elem by lateinitMutableProperty {
|
||||||
|
ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_ELSE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun createContext(attributes: TypeSafeMap): MessageEncoderContext = MessageEncoderContextImpl(attributes)
|
override fun createContext(attributes: TypeSafeMap): MessageEncoderContext = MessageEncoderContextImpl(attributes)
|
||||||
}
|
}
|
@ -9,12 +9,18 @@
|
|||||||
|
|
||||||
package net.mamoe.mirai.internal.message.protocol
|
package net.mamoe.mirai.internal.message.protocol
|
||||||
|
|
||||||
|
import net.mamoe.mirai.Bot
|
||||||
|
import net.mamoe.mirai.contact.Contact
|
||||||
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.internal.pipeline.PipelineConsumptionMarker
|
||||||
import net.mamoe.mirai.internal.pipeline.Processor
|
import net.mamoe.mirai.internal.pipeline.Processor
|
||||||
import net.mamoe.mirai.internal.pipeline.ProcessorPipeline
|
import net.mamoe.mirai.internal.pipeline.ProcessorPipeline
|
||||||
import net.mamoe.mirai.internal.pipeline.ProcessorPipelineContext
|
import net.mamoe.mirai.internal.pipeline.ProcessorPipelineContext
|
||||||
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.MessageSourceKind
|
||||||
import net.mamoe.mirai.message.data.SingleMessage
|
import net.mamoe.mirai.message.data.SingleMessage
|
||||||
|
import net.mamoe.mirai.utils.TypeKey
|
||||||
import net.mamoe.mirai.utils.uncheckedCast
|
import net.mamoe.mirai.utils.uncheckedCast
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
@ -28,12 +34,20 @@ internal abstract class ProcessorCollector {
|
|||||||
abstract fun add(decoder: MessageDecoder)
|
abstract fun add(decoder: MessageDecoder)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal abstract class MessageProtocol {
|
internal abstract class MessageProtocol(
|
||||||
|
private val priority: UInt = 1000u // the higher, the prior it being called
|
||||||
|
) {
|
||||||
fun collectProcessors(processorCollector: ProcessorCollector) {
|
fun collectProcessors(processorCollector: ProcessorCollector) {
|
||||||
processorCollector.collectProcessorsImpl()
|
processorCollector.collectProcessorsImpl()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract fun ProcessorCollector.collectProcessorsImpl()
|
protected abstract fun ProcessorCollector.collectProcessorsImpl()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PRIORITY_METADATA: UInt = 10000u
|
||||||
|
const val PRIORITY_CONTENT: UInt = 1000u
|
||||||
|
const val PRIORITY_UNSUPPORTED: UInt = 100u
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object MessageProtocols {
|
internal object MessageProtocols {
|
||||||
@ -67,10 +81,14 @@ internal object MessageProtocols {
|
|||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
internal interface MessageDecoderContext : ProcessorPipelineContext<ImMsgBody.Elem, Message> {
|
internal interface MessageDecoderContext : ProcessorPipelineContext<ImMsgBody.Elem, Message> {
|
||||||
|
companion object {
|
||||||
|
val BOT = TypeKey<Bot>("bot")
|
||||||
|
val MESSAGE_SOURCE_KIND = TypeKey<MessageSourceKind>("messageSourceKind")
|
||||||
|
val GROUP_ID = TypeKey<Long>("groupId") // zero if not group
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal interface MessageDecoder {
|
internal interface MessageDecoder : PipelineConsumptionMarker {
|
||||||
suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem)
|
suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +100,7 @@ internal class MessageDecoderProcessor(
|
|||||||
) : Processor<MessageDecoderContext, ImMsgBody.Elem> {
|
) : Processor<MessageDecoderContext, ImMsgBody.Elem> {
|
||||||
override suspend fun process(context: MessageDecoderContext, data: ImMsgBody.Elem) {
|
override suspend fun process(context: MessageDecoderContext, data: ImMsgBody.Elem) {
|
||||||
decoder.run { context.process(data) }
|
decoder.run { context.process(data) }
|
||||||
|
// TODO: 2022/4/27 handle exceptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,10 +112,39 @@ internal interface MessageDecoderPipeline : ProcessorPipeline<MessageDecoderProc
|
|||||||
|
|
||||||
internal interface MessageEncoderContext : ProcessorPipelineContext<SingleMessage, ImMsgBody.Elem> {
|
internal interface MessageEncoderContext : ProcessorPipelineContext<SingleMessage, ImMsgBody.Elem> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General flags that should be appended to the end of the result.
|
||||||
|
*
|
||||||
|
* Do not update this property directly, but call [collectGeneralFlags].
|
||||||
|
*/
|
||||||
|
var generalFlags: ImMsgBody.Elem
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val ADD_GENERAL_FLAGS = TypeKey<Boolean>("addGeneralFlags")
|
||||||
|
val MessageEncoderContext.addGeneralFlags get() = attributes[ADD_GENERAL_FLAGS]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override default generalFlags if needed
|
||||||
|
*/
|
||||||
|
inline fun MessageEncoderContext.collectGeneralFlags(block: () -> ImMsgBody.Elem) {
|
||||||
|
if (addGeneralFlags) {
|
||||||
|
generalFlags = block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val CONTACT = TypeKey<Contact>("contact")
|
||||||
|
val MessageEncoderContext.contact get() = attributes[CONTACT]
|
||||||
|
|
||||||
|
val ORIGINAL_MESSAGE = TypeKey<MessageChain>("originalMessage")
|
||||||
|
val MessageEncoderContext.originalMessage get() = attributes[ORIGINAL_MESSAGE]
|
||||||
|
|
||||||
|
val IS_FORWARD = TypeKey<Boolean>("isForward")
|
||||||
|
val MessageEncoderContext.isForward get() = attributes[IS_FORWARD]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal interface MessageEncoder<T : SingleMessage> {
|
internal fun interface MessageEncoder<T : SingleMessage> : PipelineConsumptionMarker {
|
||||||
suspend fun MessageEncoderContext.process(data: T)
|
suspend fun MessageEncoderContext.process(data: T)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +158,7 @@ internal class MessageEncoderProcessor<T : SingleMessage>(
|
|||||||
override suspend fun process(context: MessageEncoderContext, data: SingleMessage) {
|
override suspend fun process(context: MessageEncoderContext, data: SingleMessage) {
|
||||||
if (elementType.isInstance(data)) {
|
if (elementType.isInstance(data)) {
|
||||||
encoder.run { context.process(data.uncheckedCast()) }
|
encoder.run { context.process(data.uncheckedCast()) }
|
||||||
|
// TODO: 2022/4/27 handle exceptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import net.mamoe.mirai.internal.message.MIRAI_CUSTOM_ELEM_TYPE
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.*
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.message.data.CustomMessage
|
||||||
|
import net.mamoe.mirai.utils.read
|
||||||
|
import net.mamoe.mirai.utils.toUHexString
|
||||||
|
|
||||||
|
internal class CustomMessageProtocol : MessageProtocol() {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
add(Encoder())
|
||||||
|
add(Decoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Encoder : MessageEncoder<CustomMessage> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: CustomMessage) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
collect(
|
||||||
|
ImMsgBody.Elem(
|
||||||
|
customElem = ImMsgBody.CustomElem(
|
||||||
|
enumType = MIRAI_CUSTOM_ELEM_TYPE,
|
||||||
|
data = CustomMessage.dump(
|
||||||
|
data.getFactory() as CustomMessage.Factory<CustomMessage>,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Decoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
if (data.customElem == null) return
|
||||||
|
|
||||||
|
kotlin.runCatching {
|
||||||
|
data.customElem.data.read {
|
||||||
|
CustomMessage.load(this)
|
||||||
|
}
|
||||||
|
}.fold(
|
||||||
|
onFailure = {
|
||||||
|
if (it is CustomMessage.Companion.CustomMessageFullDataDeserializeInternalException) {
|
||||||
|
throw IllegalStateException(
|
||||||
|
"Internal error: " +
|
||||||
|
"exception while deserializing CustomMessage head data," +
|
||||||
|
" data=${data.customElem.data.toUHexString()}", it
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
it as CustomMessage.Companion.CustomMessageFullDataDeserializeUserException
|
||||||
|
throw IllegalStateException(
|
||||||
|
"User error: " +
|
||||||
|
"exception while deserializing CustomMessage body," +
|
||||||
|
" body=${it.body.toUHexString()}", it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
onSuccess = {
|
||||||
|
if (it != null) {
|
||||||
|
collect(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import kotlinx.io.core.toByteArray
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.*
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.HummerCommelem
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
||||||
|
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||||
|
import net.mamoe.mirai.message.data.Face
|
||||||
|
import net.mamoe.mirai.utils.hexToBytes
|
||||||
|
import net.mamoe.mirai.utils.toByteArray
|
||||||
|
|
||||||
|
internal class FaceProtocol : MessageProtocol() {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
add(Encoder())
|
||||||
|
add(Type1Decoder())
|
||||||
|
add(Type2Decoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Encoder : MessageEncoder<Face> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: Face) {
|
||||||
|
collect(
|
||||||
|
if (data.id >= 260) {
|
||||||
|
ImMsgBody.Elem(commonElem = data.toCommData())
|
||||||
|
} else {
|
||||||
|
ImMsgBody.Elem(face = data.toJceData())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private val FACE_BUF = "00 01 00 04 52 CC F5 D0".hexToBytes()
|
||||||
|
|
||||||
|
fun Face.toJceData(): ImMsgBody.Face {
|
||||||
|
return ImMsgBody.Face(
|
||||||
|
index = this.id,
|
||||||
|
old = (0x1445 - 4 + this.id).toShort().toByteArray(),
|
||||||
|
buf = FACE_BUF
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Face.toCommData(): ImMsgBody.CommonElem {
|
||||||
|
return ImMsgBody.CommonElem(
|
||||||
|
serviceType = 33,
|
||||||
|
pbElem = HummerCommelem.MsgElemInfoServtype33(
|
||||||
|
index = this.id,
|
||||||
|
name = "/${this.name}".toByteArray(),
|
||||||
|
compat = "/${this.name}".toByteArray()
|
||||||
|
).toByteArray(HummerCommelem.MsgElemInfoServtype33.serializer()),
|
||||||
|
businessType = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Type1Decoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
val commonElem = data.commonElem ?: return
|
||||||
|
if (commonElem.serviceType != 33) return
|
||||||
|
|
||||||
|
val proto =
|
||||||
|
commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype33.serializer())
|
||||||
|
collect(Face(proto.index))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Type2Decoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
val face = data.face ?: return
|
||||||
|
markAsConsumed()
|
||||||
|
collect(Face(face.index))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import kotlinx.io.core.readUShort
|
||||||
|
import net.mamoe.mirai.internal.message.data.FileMessageImpl
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageDecoder
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageDecoderContext
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageProtocol
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.ProcessorCollector
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ObjMsg
|
||||||
|
import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
|
||||||
|
import net.mamoe.mirai.utils.read
|
||||||
|
|
||||||
|
internal class FileMessageProtocol : MessageProtocol() {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
// no encoder
|
||||||
|
add(Decoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Decoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
if (data.transElemInfo == null) return
|
||||||
|
if (data.transElemInfo.elemType != 24) return
|
||||||
|
|
||||||
|
data.transElemInfo.elemValue.read {
|
||||||
|
// group file feed
|
||||||
|
// 01 00 77 08 06 12 0A 61 61 61 61 61 61 2E 74 78 74 1A 06 31 35 42 79 74 65 3A 5F 12 5D 08 66 12 25 2F 64 37 34 62 62 66 33 61 2D 37 62 32 35 2D 31 31 65 62 2D 38 34 66 38 2D 35 34 35 32 30 30 37 62 35 64 39 66 18 0F 22 0A 61 61 61 61 61 61 2E 74 78 74 28 00 3A 00 42 20 61 33 32 35 66 36 33 34 33 30 65 37 61 30 31 31 66 37 64 30 38 37 66 63 33 32 34 37 35 34 39 63
|
||||||
|
// fun getFileRsrvAttr(file: ObjMsg.MsgContentInfo.MsgFile): HummerResv21.ResvAttr? {
|
||||||
|
// if (file.ext.isEmpty()) return null
|
||||||
|
// val element = kotlin.runCatching {
|
||||||
|
// jsonForFileDecode.parseToJsonElement(file.ext) as? JsonObject
|
||||||
|
// }.getOrNull() ?: return null
|
||||||
|
// val extInfo = element["ExtInfo"]?.toString()?.decodeBase64() ?: return null
|
||||||
|
// return extInfo.loadAs(HummerResv21.ResvAttr.serializer())
|
||||||
|
// }
|
||||||
|
|
||||||
|
val var7 = readByte()
|
||||||
|
if (var7 == 1.toByte()) {
|
||||||
|
while (remaining > 2) {
|
||||||
|
val proto = readProtoBuf(ObjMsg.ObjMsg.serializer(), readUShort().toInt())
|
||||||
|
// proto.msgType=6
|
||||||
|
|
||||||
|
val file = proto.msgContentInfo.firstOrNull()?.msgFile ?: continue // officially get(0) only.
|
||||||
|
// val attr = getFileRsrvAttr(file) ?: continue
|
||||||
|
// val info = attr.forwardExtFileInfo ?: continue
|
||||||
|
|
||||||
|
collect(
|
||||||
|
FileMessageImpl(
|
||||||
|
id = file.filePath,
|
||||||
|
busId = file.busId, // path i.e. /a99e95fa-7b2d-11eb-adae-5452007b698a
|
||||||
|
name = file.fileName,
|
||||||
|
size = file.fileSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import net.mamoe.mirai.contact.ContactOrBot
|
||||||
|
import net.mamoe.mirai.contact.User
|
||||||
|
import net.mamoe.mirai.internal.message.UNSUPPORTED_FLASH_MESSAGE_PLAIN
|
||||||
|
import net.mamoe.mirai.internal.message.image.OnlineFriendImageImpl
|
||||||
|
import net.mamoe.mirai.internal.message.image.OnlineGroupImageImpl
|
||||||
|
import net.mamoe.mirai.internal.message.image.friendImageId
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.*
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageEncoderContext.Companion.collectGeneralFlags
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageEncoderContext.Companion.contact
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.HummerCommelem
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
||||||
|
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||||
|
import net.mamoe.mirai.message.data.FlashImage
|
||||||
|
import net.mamoe.mirai.utils.hexToBytes
|
||||||
|
|
||||||
|
internal class FlashImageProtocol : MessageProtocol() {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
add(Decoder())
|
||||||
|
add(Encoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Decoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
if (data.commonElem == null) return
|
||||||
|
if (data.commonElem.serviceType != 3) return
|
||||||
|
|
||||||
|
val proto =
|
||||||
|
data.commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype3.serializer())
|
||||||
|
if (proto.flashTroopPic != null) {
|
||||||
|
collect(FlashImage(OnlineGroupImageImpl(proto.flashTroopPic)))
|
||||||
|
}
|
||||||
|
if (proto.flashC2cPic != null) {
|
||||||
|
collect(FlashImage(OnlineFriendImageImpl(proto.flashC2cPic)))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Encoder : MessageEncoder<FlashImage> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: FlashImage) {
|
||||||
|
collect(data.toJceData(contact))
|
||||||
|
processAlso(UNSUPPORTED_FLASH_MESSAGE_PLAIN)
|
||||||
|
collectGeneralFlags {
|
||||||
|
ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_DOUTU))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
@Suppress("SpellCheckingInspection")
|
||||||
|
private val PB_RESERVE_FOR_DOUTU = "78 00 90 01 01 F8 01 00 A0 02 00 C8 02 00".hexToBytes()
|
||||||
|
|
||||||
|
private fun FlashImage.toJceData(messageTarget: ContactOrBot?): ImMsgBody.Elem {
|
||||||
|
return if (messageTarget is User) {
|
||||||
|
ImMsgBody.Elem(
|
||||||
|
commonElem = ImMsgBody.CommonElem(
|
||||||
|
serviceType = 3,
|
||||||
|
businessType = 0,
|
||||||
|
pbElem = HummerCommelem.MsgElemInfoServtype3(
|
||||||
|
flashC2cPic = ImMsgBody.NotOnlineImage(
|
||||||
|
filePath = image.friendImageId,
|
||||||
|
resId = image.friendImageId,
|
||||||
|
picMd5 = image.md5,
|
||||||
|
oldPicMd5 = false,
|
||||||
|
pbReserve = byteArrayOf(0x78, 0x06)
|
||||||
|
)
|
||||||
|
).toByteArray(HummerCommelem.MsgElemInfoServtype3.serializer())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ImMsgBody.Elem(
|
||||||
|
commonElem = ImMsgBody.CommonElem(
|
||||||
|
serviceType = 3,
|
||||||
|
businessType = 0,
|
||||||
|
pbElem = HummerCommelem.MsgElemInfoServtype3(
|
||||||
|
flashTroopPic = ImMsgBody.CustomFace(
|
||||||
|
filePath = image.imageId,
|
||||||
|
picMd5 = image.md5,
|
||||||
|
pbReserve = byteArrayOf(0x78, 0x06)
|
||||||
|
)
|
||||||
|
).toByteArray(HummerCommelem.MsgElemInfoServtype3.serializer())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageEncoder
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageEncoderContext
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageProtocol
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.ProcessorCollector
|
||||||
|
import net.mamoe.mirai.message.data.SingleMessage
|
||||||
|
|
||||||
|
internal class GeneralFlagsProtocol : MessageProtocol() {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
add(Encoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Encoder : MessageEncoder<SingleMessage> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: SingleMessage) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import net.mamoe.mirai.internal.message.flags.InternalFlagOnlyMessage
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.*
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.message.data.ForwardMessage
|
||||||
|
import net.mamoe.mirai.message.data.MessageSource
|
||||||
|
import net.mamoe.mirai.message.data.ShowImageFlag
|
||||||
|
import net.mamoe.mirai.message.data.SingleMessage
|
||||||
|
|
||||||
|
internal class IgnoredMessagesProtocol : MessageProtocol() {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
add(Encoder())
|
||||||
|
add(Decoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Decoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
when (data) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Encoder : MessageEncoder<SingleMessage> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: SingleMessage) {
|
||||||
|
when (data) {
|
||||||
|
is ForwardMessage, // TODO: 2022/4/27 check this
|
||||||
|
is MessageSource, // mirai metadata only
|
||||||
|
-> {
|
||||||
|
markAsConsumed()
|
||||||
|
}
|
||||||
|
is InternalFlagOnlyMessage, is ShowImageFlag -> {
|
||||||
|
// ignored
|
||||||
|
markAsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import net.mamoe.mirai.contact.User
|
||||||
|
import net.mamoe.mirai.internal.message.image.*
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.*
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageEncoderContext.Companion.contact
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.CustomFace
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
||||||
|
import net.mamoe.mirai.message.data.ImageType
|
||||||
|
import net.mamoe.mirai.message.data.ShowImageFlag
|
||||||
|
import net.mamoe.mirai.utils.generateImageId
|
||||||
|
import net.mamoe.mirai.utils.toUHexString
|
||||||
|
|
||||||
|
internal class ImageProtocol : MessageProtocol() {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
add(ImageEncoder())
|
||||||
|
add(ImageDecoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ImageDecoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
if (data.notOnlineImage != null) collect(OnlineFriendImageImpl(data.notOnlineImage))
|
||||||
|
if (data.customFace != null) {
|
||||||
|
collect(OnlineGroupImageImpl(data.customFace))
|
||||||
|
data.customFace.pbReserve.let {
|
||||||
|
if (it.isNotEmpty() && it.loadAs(CustomFace.ResvAttr.serializer()).msgImageShow != null) {
|
||||||
|
collect(ShowImageFlag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ImageEncoder : MessageEncoder<AbstractImage> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: AbstractImage) {
|
||||||
|
when (data) {
|
||||||
|
is OfflineGroupImage -> {
|
||||||
|
if (contact is User) {
|
||||||
|
collect(ImMsgBody.Elem(notOnlineImage = data.toJceData().toNotOnlineImage()))
|
||||||
|
} else {
|
||||||
|
collect(ImMsgBody.Elem(customFace = data.toJceData()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is OnlineGroupImageImpl -> {
|
||||||
|
if (contact is User) {
|
||||||
|
collect(ImMsgBody.Elem(notOnlineImage = data.delegate.toNotOnlineImage()))
|
||||||
|
} else {
|
||||||
|
collect(ImMsgBody.Elem(customFace = data.delegate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is OnlineFriendImageImpl -> {
|
||||||
|
if (contact is User) {
|
||||||
|
collect(ImMsgBody.Elem(notOnlineImage = data.delegate))
|
||||||
|
} else {
|
||||||
|
collect(ImMsgBody.Elem(customFace = data.delegate.toCustomFace()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is OfflineFriendImage -> {
|
||||||
|
if (contact is User) {
|
||||||
|
collect(ImMsgBody.Elem(notOnlineImage = data.toJceData()))
|
||||||
|
} else {
|
||||||
|
collect(ImMsgBody.Elem(customFace = data.toJceData().toCustomFace()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun OfflineGroupImage.toJceData(): ImMsgBody.CustomFace {
|
||||||
|
return ImMsgBody.CustomFace(
|
||||||
|
fileId = this.fileId ?: 0,
|
||||||
|
filePath = this.imageId,
|
||||||
|
picMd5 = this.md5,
|
||||||
|
flag = ByteArray(4),
|
||||||
|
size = size.toInt(),
|
||||||
|
width = width.coerceAtLeast(1),
|
||||||
|
height = height.coerceAtLeast(1),
|
||||||
|
imageType = getIdByImageType(imageType),
|
||||||
|
origin = if (imageType == ImageType.GIF) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
},
|
||||||
|
//_400Height = 235,
|
||||||
|
//_400Url = "/gchatpic_new/000000000/1041235568-2195821338-01E9451B70EDEAE3B37C101F1EEBF5B5/400?term=2",
|
||||||
|
//_400Width = 351,
|
||||||
|
// pbReserve = "08 00 10 00 32 00 50 00 78 08".autoHexToBytes(),
|
||||||
|
bizType = 5,
|
||||||
|
fileType = 66,
|
||||||
|
useful = 1,
|
||||||
|
// pbReserve = CustomFaceExtPb.ResvAttr().toByteArray(CustomFaceExtPb.ResvAttr.serializer())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ImMsgBody.CustomFace.toNotOnlineImage(): ImMsgBody.NotOnlineImage {
|
||||||
|
val resId = calculateResId()
|
||||||
|
|
||||||
|
return ImMsgBody.NotOnlineImage(
|
||||||
|
filePath = filePath,
|
||||||
|
resId = resId,
|
||||||
|
oldPicMd5 = false,
|
||||||
|
picWidth = width,
|
||||||
|
picHeight = height,
|
||||||
|
imgType = imageType,
|
||||||
|
picMd5 = picMd5,
|
||||||
|
fileLen = size.toLong(),
|
||||||
|
oldVerSendFile = oldData,
|
||||||
|
downloadPath = resId,
|
||||||
|
original = origin,
|
||||||
|
bizType = bizType,
|
||||||
|
pbReserve = byteArrayOf(0x78, 0x02),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ImMsgBody.NotOnlineImage.toCustomFace(): ImMsgBody.CustomFace {
|
||||||
|
return ImMsgBody.CustomFace(
|
||||||
|
filePath = generateImageId(picMd5, getImageType(imgType)),
|
||||||
|
picMd5 = picMd5,
|
||||||
|
bizType = 5,
|
||||||
|
fileType = 66,
|
||||||
|
useful = 1,
|
||||||
|
flag = ByteArray(4),
|
||||||
|
bigUrl = bigUrl,
|
||||||
|
origUrl = origUrl,
|
||||||
|
width = picWidth.coerceAtLeast(1),
|
||||||
|
height = picHeight.coerceAtLeast(1),
|
||||||
|
imageType = imgType,
|
||||||
|
//_400Height = 235,
|
||||||
|
//_400Url = "/gchatpic_new/000000000/1041235568-2195821338-01E9451B70EDEAE3B37C101F1EEBF5B5/400?term=2",
|
||||||
|
//_400Width = 351,
|
||||||
|
origin = original,
|
||||||
|
size = fileLen.toInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// aka friend image id
|
||||||
|
private fun ImMsgBody.NotOnlineImageOrCustomFace.calculateResId(): String {
|
||||||
|
val url = origUrl.takeIf { it.isNotBlank() }
|
||||||
|
?: thumbUrl.takeIf { it.isNotBlank() }
|
||||||
|
?: _400Url.takeIf { it.isNotBlank() }
|
||||||
|
?: ""
|
||||||
|
|
||||||
|
// gchatpic_new
|
||||||
|
// offpic_new
|
||||||
|
val picSenderId = url.substringAfter("pic_new/").substringBefore("/")
|
||||||
|
.takeIf { it.isNotBlank() } ?: "000000000"
|
||||||
|
val unknownInt = url.substringAfter("-").substringBefore("-")
|
||||||
|
.takeIf { it.isNotBlank() } ?: "000000000"
|
||||||
|
|
||||||
|
return "/$picSenderId-$unknownInt-${picMd5.toUHexString("")}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun OfflineFriendImage.toJceData(): ImMsgBody.NotOnlineImage {
|
||||||
|
val friendImageId = this.friendImageId
|
||||||
|
return ImMsgBody.NotOnlineImage(
|
||||||
|
filePath = friendImageId,
|
||||||
|
resId = friendImageId,
|
||||||
|
oldPicMd5 = false,
|
||||||
|
picMd5 = this.md5,
|
||||||
|
fileLen = size,
|
||||||
|
downloadPath = friendImageId,
|
||||||
|
original = if (imageType == ImageType.GIF) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
},
|
||||||
|
picWidth = width,
|
||||||
|
picHeight = height,
|
||||||
|
imgType = getIdByImageType(imageType),
|
||||||
|
pbReserve = byteArrayOf(0x78, 0x02)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import net.mamoe.mirai.internal.message.data.MarketFaceImpl
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.*
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageEncoderContext.Companion.collectGeneralFlags
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.message.data.Dice
|
||||||
|
import net.mamoe.mirai.message.data.PlainText
|
||||||
|
import net.mamoe.mirai.utils.hexToBytes
|
||||||
|
|
||||||
|
|
||||||
|
internal class MarketFaceProtocol : MessageProtocol() {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
add(DiceEncoder())
|
||||||
|
add(MarketFaceImplEncoder())
|
||||||
|
|
||||||
|
add(MarketFaceDecoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MarketFaceImplEncoder : MessageEncoder<MarketFaceImpl> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: MarketFaceImpl) {
|
||||||
|
collect(ImMsgBody.Elem(marketFace = data.delegate))
|
||||||
|
processAlso(PlainText(data.name))
|
||||||
|
collect(ImMsgBody.Elem(extraInfo = ImMsgBody.ExtraInfo(flags = 8, groupMask = 1)))
|
||||||
|
collectGeneralFlags {
|
||||||
|
ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_MARKET_FACE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private val PB_RESERVE_FOR_MARKET_FACE =
|
||||||
|
"02 78 80 80 04 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 00 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 04 08 02 10 3B 90 04 80 C0 80 80 04 B8 04 00 C0 04 00 CA 04 00 F8 04 80 80 04 88 05 00".hexToBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DiceEncoder : MessageEncoder<Dice> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: Dice) {
|
||||||
|
processAlso(MarketFaceImpl(data.toJceStruct()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MarketFaceDecoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
val proto = data.marketFace ?: return
|
||||||
|
|
||||||
|
proto.toDiceOrNull()?.let {
|
||||||
|
collect(it)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
collect(MarketFaceImpl(proto))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
/**
|
||||||
|
* PC 客户端没有 [ImMsgBody.MarketFace.mobileParam], 是按 [ImMsgBody.MarketFace.faceId] 发的...
|
||||||
|
*/
|
||||||
|
@Suppress("SpellCheckingInspection")
|
||||||
|
private val DICE_PC_FACE_IDS = mapOf(
|
||||||
|
1 to "E6EEDE15CDFBEB4DF0242448535354F1".hexToBytes(),
|
||||||
|
2 to "C5A95816FB5AFE34A58AF0E837A3B5A0".hexToBytes(),
|
||||||
|
3 to "382131D722EEA4624F087C5B8035AF5F".hexToBytes(),
|
||||||
|
4 to "FA90E956DCAD76742F2DB87723D3B669".hexToBytes(),
|
||||||
|
5 to "D51FA892017647431BB243920EC9FB8E".hexToBytes(),
|
||||||
|
6 to "7A2303AD80755FCB6BBFAC38327E0C01".hexToBytes(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun ImMsgBody.MarketFace.toDiceOrNull(): Dice? {
|
||||||
|
if (this.tabId != 11464) return null
|
||||||
|
val value = when {
|
||||||
|
mobileParam.isNotEmpty() -> mobileParam.lastOrNull()?.toInt()?.and(0xff)?.minus(47) ?: return null
|
||||||
|
else -> DICE_PC_FACE_IDS.entries.find { it.value.contentEquals(faceId) }?.key ?: return null
|
||||||
|
}
|
||||||
|
if (value in 1..6) {
|
||||||
|
return Dice(value)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://github.com/mamoe/mirai/issues/1012
|
||||||
|
private fun Dice.toJceStruct(): ImMsgBody.MarketFace {
|
||||||
|
return ImMsgBody.MarketFace(
|
||||||
|
faceName = byteArrayOf(91, -23, -86, -80, -27, -83, -112, 93),
|
||||||
|
itemType = 6,
|
||||||
|
faceInfo = 1,
|
||||||
|
faceId = byteArrayOf(
|
||||||
|
72, 35, -45, -83, -79, 93,
|
||||||
|
-16, -128, 20, -50, 93, 103,
|
||||||
|
-106, -73, 110, -31
|
||||||
|
),
|
||||||
|
tabId = 11464,
|
||||||
|
subType = 3,
|
||||||
|
key = byteArrayOf(52, 48, 57, 101, 50, 97, 54, 57, 98, 49, 54, 57, 49, 56, 102, 57),
|
||||||
|
mediaType = 0,
|
||||||
|
imageWidth = 200,
|
||||||
|
imageHeight = 200,
|
||||||
|
mobileParam = byteArrayOf(
|
||||||
|
114, 115, 99, 84, 121, 112, 101,
|
||||||
|
63, 49, 59, 118, 97, 108, 117,
|
||||||
|
101, 61,
|
||||||
|
(47 + value).toByte()
|
||||||
|
),
|
||||||
|
pbReserve = byteArrayOf(
|
||||||
|
10, 6, 8, -56, 1, 16, -56, 1, 64,
|
||||||
|
1, 88, 0, 98, 9, 35, 48, 48, 48,
|
||||||
|
48, 48, 48, 48, 48, 106, 9, 35,
|
||||||
|
48, 48, 48, 48, 48, 48, 48, 48
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.*
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.message.data.MusicShare
|
||||||
|
import net.mamoe.mirai.message.data.PlainText
|
||||||
|
import net.mamoe.mirai.message.data.content
|
||||||
|
|
||||||
|
internal class MusicShareProtocol : MessageProtocol() {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Encoder : MessageEncoder<MusicShare> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: MusicShare) {
|
||||||
|
// 只有在 QuoteReply 的 source 里才会进行 MusicShare 转换, 因此可以转 PT.
|
||||||
|
// 发送消息时会被特殊处理
|
||||||
|
processAlso(PlainText(data.content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Decoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import net.mamoe.mirai.internal.message.UNSUPPORTED_POKE_MESSAGE_PLAIN
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.*
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.HummerCommelem
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
||||||
|
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||||
|
import net.mamoe.mirai.message.data.PokeMessage
|
||||||
|
|
||||||
|
internal class PokeMessageProtocol : MessageProtocol() {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
add(Encoder())
|
||||||
|
add(Decoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Encoder : MessageEncoder<PokeMessage> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: PokeMessage) {
|
||||||
|
collect(
|
||||||
|
ImMsgBody.Elem(
|
||||||
|
commonElem = ImMsgBody.CommonElem(
|
||||||
|
serviceType = 2,
|
||||||
|
businessType = data.pokeType,
|
||||||
|
pbElem = HummerCommelem.MsgElemInfoServtype2(
|
||||||
|
pokeType = data.pokeType,
|
||||||
|
vaspokeId = data.id,
|
||||||
|
vaspokeMinver = "7.2.0",
|
||||||
|
vaspokeName = data.name
|
||||||
|
).toByteArray(HummerCommelem.MsgElemInfoServtype2.serializer())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
processAlso(UNSUPPORTED_POKE_MESSAGE_PLAIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Decoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
if (data.commonElem == null) return
|
||||||
|
if (data.commonElem.serviceType != 2) return
|
||||||
|
|
||||||
|
val proto =
|
||||||
|
data.commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype2.serializer())
|
||||||
|
val name = proto.vaspokeName.takeIf { it.isNotEmpty() }
|
||||||
|
?: PokeMessage.values.firstOrNull { it.id == proto.vaspokeId && it.pokeType == proto.pokeType }?.name
|
||||||
|
.orEmpty()
|
||||||
|
collect(
|
||||||
|
PokeMessage(
|
||||||
|
name = name,
|
||||||
|
pokeType = proto.pokeType,
|
||||||
|
id = proto.vaspokeId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageEncoder
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageEncoderContext
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageEncoderContext.Companion.collectGeneralFlags
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageProtocol
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.ProcessorCollector
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.message.data.PttMessage
|
||||||
|
import net.mamoe.mirai.utils.hexToBytes
|
||||||
|
|
||||||
|
internal class PttMessageProtocol : MessageProtocol() {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
|
||||||
|
add(Encoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Encoder : MessageEncoder<PttMessage> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: PttMessage) {
|
||||||
|
collect(
|
||||||
|
ImMsgBody.Elem(
|
||||||
|
extraInfo = ImMsgBody.ExtraInfo(flags = 16, groupMask = 1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
collect(
|
||||||
|
ImMsgBody.Elem(
|
||||||
|
elemFlags2 = ImMsgBody.ElemFlags2(
|
||||||
|
vipStatus = 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
collectGeneralFlags {
|
||||||
|
ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_PTT))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private val PB_RESERVE_FOR_PTT =
|
||||||
|
"78 00 F8 01 00 C8 02 00 AA 03 26 08 22 12 22 41 20 41 3B 25 3E 16 45 3F 43 2F 29 3E 44 24 14 18 46 3D 2B 4A 44 3A 18 2E 19 29 1B 26 32 31 31 29 43".hexToBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import net.mamoe.mirai.contact.AnonymousMember
|
||||||
|
import net.mamoe.mirai.contact.Group
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.*
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageDecoderContext.Companion.BOT
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageDecoderContext.Companion.GROUP_ID
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageDecoderContext.Companion.MESSAGE_SOURCE_KIND
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageEncoderContext.Companion.contact
|
||||||
|
import net.mamoe.mirai.internal.message.source.MessageSourceInternal
|
||||||
|
import net.mamoe.mirai.internal.message.source.OfflineMessageSourceImplData
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.message.data.At
|
||||||
|
import net.mamoe.mirai.message.data.OnlineMessageSource
|
||||||
|
import net.mamoe.mirai.message.data.QuoteReply
|
||||||
|
|
||||||
|
internal class QuoteReplyProtocol : MessageProtocol(PRIORITY_METADATA) {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
add(Encoder())
|
||||||
|
add(Decoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Decoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
if (data.srcMsg == null) return
|
||||||
|
OfflineMessageSourceImplData(
|
||||||
|
data.srcMsg,
|
||||||
|
attributes[BOT],
|
||||||
|
attributes[MESSAGE_SOURCE_KIND],
|
||||||
|
attributes[GROUP_ID]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Encoder : MessageEncoder<QuoteReply> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: QuoteReply) {
|
||||||
|
val source = data.source as? MessageSourceInternal ?: return
|
||||||
|
collect(ImMsgBody.Elem(srcMsg = source.toJceData()))
|
||||||
|
if (contact is Group) {
|
||||||
|
if (source is OnlineMessageSource.Incoming.FromGroup) {
|
||||||
|
val sender0 = source.sender
|
||||||
|
if (sender0 !is AnonymousMember) {
|
||||||
|
processAlso(At(sender0))
|
||||||
|
}
|
||||||
|
// transformOneMessage(PlainText(" "))
|
||||||
|
// removed by https://github.com/mamoe/mirai/issues/524
|
||||||
|
// 发送 QuoteReply 消息时无可避免的产生多余空格 #524
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,212 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import kotlinx.io.core.toByteArray
|
||||||
|
import net.mamoe.mirai.internal.message.UNSUPPORTED_MERGED_MESSAGE_PLAIN
|
||||||
|
import net.mamoe.mirai.internal.message.data.ForwardMessageInternal
|
||||||
|
import net.mamoe.mirai.internal.message.data.LightAppInternal
|
||||||
|
import net.mamoe.mirai.internal.message.data.LongMessageInternal
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.*
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageEncoderContext.Companion.collectGeneralFlags
|
||||||
|
import net.mamoe.mirai.internal.message.runWithBugReport
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.message.data.*
|
||||||
|
import net.mamoe.mirai.utils.hexToBytes
|
||||||
|
import net.mamoe.mirai.utils.toUHexString
|
||||||
|
import net.mamoe.mirai.utils.unzip
|
||||||
|
import net.mamoe.mirai.utils.zip
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles:
|
||||||
|
* - [RichMessage]
|
||||||
|
* - [LongMessageInternal]
|
||||||
|
* - [ServiceMessage]
|
||||||
|
* - [ForwardMessage]
|
||||||
|
*/
|
||||||
|
internal class RichMessageProtocol : MessageProtocol() {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
add(RichMsgDecoder())
|
||||||
|
add(LightAppDecoder())
|
||||||
|
|
||||||
|
add(Encoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Encoder : MessageEncoder<RichMessage> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: RichMessage) {
|
||||||
|
val content = data.content.toByteArray().zip()
|
||||||
|
var longTextResId: String? = null
|
||||||
|
when (data) {
|
||||||
|
is ForwardMessageInternal -> {
|
||||||
|
collect(
|
||||||
|
ImMsgBody.Elem(
|
||||||
|
richMsg = ImMsgBody.RichMsg(
|
||||||
|
serviceId = data.serviceId, // ok
|
||||||
|
template1 = byteArrayOf(1) + content
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN)
|
||||||
|
}
|
||||||
|
is LongMessageInternal -> {
|
||||||
|
collect(
|
||||||
|
ImMsgBody.Elem(
|
||||||
|
richMsg = ImMsgBody.RichMsg(
|
||||||
|
serviceId = data.serviceId, // ok
|
||||||
|
template1 = byteArrayOf(1) + content
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
processAlso(UNSUPPORTED_MERGED_MESSAGE_PLAIN)
|
||||||
|
longTextResId = data.resId
|
||||||
|
}
|
||||||
|
is LightApp -> collect(
|
||||||
|
ImMsgBody.Elem(
|
||||||
|
lightApp = ImMsgBody.LightAppElem(
|
||||||
|
data = byteArrayOf(1) + content
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else -> collect(
|
||||||
|
ImMsgBody.Elem(
|
||||||
|
richMsg = ImMsgBody.RichMsg(
|
||||||
|
serviceId = when (data) {
|
||||||
|
is ServiceMessage -> data.serviceId
|
||||||
|
else -> error("unsupported RichMessage: ${data::class.simpleName}")
|
||||||
|
},
|
||||||
|
template1 = byteArrayOf(1) + content
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
collectGeneralFlags {
|
||||||
|
if (longTextResId != null) {
|
||||||
|
ImMsgBody.Elem(
|
||||||
|
generalFlags = ImMsgBody.GeneralFlags(
|
||||||
|
longTextFlag = 1,
|
||||||
|
longTextResid = longTextResId!!,
|
||||||
|
pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 08 09 78 00 A0 01 81 DC 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00
|
||||||
|
ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_RICH_MESSAGE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private val PB_RESERVE_FOR_RICH_MESSAGE =
|
||||||
|
"08 09 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00".hexToBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LightAppDecoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
val lightApp = data.lightApp ?: return
|
||||||
|
|
||||||
|
val content = runWithBugReport("解析 lightApp",
|
||||||
|
{ "resId=" + lightApp.msgResid + "data=" + lightApp.data.toUHexString() }) {
|
||||||
|
when (lightApp.data[0].toInt()) {
|
||||||
|
0 -> lightApp.data.decodeToString(startIndex = 1)
|
||||||
|
1 -> lightApp.data.unzip(1).decodeToString()
|
||||||
|
else -> error("unknown compression flag=${lightApp.data[0]}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collect(LightAppInternal(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RichMsgDecoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
if (data.richMsg == null) return
|
||||||
|
|
||||||
|
val richMsg = data.richMsg
|
||||||
|
|
||||||
|
val content = runWithBugReport("解析 richMsg", { richMsg.template1.toUHexString() }) {
|
||||||
|
when (richMsg.template1[0].toInt()) {
|
||||||
|
0 -> richMsg.template1.decodeToString(startIndex = 1)
|
||||||
|
1 -> richMsg.template1.unzip(1).decodeToString()
|
||||||
|
else -> error("unknown compression flag=${richMsg.template1[0]}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findStringProperty(name: String): String {
|
||||||
|
return content.substringAfter("$name=\"", "").substringBefore("\"", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
val serviceId = when (val sid = richMsg.serviceId) {
|
||||||
|
0 -> {
|
||||||
|
val serviceIdStr = findStringProperty("serviceID")
|
||||||
|
if (serviceIdStr.isEmpty() || serviceIdStr.isBlank()) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
serviceIdStr.toIntOrNull() ?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> sid
|
||||||
|
}
|
||||||
|
when (serviceId) {
|
||||||
|
// 5: 使用微博长图转换功能分享到QQ群
|
||||||
|
/*
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><msg serviceID="5" templateID="12345" brief="[分享]想要沐浴阳光,就别钻进
|
||||||
|
阴影。 ???" ><item layout="0"><image uuid="{E5F68BD5-05F8-148B-9DA7-FECD026D30AD}.jpg" md5="E5F68BD505F8148B9DA7FECD026D
|
||||||
|
30AD" GroupFiledid="2167263882" minWidth="120" minHeight="120" maxWidth="180" maxHeight="180" /></item><source name="新
|
||||||
|
浪微博" icon="http://i.gtimg.cn/open/app_icon/00/73/69/03//100736903_100_m.png" appid="100736903" action="" i_actionData
|
||||||
|
="" a_actionData="" url=""/></msg>
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* json?
|
||||||
|
*/
|
||||||
|
1 -> @Suppress("DEPRECATION_ERROR")
|
||||||
|
collect(SimpleServiceMessage(1, content))
|
||||||
|
/**
|
||||||
|
* [LongMessageInternal], [ForwardMessage]
|
||||||
|
*/
|
||||||
|
35 -> {
|
||||||
|
|
||||||
|
val resId = findStringProperty("m_resid")
|
||||||
|
val fileName = findStringProperty("m_fileName").takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
|
val msg = if (resId.isEmpty()) {
|
||||||
|
// Nested ForwardMessage
|
||||||
|
if (fileName != null && findStringProperty("action") == "viewMultiMsg") {
|
||||||
|
ForwardMessageInternal(content, null, fileName)
|
||||||
|
} else {
|
||||||
|
SimpleServiceMessage(35, content)
|
||||||
|
}
|
||||||
|
} else when (findStringProperty("multiMsgFlag").toIntOrNull()) {
|
||||||
|
1 -> LongMessageInternal(content, resId)
|
||||||
|
0 -> ForwardMessageInternal(content, resId, fileName)
|
||||||
|
else -> {
|
||||||
|
// from PC QQ
|
||||||
|
if (findStringProperty("action") == "viewMultiMsg") {
|
||||||
|
ForwardMessageInternal(content, resId, fileName)
|
||||||
|
} else {
|
||||||
|
SimpleServiceMessage(35, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collect(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 104 新群员入群的消息
|
||||||
|
else -> {
|
||||||
|
collect(SimpleServiceMessage(serviceId, content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import kotlinx.io.core.buildPacket
|
||||||
|
import kotlinx.io.core.discardExact
|
||||||
|
import kotlinx.io.core.readBytes
|
||||||
|
import kotlinx.io.core.readUInt
|
||||||
|
import net.mamoe.mirai.contact.Group
|
||||||
|
import net.mamoe.mirai.contact.nameCardOrNick
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.*
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageEncoderContext.Companion.CONTACT
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageEncoderContext.Companion.isForward
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.MessageEncoderContext.Companion.originalMessage
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.message.data.*
|
||||||
|
import net.mamoe.mirai.utils.read
|
||||||
|
import net.mamoe.mirai.utils.safeCast
|
||||||
|
import net.mamoe.mirai.utils.withUse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For [PlainText] and [At]
|
||||||
|
*/
|
||||||
|
internal class TextProtocol : MessageProtocol() {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
add(PlainTextEncoder())
|
||||||
|
add(AtEncoder())
|
||||||
|
add(AtAllEncoder())
|
||||||
|
|
||||||
|
add(Decoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Decoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
val text = data.text ?: return
|
||||||
|
if (text.attr6Buf.isEmpty()) {
|
||||||
|
collect(PlainText(text.str))
|
||||||
|
} else {
|
||||||
|
val id = text.attr6Buf.read {
|
||||||
|
discardExact(7)
|
||||||
|
readUInt().toLong()
|
||||||
|
}
|
||||||
|
if (id == 0L) {
|
||||||
|
collect(AtAll)
|
||||||
|
} else {
|
||||||
|
collect(At(id)) // element.text.str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PlainTextEncoder : MessageEncoder<PlainText> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: PlainText) {
|
||||||
|
collect(ImMsgBody.Elem(text = ImMsgBody.Text(str = data.content)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AtEncoder : MessageEncoder<At> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: At) {
|
||||||
|
collected += ImMsgBody.Elem(
|
||||||
|
text = data.toJceData(
|
||||||
|
attributes[CONTACT].safeCast(),
|
||||||
|
originalMessage[MessageSource],
|
||||||
|
isForward,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " ")))
|
||||||
|
// removed by https://github.com/mamoe/mirai/issues/524
|
||||||
|
// 发送 QuoteReply 消息时无可避免的产生多余空格 #524
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun At.toJceData(
|
||||||
|
group: Group?,
|
||||||
|
source: MessageSource?,
|
||||||
|
isForward: Boolean,
|
||||||
|
): ImMsgBody.Text {
|
||||||
|
fun findFromGroup(g: Group?): String? {
|
||||||
|
return g?.members?.get(this.target)?.nameCardOrNick
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findFromSource(): String? {
|
||||||
|
return when (source) {
|
||||||
|
is OnlineMessageSource -> {
|
||||||
|
return findFromGroup(source.target.safeCast())
|
||||||
|
}
|
||||||
|
is OfflineMessageSource -> {
|
||||||
|
if (source.kind == MessageSourceKind.GROUP) {
|
||||||
|
return findFromGroup(group?.bot?.getGroup(source.targetId))
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val nick = if (isForward) {
|
||||||
|
findFromSource() ?: findFromGroup(group)
|
||||||
|
} else {
|
||||||
|
findFromGroup(group) ?: findFromSource()
|
||||||
|
} ?: target.toString()
|
||||||
|
|
||||||
|
val text = "@$nick".dropEmoji()
|
||||||
|
return ImMsgBody.Text(
|
||||||
|
str = text,
|
||||||
|
attr6Buf = buildPacket {
|
||||||
|
// MessageForText$AtTroopMemberInfo
|
||||||
|
writeShort(1) // const
|
||||||
|
writeShort(0) // startPos
|
||||||
|
writeShort(text.length.toShort()) // textLen
|
||||||
|
writeByte(0) // flag, may=1
|
||||||
|
writeInt(target.toInt()) // uin
|
||||||
|
writeShort(0) // const
|
||||||
|
}.readBytes()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// region Emoji pattern. <Licenced under the MIT LICENSE>
|
||||||
|
//
|
||||||
|
// https://github.com/mathiasbynens/emoji-test-regex-pattern
|
||||||
|
// https://github.com/mathiasbynens/emoji-test-regex-pattern/blob/main/dist/latest/java.txt
|
||||||
|
//
|
||||||
|
|
||||||
|
@Suppress("RegExpSingleCharAlternation", "RegExpRedundantEscape")
|
||||||
|
private val EMOJI_PATTERN: Regex? = runCatching {
|
||||||
|
val resource =
|
||||||
|
AtEncoder::class.java.classLoader.getResourceAsStream("emoji-pattern.regex")
|
||||||
|
?.withUse { readBytes().decodeToString() }
|
||||||
|
?: return@runCatching null
|
||||||
|
Regex(resource)
|
||||||
|
}.getOrNull() // May some java runtime unsupported
|
||||||
|
|
||||||
|
fun String.dropEmoji(): String {
|
||||||
|
EMOJI_PATTERN?.let { regex -> return replace(regex, "") }
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AtAllEncoder : MessageEncoder<AtAll> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: AtAll) {
|
||||||
|
collect(jceData)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val jceData by lazy {
|
||||||
|
ImMsgBody.Elem(
|
||||||
|
text = ImMsgBody.Text(
|
||||||
|
str = AtAll.display,
|
||||||
|
attr6Buf = buildPacket {
|
||||||
|
// MessageForText$AtTroopMemberInfo
|
||||||
|
writeShort(1) // const
|
||||||
|
writeShort(0) // startPos
|
||||||
|
writeShort(AtAll.display.length.toShort()) // textLen
|
||||||
|
writeByte(1) // flag, may=1
|
||||||
|
writeInt(0) // uin
|
||||||
|
writeShort(0) // const
|
||||||
|
}.readBytes()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import net.mamoe.mirai.internal.message.data.UnsupportedMessageImpl
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.*
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
|
||||||
|
internal class UnsupportedMessageProtocol : MessageProtocol(priority = 100u) {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
add(Decoder())
|
||||||
|
add(Encoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Decoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
val struct = UnsupportedMessageImpl(data).takeIf { it.struct.isNotEmpty() } ?: return
|
||||||
|
collect(struct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Encoder : MessageEncoder<UnsupportedMessageImpl> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: UnsupportedMessageImpl) {
|
||||||
|
collect(data.structElem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.*
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.HummerCommelem
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
|
||||||
|
import net.mamoe.mirai.message.data.PlainText
|
||||||
|
import net.mamoe.mirai.message.data.VipFace
|
||||||
|
|
||||||
|
internal class VipFaceProtocol : MessageProtocol() {
|
||||||
|
override fun ProcessorCollector.collectProcessorsImpl() {
|
||||||
|
add(Encoder())
|
||||||
|
add(Decoder())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Encoder : MessageEncoder<VipFace> {
|
||||||
|
override suspend fun MessageEncoderContext.process(data: VipFace) {
|
||||||
|
processAlso(PlainText(data.contentToString()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Decoder : MessageDecoder {
|
||||||
|
override suspend fun MessageDecoderContext.process(data: ImMsgBody.Elem) {
|
||||||
|
if (data.commonElem == null) return
|
||||||
|
if (data.commonElem.serviceType != 23) return
|
||||||
|
|
||||||
|
val proto =
|
||||||
|
data.commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype23.serializer())
|
||||||
|
collect(VipFace(VipFace.Kind(proto.faceType, proto.faceSummary), proto.faceBubbleCount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,6 @@ package net.mamoe.mirai.internal.message.visitor
|
|||||||
import net.mamoe.mirai.internal.message.data.ForwardMessageInternal
|
import net.mamoe.mirai.internal.message.data.ForwardMessageInternal
|
||||||
import net.mamoe.mirai.internal.message.data.LongMessageInternal
|
import net.mamoe.mirai.internal.message.data.LongMessageInternal
|
||||||
import net.mamoe.mirai.internal.message.data.MarketFaceImpl
|
import net.mamoe.mirai.internal.message.data.MarketFaceImpl
|
||||||
import net.mamoe.mirai.internal.message.data.MarketFaceInternal
|
|
||||||
import net.mamoe.mirai.internal.message.flags.*
|
import net.mamoe.mirai.internal.message.flags.*
|
||||||
import net.mamoe.mirai.internal.message.source.MessageSourceInternal
|
import net.mamoe.mirai.internal.message.source.MessageSourceInternal
|
||||||
import net.mamoe.mirai.message.data.MessageSource
|
import net.mamoe.mirai.message.data.MessageSource
|
||||||
@ -38,10 +37,6 @@ internal interface MessageVisitorEx<in D, out R> : MessageVisitor<D, R> {
|
|||||||
return visitAbstractServiceMessage(message, data)
|
return visitAbstractServiceMessage(message, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun visitMarketFaceInternal(message: MarketFaceInternal, data: D): R {
|
|
||||||
return visitMarketFace(message, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun visitMarketFaceImpl(message: MarketFaceImpl, data: D): R {
|
fun visitMarketFaceImpl(message: MarketFaceImpl, data: D): R {
|
||||||
return visitMarketFace(message, data)
|
return visitMarketFace(message, data)
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import java.io.Closeable
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
internal interface Processor<C : ProcessorPipelineContext<D, *>, D> {
|
internal interface Processor<C : ProcessorPipelineContext<D, *>, D> : PipelineConsumptionMarker {
|
||||||
suspend fun process(context: C, data: D)
|
suspend fun process(context: C, data: D)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +46,8 @@ internal value class MutableProcessResult<R>(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
internal interface PipelineConsumptionMarker
|
||||||
|
|
||||||
internal interface ProcessorPipelineContext<D, R> {
|
internal interface ProcessorPipelineContext<D, R> {
|
||||||
val attributes: TypeSafeMap
|
val attributes: TypeSafeMap
|
||||||
|
|
||||||
@ -79,13 +81,13 @@ internal interface ProcessorPipelineContext<D, R> {
|
|||||||
* and throws a [contextualBugReportException] or logs something.
|
* and throws a [contextualBugReportException] or logs something.
|
||||||
*/
|
*/
|
||||||
@ConsumptionMarker
|
@ConsumptionMarker
|
||||||
fun Processor<*, D>.markAsConsumed(marker: Any = this)
|
fun PipelineConsumptionMarker.markAsConsumed(marker: Any = this)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the input as not consumed, if it was marked by this [NoticeProcessor].
|
* Marks the input as not consumed, if it was marked by this [NoticeProcessor].
|
||||||
*/
|
*/
|
||||||
@ConsumptionMarker
|
@ConsumptionMarker
|
||||||
fun Processor<*, D>.markNotConsumed(marker: Any = this)
|
fun PipelineConsumptionMarker.markNotConsumed(marker: Any = this)
|
||||||
|
|
||||||
@DslMarker
|
@DslMarker
|
||||||
annotation class ConsumptionMarker // to give an explicit color.
|
annotation class ConsumptionMarker // to give an explicit color.
|
||||||
@ -106,12 +108,12 @@ internal abstract class AbstractProcessorPipelineContext<D, R>(
|
|||||||
private val consumers: Stack<Any> = Stack()
|
private val consumers: Stack<Any> = Stack()
|
||||||
|
|
||||||
override val isConsumed: Boolean get() = consumers.isNotEmpty()
|
override val isConsumed: Boolean get() = consumers.isNotEmpty()
|
||||||
override fun Processor<*, D>.markAsConsumed(marker: Any) {
|
override fun PipelineConsumptionMarker.markAsConsumed(marker: Any) {
|
||||||
traceLogging.info { "markAsConsumed: marker=$marker" }
|
traceLogging.info { "markAsConsumed: marker=$marker" }
|
||||||
consumers.push(marker)
|
consumers.push(marker)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun Processor<*, D>.markNotConsumed(marker: Any) {
|
override fun PipelineConsumptionMarker.markNotConsumed(marker: Any) {
|
||||||
if (consumers.peek() === marker) {
|
if (consumers.peek() === marker) {
|
||||||
consumers.pop()
|
consumers.pop()
|
||||||
traceLogging.info { "markNotConsumed: Y, marker=$marker" }
|
traceLogging.info { "markNotConsumed: Y, marker=$marker" }
|
||||||
|
1
mirai-core/src/commonMain/resources/emoji-pattern.regex
Normal file
1
mirai-core/src/commonMain/resources/emoji-pattern.regex
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import net.mamoe.mirai.internal.message.protocol.*
|
||||||
|
import net.mamoe.mirai.internal.network.framework.AbstractMockNetworkHandlerTest
|
||||||
|
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
|
||||||
|
import net.mamoe.mirai.internal.utils.structureToString
|
||||||
|
import net.mamoe.mirai.message.data.MessageChain
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import kotlin.test.asserter
|
||||||
|
|
||||||
|
internal abstract class AbstractMessageProtocolTest : AbstractMockNetworkHandlerTest() {
|
||||||
|
|
||||||
|
private var decoderLoggerEnabled = false
|
||||||
|
private var encoderLoggerEnabled = false
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun beforeEach() {
|
||||||
|
decoderLoggerEnabled = MessageDecoderPipelineImpl.defaultTraceLogging.isEnabled
|
||||||
|
MessageDecoderPipelineImpl.defaultTraceLogging.enable()
|
||||||
|
encoderLoggerEnabled = MessageEncoderPipelineImpl.defaultTraceLogging.isEnabled
|
||||||
|
MessageEncoderPipelineImpl.defaultTraceLogging.enable()
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun afterEach() {
|
||||||
|
if (!decoderLoggerEnabled) {
|
||||||
|
MessageDecoderPipelineImpl.defaultTraceLogging.disable()
|
||||||
|
}
|
||||||
|
if (!encoderLoggerEnabled) {
|
||||||
|
MessageEncoderPipelineImpl.defaultTraceLogging.disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun facadeOf(vararg protocols: MessageProtocol): MessageProtocolFacade {
|
||||||
|
return MessageProtocolFacadeImpl(protocols.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun doEncoderChecks(
|
||||||
|
expectedStruct: List<ImMsgBody.Elem>,
|
||||||
|
protocol: MessageProtocol,
|
||||||
|
encode: MessageProtocolFacade.() -> List<ImMsgBody.Elem>
|
||||||
|
) {
|
||||||
|
assertEquals(
|
||||||
|
expectedStruct,
|
||||||
|
facadeOf(protocol).encode(),
|
||||||
|
message = "Failed to check single Protocol"
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
expectedStruct,
|
||||||
|
MessageProtocolFacade.INSTANCE.encode(),
|
||||||
|
message = "Failed to check with all protocols"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||||
|
private fun <@kotlin.internal.OnlyInputTypes T> assertEquals(
|
||||||
|
expected: List<T>,
|
||||||
|
actual: List<T>,
|
||||||
|
message: String? = null
|
||||||
|
) {
|
||||||
|
if (expected.size == 1 && actual.size == 1) {
|
||||||
|
asserter.assertEquals(message, expected.single().structureToString(), actual.single().structureToString())
|
||||||
|
} else {
|
||||||
|
asserter.assertEquals(
|
||||||
|
message,
|
||||||
|
expected.joinToString { it.structureToString() },
|
||||||
|
actual.joinToString { it.structureToString() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun doDecoderChecks(
|
||||||
|
expectedChain: MessageChain,
|
||||||
|
protocol: MessageProtocol,
|
||||||
|
decode: MessageProtocolFacade.() -> MessageChain
|
||||||
|
) {
|
||||||
|
assertEquals(
|
||||||
|
expectedChain.toList(),
|
||||||
|
facadeOf(protocol).decode().toList(),
|
||||||
|
message = "Failed to check single Protocol"
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
expectedChain.toList(),
|
||||||
|
MessageProtocolFacade.INSTANCE.decode().toList(),
|
||||||
|
message = "Failed to check with all protocols"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun doEncoderChecks(
|
||||||
|
expectedStruct: ImMsgBody.Elem,
|
||||||
|
protocol: MessageProtocol,
|
||||||
|
encode: MessageProtocolFacade.() -> List<ImMsgBody.Elem>
|
||||||
|
): Unit = doEncoderChecks(mutableListOf(expectedStruct), protocol, encode)
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* 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.internal.message.protocol.impl
|
||||||
|
|
||||||
|
import net.mamoe.mirai.message.data.Face
|
||||||
|
import net.mamoe.mirai.message.data.MessageSourceKind
|
||||||
|
import net.mamoe.mirai.message.data.messageChainOf
|
||||||
|
import net.mamoe.mirai.utils.hexToBytes
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
internal class FaceProtocolTest : AbstractMessageProtocolTest() {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `can encode`() {
|
||||||
|
doEncoderChecks(
|
||||||
|
net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
|
||||||
|
face = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Face(
|
||||||
|
index = 1,
|
||||||
|
old = "14 42".hexToBytes(),
|
||||||
|
buf = "00 01 00 04 52 CC F5 D0".hexToBytes(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FaceProtocol()
|
||||||
|
) {
|
||||||
|
encode(
|
||||||
|
messageChainOf(Face(Face.PIE_ZUI)),
|
||||||
|
messageTarget = null, withGeneralFlags = true, isForward = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `can decode`() {
|
||||||
|
doDecoderChecks(
|
||||||
|
messageChainOf(Face(Face.YIN_XIAN)),
|
||||||
|
FaceProtocol()
|
||||||
|
) {
|
||||||
|
decode(
|
||||||
|
listOf(
|
||||||
|
net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Elem(
|
||||||
|
face = net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody.Face(
|
||||||
|
index = 108,
|
||||||
|
old = "14 AD".hexToBytes(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
groupIdOrZero = 0,
|
||||||
|
MessageSourceKind.GROUP,
|
||||||
|
bot,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user