7 Commits

Author SHA1 Message Date
8f8580d542 Shamrock: remove debug log #242
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-18 21:16:23 +08:00
0ed4480878 Shamrock: fix #242 2024-02-18 21:16:04 +08:00
c3e0031aa4 Shamrock: debug #242 2024-02-18 20:36:42 +08:00
388c963e88 Shamrock: fix markdown parser 2024-02-18 20:28:45 +08:00
4283651b1e Shamrock: fix comm_elem parser 2024-02-18 20:08:57 +08:00
50d7dfa06d Shamrock: support inline_keyboard msg 2024-02-18 20:00:37 +08:00
b3a2e605fb Shamrock: fix NT image acquisition 2024-02-18 18:52:56 +08:00
9 changed files with 158 additions and 25 deletions

View File

@ -8,5 +8,5 @@ import protobuf.message.element.*
data class MessageElement( data class MessageElement(
@ProtoNumber(1) val text: TextElement? = null, @ProtoNumber(1) val text: TextElement? = null,
@ProtoNumber(51) val json: JsonElement? = null, @ProtoNumber(51) val json: JsonElement? = null,
@ProtoNumber(53) val richMedia: RichMediaElement? = null, @ProtoNumber(53) val commElem: CommonElement? = null,
) )

View File

@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
@Serializable @Serializable
data class RichMediaElement( data class CommonElement(
@ProtoNumber(1) val type: Int? = null, @ProtoNumber(1) val type: Int? = null,
@ProtoNumber(2) val data: ByteArray? = null, @ProtoNumber(2) val data: ByteArray? = null,
@ProtoNumber(3) val u1: Int? = null, @ProtoNumber(3) val u1: Int? = null,

View File

@ -79,6 +79,10 @@ public final class InlineKeyboardButton {
return "InlineKeyboardButton{id=" + this.id + ",label=" + this.label + ",visitedLabel=" + this.visitedLabel + ",style=" + this.style + ",type=" + this.type + ",clickLimit=" + this.clickLimit + ",unsupportTips=" + this.unsupportTips + ",data=" + this.data + ",atBotShowChannelList=" + this.atBotShowChannelList + ",permissionType=" + this.permissionType + ",specifyRoleIds=" + this.specifyRoleIds + ",specifyTinyids=" + this.specifyTinyids + ",}"; return "InlineKeyboardButton{id=" + this.id + ",label=" + this.label + ",visitedLabel=" + this.visitedLabel + ",style=" + this.style + ",type=" + this.type + ",clickLimit=" + this.clickLimit + ",unsupportTips=" + this.unsupportTips + ",data=" + this.data + ",atBotShowChannelList=" + this.atBotShowChannelList + ",permissionType=" + this.permissionType + ",specifyRoleIds=" + this.specifyRoleIds + ",specifyTinyids=" + this.specifyTinyids + ",}";
} }
public InlineKeyboardButton(String str, String str2, String str3, int i, int i2, int i3, String str4, String str5, boolean z, int i4, ArrayList<String> arrayList, ArrayList<String> arrayList2, boolean z2, int i5, boolean z3, ArrayList<SubscribeMsgTemplateID> arrayList3) {
}
public InlineKeyboardButton(String str, String str2, String str3, int i2, int i3, int i4, String str4, String str5, boolean z, int i5, ArrayList<String> arrayList, ArrayList<String> arrayList2) { public InlineKeyboardButton(String str, String str2, String str3, int i2, int i3, int i4, String str4, String str5, boolean z, int i5, ArrayList<String> arrayList, ArrayList<String> arrayList2) {
this.id = ""; this.id = "";
this.label = ""; this.label = "";

View File

@ -0,0 +1,4 @@
package com.tencent.qqnt.kernel.nativeinterface;
public class SubscribeMsgTemplateID {
}

View File

@ -11,6 +11,9 @@ import com.tencent.qqnt.aio.adapter.api.IAIOPttApi
import com.tencent.qqnt.kernel.nativeinterface.ArkElement import com.tencent.qqnt.kernel.nativeinterface.ArkElement
import com.tencent.qqnt.kernel.nativeinterface.FaceBubbleElement import com.tencent.qqnt.kernel.nativeinterface.FaceBubbleElement
import com.tencent.qqnt.kernel.nativeinterface.FaceElement import com.tencent.qqnt.kernel.nativeinterface.FaceElement
import com.tencent.qqnt.kernel.nativeinterface.InlineKeyboardButton
import com.tencent.qqnt.kernel.nativeinterface.InlineKeyboardElement
import com.tencent.qqnt.kernel.nativeinterface.InlineKeyboardRow
import com.tencent.qqnt.kernel.nativeinterface.MarkdownElement import com.tencent.qqnt.kernel.nativeinterface.MarkdownElement
import com.tencent.qqnt.kernel.nativeinterface.MarketFaceElement import com.tencent.qqnt.kernel.nativeinterface.MarketFaceElement
import com.tencent.qqnt.kernel.nativeinterface.MarketFaceSupportSize import com.tencent.qqnt.kernel.nativeinterface.MarketFaceSupportSize
@ -53,9 +56,11 @@ import moe.fuqiuluo.shamrock.helper.LogicException
import moe.fuqiuluo.shamrock.helper.MessageHelper import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.MusicHelper import moe.fuqiuluo.shamrock.helper.MusicHelper
import moe.fuqiuluo.shamrock.helper.ParamsException import moe.fuqiuluo.shamrock.helper.ParamsException
import moe.fuqiuluo.shamrock.tools.asBoolean
import moe.fuqiuluo.shamrock.tools.asBooleanOrNull import moe.fuqiuluo.shamrock.tools.asBooleanOrNull
import moe.fuqiuluo.shamrock.tools.asInt import moe.fuqiuluo.shamrock.tools.asInt
import moe.fuqiuluo.shamrock.tools.asIntOrNull import moe.fuqiuluo.shamrock.tools.asIntOrNull
import moe.fuqiuluo.shamrock.tools.asJsonArray
import moe.fuqiuluo.shamrock.tools.asJsonObject import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asLong import moe.fuqiuluo.shamrock.tools.asLong
import moe.fuqiuluo.shamrock.tools.asString import moe.fuqiuluo.shamrock.tools.asString
@ -108,8 +113,62 @@ internal object MessageMaker {
//"node" to MessageMaker::createNodeElem, //"node" to MessageMaker::createNodeElem,
//"multi_msg" to MessageMaker::createLongMsgStruct, //"multi_msg" to MessageMaker::createLongMsgStruct,
"bubble_face" to MessageMaker::createBubbleFaceElem, "bubble_face" to MessageMaker::createBubbleFaceElem,
"inline_keyboard" to MessageMaker::createInlineKeywordElem
) )
private suspend fun createInlineKeywordElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
fun tryNewKeyboardButton(btn: JsonObject): InlineKeyboardButton {
return runCatching {
InlineKeyboardButton(
btn["id"].asString,
btn["label"].asString,
btn["visited_label"].asString,
btn["style"].asInt,
btn["type"].asInt,
btn["click_limit"].asInt,
btn["unsupport_tips"].asString,
btn["data"].asString,
btn["at_bot_show_channel_list"].asBoolean,
btn["permission_type"].asInt,
ArrayList(btn["specify_role_ids"].asJsonArray.map { it.asString }),
ArrayList(btn["specify_tinyids"].asJsonArray.map { it.asString }),
false, 0, false, arrayListOf()
)
}.getOrElse {
InlineKeyboardButton(
btn["id"].asString,
btn["label"].asString,
btn["visited_label"].asString,
btn["style"].asInt,
btn["type"].asInt,
btn["click_limit"].asInt,
btn["unsupport_tips"].asString,
btn["data"].asString,
btn["at_bot_show_channel_list"].asBoolean,
btn["permission_type"].asInt,
ArrayList(btn["specify_role_ids"].asJsonArray.map { it.asString }),
ArrayList(btn["specify_tinyids"].asJsonArray.map { it.asString }),
)
}
}
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEINLINEKEYBOARD
val rows = arrayListOf<InlineKeyboardRow>()
val keyboard = Json.parseToJsonElement(data["data"].asString).asJsonObject
keyboard["rows"].asJsonArray.forEach {
val row = it.asJsonObject
val buttons = arrayListOf<InlineKeyboardButton>()
row["buttons"].asJsonArray.forEach { button ->
val btn = button.asJsonObject
buttons.add(tryNewKeyboardButton(btn))
}
rows.add(InlineKeyboardRow(buttons))
}
elem.inlineKeyboardElement = InlineKeyboardElement(rows, keyboard["bot_appid"].asLong)
return Result.success(elem)
}
private suspend fun createBubbleFaceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createBubbleFaceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
data.checkAndThrow("id", "count") data.checkAndThrow("id", "count")
val faceId = data["id"].asInt val faceId = data["id"].asInt
@ -569,10 +628,10 @@ internal object MessageMaker {
} }
private suspend fun createMarkdownElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createMarkdownElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
data.checkAndThrow("text") data.checkAndThrow("content")
val elem = MsgElement() val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEMARKDOWN elem.elementType = MsgConstant.KELEMTYPEMARKDOWN
val markdown = MarkdownElement(data["text"].asString) val markdown = MarkdownElement(data["content"].asString)
elem.markdownElement = markdown elem.markdownElement = markdown
return Result.success(elem) return Result.success(elem)
} }

View File

@ -58,6 +58,7 @@ internal object MessageConvert {
//MsgConstant.KELEMTYPEMULTIFORWARD to XmlMultiMsgConverter, //MsgConstant.KELEMTYPEMULTIFORWARD to XmlMultiMsgConverter,
//MsgConstant.KELEMTYPESTRUCTLONGMSG to XmlLongMsgConverter, //MsgConstant.KELEMTYPESTRUCTLONGMSG to XmlLongMsgConverter,
MsgConstant.KELEMTYPEFACEBUBBLE to BubbleFaceConverter, MsgConstant.KELEMTYPEFACEBUBBLE to BubbleFaceConverter,
MsgConstant.KELEMTYPEINLINEKEYBOARD to InlineKeyboardConverter,
) )
} }

View File

@ -3,6 +3,10 @@ package moe.fuqiuluo.qqinterface.servlet.msg.convert
import com.tencent.mobileqq.qmmkv.QMMKV import com.tencent.mobileqq.qmmkv.QMMKV
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
import moe.fuqiuluo.shamrock.helper.ContactHelper import moe.fuqiuluo.shamrock.helper.ContactHelper
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
@ -13,6 +17,7 @@ import moe.fuqiuluo.shamrock.helper.db.ImageMapping
import moe.fuqiuluo.shamrock.helper.db.MessageDB import moe.fuqiuluo.shamrock.helper.db.MessageDB
import moe.fuqiuluo.shamrock.tools.asJsonObject import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
import moe.fuqiuluo.shamrock.tools.json import moe.fuqiuluo.shamrock.tools.json
import mqq.app.MobileQQ import mqq.app.MobileQQ
import kotlin.jvm.internal.Intrinsics import kotlin.jvm.internal.Intrinsics
@ -209,7 +214,15 @@ internal sealed class MessageElemConverter: IMessageConvert {
element: MsgElement element: MsgElement
): MessageSegment { ): MessageSegment {
val video = element.videoElement val video = element.videoElement
val md5 = video.fileName.split(".")[0] 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()
//LogCenter.log({ "receive video msg: $video" }, Level.DEBUG)
return MessageSegment( return MessageSegment(
type = "video", type = "video",
@ -218,7 +231,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
"url" to when(chatType) { "url" to when(chatType) {
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid) MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid) MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl(peerId, md5, video.fileUuid) MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
else -> unknownChatType(chatType) else -> unknownChatType(chatType)
} }
).also { ).also {
@ -495,6 +508,53 @@ internal sealed class MessageElemConverter: IMessageConvert {
} }
} }
data object InlineKeyboardConverter: MessageElemConverter() {
override suspend fun convert(
chatType: Int,
peerId: String,
subPeer: String,
element: MsgElement
): MessageSegment {
val keyboard = element.inlineKeyboardElement
return MessageSegment(
type = "inline_keyboard",
data = mapOf(
"data" to buildJsonObject {
putJsonArray("rows") {
keyboard.rows.forEach { row ->
add(buildJsonObject row@{
putJsonArray("buttons") {
row.buttons.forEach { button ->
add(buildJsonObject {
put("id", button.id ?: "")
put("label", button.label ?: "")
put("visited_label", button.visitedLabel ?: "")
put("style", button.style)
put("type", button.type)
put("click_limit", button.clickLimit)
put("unsupport_tips", button.unsupportTips ?: "")
put("data", button.data)
put("at_bot_show_channel_list", button.atBotShowChannelList)
put("permission_type", button.permissionType)
putJsonArray("specify_role_ids") {
button.specifyRoleIds?.forEach { add(it) }
}
putJsonArray("specify_tinyids") {
button.specifyTinyids?.forEach { add(it) }
}
})
}
}
})
}
}
put("bot_appid", keyboard.botAppid)
}.toString()
)
)
}
}
protected fun unknownChatType(chatType: Int) { protected fun unknownChatType(chatType: Int) {
throw UnsupportedOperationException("Not supported chat type: $chatType") throw UnsupportedOperationException("Not supported chat type: $chatType")
} }

