MessageProtocol implementations

This commit is contained in:
Him188 2022-04-28 13:51:12 +01:00
parent d6343870b8
commit c47779c726
30 changed files with 1741 additions and 850 deletions

File diff suppressed because one or more lines are too long

View File

@ -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(

View File

@ -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
} }

View File

@ -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()
)
)
}

View File

@ -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
)
}

View File

@ -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())
)
)
}
}

View File

@ -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()

View File

@ -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)
} }

View File

@ -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
} }
} }
} }

View File

@ -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)
}
}
)
}
}
}

View File

@ -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))
}
}
}

View File

@ -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
)
)
}
}
}
}
}
}

View File

@ -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())
)
)
}
}
}
}
}

View File

@ -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) {
}
}
}

View File

@ -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()
}
}
}
}
}

View File

@ -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)
)
}
}
}
}

View File

@ -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
)
)
}
}
}

View File

@ -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) {
}
}
}

View File

@ -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
)
)
}
}
}

View File

@ -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()
}
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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))
}
}
}
}
}

View File

@ -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()
)
)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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))
}
}
}

View File

@ -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)
} }

View File

@ -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" }

File diff suppressed because one or more lines are too long

View File

@ -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)
}

View File

@ -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,
)
}
}
}