3 Commits

Author SHA1 Message Date
c97f79335a Shamrock: Support /set_group_comment_face 2024-01-28 23:45:05 +08:00
e07e75747a Shamrock: Support big face and bubble face 2024-01-28 23:22:06 +08:00
9482641c38 Shamrock: fix #213 2024-01-28 22:38:54 +08:00
9 changed files with 185 additions and 46 deletions

View File

@ -0,0 +1,14 @@
package moe.whitechi73.protobuf.oidb.cmd0x9082
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class Oidb0x9082(
@ProtoNumber(2) val peer: ULong = ULong.MIN_VALUE,
@ProtoNumber(3) val msgSeq: ULong = ULong.MIN_VALUE,
@ProtoNumber(4) val faceIndex: String = "",
@ProtoNumber(5) val flag: UInt = UInt.MIN_VALUE,
@ProtoNumber(6) val u1: UInt = UInt.MIN_VALUE,
@ProtoNumber(7) val u2: UInt = UInt.MIN_VALUE,
)

View File

@ -19,4 +19,11 @@ public class QQSysFaceUtil {
public static String getFaceDescription(int localId) {
return "";
}
public static String getPrueFaceDescription(String str) {
if (str == null) {
return null;
}
return str.startsWith("/") ? str.substring(1) : str;
}
}

View File

@ -0,0 +1,19 @@
package moe.fuqiuluo.qqinterface.servlet
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.protobuf.ProtoBuf
import moe.whitechi73.protobuf.oidb.cmd0x9082.Oidb0x9082
internal object ChatSvc: BaseSvc() {
fun setGroupMessageCommentFace(peer: Long, msgSeq: ULong, faceIndex: String, isSet: Boolean) {
val serviceId = if (isSet) 1 else 2
sendOidb("OidbSvcTrpcTcp.0x9082_$serviceId", 36994, serviceId, ProtoBuf.encodeToByteArray(Oidb0x9082(
peer = peer.toULong(),
msgSeq = msgSeq,
faceIndex = faceIndex,
flag = 1u,
u1 = 0u,
u2 = 0u
)))
}
}

View File

