Commit 52f85435 authored by Him188's avatar Him188

Support poke message, close #132

parent f7040c18
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.qqandroid.message
import kotlinx.io.core.buildPacket
import kotlinx.io.core.readBytes
import net.mamoe.mirai.message.data.At
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
internal fun At.toJceData(): ImMsgBody.Text {
val text = this.toString()
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()
)
}
internal val atAllData = ImMsgBody.Elem(
text = ImMsgBody.Text(
str = "@全体成员",
attr6Buf = buildPacket {
// MessageForText$AtTroopMemberInfo
writeShort(1) // const
writeShort(0) // startPos
writeShort("@全体成员".length.toShort()) // textLen
writeByte(1) // flag, may=1
writeInt(0) // uin
writeShort(0) // const
}.readBytes()
)
)
\ No newline at end of file
......@@ -11,108 +11,25 @@
package net.mamoe.mirai.qqandroid.message
import kotlinx.io.core.*
import kotlinx.io.core.discardExact
import kotlinx.io.core.readUInt
import kotlinx.io.core.toByteArray
import net.mamoe.mirai.LowLevelAPI
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.qqandroid.io.serialization.loadAs
import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.HummerCommelem
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgComm
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.io.encodeToString
import net.mamoe.mirai.utils.io.hexToBytes
import net.mamoe.mirai.utils.io.read
import net.mamoe.mirai.utils.io.toByteArray
internal fun At.toJceData(): ImMsgBody.Text {
val text = this.toString()
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()
)
}
internal fun OfflineFriendImage.toJceData(): ImMsgBody.NotOnlineImage {
return ImMsgBody.NotOnlineImage(
filePath = this.filepath,
resId = this.resourceId,
oldPicMd5 = false,
picMd5 = this.md5,
fileLen = this.fileLength,
picHeight = this.height,
picWidth = this.width,
bizType = this.bizType,
imgType = this.imageType,
downloadPath = this.downloadPath,
original = this.original,
fileId = this.fileId,
pbReserve = byteArrayOf(0x78, 0x02)
)
}
internal 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 OfflineGroupImage.toJceData(): ImMsgBody.CustomFace {
return ImMsgBody.CustomFace(
filePath = this.filepath,
fileId = this.fileId,
serverIp = this.serverIp,
serverPort = this.serverPort,
fileType = this.fileType,
signature = this.signature,
useful = this.useful,
md5 = this.md5,
bizType = this.bizType,
imageType = this.imageType,
width = this.width,
height = this.height,
source = this.source,
size = this.size,
origin = this.original,
pbReserve = this.pbReserve,
flag = ByteArray(4),
//_400Height = 235,
//_400Url = "/gchatpic_new/1040400290/1041235568-2195821338-01E9451B70EDEAE3B37C101F1EEBF5B5/400?term=2",
//_400Width = 351,
oldData = oldData
)
}
private val oldData: ByteArray =
"15 36 20 39 32 6B 41 31 00 38 37 32 66 30 36 36 30 33 61 65 31 30 33 62 37 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 30 31 45 39 34 35 31 42 2D 37 30 45 44 2D 45 41 45 33 2D 42 33 37 43 2D 31 30 31 46 31 45 45 42 46 35 42 35 7D 2E 70 6E 67 41".hexToBytes()
private val atAllData = ImMsgBody.Elem(
text = ImMsgBody.Text(
str = "@全体成员",
attr6Buf = buildPacket {
// MessageForText$AtTroopMemberInfo
writeShort(1) // const
writeShort(0) // startPos
writeShort("@全体成员".length.toShort()) // textLen
writeByte(1) // flag, may=1
writeInt(0) // uin
writeShort(0) // const
}.readBytes()
)
)
private val UNSUPPORTED_MERGED_MESSAGE_PLAIN = PlainText("你的QQ暂不支持查看[转发多条消息],请期待后续版本。")
private val UNSUPPORTED_POKE_MESSAGE_PLAIN = PlainText("[戳一戳]请使用最新版手机QQ体验新功能。")
@OptIn(MiraiInternalAPI::class, MiraiExperimentalAPI::class)
internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: Boolean): MutableList<ImMsgBody.Elem> {
......@@ -175,6 +92,21 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: B
elements.add(ImMsgBody.Elem(text = it.toJceData()))
elements.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = " ")))
}
is PokeMessage -> {
elements.add(
ImMsgBody.Elem(
commonElem = ImMsgBody.CommonElem(
serviceType = 2,
businessType = it.type,
pbElem = HummerCommelem.MsgElemInfoServtype2(
pokeType = it.type,
vaspokeId = it.id
).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))
......@@ -231,65 +163,6 @@ 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 val PB_RESERVE_FOR_ELSE = "78 00 F8 01 00 C8 02 00".hexToBytes()
internal class OnlineGroupImageImpl(
internal val delegate: ImMsgBody.CustomFace
) : OnlineGroupImage() {
override val filepath: String = delegate.filePath
override val fileId: Int get() = delegate.fileId
override val serverIp: Int get() = delegate.serverIp
override val serverPort: Int get() = delegate.serverPort
override val fileType: Int get() = delegate.fileType
override val signature: ByteArray get() = delegate.signature
override val useful: Int get() = delegate.useful
override val md5: ByteArray get() = delegate.md5
override val bizType: Int get() = delegate.bizType
override val imageType: Int get() = delegate.imageType
override val width: Int get() = delegate.width
override val height: Int get() = delegate.height
override val source: Int get() = delegate.source
override val size: Int get() = delegate.size
override val original: Int get() = delegate.origin
override val pbReserve: ByteArray get() = delegate.pbReserve
override val imageId: String = ExternalImage.generateImageId(delegate.md5, imageType)
override val originUrl: String
get() = "http://gchat.qpic.cn" + delegate.origUrl
override fun equals(other: Any?): Boolean {
return other is OnlineGroupImageImpl && other.filepath == this.filepath && other.md5.contentEquals(this.md5)
}
override fun hashCode(): Int {
return imageId.hashCode() + 31 * md5.hashCode()
}
}
internal class OnlineFriendImageImpl(
internal val delegate: ImMsgBody.NotOnlineImage
) : OnlineFriendImage() {
override val resourceId: String get() = delegate.resId
override val md5: ByteArray get() = delegate.picMd5
override val filepath: String get() = delegate.filePath
override val fileLength: Int get() = delegate.fileLen
override val height: Int get() = delegate.picHeight
override val width: Int get() = delegate.picWidth
override val bizType: Int get() = delegate.bizType
override val imageType: Int get() = delegate.imgType
override val downloadPath: String get() = delegate.downloadPath
override val fileId: Int get() = delegate.fileId
override val original: Int get() = delegate.original
override val originUrl: String
get() = "http://c2cpicdw.qpic.cn" + this.delegate.origUrl
override fun equals(other: Any?): Boolean {
return other is OnlineFriendImageImpl && other.resourceId == this.resourceId && other.md5
.contentEquals(this.md5)
}
override fun hashCode(): Int {
return imageId.hashCode() + 31 * md5.hashCode()
}
}
@OptIn(ExperimentalUnsignedTypes::class, MiraiInternalAPI::class)
internal fun MsgComm.Msg.toMessageChain(): MessageChain {
val elements = this.msgBody.richText.elems
......@@ -337,6 +210,7 @@ internal inline fun <reified R> Iterable<*>.firstIsInstance(): R {
throw NoSuchElementException("Collection contains no element matching the predicate.")
}
@OptIn(MiraiInternalAPI::class, LowLevelAPI::class)
internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilder) {
this.forEach {
when {
......@@ -383,6 +257,22 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilde
}
}
}
it.elemFlags2 != null
|| it.extraInfo != null
|| it.generalFlags != null -> {
}
it.commonElem != null -> {
when (it.commonElem.serviceType) {
2 -> {
val proto = it.commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype2.serializer())
message.add(PokeMessage(proto.pokeType, proto.vaspokeId))
}
}
}
else -> {
println(it._miraiContentToString())
}
}
}
......
package net.mamoe.mirai.qqandroid.message
import net.mamoe.mirai.message.data.Face
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.utils.io.hexToBytes
import net.mamoe.mirai.utils.io.toByteArray
internal 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
)
}
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.qqandroid.message
import net.mamoe.mirai.message.data.OfflineFriendImage
import net.mamoe.mirai.message.data.OfflineGroupImage
import net.mamoe.mirai.message.data.OnlineFriendImage
import net.mamoe.mirai.message.data.OnlineGroupImage
import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
import net.mamoe.mirai.utils.ExternalImage
import net.mamoe.mirai.utils.io.hexToBytes
internal class OnlineGroupImageImpl(
internal val delegate: ImMsgBody.CustomFace
) : OnlineGroupImage() {
override val filepath: String = delegate.filePath
override val fileId: Int get() = delegate.fileId
override val serverIp: Int get() = delegate.serverIp
override val serverPort: Int get() = delegate.serverPort
override val fileType: Int get() = delegate.fileType
override val signature: ByteArray get() = delegate.signature
override val useful: Int get() = delegate.useful
override val md5: ByteArray get() = delegate.md5
override val bizType: Int get() = delegate.bizType
override val imageType: Int get() = delegate.imageType
override val width: Int get() = delegate.width
override val height: Int get() = delegate.height
override val source: Int get() = delegate.source
override val size: Int get() = delegate.size
override val original: Int get() = delegate.origin
override val pbReserve: ByteArray get() = delegate.pbReserve
override val imageId: String = ExternalImage.generateImageId(delegate.md5, imageType)
override val originUrl: String
get() = "http://gchat.qpic.cn" + delegate.origUrl
override fun equals(other: Any?): Boolean {
return other is OnlineGroupImageImpl && other.filepath == this.filepath && other.md5.contentEquals(this.md5)
}
override fun hashCode(): Int {
return imageId.hashCode() + 31 * md5.hashCode()
}
}
internal class OnlineFriendImageImpl(
internal val delegate: ImMsgBody.NotOnlineImage
) : OnlineFriendImage() {
override val resourceId: String get() = delegate.resId
override val md5: ByteArray get() = delegate.picMd5
override val filepath: String get() = delegate.filePath
override val fileLength: Int get() = delegate.fileLen
override val height: Int get() = delegate.picHeight
override val width: Int get() = delegate.picWidth
override val bizType: Int get() = delegate.bizType
override val imageType: Int get() = delegate.imgType
override val downloadPath: String get() = delegate.downloadPath
override val fileId: Int get() = delegate.fileId
override val original: Int get() = delegate.original
override val originUrl: String
get() = "http://c2cpicdw.qpic.cn" + this.delegate.origUrl
override fun equals(other: Any?): Boolean {
return other is OnlineFriendImageImpl && other.resourceId == this.resourceId && other.md5
.contentEquals(this.md5)
}
override fun hashCode(): Int {
return imageId.hashCode() + 31 * md5.hashCode()
}
}
internal fun OfflineGroupImage.toJceData(): ImMsgBody.CustomFace {
return ImMsgBody.CustomFace(
filePath = this.filepath,
fileId = this.fileId,
serverIp = this.serverIp,
serverPort = this.serverPort,
fileType = this.fileType,
signature = this.signature,
useful = this.useful,
md5 = this.md5,
bizType = this.bizType,
imageType = this.imageType,
width = this.width,
height = this.height,
source = this.source,
size = this.size,
origin = this.original,
pbReserve = this.pbReserve,
flag = ByteArray(4),
//_400Height = 235,
//_400Url = "/gchatpic_new/1040400290/1041235568-2195821338-01E9451B70EDEAE3B37C101F1EEBF5B5/400?term=2",
//_400Width = 351,
oldData = oldData
)
}
private val oldData: ByteArray =
"15 36 20 39 32 6B 41 31 00 38 37 32 66 30 36 36 30 33 61 65 31 30 33 62 37 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 30 31 45 39 34 35 31 42 2D 37 30 45 44 2D 45 41 45 33 2D 42 33 37 43 2D 31 30 31 46 31 45 45 42 46 35 42 35 7D 2E 70 6E 67 41".hexToBytes()
internal fun OfflineFriendImage.toJceData(): ImMsgBody.NotOnlineImage {
return ImMsgBody.NotOnlineImage(
filePath = this.filepath,
resId = this.resourceId,
oldPicMd5 = false,
picMd5 = this.md5,
fileLen = this.fileLength,
picHeight = this.height,
picWidth = this.width,
bizType = this.bizType,
imgType = this.imageType,
downloadPath = this.downloadPath,
original = this.original,
fileId = this.fileId,
pbReserve = byteArrayOf(0x78, 0x02)
)
}
\ No newline at end of file
......@@ -223,9 +223,7 @@ internal class MessageSvc {
friend.lastMessageSequence.loop { instant ->
if (msg.msgHead.msgSeq > instant) {
println("bigger")
if (friend.lastMessageSequence.compareAndSet(instant, msg.msgHead.msgSeq)) {
println("set ok")
return@mapNotNull FriendMessage(
friend,
msg.toMessageChain()
......
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.message.data
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.SinceMirai
import kotlin.jvm.JvmField
@SinceMirai("0.31.0")
@MiraiExperimentalAPI
sealed class HummerMessage : MessageContent {
companion object Key : Message.Key<HummerMessage>
}
/**
* 戳一戳
*/
@MiraiExperimentalAPI
@SinceMirai("0.31.0")
@OptIn(MiraiInternalAPI::class)
class PokeMessage @MiraiInternalAPI(message = "使用伴生对象中的常量") constructor(
val type: Int,
@MiraiExperimentalAPI
val id: Int
) : HummerMessage() {
companion object Types : Message.Key<PokeMessage> {
/**
* 戳一戳
*/
@JvmField
val Poke = PokeMessage(1, -1)
/**
* 比心
*/
@JvmField
val ShowLove = PokeMessage(2, -1)
/**
* 点赞
*/
@JvmField
val Like = PokeMessage(3, -1)
/**
* 心碎
*/
@JvmField
val Heartbroken = PokeMessage(4, -1)
/**
* 666
*/
@JvmField
val SixSixSix = PokeMessage(5, -1)
/**
* 放大招
*/
@JvmField
val FangDaZhao = PokeMessage(6, -1)
}
private val stringValue = "[mirai:Poke($type, $id)]"
override fun toString(): String = stringValue
override val length: Int get() = stringValue.length
override fun get(index: Int): Char = stringValue[index]
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence =
stringValue.subSequence(startIndex, endIndex)
override fun compareTo(other: String): Int = stringValue.compareTo(other)
//businessType=0x00000001(1)
//pbElem=08 01 18 00 20 FF FF FF FF 0F 2A 00 32 00 38 00 50 00
//serviceType=0x00000002(2)
}
\ No newline at end of file
......@@ -15,6 +15,7 @@ package net.mamoe.mirai.message.data
import net.mamoe.mirai.message.data.NullMessageChain.equals
import net.mamoe.mirai.message.data.NullMessageChain.toString
import net.mamoe.mirai.utils.MiraiExperimentalAPI
import net.mamoe.mirai.utils.MiraiInternalAPI
import net.mamoe.mirai.utils.SinceMirai
import kotlin.js.JsName
......@@ -44,6 +45,10 @@ import kotlin.reflect.KProperty
*/
interface MessageChain : Message, Iterable<SingleMessage> {
override operator fun contains(sub: String): Boolean
/**
* 得到易读的字符串
*/
override fun toString(): String
/**
......@@ -127,25 +132,27 @@ inline fun <reified M : Message> MessageChain.any(): Boolean = this.any { it is
/**
* 获取第一个 [M] 类型的 [Message] 实例
*/
@OptIn(ExperimentalMessageSource::class)
@OptIn(ExperimentalMessageSource::class, MiraiExperimentalAPI::class)
@JvmSynthetic
@Suppress("UNCHECKED_CAST")
fun <M : Message> MessageChain.firstOrNull(key: Message.Key<M>): M? = when (key) {
At -> first<At>()
AtAll -> first<AtAll>()
PlainText -> first<PlainText>()
Image -> first<Image>()
OnlineImage -> first<OnlineImage>()
OfflineImage -> first<OfflineImage>()
GroupImage -> first<GroupImage>()
FriendImage -> first<FriendImage>()
Face -> first<Face>()
QuoteReply -> first<QuoteReply>()
MessageSource -> first<MessageSource>()
XmlMessage -> first<XmlMessage>()
JsonMessage -> first<JsonMessage>()
RichMessage -> first<RichMessage>()
LightApp -> first<LightApp>()
At -> firstOrNull<At>()
AtAll -> firstOrNull<AtAll>()
PlainText -> firstOrNull<PlainText>()
Image -> firstOrNull<Image>()
OnlineImage -> firstOrNull<OnlineImage>()
OfflineImage -> firstOrNull<OfflineImage>()
GroupImage -> firstOrNull<GroupImage>()
FriendImage -> firstOrNull<FriendImage>()
Face -> firstOrNull<Face>()
QuoteReply -> firstOrNull<QuoteReply>()
MessageSource -> firstOrNull<MessageSource>()
XmlMessage -> firstOrNull<XmlMessage>()
JsonMessage -> firstOrNull<JsonMessage>()
RichMessage -> firstOrNull<RichMessage>()
LightApp -> firstOrNull<LightApp>()
PokeMessage -> firstOrNull<PokeMessage>()
HummerMessage -> firstOrNull<HummerMessage>()
else -> null
} as M?
......
......@@ -163,6 +163,13 @@ class LongMessage(override val content: String, val resId: String) : RichMessage
override fun toString(): String = content
}
/*
commonElem=CommonElem#750141174 {
businessType=0x00000001(1)
pbElem=08 01 18 00 20 FF FF FF FF 0F 2A 00 32 00 38 00 50 00
serviceType=0x00000002(2)
}
*/
/**
* 构造一条 XML 消息
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment