Support sending group image to friend, #307

This commit is contained in:
Him188 2021-01-05 22:19:30 +08:00
parent 54add6df78
commit ac03d405f9
4 changed files with 160 additions and 64 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
* Copyright 2019-2021 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.
@ -45,7 +45,7 @@ internal fun FlashImage.toJceData(): ImMsgBody.Elem {
pbElem = HummerCommelem.MsgElemInfoServtype3(
flashTroopPic = ImMsgBody.CustomFace(
filePath = image.imageId,
md5 = image.md5,
picMd5 = image.md5,
pbReserve = byteArrayOf(0x78, 0x06)
)
).toByteArray(HummerCommelem.MsgElemInfoServtype3.serializer())

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
* Copyright 2019-2021 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.
@ -21,6 +21,7 @@ import net.mamoe.mirai.LowLevelApi
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.network.protocol.data.proto.*
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.internal.utils.*
@ -56,15 +57,15 @@ internal fun MessageChain.toRichTextElems(
var longTextResId: String? = null
fun transformOneMessage(it: Message) {
if (it is RichMessage) {
val content = it.content.toByteArray().zip()
when (it) {
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 = it.serviceId, // ok
serviceId = currentMessage.serviceId, // ok
template1 = byteArrayOf(1) + content
)
)
@ -76,13 +77,13 @@ internal fun MessageChain.toRichTextElems(
elements.add(
ImMsgBody.Elem(
richMsg = ImMsgBody.RichMsg(
serviceId = it.serviceId, // ok
serviceId = currentMessage.serviceId, // ok
template1 = byteArrayOf(1) + content
)
)
)
transformOneMessage(UNSUPPORTED_MERGED_MESSAGE_PLAIN)
longTextResId = it.resId
longTextResId = currentMessage.resId
}
is LightApp -> elements.add(
ImMsgBody.Elem(
@ -94,9 +95,9 @@ internal fun MessageChain.toRichTextElems(
else -> elements.add(
ImMsgBody.Elem(
richMsg = ImMsgBody.RichMsg(
serviceId = when (it) {
is ServiceMessage -> it.serviceId
else -> error("unsupported RichMessage: ${it::class.simpleName}")
serviceId = when (currentMessage) {
is ServiceMessage -> currentMessage.serviceId
else -> error("unsupported RichMessage: ${currentMessage::class.simpleName}")
},
template1 = byteArrayOf(1) + content
)
@ -105,21 +106,24 @@ internal fun MessageChain.toRichTextElems(
}
}
when (it) {
is PlainText -> elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = it.content)))
when (currentMessage) {
is PlainText -> elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = currentMessage.content)))
is CustomMessage -> {
@Suppress("UNCHECKED_CAST")
elements.add(
ImMsgBody.Elem(
customElem = ImMsgBody.CustomElem(
enumType = MIRAI_CUSTOM_ELEM_TYPE,
data = CustomMessage.dump(it.getFactory() as CustomMessage.Factory<CustomMessage>, it)
data = CustomMessage.dump(
currentMessage.getFactory() as CustomMessage.Factory<CustomMessage>,
currentMessage
)
)
)
)
}
is At -> {
elements.add(ImMsgBody.Elem(text = it.toJceData(messageTarget.safeCast())))
elements.add(ImMsgBody.Elem(text = currentMessage.toJceData(messageTarget.safeCast())))
// elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " ")))
// removed by https://github.com/mamoe/mirai/issues/524
// 发送 QuoteReply 消息时无可避免的产生多余空格 #524
@ -129,34 +133,63 @@ internal fun MessageChain.toRichTextElems(
ImMsgBody.Elem(
commonElem = ImMsgBody.CommonElem(
serviceType = 2,
businessType = it.pokeType,
businessType = currentMessage.pokeType,
pbElem = HummerCommelem.MsgElemInfoServtype2(
pokeType = it.pokeType,
vaspokeId = it.id,
pokeType = currentMessage.pokeType,
vaspokeId = currentMessage.id,
vaspokeMinver = "7.2.0",
vaspokeName = it.name
vaspokeName = currentMessage.name
).toByteArray(HummerCommelem.MsgElemInfoServtype2.serializer())
)
)
)
transformOneMessage(UNSUPPORTED_POKE_MESSAGE_PLAIN)
}
is OfflineGroupImage -> elements.add(ImMsgBody.Elem(customFace = it.toJceData()))
is OnlineGroupImageImpl -> elements.add(ImMsgBody.Elem(customFace = it.delegate))
is OnlineFriendImageImpl -> elements.add(ImMsgBody.Elem(notOnlineImage = it.delegate))
is OfflineFriendImage -> elements.add(ImMsgBody.Elem(notOnlineImage = it.toJceData()))
is FlashImage -> elements.add(it.toJceData()).also { transformOneMessage(UNSUPPORTED_FLASH_MESSAGE_PLAIN) }
is OfflineGroupImage -> {
if (messageTarget is User) {
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.toJceData().toNotOnlineImage()))
} else {
elements.add(ImMsgBody.Elem(customFace = currentMessage.toJceData()))
}
}
is OnlineGroupImageImpl -> {
if (messageTarget is User) {
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.delegate.toNotOnlineImage()))
} else {
elements.add(ImMsgBody.Elem(customFace = currentMessage.delegate))
}
}
is OnlineFriendImageImpl -> {
if (messageTarget is User) {
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.delegate))
} else {
elements.add(ImMsgBody.Elem(customFace = currentMessage.delegate.toCustomFace()))
}
}
is OfflineFriendImage -> {
if (messageTarget is User) {
elements.add(ImMsgBody.Elem(notOnlineImage = currentMessage.toJceData()))
} else {
elements.add(ImMsgBody.Elem(customFace = currentMessage.toJceData().toCustomFace()))
}
}
is FlashImage -> elements.add(currentMessage.toJceData())
.also { transformOneMessage(UNSUPPORTED_FLASH_MESSAGE_PLAIN) }
is AtAll -> elements.add(atAllData)
is Face -> elements.add(
if (it.id >= 260) {
ImMsgBody.Elem(commonElem = it.toCommData())
if (currentMessage.id >= 260) {
ImMsgBody.Elem(commonElem = currentMessage.toCommData())
} else {
ImMsgBody.Elem(face = it.toJceData())
ImMsgBody.Elem(face = currentMessage.toJceData())
}
)
is QuoteReply -> {
if (forGroup) {
when (val source = it.source) {
when (val source = currentMessage.source) {
is OnlineMessageSource.Incoming.FromGroup -> {
val sender0 = source.sender
if (sender0 !is AnonymousMember)
@ -170,9 +203,9 @@ internal fun MessageChain.toRichTextElems(
}
//MarketFaceImpl继承于MarketFace 会自动添加兼容信息
//如果有用户不慎/强行使用也会转换为文本信息
is MarketFaceImpl -> elements.add(ImMsgBody.Elem(marketFace = it.delegate))
is MarketFace -> transformOneMessage(PlainText(it.contentToString()))
is VipFace -> transformOneMessage(PlainText(it.contentToString()))
is MarketFaceImpl -> elements.add(ImMsgBody.Elem(marketFace = currentMessage.delegate))
is MarketFace -> transformOneMessage(PlainText(currentMessage.contentToString()))
is VipFace -> transformOneMessage(PlainText(currentMessage.contentToString()))
is PttMessage -> {
elements.add(
ImMsgBody.Elem(
@ -193,7 +226,7 @@ internal fun MessageChain.toRichTextElems(
-> {
}
else -> error("unsupported message type: ${it::class.simpleName}")
else -> error("unsupported message type: ${currentMessage::class.simpleName}")
}
}
this.forEach(::transformOneMessage)

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
* Copyright 2019-2021 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.
@ -15,6 +15,7 @@ import kotlinx.serialization.Serializable
import net.mamoe.mirai.Bot
import net.mamoe.mirai.IMirai
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Contact.Companion.uploadImage
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.message.data.FriendImage
@ -24,32 +25,19 @@ import net.mamoe.mirai.message.data.Image.Key.FRIEND_IMAGE_ID_REGEX_1
import net.mamoe.mirai.message.data.Image.Key.FRIEND_IMAGE_ID_REGEX_2
import net.mamoe.mirai.message.data.Image.Key.GROUP_IMAGE_ID_REGEX
import net.mamoe.mirai.message.data.md5
import net.mamoe.mirai.utils.ExternalResource
import net.mamoe.mirai.utils.MiraiExperimentalApi
import net.mamoe.mirai.utils.generateImageId
import net.mamoe.mirai.utils.hexToBytes
/*
* ImgType:
* JPG: 1000
* PNG: 1001
* WEBP: 1002
* BMP: 1005
* GIG: 2000 // gig? gif?
* APNG: 2001
* SHARPP: 1004
*/
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.ExternalResource.Companion.DEFAULT_FORMAT_NAME
internal class OnlineGroupImageImpl(
internal val delegate: ImMsgBody.CustomFace
) : @Suppress("DEPRECATION")
OnlineGroupImage() {
override val imageId: String = generateImageId(
delegate.md5,
delegate.picMd5,
delegate.filePath.substringAfterLast('.')
).takeIf {
GROUP_IMAGE_ID_REGEX.matches(it)
} ?: generateImageId(delegate.md5)
} ?: generateImageId(delegate.picMd5)
override val originUrl: String
get() = if (delegate.origUrl.isBlank()) {
@ -90,11 +78,79 @@ OnlineFriendImage() {
}
}
/*
* ImgType:
* JPG: 1000
* PNG: 1001
* WEBP: 1002
* BMP: 1005
* GIG: 2000 // gig? gif?
* APNG: 2001
* SHARPP: 1004
*/
internal fun getImageType(id: Int): String {
return when (id) {
1000 -> "jpg"
1001 -> "png"
1002 -> "webp"
1005 -> "bmp"
2000 -> "gif"
2001 -> "png"
else -> DEFAULT_FORMAT_NAME
}
}
internal fun ImMsgBody.NotOnlineImage.toCustomFace(): ImMsgBody.CustomFace {
return ImMsgBody.CustomFace(
filePath = generateImageId(picMd5, getImageType(imgType)),
picMd5 = picMd5,
flag = ByteArray(4),
bigUrl = bigUrl,
origUrl = origUrl,
//_400Height = 235,
//_400Url = "/gchatpic_new/1040400290/1041235568-2195821338-01E9451B70EDEAE3B37C101F1EEBF5B5/400?term=2",
//_400Width = 351,
oldData = this.oldVerSendFile
)
}
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("")}"
}
internal fun ImMsgBody.CustomFace.toNotOnlineImage(): ImMsgBody.NotOnlineImage {
val resId = calculateResId()
return ImMsgBody.NotOnlineImage(
filePath = filePath,
resId = resId,
oldPicMd5 = false,
picMd5 = picMd5,
downloadPath = resId,
original = 1,
pbReserve = byteArrayOf(0x78, 0x02),
)
}
@Suppress("DEPRECATION")
internal fun OfflineGroupImage.toJceData(): ImMsgBody.CustomFace {
return ImMsgBody.CustomFace(
filePath = this.imageId,
md5 = this.md5,
picMd5 = this.md5,
flag = ByteArray(4),
//_400Height = 235,
//_400Url = "/gchatpic_new/1040400290/1041235568-2195821338-01E9451B70EDEAE3B37C101F1EEBF5B5/400?term=2",

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
* Copyright 2019-2021 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.
@ -270,10 +270,10 @@ internal class ImMsgBody : ProtoBuf {
@ProtoNumber(10) @JvmField val fileType: Int = 0,
@ProtoNumber(11) @JvmField val signature: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(12) @JvmField val useful: Int = 0,
@ProtoNumber(13) @JvmField val md5: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(14) @JvmField val thumbUrl: String = "",
@ProtoNumber(13) override val picMd5: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(14) override val thumbUrl: String = "",
@ProtoNumber(15) @JvmField val bigUrl: String = "",
@ProtoNumber(16) @JvmField val origUrl: String = "",
@ProtoNumber(16) override val origUrl: String = "",
@ProtoNumber(17) @JvmField val bizType: Int = 0,
@ProtoNumber(18) @JvmField val repeatIndex: Int = 0,
@ProtoNumber(19) @JvmField val repeatImage: Int = 0,
@ -288,11 +288,11 @@ internal class ImMsgBody : ProtoBuf {
@ProtoNumber(28) @JvmField val thumbHeight: Int = 0,
@ProtoNumber(29) @JvmField val showLen: Int = 0,
@ProtoNumber(30) @JvmField val downloadLen: Int = 0,
@ProtoNumber(31) @JvmField val _400Url: String = "",
@ProtoNumber(31) override val _400Url: String = "",
@ProtoNumber(32) @JvmField val _400Width: Int = 0,
@ProtoNumber(33) @JvmField val _400Height: Int = 0,
@ProtoNumber(34) @JvmField val pbReserve: ByteArray = EMPTY_BYTE_ARRAY
) : ProtoBuf
) : ProtoBuf, NotOnlineImageOrCustomFace
@Serializable
internal class DeliverGiftMsg(
@ -671,6 +671,13 @@ internal class ImMsgBody : ProtoBuf {
@ProtoNumber(56) @JvmField val pbReserve: ByteArray = EMPTY_BYTE_ARRAY
) : ProtoBuf
interface NotOnlineImageOrCustomFace {
val thumbUrl: String
val origUrl: String
val _400Url: String
val picMd5: ByteArray
}
@Serializable
internal class NotOnlineImage(
@ProtoNumber(1) @JvmField val filePath: String = "",
@ -679,15 +686,15 @@ internal class ImMsgBody : ProtoBuf {
@ProtoNumber(4) @JvmField val oldVerSendFile: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(5) @JvmField val imgType: Int = 0,
@ProtoNumber(6) @JvmField val previewsImage: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(7) @JvmField val picMd5: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(7) override val picMd5: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(8) @JvmField val picHeight: Int = 0,
@ProtoNumber(9) @JvmField val picWidth: Int = 0,
@ProtoNumber(10) @JvmField val resId: String = "",
@ProtoNumber(11) @JvmField val flag: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(12) @JvmField val thumbUrl: String = "",
@ProtoNumber(12) override val thumbUrl: String = "",
@ProtoNumber(13) @JvmField val original: Int = 0,
@ProtoNumber(14) @JvmField val bigUrl: String = "",
@ProtoNumber(15) @JvmField val origUrl: String = "",
@ProtoNumber(15) override val origUrl: String = "",
@ProtoNumber(16) @JvmField val bizType: Int = 0,
@ProtoNumber(17) @JvmField val result: Int = 0,
@ProtoNumber(18) @JvmField val index: Int = 0,
@ -698,11 +705,11 @@ internal class ImMsgBody : ProtoBuf {
@ProtoNumber(23) @JvmField val fileId: Int = 0,
@ProtoNumber(24) @JvmField val showLen: Int = 0,
@ProtoNumber(25) @JvmField val downloadLen: Int = 0,
@ProtoNumber(26) @JvmField val _400Url: String = "",
@ProtoNumber(26) override val _400Url: String = "",
@ProtoNumber(27) @JvmField val _400Width: Int = 0,
@ProtoNumber(28) @JvmField val _400Height: Int = 0,
@ProtoNumber(29) @JvmField val pbReserve: ByteArray = EMPTY_BYTE_ARRAY
) : ProtoBuf
) : ProtoBuf, NotOnlineImageOrCustomFace
@Serializable // 非官方.
internal class PbReserve(