@ -9,6 +9,7 @@ import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qphone.base.remote.ToServiceMsg
import com.tencent.qqnt.aio.adapter.api.IAIOPttApi
import com.tencent.qqnt.kernel.nativeinterface.ArkElement
import com.tencent.qqnt.kernel.nativeinterface.FaceBubbleElement
import com.tencent.qqnt.kernel.nativeinterface.FaceElement
import com.tencent.qqnt.kernel.nativeinterface.MarkdownElement
import com.tencent.qqnt.kernel.nativeinterface.MarketFaceElement
@ -20,9 +21,9 @@ import com.tencent.qqnt.kernel.nativeinterface.PttElement
import com.tencent.qqnt.kernel.nativeinterface.QQNTWrapperUtil
import com.tencent.qqnt.kernel.nativeinterface.ReplyElement
import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo
import com.tencent.qqnt.kernel.nativeinterface.SmallYellowFaceInfo
import com.tencent.qqnt.kernel.nativeinterface.TextElement
import com.tencent.qqnt.kernel.nativeinterface.VideoElement
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
@ -45,7 +46,9 @@ import moe.fuqiuluo.qqinterface.servlet.transfile.with
import moe.fuqiuluo.shamrock.helper.ActionMsgException
import moe.fuqiuluo.shamrock.helper.ContactHelper
import moe.fuqiuluo.shamrock.helper.IllegalParamsException
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LocalCacheHelper
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.LogicException
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.MusicHelper
@ -62,8 +65,6 @@ import moe.fuqiuluo.shamrock.utils.AudioUtils
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MediaType
import moe.fuqiuluo.shamrock.utils.PlatformUtils
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
import moe.fuqiuluo.shamrock.xposed.helper.msgService
@ -106,8 +107,32 @@ internal object MessageMaker {
"basketball" to MessageMaker::createBasketballElem,
//"node" to MessageMaker::createNodeElem,
//"multi_msg" to MessageMaker::createLongMsgStruct,
"bubble_face" to MessageMaker::createBubbleFaceElem,
)
private suspend fun createBubbleFaceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
data.checkAndThrow("id", "count")
val faceId = data["id"].asInt
val local = QQSysFaceUtil.convertToLocal(faceId)
val name = QQSysFaceUtil.getFaceDescription(local)
val count = data["count"].asInt
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEFACEBUBBLE
val face = FaceBubbleElement()
face.faceType = 13
face.faceCount = count
face.faceSummary = QQSysFaceUtil.getPrueFaceDescription(name)
val smallYellowFaceInfo = SmallYellowFaceInfo()
smallYellowFaceInfo.index = faceId
smallYellowFaceInfo.compatibleText = face.faceSummary
smallYellowFaceInfo.text = face.faceSummary
face.yellowFaceInfo = smallYellowFaceInfo
face.faceFlag = 0
face.content = data["text"].asStringOrNull ?: "[${face.faceSummary}]x$count"
elem.faceBubbleElement = face
return Result.success(elem)
}
// private suspend fun createNodeElem(
// chatType: Int,
// msgId: Long,
@ -475,19 +500,31 @@ internal object MessageMaker {
private suspend fun createFaceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
data.checkAndThrow("id")
val big = data["big"].asBooleanOrNull ?: false
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEFACE
val face = FaceElement()
// 1 old face
// 2 normal face
// 3 super face
// 4 is market face
// 5 is vas poke
face.faceType = 0
face.faceType = if (big) 3 else 2
val serverId = data["id"].asInt
val localId = QQSysFaceUtil.convertToLocal(serverId)
face.faceIndex = serverId
face.faceText = QQSysFaceUtil.getFaceDescription(localId)
face.faceText = QQSysFaceUtil.getFaceDescription(QQSysFaceUtil.convertToLocal(serverId))
face.imageType = 0
face.packId = "0"
if (big) {
face.stickerId = 30.toString()
face.packId = "1"
face.sourceType = 1
face.stickerType = 1
face.randomType = 1
} else {
face.packId = "0"
}
elem.faceElement = face
return Result.success(elem)

View File

@ -4,6 +4,7 @@ import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.msg.convert.MessageElemConverter.*
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
@ -43,19 +44,20 @@ internal suspend fun List<MsgElement>.toCQCode(chatType: Int, peerId: String): S
internal object MessageConvert {
private val convertMap by lazy {
mutableMapOf<Int, IMessageConvert>(
MsgConstant.KELEMTYPETEXT to MessageElemConverter.TextConverter,
MsgConstant.KELEMTYPEFACE to MessageElemConverter.FaceConverter,
MsgConstant.KELEMTYPEPIC to MessageElemConverter.ImageConverter,
MsgConstant.KELEMTYPEPTT to MessageElemConverter.VoiceConverter,
MsgConstant.KELEMTYPEVIDEO to MessageElemConverter.VideoConverter,
MsgConstant.KELEMTYPEMARKETFACE to MessageElemConverter.MarketFaceConverter,
MsgConstant.KELEMTYPEARKSTRUCT to MessageElemConverter.StructJsonConverter,
MsgConstant.KELEMTYPEREPLY to MessageElemConverter.ReplyConverter,
MsgConstant.KELEMTYPEGRAYTIP to MessageElemConverter.GrayTipsConverter,
MsgConstant.KELEMTYPEFILE to MessageElemConverter.FileConverter,
MsgConstant.KELEMTYPEMARKDOWN to MessageElemConverter.MarkdownConverter,
//MsgConstant.KELEMTYPEMULTIFORWARD to MessageElemConverter.XmlMultiMsgConverter,
//MsgConstant.KELEMTYPESTRUCTLONGMSG to MessageElemConverter.XmlLongMsgConverter,
MsgConstant.KELEMTYPETEXT to TextConverter,
MsgConstant.KELEMTYPEFACE to FaceConverter,
MsgConstant.KELEMTYPEPIC to ImageConverter,
MsgConstant.KELEMTYPEPTT to VoiceConverter,
MsgConstant.KELEMTYPEVIDEO to VideoConverter,
MsgConstant.KELEMTYPEMARKETFACE to MarketFaceConverter,
MsgConstant.KELEMTYPEARKSTRUCT to StructJsonConverter,
MsgConstant.KELEMTYPEREPLY to ReplyConverter,
MsgConstant.KELEMTYPEGRAYTIP to GrayTipsConverter,
MsgConstant.KELEMTYPEFILE to FileConverter,
MsgConstant.KELEMTYPEMARKDOWN to MarkdownConverter,
//MsgConstant.KELEMTYPEMULTIFORWARD to XmlMultiMsgConverter,
//MsgConstant.KELEMTYPESTRUCTLONGMSG to XmlLongMsgConverter,
MsgConstant.KELEMTYPEFACEBUBBLE to BubbleFaceConverter,
)
}
@ -65,11 +67,11 @@ internal object MessageConvert {
peerId: String
): ArrayList<MessageSegment> {
val messageData = arrayListOf<MessageSegment>()
elements.forEach {
elements.forEach { msg ->
kotlin.runCatching {
val elementId = it.elementType
val elementId = msg.elementType
val converter = convertMap[elementId]
converter?.convert(chatType, peerId, it)
converter?.convert(chatType, peerId, msg)
?: throw UnsupportedOperationException("不支持的消息element类型$elementId")
}.onSuccess {
messageData.add(it)
@ -77,7 +79,7 @@ internal object MessageConvert {
if (it is UnknownError) {
// 不处理的消息类型抛出unknown error
} else {
LogCenter.log("消息element转换错误$it", Level.WARN)
LogCenter.log("消息element转换错误$it, elementType: ${msg.elementType}", Level.WARN)
}
}
}

View File

@ -18,7 +18,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
/**
* 文本 / 艾特 消息转换消息段
*/
object TextConverter: MessageElemConverter() {
data object TextConverter: MessageElemConverter() {
override suspend fun convert(chatType: Int, peerId: String, element: MsgElement): MessageSegment {
val text = element.textElement
return if (text.atType != MsgConstant.ATTYPEUNKNOWN) {
@ -42,9 +42,10 @@ internal sealed class MessageElemConverter: IMessageConvert {
/**
* 小表情 / 戳一戳 消息转换消息段
*/
object FaceConverter: MessageElemConverter() {
data object FaceConverter: MessageElemConverter() {
override suspend fun convert(chatType: Int, peerId: String, element: MsgElement): MessageSegment {
val face = element.faceElement
if (face.faceType == 5) {
return MessageSegment(
type = "poke",
@ -55,8 +56,6 @@ internal sealed class MessageElemConverter: IMessageConvert {
)
)
}
when (face.faceIndex) {
114 -> {
return MessageSegment(
@ -87,7 +86,8 @@ internal sealed class MessageElemConverter: IMessageConvert {
else -> return MessageSegment(
type = "face",
data = hashMapOf(
"id" to face.faceIndex
"id" to face.faceIndex,
"big" to (face.faceType == 3)
)
)
}
@ -97,7 +97,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
/**
* 图片消息转换消息段
*/
object ImageConverter: MessageElemConverter() {
data object ImageConverter: MessageElemConverter() {
override suspend fun convert(
chatType: Int,
peerId: String,
@ -121,7 +121,9 @@ internal sealed class MessageElemConverter: IMessageConvert {
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(md5)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(md5)
else -> unknownChatType(chatType)
}
},
"subType" to image.picSubType,
"type" to if (image.isFlashPic) "flash" else "show"
)
)
}
@ -130,7 +132,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
/**
* 语音消息转换消息段
*/
object VoiceConverter: MessageElemConverter() {
data object VoiceConverter: MessageElemConverter() {
override suspend fun convert(
chatType: Int,
peerId: String,
@ -166,7 +168,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
/**
* 视频消息转换消息段
*/
object VideoConverter: MessageElemConverter() {
data object VideoConverter: MessageElemConverter() {
override suspend fun convert(
chatType: Int,
peerId: String,
@ -195,7 +197,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
/**
* 商城大表情消息转换消息段
*/
object MarketFaceConverter: MessageElemConverter() {
data object MarketFaceConverter: MessageElemConverter() {
override suspend fun convert(
chatType: Int,
peerId: String,
@ -218,7 +220,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
/**
* JSON消息转消息段
*/
object StructJsonConverter: MessageElemConverter() {
data object StructJsonConverter: MessageElemConverter() {
override suspend fun convert(
chatType: Int,
peerId: String,
@ -280,7 +282,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
/**
* 回复消息转消息段
*/
object ReplyConverter: MessageElemConverter() {
data object ReplyConverter: MessageElemConverter() {
override suspend fun convert(
chatType: Int,
peerId: String,
@ -312,7 +314,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
/**
* 灰色提示条消息过滤
*/
object GrayTipsConverter: MessageElemConverter() {
data object GrayTipsConverter: MessageElemConverter() {
override suspend fun convert(
chatType: Int,
peerId: String,
@ -347,7 +349,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
/**
* 文件消息转换消息段
*/
object FileConverter: MessageElemConverter() {
data object FileConverter: MessageElemConverter() {
override suspend fun convert(
chatType: Int,
peerId: String,
@ -381,7 +383,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
/**
* 老板QQ的合并转发信息
*/
object XmlMultiMsgConverter: MessageElemConverter() {
data object XmlMultiMsgConverter: MessageElemConverter() {
override suspend fun convert(
chatType: Int,
peerId: String,
@ -397,7 +399,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
}
}
object XmlLongMsgConverter: MessageElemConverter() {
data object XmlLongMsgConverter: MessageElemConverter() {
override suspend fun convert(
chatType: Int,
peerId: String,
@ -413,7 +415,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
}
}
object MarkdownConverter: MessageElemConverter() {
data object MarkdownConverter: MessageElemConverter() {
override suspend fun convert(
chatType: Int,
peerId: String,
@ -429,6 +431,23 @@ internal sealed class MessageElemConverter: IMessageConvert {
}
}
data object BubbleFaceConverter: MessageElemConverter() {
override suspend fun convert(
chatType: Int,
peerId: String,
element: MsgElement
): MessageSegment {
val bubbleElement = element.faceBubbleElement
return MessageSegment(
type = "bubble_face",
data = mapOf(
"id" to bubbleElement.yellowFaceInfo.index,
"count" to (bubbleElement.faceCount ?: 1),
)
)
}
}
protected fun unknownChatType(chatType: Int) {
throw UnsupportedOperationException("Not supported chat type: $chatType")
}

View File

@ -0,0 +1,30 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.ChatSvc
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.symbols.OneBotHandler
@OneBotHandler("set_group_comment_face")
internal object SetGroupCommentFace: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getLong("group_id")
val msgId = session.getIntOrNull("msg_id") ?: session.getInt("message_id")
val faceId = session.getInt("face_id")
val isSet = session.getBooleanOrDefault("is_set", true)
return invoke(groupId, msgId, faceId, isSet, session.echo)
}
operator fun invoke(groupId: Long, msgHash: Int, faceIndex: Int, isSet: Boolean, echo: JsonElement = EmptyJsonString): String {
val mapping = MessageHelper.getMsgMappingByHash(msgHash)
?: return error("failed to locate message", echo = echo)
ChatSvc.setGroupMessageCommentFace(groupId, mapping.msgSeq.toULong(), faceIndex.toString(), isSet)
return ok("success", echo = echo)
}
override val requiredParams: Array<String> = arrayOf("group_id", "face_id")
}

View File

@ -1,19 +1,30 @@
package moe.fuqiuluo.shamrock.remote.api
import io.ktor.http.ContentType
import moe.fuqiuluo.shamrock.helper.LogicException
import io.ktor.server.application.call
import io.ktor.server.response.respondText
import io.ktor.server.routing.Routing
import moe.fuqiuluo.shamrock.remote.action.ActionManager
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.handlers.*
import moe.fuqiuluo.shamrock.tools.fetch
import moe.fuqiuluo.shamrock.tools.fetchGetOrNull
import moe.fuqiuluo.shamrock.tools.fetchOrNull
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
import moe.fuqiuluo.shamrock.tools.getOrPost
fun Routing.troopAction() {
getOrPost("/set_group_comment_face") {
val groupId = fetchOrThrow("group_id").toLong()
val msgId = fetchOrNull("msg_id")?.toIntOrNull() ?: fetchOrThrow("message_id").toInt()
val faceId = fetchOrThrow("face_id").toInt()
val isSet = fetchGetOrNull("is_set") ?: "true"
call.respondText(SetGroupCommentFace(groupId, msgId, faceId, when(isSet) {
"true" -> true
"false" -> false
"1" -> true
"0" -> false
else -> true
}), ContentType.Application.Json)
}
getOrPost("/get_not_joined_group_info") {
val groupId = fetchOrThrow("group_id")
call.respondText(GetNotJoinedGroupInfo(groupId), ContentType.Application.Json)

View File

@ -16,7 +16,7 @@ import moe.fuqiuluo.shamrock.xposed.ipc.ShamrockIpc
import moe.fuqiuluo.symbols.Process
import moe.fuqiuluo.symbols.XposedHook
@XposedHook(Process.MSF, 0)
@XposedHook(Process.MSF, priority = 0)
internal class IpcService: IAction {
override fun invoke(ctx: Context) {
if (!PlatformUtils.isMsfProcess()) return