View File

@ -167,9 +167,10 @@ internal object RichProtoSvc: BaseSvc() {
originalUrl: String, originalUrl: String,
md5: String, md5: String,
): String { ): String {
val domain = if (originalUrl.startsWith("/download")) GPRO_PIC_NT else GPRO_PIC val isNtServer = originalUrl.startsWith("/download")
val domain = if (isNtServer) GPRO_PIC_NT else GPRO_PIC
if (originalUrl.isNotEmpty()) { if (originalUrl.isNotEmpty()) {
if (!originalUrl.contains("rkey=")) { if (isNtServer && !originalUrl.contains("rkey=")) {
return "https://$domain$originalUrl&rkey=$multiMediaRKey" return "https://$domain$originalUrl&rkey=$multiMediaRKey"
} }
return "https://$domain$originalUrl" return "https://$domain$originalUrl"
@ -191,9 +192,10 @@ internal object RichProtoSvc: BaseSvc() {
originalUrl: String, originalUrl: String,
md5: String md5: String
): String { ): String {
val domain = if (originalUrl.startsWith("/download")) GPRO_PIC_NT else GPRO_PIC val isNtServer = originalUrl.startsWith("/download")
val domain = if (isNtServer) GPRO_PIC_NT else GPRO_PIC
if (originalUrl.isNotEmpty()) { if (originalUrl.isNotEmpty()) {
if (!originalUrl.contains("rkey=")) { if (isNtServer && !originalUrl.contains("rkey=")) {
return "https://$domain$originalUrl&rkey=$multiMediaRKey" return "https://$domain$originalUrl&rkey=$multiMediaRKey"
} }
return "https://$domain$originalUrl" return "https://$domain$originalUrl"
@ -203,7 +205,7 @@ internal object RichProtoSvc: BaseSvc() {
suspend fun getC2CVideoDownUrl( suspend fun getC2CVideoDownUrl(
peerId: String, peerId: String,
md5Hex: String, md5: ByteArray,
fileUUId: String fileUUId: String
): String { ): String {
return suspendCancellableCoroutine { return suspendCancellableCoroutine {
@ -219,7 +221,7 @@ internal object RichProtoSvc: BaseSvc() {
downReq.troopUin = peerId downReq.troopUin = peerId
downReq.clientType = 2 downReq.clientType = 2
downReq.fileId = fileUUId downReq.fileId = fileUUId
downReq.md5 = md5Hex.hex2ByteArray() downReq.md5 = md5
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
downReq.subBusiType = 0 downReq.subBusiType = 0
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4 downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4
@ -246,7 +248,7 @@ internal object RichProtoSvc: BaseSvc() {
suspend fun getGroupVideoDownUrl( suspend fun getGroupVideoDownUrl(
peerId: String, peerId: String,
md5Hex: String, md5: ByteArray,
fileUUId: String fileUUId: String
): String { ): String {
return suspendCancellableCoroutine { return suspendCancellableCoroutine {
@ -262,7 +264,7 @@ internal object RichProtoSvc: BaseSvc() {
downReq.troopUin = peerId downReq.troopUin = peerId
downReq.clientType = 2 downReq.clientType = 2
downReq.fileId = fileUUId downReq.fileId = fileUUId
downReq.md5 = md5Hex.hex2ByteArray() downReq.md5 = md5
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
downReq.subBusiType = 0 downReq.subBusiType = 0
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4 downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4

View File

@ -109,19 +109,22 @@ internal object PrimitiveListener {
} }
private fun onGroupMessage(msgTime: Long, body: MessageBody) { private fun onGroupMessage(msgTime: Long, body: MessageBody) {
body.rich?.elements?.filter { runCatching {
it.richMedia != null body.rich?.elements?.filter {
}?.map { it.commElem != null && it.commElem!!.type == 48
ProtoBuf.decodeFromByteArray<RichMediaForPicData>(it.richMedia!!.data!!) }?.map {
}?.forEach { ProtoBuf.decodeFromByteArray<RichMediaForPicData>(it.commElem!!.data!!)
it.display?.show?.download?.url?.let { }?.forEach {
RKEY_PATTERN.matcher(it).takeIf { it.display?.show?.download?.url?.let {
it.find() RKEY_PATTERN.matcher(it).takeIf {
}?.group(1)?.let { rkey -> it.find()
LogCenter.log("更新NT RKEY成功$rkey") }?.group(1)?.let { rkey ->
RichProtoSvc.multiMediaRKey = rkey LogCenter.log("更新NT RKEY成功$rkey")
RichProtoSvc.multiMediaRKey = rkey
}
} }
} }
} }
} }