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