mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 13:12:17 +08:00
457 lines
18 KiB
Kotlin
457 lines
18 KiB
Kotlin
package qq.service.msg
|
||
|
||
import com.tencent.mobileqq.qroute.QRoute
|
||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
||
import com.tencent.qqnt.msg.api.IMsgService
|
||
import io.kritor.common.*
|
||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||
import kotlinx.coroutines.withTimeoutOrNull
|
||
import moe.fuqiuluo.shamrock.helper.ActionMsgException
|
||
import moe.fuqiuluo.shamrock.helper.Level
|
||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||
import moe.fuqiuluo.shamrock.helper.db.ImageDB
|
||
import moe.fuqiuluo.shamrock.helper.db.ImageMapping
|
||
import moe.fuqiuluo.shamrock.tools.asJsonArray
|
||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||
import moe.fuqiuluo.shamrock.tools.asString
|
||
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
|
||
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
||
import moe.fuqiuluo.shamrock.tools.toHexString
|
||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||
import moe.fuqiuluo.shamrock.utils.PlatformUtils.QQ_9_0_8_VER
|
||
import qq.service.bdh.RichProtoSvc
|
||
import qq.service.contact.ContactHelper
|
||
import kotlin.coroutines.resume
|
||
|
||
/**
|
||
* 将NT消息(com.tencent.qqnt.*)转换为事件消息(io.kritor.event.*)推送
|
||
*/
|
||
|
||
typealias NtMessages = ArrayList<MsgElement>
|
||
typealias Convertor = suspend (MsgRecord, MsgElement) -> Result<Element>
|
||
|
||
private object MsgConvertor {
|
||
private val convertorMap = hashMapOf(
|
||
MsgConstant.KELEMTYPETEXT to ::convertText,
|
||
MsgConstant.KELEMTYPEFACE to ::convertFace,
|
||
MsgConstant.KELEMTYPEPIC to ::convertImage,
|
||
MsgConstant.KELEMTYPEPTT to ::convertVoice,
|
||
MsgConstant.KELEMTYPEVIDEO to ::convertVideo,
|
||
MsgConstant.KELEMTYPEMARKETFACE to ::convertMarketFace,
|
||
MsgConstant.KELEMTYPEARKSTRUCT to ::convertStructJson,
|
||
MsgConstant.KELEMTYPEREPLY to ::convertReply,
|
||
//MsgConstant.KELEMTYPEGRAYTIP to ::convertGrayTips,
|
||
MsgConstant.KELEMTYPEFILE to ::convertFile,
|
||
MsgConstant.KELEMTYPEMARKDOWN to ::convertMarkdown,
|
||
//MsgConstant.KELEMTYPEMULTIFORWARD to MsgElementConverter::convertXmlMultiMsgElem,
|
||
//MsgConstant.KELEMTYPESTRUCTLONGMSG to MsgElementConverter::convertXmlLongMsgElem,
|
||
MsgConstant.KELEMTYPEFACEBUBBLE to ::convertBubbleFace,
|
||
MsgConstant.KELEMTYPEINLINEKEYBOARD to ::convertInlineKeyboard
|
||
)
|
||
|
||
suspend fun convertText(record: MsgRecord, element: MsgElement): Result<Element> {
|
||
val text = element.textElement
|
||
val elem = Element.newBuilder()
|
||
if (text.atType != MsgConstant.ATTYPEUNKNOWN) {
|
||
elem.type = ElementType.AT
|
||
elem.setAt(AtElement.newBuilder().apply {
|
||
this.uid = text.atNtUid
|
||
this.uin = ContactHelper.getUinByUidAsync(text.atNtUid).toLong()
|
||
})
|
||
} else {
|
||
elem.type = ElementType.TEXT
|
||
elem.setText(TextElement.newBuilder().apply {
|
||
this.text = text.content
|
||
})
|
||
}
|
||
return Result.success(elem.build())
|
||
}
|
||
|
||
suspend fun convertFace(record: MsgRecord, element: MsgElement): Result<Element> {
|
||
val face = element.faceElement
|
||
val elem = Element.newBuilder()
|
||
if (face.faceType == 5) {
|
||
elem.type = ElementType.POKE
|
||
elem.setPoke(PokeElement.newBuilder().apply {
|
||
this.id = face.vaspokeId
|
||
this.type = face.pokeType
|
||
this.strength = face.pokeStrength
|
||
})
|
||
} else {
|
||
when (face.faceIndex) {
|
||
114 -> {
|
||
elem.type = ElementType.BASKETBALL
|
||
elem.setBasketball(BasketballElement.newBuilder().apply {
|
||
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
|
||
})
|
||
}
|
||
|
||
358 -> {
|
||
elem.type = ElementType.DICE
|
||
elem.setDice(DiceElement.newBuilder().apply {
|
||
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
|
||
})
|
||
}
|
||
|
||
359 -> {
|
||
elem.type = ElementType.RPS
|
||
elem.setRps(RpsElement.newBuilder().apply {
|
||
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
|
||
})
|
||
}
|
||
|
||
394 -> {
|
||
elem.type = ElementType.FACE
|
||
elem.setFace(FaceElement.newBuilder().apply {
|
||
this.id = face.faceIndex
|
||
this.isBig = face.faceType == 3
|
||
this.result = face.resultId.ifNullOrEmpty { "1" }?.toInt() ?: 1
|
||
})
|
||
}
|
||
|
||
else -> {
|
||
elem.type = ElementType.FACE
|
||
elem.setFace(FaceElement.newBuilder().apply {
|
||
this.id = face.faceIndex
|
||
this.isBig = face.faceType == 3
|
||
})
|
||
}
|
||
}
|
||
}
|
||
return Result.success(elem.build())
|
||
}
|
||
|
||
suspend fun convertImage(record: MsgRecord, element: MsgElement): Result<Element> {
|
||
val image = element.picElement
|
||
val md5 = (image.md5HexStr ?: image.fileName
|
||
.replace("{", "")
|
||
.replace("}", "")
|
||
.replace("-", "").split(".")[0])
|
||
.uppercase()
|
||
|
||
var storeId = 0
|
||
if (PlatformUtils.getQQVersionCode() > QQ_9_0_8_VER) {
|
||
storeId = image.storeID
|
||
}
|
||
|
||
ImageDB.getInstance().imageMappingDao().insert(
|
||
ImageMapping(
|
||
fileName = md5,
|
||
md5 = md5,
|
||
chatType = record.chatType,
|
||
size = image.fileSize,
|
||
sha = "",
|
||
fileId = image.fileUuid,
|
||
storeId = storeId,
|
||
)
|
||
)
|
||
|
||
val originalUrl = image.originImageUrl ?: ""
|
||
LogCenter.log({ "receive image: $image" }, Level.DEBUG)
|
||
|
||
val elem = Element.newBuilder()
|
||
elem.type = ElementType.IMAGE
|
||
elem.setImage(ImageElement.newBuilder().apply {
|
||
this.file = md5
|
||
this.url = when (record.chatType) {
|
||
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
|
||
originalUrl = originalUrl,
|
||
md5 = md5,
|
||
fileId = image.fileUuid,
|
||
width = image.picWidth.toUInt(),
|
||
height = image.picHeight.toUInt(),
|
||
sha = "",
|
||
fileSize = image.fileSize.toULong(),
|
||
peer = record.peerUin.toString()
|
||
)
|
||
|
||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(
|
||
originalUrl = originalUrl,
|
||
md5 = md5,
|
||
fileId = image.fileUuid,
|
||
width = image.picWidth.toUInt(),
|
||
height = image.picHeight.toUInt(),
|
||
sha = "",
|
||
fileSize = image.fileSize.toULong(),
|
||
peer = record.senderUin.toString(),
|
||
storeId = storeId
|
||
)
|
||
|
||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(
|
||
originalUrl = originalUrl,
|
||
md5 = md5,
|
||
fileId = image.fileUuid,
|
||
width = image.picWidth.toUInt(),
|
||
height = image.picHeight.toUInt(),
|
||
sha = "",
|
||
fileSize = image.fileSize.toULong(),
|
||
peer = record.channelId.ifNullOrEmpty { record.peerUin.toString() } ?: "0",
|
||
subPeer = record.guildId ?: "0"
|
||
)
|
||
|
||
else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}")
|
||
}
|
||
this.type =
|
||
if (image.isFlashPic == true) ImageType.FLASH else if (image.original) ImageType.ORIGIN else ImageType.COMMON
|
||
this.subType = image.picSubType
|
||
})
|
||
|
||
return Result.success(elem.build())
|
||
}
|
||
|
||
suspend fun convertVoice(record: MsgRecord, element: MsgElement): Result<Element> {
|
||
val ptt = element.pttElement
|
||
val elem = Element.newBuilder()
|
||
|
||
val md5 = if (ptt.fileName.startsWith("silk"))
|
||
ptt.fileName.substring(5)
|
||
else ptt.md5HexStr
|
||
|
||
elem.type = ElementType.VOICE
|
||
elem.setVoice(VoiceElement.newBuilder().apply {
|
||
this.url = when (record.chatType) {
|
||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", ptt.fileUuid)
|
||
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
|
||
"0",
|
||
md5.hex2ByteArray(),
|
||
ptt.fileUuid
|
||
)
|
||
|
||
else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}")
|
||
}
|
||
this.file = md5
|
||
this.magic = ptt.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE
|
||
})
|
||
|
||
return Result.success(elem.build())
|
||
}
|
||
|
||
suspend fun convertVideo(record: MsgRecord, element: MsgElement): Result<Element> {
|
||
val video = element.videoElement
|
||
val elem = Element.newBuilder()
|
||
val md5 = if (video.fileName.contains("/")) {
|
||
video.videoMd5.takeIf {
|
||
!it.isNullOrEmpty()
|
||
}?.hex2ByteArray() ?: video.fileName.split("/").let {
|
||
it[it.size - 2].hex2ByteArray()
|
||
}
|
||
} else video.fileName.split(".")[0].hex2ByteArray()
|
||
elem.type = ElementType.VIDEO
|
||
elem.setVideo(VideoElement.newBuilder().apply {
|
||
this.file = md5.toHexString()
|
||
this.url = when (record.chatType) {
|
||
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
|
||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
||
else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}")
|
||
}
|
||
})
|
||
return Result.success(elem.build())
|
||
}
|
||
|
||
suspend fun convertMarketFace(record: MsgRecord, element: MsgElement): Result<Element> {
|
||
val marketFace = element.marketFaceElement
|
||
val elem = Element.newBuilder()
|
||
elem.type = ElementType.MARKET_FACE
|
||
elem.setMarketFace(MarketFaceElement.newBuilder().apply {
|
||
this.id = marketFace.emojiId.lowercase()
|
||
})
|
||
return Result.success(elem.build())
|
||
}
|
||
|
||
suspend fun convertStructJson(record: MsgRecord, element: MsgElement): Result<Element> {
|
||
val data = element.arkElement.bytesData.asJsonObject
|
||
val elem = Element.newBuilder()
|
||
when (data["app"].asString) {
|
||
"com.tencent.multimsg" -> {
|
||
val info = data["meta"].asJsonObject["detail"].asJsonObject
|
||
elem.type = ElementType.FORWARD
|
||
elem.setForward(ForwardElement.newBuilder().apply {
|
||
this.resId = info["resid"].asString
|
||
this.uniseq = info["uniseq"].asString
|
||
this.summary = info["summary"].asString
|
||
this.description = info["news"].asJsonArray.joinToString("\n") {
|
||
it.asJsonObject["text"].asString
|
||
}
|
||
})
|
||
}
|
||
|
||
"com.tencent.troopsharecard" -> {
|
||
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
||
elem.type = ElementType.CONTACT
|
||
elem.setContact(ContactElement.newBuilder().apply {
|
||
this.scene = Scene.GROUP
|
||
this.peer = info["jumpUrl"].asString.split("group_code=")[1]
|
||
})
|
||
}
|
||
|
||
"com.tencent.contact.lua" -> {
|
||
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
||
elem.type = ElementType.CONTACT
|
||
elem.setContact(ContactElement.newBuilder().apply {
|
||
this.scene = Scene.FRIEND
|
||
this.peer = info["jumpUrl"].asString.split("uin=")[1]
|
||
})
|
||
}
|
||
|
||
"com.tencent.map" -> {
|
||
val info = data["meta"].asJsonObject["Location.Search"].asJsonObject
|
||
elem.type = ElementType.LOCATION
|
||
elem.setLocation(LocationElement.newBuilder().apply {
|
||
this.lat = info["lat"].asString.toFloat()
|
||
this.lon = info["lng"].asString.toFloat()
|
||
this.address = info["address"].asString
|
||
this.title = info["name"].asString
|
||
})
|
||
}
|
||
|
||
else -> {
|
||
elem.type = ElementType.JSON
|
||
elem.setJson(JsonElement.newBuilder().apply {
|
||
this.json = data.toString()
|
||
})
|
||
}
|
||
}
|
||
return Result.success(elem.build())
|
||
}
|
||
|
||
suspend fun convertReply(record: MsgRecord, element: MsgElement): Result<Element> {
|
||
val reply = element.replyElement
|
||
val elem = Element.newBuilder()
|
||
elem.type = ElementType.REPLY
|
||
elem.setReply(ReplyElement.newBuilder().apply {
|
||
val msgSeq = reply.replayMsgSeq
|
||
val contact = MessageHelper.generateContact(record)
|
||
val sourceRecords = withTimeoutOrNull(3000) {
|
||
suspendCancellableCoroutine {
|
||
QRoute.api(IMsgService::class.java)
|
||
.getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records ->
|
||
it.resume(records)
|
||
}
|
||
}
|
||
}
|
||
if (sourceRecords.isNullOrEmpty()) {
|
||
LogCenter.log("无法查询到回复的消息ID: seq = $msgSeq", Level.WARN)
|
||
this.messageId = reply.replayMsgId
|
||
} else {
|
||
this.messageId = sourceRecords.first().msgId
|
||
}
|
||
})
|
||
return Result.success(elem.build())
|
||
}
|
||
|
||
suspend fun convertFile(record: MsgRecord, element: MsgElement): Result<Element> {
|
||
val fileMsg = element.fileElement
|
||
val fileName = fileMsg.fileName
|
||
val fileSize = fileMsg.fileSize
|
||
val expireTime = fileMsg.expireTime ?: 0
|
||
val fileId = fileMsg.fileUuid
|
||
val bizId = fileMsg.fileBizId ?: 0
|
||
val fileSubId = fileMsg.fileSubId ?: ""
|
||
val url = when (record.chatType) {
|
||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
|
||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(
|
||
record.guildId,
|
||
record.channelId,
|
||
fileId,
|
||
bizId
|
||
)
|
||
|
||
else -> RichProtoSvc.getGroupFileDownUrl(record.peerUin, fileId, bizId)
|
||
}
|
||
val elem = Element.newBuilder()
|
||
elem.type = ElementType.FILE
|
||
elem.setFile(FileElement.newBuilder().apply {
|
||
this.name = fileName
|
||
this.size = fileSize
|
||
this.url = url
|
||
this.expireTime = expireTime
|
||
this.id = fileId
|
||
this.subId = fileSubId
|
||
this.biz = bizId
|
||
})
|
||
return Result.success(elem.build())
|
||
}
|
||
|
||
suspend fun convertMarkdown(record: MsgRecord, element: MsgElement): Result<Element> {
|
||
val markdown = element.markdownElement
|
||
val elem = Element.newBuilder()
|
||
elem.type = ElementType.MARKDOWN
|
||
elem.setMarkdown(MarkdownElement.newBuilder().apply {
|
||
this.markdown = markdown.content
|
||
})
|
||
return Result.success(elem.build())
|
||
}
|
||
|
||
suspend fun convertBubbleFace(record: MsgRecord, element: MsgElement): Result<Element> {
|
||
val bubbleFace = element.faceBubbleElement
|
||
val elem = Element.newBuilder()
|
||
elem.type = ElementType.BUBBLE_FACE
|
||
elem.setBubbleFace(BubbleFaceElement.newBuilder().apply {
|
||
this.id = bubbleFace.yellowFaceInfo.index
|
||
this.count = bubbleFace.faceCount ?: 1
|
||
})
|
||
return Result.success(elem.build())
|
||
}
|
||
|
||
suspend fun convertInlineKeyboard(record: MsgRecord, element: MsgElement): Result<Element> {
|
||
val inlineKeyboard = element.inlineKeyboardElement
|
||
val elem = Element.newBuilder()
|
||
elem.type = ElementType.BUTTON
|
||
elem.setButton(ButtonElement.newBuilder().apply {
|
||
inlineKeyboard.rows.forEach { row ->
|
||
this.addRows(ButtonRow.newBuilder().apply {
|
||
row.buttons.forEach buttonsLoop@{ button ->
|
||
if (button == null) return@buttonsLoop
|
||
this.addButtons(Button.newBuilder().apply {
|
||
this.id = button.id
|
||
this.action = ButtonAction.newBuilder().apply {
|
||
this.type = button.type
|
||
this.permission = ButtonActionPermission.newBuilder().apply {
|
||
this.type = button.permissionType
|
||
button.specifyRoleIds?.let {
|
||
this.addAllRoleIds(it)
|
||
}
|
||
button.specifyTinyids?.let {
|
||
this.addAllUserIds(it)
|
||
}
|
||
}.build()
|
||
this.unsupportedTips = button.unsupportTips ?: ""
|
||
this.data = button.data ?: ""
|
||
this.reply = button.isReply
|
||
this.enter = button.enter
|
||
}.build()
|
||
this.renderData = ButtonRender.newBuilder().apply {
|
||
this.label = button.label ?: ""
|
||
this.visitedLabel = button.visitedLabel ?: ""
|
||
this.style = button.style
|
||
}.build()
|
||
})
|
||
}
|
||
})
|
||
}
|
||
})
|
||
return Result.success(elem.build())
|
||
}
|
||
|
||
operator fun get(case: Int): Convertor? {
|
||
return convertorMap[case]
|
||
}
|
||
}
|
||
|
||
suspend fun NtMessages.toKritorEventMessages(record: MsgRecord): ArrayList<Element> {
|
||
val result = arrayListOf<Element>()
|
||
forEach {
|
||
MsgConvertor[it.elementType]?.invoke(record, it)?.onSuccess {
|
||
result.add(it)
|
||
}?.onFailure {
|
||
if (it !is ActionMsgException) {
|
||
LogCenter.log("消息转换异常: " + it.stackTraceToString(), Level.WARN)
|
||
}
|
||
}
|
||
}
|
||
return result
|
||
}
|