diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/app/ShamrockConfig.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/app/ShamrockConfig.kt index f978d16..00ead85 100644 --- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/app/ShamrockConfig.kt +++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/app/ShamrockConfig.kt @@ -249,6 +249,11 @@ object ShamrockConfig { return preferences.getBoolean("enable_auto_start", false) } + fun disableAutoSyncSetting(ctx: Context): Boolean { + val preferences = ctx.getSharedPreferences("config", 0) + return preferences.getBoolean("disable_auto_sync_setting", false) + } + fun enableAliveReply(ctx: Context): Boolean { val preferences = ctx.getSharedPreferences("config", 0) return preferences.getBoolean("alive_reply", false) @@ -264,6 +269,11 @@ object ShamrockConfig { preferences.edit().putBoolean("enable_auto_start", v).apply() } + fun setDisableAutoSyncSetting(ctx: Context, v: Boolean) { + val preferences = ctx.getSharedPreferences("config", 0) + preferences.edit().putBoolean("disable_auto_sync_setting", v).apply() + } + fun setAliveReply(ctx: Context, v: Boolean) { val preferences = ctx.getSharedPreferences("config", 0) preferences.edit().putBoolean("alive_reply", v).apply() @@ -322,6 +332,7 @@ object ShamrockConfig { "shell" to preferences.getBoolean("shell", false), "alive_reply" to preferences.getBoolean("alive_reply", false), "enable_sync_msg_as_sent_msg" to preferences.getBoolean("enable_sync_msg_as_sent_msg", false), + "disable_auto_sync_setting" to preferences.getBoolean("disable_auto_sync_setting", false), ) } diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt index 6019dfd..b8b1ee4 100644 --- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt +++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt @@ -91,7 +91,7 @@ fun LabFragment() { ActionBox( modifier = Modifier.padding(top = 12.dp), painter = painterResource(id = R.drawable.round_logo_dev_24), - title = "实验功能" + title = "基础设置" ) { color -> Column { Divider( @@ -142,6 +142,16 @@ fun LabFragment() { return@Function true } + Function( + title = "禁止Shamrock同步设置", + desc = "禁止Shamrock同步设置,防止恢复手动修改后的配置文件。", + descColor = color, + isSwitch = ShamrockConfig.disableAutoSyncSetting(ctx) + ) { + ShamrockConfig.setDisableAutoSyncSetting(ctx, it) + return@Function true + } + kotlin.runCatching { ctx.getSharedPreferences("shared_config", Context.MODE_WORLD_READABLE) }.onSuccess { diff --git a/protobuf/src/main/java/moe/whitechi73/protobuf/oidb/cmd0xfc2/Oidb0xfc2.kt b/protobuf/src/main/java/moe/whitechi73/protobuf/oidb/cmd0xfc2/Oidb0xfc2.kt new file mode 100644 index 0000000..6113fea --- /dev/null +++ b/protobuf/src/main/java/moe/whitechi73/protobuf/oidb/cmd0xfc2/Oidb0xfc2.kt @@ -0,0 +1,61 @@ +@file:OptIn(ExperimentalSerializationApi::class) +package moe.whitechi73.protobuf.oidb.cmd0xfc2 + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber + +@Serializable +data class Oidb0xfc2ReqBody( + @ProtoNumber(1) var msgCmd: Int? = null, + @ProtoNumber(3) var msgBusType: Int? = null, + @ProtoNumber(4) var msgChannelInfo: Oidb0xfc2ChannelInfo? = null, + @ProtoNumber(5) var msgTerminalType: Int? = null, + //@ProtoNumber(100) var msg_apply_upload_req: Any? = null, + //@ProtoNumber(200) var msg_upload_completed_req: Any? = null, + @ProtoNumber(300) var msgApplyDownloadReq: Oidb0xfc2MsgApplyDownloadReq? = null, + //@ProtoNumber(400) var msg_apply_preview_req: Any? = null, + //@ProtoNumber(500) var msg_apply_security_strike_req: Any? = null, +) + +@Serializable +data class Oidb0xfc2RspBody( + @ProtoNumber(1) var msgCmd: Int? = null, + @ProtoNumber(5) var msgBusType: Int? = null, + //@ProtoNumber(110) var msg_apply_upload_rsp: Any? = null, + //@ProtoNumber(210) var msg_upload_completed_rsp: Any? = null, + @ProtoNumber(310) var msgApplyDownloadRsp: Oidb0xfc2MsgApplyDownloadRsp? = null, + //@ProtoNumber(410) var msg_apply_preview_rsp: Any? = null, + //@ProtoNumber(510) var msg_apply_security_strike_rsp: Any? = null, +) + +@Serializable +data class Oidb0xfc2MsgApplyDownloadRsp( + @ProtoNumber(1) var msgDownloadInfo: Oidb0xfc2MsgDownloadInfo? = null, + //@ProtoNumber(2) var msgFileInfo: Any? = null, + //@ProtoNumber(3) var msgChacha20Param: Any? = null, + //@ProtoNumber(4) var useEncrypt: UInt = UInt.MIN_VALUE, +) + +@Serializable +data class Oidb0xfc2MsgDownloadInfo( + @ProtoNumber(1) var downloadKey: ByteArray? = null, + //@ProtoNumber(2) var msg_out_addr: Any? = null, + //@ProtoNumber(3) var msg_inner_addr: Any? = null, + //@ProtoNumber(4) var msg_out_addr_ipv6: Any? = null, + @ProtoNumber(5) var downloadDomain: String? = null, + @ProtoNumber(6) var downloadUrl: String? = null, + //@ProtoNumber(7) var str_cookie: Any? = null, +) + +@Serializable +data class Oidb0xfc2MsgApplyDownloadReq( + @ProtoNumber(1) val fieldId: String, + @ProtoNumber(2) val supportEncrypt: Int, +) + +@Serializable +data class Oidb0xfc2ChannelInfo( + @ProtoNumber(3) val guildId: ULong, + @ProtoNumber(4) val channelId: ULong, +) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageConvert.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageConvert.kt index 361c4be..e02dac5 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageConvert.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageConvert.kt @@ -32,12 +32,12 @@ internal suspend fun MsgRecord.toCQCode(): String { return MessageConvert.convertMessageRecordToCQCode(this) } -internal suspend fun List.toSegments(chatType: Int, peerId: String): MessageSegmentList { - return MessageConvert.convertMessageElementsToMsgSegment(chatType, this, peerId) +internal suspend fun List.toSegments(chatType: Int, peerId: String, subPeer: String): MessageSegmentList { + return MessageConvert.convertMessageElementsToMsgSegment(chatType, this, peerId, subPeer) } -internal suspend fun List.toCQCode(chatType: Int, peerId: String): String { - return MessageConvert.convertMsgElementsToCQCode(this, chatType, peerId) +internal suspend fun List.toCQCode(chatType: Int, peerId: String, subPeer: String): String { + return MessageConvert.convertMsgElementsToCQCode(this, chatType, peerId, subPeer) } @@ -64,14 +64,15 @@ internal object MessageConvert { suspend fun convertMessageElementsToMsgSegment( chatType: Int, elements: List, - peerId: String + peerId: String, + subPeer: String ): ArrayList { val messageData = arrayListOf() elements.forEach { msg -> kotlin.runCatching { val elementId = msg.elementType val converter = convertMap[elementId] - converter?.convert(chatType, peerId, msg) + converter?.convert(chatType, peerId, subPeer, msg) ?: throw UnsupportedOperationException("不支持的消息element类型:$elementId") }.onSuccess { messageData.add(it) @@ -87,35 +88,45 @@ internal object MessageConvert { } suspend fun convertMessageRecordToMsgSegment(record: MsgRecord, chatType: Int = record.chatType): ArrayList { - return convertMessageElementsToMsgSegment(chatType, record.elements, record.peerUin.toString()) + val peerId = when(chatType) { + MsgConstant.KCHATTYPEGUILD -> record.guildId + else -> record.peerUin.toString() + } + return convertMessageElementsToMsgSegment(chatType, record.elements, peerId, record.channelId ?: peerId) } suspend fun convertMsgElementsToCQCode( elements: List, chatType: Int, - peerId: String + peerId: String, + subPeer: String ): String { if(elements.isEmpty()) { return "" } - val msgList = convertMessageElementsToMsgSegment(chatType, elements, peerId).map { + val msgList = convertMessageElementsToMsgSegment(chatType, elements, peerId, subPeer).map { it.toJson() } return MessageHelper.encodeCQCode(msgList) } suspend fun convertMessageRecordToCQCode(record: MsgRecord, chatType: Int = record.chatType): String { + val peerId = when(chatType) { + MsgConstant.KCHATTYPEGUILD -> record.guildId + else -> record.peerUin.toString() + } return MessageHelper.encodeCQCode( convertMessageElementsToMsgSegment( chatType, record.elements, - record.peerUin.toString() + peerId, + record.channelId ?: peerId ).map { it.toJson() } ) } } internal fun interface IMessageConvert { - suspend fun convert(chatType: Int, peerId: String, element: MsgElement): MessageSegment + suspend fun convert(chatType: Int, peerId: String, subPeer: String, element: MsgElement): MessageSegment } diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageElemConverter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageElemConverter.kt index 25f4b5f..b7d810b 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageElemConverter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageElemConverter.kt @@ -19,7 +19,12 @@ internal sealed class MessageElemConverter: IMessageConvert { * 文本 / 艾特 消息转换消息段 */ data object TextConverter: MessageElemConverter() { - override suspend fun convert(chatType: Int, peerId: String, element: MsgElement): MessageSegment { + override suspend fun convert( + chatType: Int, + peerId: String, + subPeer: String, + element: MsgElement + ): MessageSegment { val text = element.textElement return if (text.atType != MsgConstant.ATTYPEUNKNOWN) { MessageSegment( @@ -43,7 +48,12 @@ internal sealed class MessageElemConverter: IMessageConvert { * 小表情 / 戳一戳 消息转换消息段 */ data object FaceConverter: MessageElemConverter() { - override suspend fun convert(chatType: Int, peerId: String, element: MsgElement): MessageSegment { + override suspend fun convert( + chatType: Int, + peerId: String, + subPeer: String, + element: MsgElement + ): MessageSegment { val face = element.faceElement if (face.faceType == 5) { @@ -112,6 +122,7 @@ internal sealed class MessageElemConverter: IMessageConvert { override suspend fun convert( chatType: Int, peerId: String, + subPeer: String, element: MsgElement ): MessageSegment { val image = element.picElement @@ -131,6 +142,7 @@ internal sealed class MessageElemConverter: IMessageConvert { "url" to when(chatType) { MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(md5) MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(md5) + MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(md5) else -> unknownChatType(chatType) }, "subType" to image.picSubType, @@ -147,6 +159,7 @@ internal sealed class MessageElemConverter: IMessageConvert { override suspend fun convert( chatType: Int, peerId: String, + subPeer: String, element: MsgElement ): MessageSegment { val record = element.pttElement @@ -162,6 +175,7 @@ internal sealed class MessageElemConverter: IMessageConvert { "url" to when(chatType) { MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPttDownUrl("0", record.md5HexStr, record.fileUuid) MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid) + MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl("0", record.md5HexStr, record.fileUuid) else -> unknownChatType(chatType) } ).also { @@ -183,6 +197,7 @@ internal sealed class MessageElemConverter: IMessageConvert { override suspend fun convert( chatType: Int, peerId: String, + subPeer: String, element: MsgElement ): MessageSegment { val video = element.videoElement @@ -195,6 +210,7 @@ internal sealed class MessageElemConverter: IMessageConvert { "url" to when(chatType) { MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid) MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid) + MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl(peerId, md5, video.fileUuid) else -> unknownChatType(chatType) } ).also { @@ -212,6 +228,7 @@ internal sealed class MessageElemConverter: IMessageConvert { override suspend fun convert( chatType: Int, peerId: String, + subPeer: String, element: MsgElement ): MessageSegment { val face = element.marketFaceElement @@ -235,6 +252,7 @@ internal sealed class MessageElemConverter: IMessageConvert { override suspend fun convert( chatType: Int, peerId: String, + subPeer: String, element: MsgElement ): MessageSegment { val data = element.arkElement.bytesData.asJsonObject @@ -297,6 +315,7 @@ internal sealed class MessageElemConverter: IMessageConvert { override suspend fun convert( chatType: Int, peerId: String, + subPeer: String, element: MsgElement ): MessageSegment { val reply = element.replyElement @@ -329,6 +348,7 @@ internal sealed class MessageElemConverter: IMessageConvert { override suspend fun convert( chatType: Int, peerId: String, + subPeer: String, element: MsgElement ): MessageSegment { val tip = element.grayTipElement @@ -364,6 +384,7 @@ internal sealed class MessageElemConverter: IMessageConvert { override suspend fun convert( chatType: Int, peerId: String, + subPeer: String, element: MsgElement ): MessageSegment { val fileMsg = element.fileElement @@ -373,8 +394,11 @@ internal sealed class MessageElemConverter: IMessageConvert { val fileId = fileMsg.fileUuid val bizId = fileMsg.fileBizId ?: 0 val fileSubId = fileMsg.fileSubId ?: "" - val url = if (chatType == MsgConstant.KCHATTYPEC2C) RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId) - else RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId) + val url = when (chatType) { + MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId) + MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(peerId, subPeer, fileId, bizId) + else -> RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId) + } return MessageSegment( type = "file", @@ -398,6 +422,7 @@ internal sealed class MessageElemConverter: IMessageConvert { override suspend fun convert( chatType: Int, peerId: String, + subPeer: String, element: MsgElement ): MessageSegment { val multiMsg = element.multiForwardMsgElement @@ -414,6 +439,7 @@ internal sealed class MessageElemConverter: IMessageConvert { override suspend fun convert( chatType: Int, peerId: String, + subPeer: String, element: MsgElement ): MessageSegment { val longMsg = element.structLongMsgElement @@ -430,6 +456,7 @@ internal sealed class MessageElemConverter: IMessageConvert { override suspend fun convert( chatType: Int, peerId: String, + subPeer: String, element: MsgElement ): MessageSegment { val markdown = element.markdownElement @@ -446,6 +473,7 @@ internal sealed class MessageElemConverter: IMessageConvert { override suspend fun convert( chatType: Int, peerId: String, + subPeer: String, element: MsgElement ): MessageSegment { val bubbleElement = element.faceBubbleElement diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt index fb51f2f..5d6dfee 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/transfile/RichProtoSvc.kt @@ -1,3 +1,4 @@ +@file:OptIn(ExperimentalSerializationApi::class) package moe.fuqiuluo.qqinterface.servlet.transfile import com.tencent.mobileqq.pb.ByteStringMicro @@ -6,6 +7,10 @@ import com.tencent.mobileqq.transfile.api.IProtoReqManager import com.tencent.mobileqq.transfile.protohandler.RichProto import com.tencent.mobileqq.transfile.protohandler.RichProtoProc import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.encodeToByteArray +import kotlinx.serialization.protobuf.ProtoBuf import moe.fuqiuluo.qqinterface.servlet.BaseSvc import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter @@ -14,6 +19,10 @@ import moe.fuqiuluo.shamrock.tools.slice import moe.fuqiuluo.shamrock.tools.toHexString import moe.fuqiuluo.shamrock.utils.PlatformUtils import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher +import moe.whitechi73.protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo +import moe.whitechi73.protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq +import moe.whitechi73.protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody +import moe.whitechi73.protobuf.oidb.cmd0xfc2.Oidb0xfc2RspBody import mqq.app.MobileQQ import tencent.im.cs.cmd0x346.cmd0x346 import tencent.im.oidb.cmd0x6d6.oidb_0x6d6 @@ -22,6 +31,30 @@ import tencent.im.oidb.oidb_sso import kotlin.coroutines.resume internal object RichProtoSvc: BaseSvc() { + suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String { + val buffer = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, ProtoBuf.encodeToByteArray(Oidb0xfc2ReqBody( + msgCmd = 1200, + msgBusType = 4202, + msgChannelInfo = Oidb0xfc2ChannelInfo( + guildId = peerId.toULong(), + channelId = channelId.toULong() + ), + msgTerminalType = 2, + msgApplyDownloadReq = Oidb0xfc2MsgApplyDownloadReq( + fieldId = fileId, + supportEncrypt = 0 + ) + ))) ?: return "" + val body = oidb_sso.OIDBSSOPkg() + body.mergeFrom(buffer.slice(4)) + ProtoBuf.decodeFromByteArray(body.bytes_bodybuffer.get().toByteArray()).msgApplyDownloadRsp?.let { + it.msgDownloadInfo?.let { + return "https://${it.downloadDomain}${it.downloadUrl}&fname=$fileId&isthumb=0" + } + } + return "" + } + suspend fun getGroupFileDownUrl( peerId: Long, fileId: String, @@ -34,27 +67,23 @@ internal object RichProtoSvc: BaseSvc() { uint32_bus_id.set(bizId) str_file_id.set(fileId) }) - }.toByteArray()) - if (buffer == null) { + }.toByteArray()) ?: return "" + val body = oidb_sso.OIDBSSOPkg() + body.mergeFrom(buffer.slice(4)) + val result = oidb_0x6d6.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) + if (body.uint32_result.get() != 0 + || result.download_file_rsp.int32_ret_code.get() != 0) { return "" - } else { - val body = oidb_sso.OIDBSSOPkg() - body.mergeFrom(buffer.slice(4)) - val result = oidb_0x6d6.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) - if (body.uint32_result.get() != 0 - || result.download_file_rsp.int32_ret_code.get() != 0) { - return "" - } - - val domain = if (!result.download_file_rsp.str_download_dns.has()) - ("https://" + result.download_file_rsp.str_download_ip.get()) - else ("http://" + result.download_file_rsp.str_download_dns.get()) - val downloadUrl = result.download_file_rsp.bytes_download_url.get().toByteArray().toHexString() - val appId = MobileQQ.getMobileQQ().appId - val version = PlatformUtils.getQQVersion(MobileQQ.getContext()) - - return "$domain/ftn_handler/$downloadUrl/?fname=$fileId&client_proto=qq&client_appid=$appId&client_type=android&client_ver=$version&client_down_type=auto&client_aio_type=unk" } + + val domain = if (!result.download_file_rsp.str_download_dns.has()) + ("https://" + result.download_file_rsp.str_download_ip.get()) + else ("http://" + result.download_file_rsp.str_download_dns.get()) + val downloadUrl = result.download_file_rsp.bytes_download_url.get().toByteArray().toHexString() + val appId = MobileQQ.getMobileQQ().appId + val version = PlatformUtils.getQQVersion(MobileQQ.getContext()) + + return "$domain/ftn_handler/$downloadUrl/?fname=$fileId&client_proto=qq&client_appid=$appId&client_type=android&client_ver=$version&client_down_type=auto&client_aio_type=unk" } suspend fun getC2CFileDownUrl( @@ -83,7 +112,7 @@ internal object RichProtoSvc: BaseSvc() { }.toByteArray()) if (buffer == null) { - if (retryCnt < 3) { + if (retryCnt < 5) { return getC2CFileDownUrl(fileId, subId, retryCnt + 1) } return "" @@ -122,6 +151,10 @@ internal object RichProtoSvc: BaseSvc() { return "https://c2cpicdw.qpic.cn/offpic_new/0/123-0-${md5.uppercase()}/0?term=2" } + fun getGuildPicDownUrl(md5: String): String { + return "https://gchat.qpic.cn/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2" + } + suspend fun getC2CVideoDownUrl( peerId: String, md5Hex: String, @@ -287,4 +320,11 @@ internal object RichProtoSvc: BaseSvc() { } } + suspend fun getGuildPttDownUrl( + peerId: String, + md5Hex: String, + fileUUId: String + ): String { + return "unsupported" + } } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt index f066005..8664b6d 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt @@ -308,6 +308,7 @@ internal object MessageHelper { MsgConstant.KCHATTYPEGROUP -> "grp$msgId" MsgConstant.KCHATTYPEC2C -> "c2c$msgId" MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> "tmpgrp$msgId" + MsgConstant.KCHATTYPEGUILD -> "guild$msgId" else -> error("不支持的消息来源类型 | generateMsgIdHash: $chatType") } return abs(key.hashCode()) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/GlobalEventTransmitter.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/GlobalEventTransmitter.kt index d11add6..7f631cb 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/GlobalEventTransmitter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/GlobalEventTransmitter.kt @@ -74,7 +74,7 @@ internal object GlobalEventTransmitter: BaseSvc() { peerId = uin, userId = record.senderUin, message = if(ShamrockConfig.useCQ()) rawMsg.json - else elements.toSegments(record.chatType, record.peerUin.toString()).map { + else elements.toSegments(record.chatType, record.peerUin.toString(), "0").map { it.toJson() }.json, rawMessage = rawMsg, @@ -129,7 +129,7 @@ internal object GlobalEventTransmitter: BaseSvc() { peerId = botUin, userId = record.senderUin, message = if(ShamrockConfig.useCQ()) rawMsg.json - else elements.toSegments(record.chatType, record.peerUin.toString()).map { + else elements.toSegments(record.chatType, record.peerUin.toString(), "0").map { it.toJson() }.json, rawMessage = rawMsg, @@ -147,6 +147,53 @@ internal object GlobalEventTransmitter: BaseSvc() { ) return true } + + /** + * 推送私聊消息 + */ + suspend fun transGuildMessage( + record: MsgRecord, + elements: ArrayList, + rawMsg: String, + msgHash: Int, + postType: PostType, + ): Boolean { + val botUin = app.longAccountUin + var nickName = record.sendNickName + if (nickName.isNullOrEmpty()) { + CardSvc.getProfileCard(record.senderUin.toString()).onSuccess { + nickName = it.strNick ?: record.peerName + } + } + transMessageEvent(record, + MessageEvent( + time = record.msgTime, + selfId = botUin, + postType = postType, + messageType = MsgType.Guild, + subType = MsgSubType.Channel, + messageId = msgHash, + targetId = record.peerUin, + peerId = botUin, + userId = record.senderUid.toLong(), + message = if(ShamrockConfig.useCQ()) rawMsg.json + else elements.toSegments(record.chatType, record.guildId, record.channelId).map { + it.toJson() + }.json, + rawMessage = rawMsg, + font = 0, + sender = Sender( + userId = record.senderUid.toLong(), + nickname = nickName, + card = record.sendMemberName, + role = MemberRole.Member, + title = record.sendNickName, + level = record.roleId.toString(), + ), + ) + ) + return true + } } /** diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/ShamrockConfig.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/ShamrockConfig.kt index 80ad947..f7fe44e 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/ShamrockConfig.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/ShamrockConfig.kt @@ -39,19 +39,46 @@ internal object ShamrockConfig { fun updateConfig(intent: Intent) { val mmkv = MMKVFetcher.mmkvWithId("shamrock_config") mmkv.apply { - putBoolean( "tablet", intent.getBooleanExtra("tablet", false)) // 强制平板模式 - putInt( "port", intent.getIntExtra("port", 5700)) // 主动HTTP端口 - putBoolean( "ws", intent.getBooleanExtra("ws", false)) // 主动WS开关 - putBoolean( "http", intent.getBooleanExtra("http", false)) // HTTP回调开关 - putString( "http_addr", intent.getStringExtra("http_addr")) // WebHook回调地址 - putBoolean( "ws_client", intent.getBooleanExtra("ws_client", false)) // 被动WS开关 - putBoolean( "use_cqcode", intent.getBooleanExtra("use_cqcode", false)) // 使用CQ码 - putBoolean( "inject_packet", intent.getBooleanExtra("inject_packet", false)) // 拦截无用包 - putBoolean( "debug", intent.getBooleanExtra("debug", false)) // 调试模式 - + if (!intent.getBooleanExtra("disable_auto_sync_setting", false)) { + putBoolean( + "tablet", + intent.getBooleanExtra("tablet", false) + ) // 强制平板模式 + putInt("port", intent.getIntExtra("port", 5700)) // 主动HTTP端口 + putBoolean("ws", intent.getBooleanExtra("ws", false)) // 主动WS开关 + putBoolean( + "http", + intent.getBooleanExtra("http", false) + ) // HTTP回调开关 + putString( + "http_addr", + intent.getStringExtra("http_addr") + ) // WebHook回调地址 + putBoolean( + "ws_client", + intent.getBooleanExtra("ws_client", false) + ) // 被动WS开关 + putBoolean( + "use_cqcode", + intent.getBooleanExtra("use_cqcode", false) + ) // 使用CQ码 + putBoolean( + "inject_packet", + intent.getBooleanExtra("inject_packet", false) + ) // 拦截无用包 + putBoolean("debug", intent.getBooleanExtra("debug", false)) // 调试模式 + putString( "key_store", intent.getStringExtra("key_store")) // 证书路径 + putString( "ssl_pwd", intent.getStringExtra("ssl_pwd")) // 证书密码 + putString( "ssl_private_pwd", intent.getStringExtra("ssl_private_pwd")) // 证书私钥密码 + putString( "ssl_alias", intent.getStringExtra("ssl_alias")) // 证书别名 + putInt( "ssl_port", intent.getIntExtra("ssl_port", 5701)) // 主动HTTP端口 + putBoolean("alive_reply", intent.getBooleanExtra("alive_reply", false)) // 自回复测试 + putBoolean("enable_self_msg", intent.getBooleanExtra("enable_self_msg", false)) // 推送自己发的消息 + putBoolean("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口 + putBoolean("enable_sync_msg_as_sent_msg", intent.getBooleanExtra("enable_sync_msg_as_sent_msg", false)) // 推送同步消息 + } Config.defaultToken = intent.getStringExtra("token") Config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true) - val wsPort = intent.getIntExtra("ws_port", 5800) Config.activeWebSocket = if (Config.activeWebSocket == null) ConnectionConfig( address = "0.0.0.0", @@ -59,28 +86,17 @@ internal object ShamrockConfig { ) else Config.activeWebSocket?.also { it.port = wsPort } - Config.passiveWebSocket = intent.getStringExtra("ws_addr")?.split(",", "|", ",")?.filter { address -> address.isNotBlank() && (address.startsWith("ws://") || address.startsWith("wss://")) }?.map { ConnectionConfig(address = it) }?.toMutableList() - putString( "key_store", intent.getStringExtra("key_store")) // 证书路径 - putString( "ssl_pwd", intent.getStringExtra("ssl_pwd")) // 证书密码 - putString( "ssl_private_pwd", intent.getStringExtra("ssl_private_pwd")) // 证书私钥密码 - putString( "ssl_alias", intent.getStringExtra("ssl_alias")) // 证书别名 - putInt( "ssl_port", intent.getIntExtra("ssl_port", 5701)) // 主动HTTP端口 - - putBoolean("alive_reply", intent.getBooleanExtra("alive_reply", false)) // 自回复测试 - - putBoolean("enable_self_msg", intent.getBooleanExtra("enable_self_msg", false)) // 推送自己发的消息 - putBoolean("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口 - putBoolean("enable_sync_msg_as_sent_msg", intent.getBooleanExtra("enable_sync_msg_as_sent_msg", false)) // 推送同步消息 - putBoolean("isInit", true) } - updateConfig() + if (!intent.getBooleanExtra("disable_auto_sync_setting", false)) { + updateConfig() + } } private val mmkv: MMKV diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/MessageEvent.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/MessageEvent.kt index 9ccf17c..1046610 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/MessageEvent.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/MessageEvent.kt @@ -21,12 +21,15 @@ internal enum class MsgSubType { @SerialName("group") GroupLess, @SerialName("friend") Friend, @SerialName("other") Other, + + @SerialName("channel") Channel } @Serializable internal enum class MsgType { @SerialName("group") Group, - @SerialName("private") Private + @SerialName("private") Private, + @SerialName("guild") Guild } @Serializable diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt index 83bf27d..a327147 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt @@ -38,8 +38,6 @@ internal object AioListener : IKernelMsgListener { private suspend fun handleMsg(record: MsgRecord) { try { - if (record.chatType == MsgConstant.KCHATTYPEGUILD) return // TODO: 频道消息暂不处理 - messageLessListenerMap.firstNotNullOfOrNull { if (it.key == record.msgSeq) it else null }?.let { @@ -60,7 +58,12 @@ internal object AioListener : IKernelMsgListener { time = record.msgTime ) - val rawMsg = record.elements.toCQCode(record.chatType, record.peerUin.toString()) + val peerId = when(record.chatType) { + MsgConstant.KCHATTYPEGUILD -> record.guildId + else -> record.peerUin.toString() + } + + val rawMsg = record.elements.toCQCode(record.chatType, peerId, record.channelId ?: peerId) if (rawMsg.isEmpty()) return if (ShamrockConfig.aliveReply() && rawMsg == "ping") { @@ -119,6 +122,16 @@ internal object AioListener : IKernelMsgListener { LogCenter.log("私聊临时消息推送失败 -> MessageTransmitter", Level.WARN) } } + + MsgConstant.KCHATTYPEGUILD -> { + LogCenter.log("频道消息(guildId = ${record.guildId}, id = [$msgHash | ${record.msgId}], msg = $rawMsg)") + if(!GlobalEventTransmitter.MessageTransmitter + .transGuildMessage(record, record.elements, rawMsg, msgHash, postType = postType) + ) { + LogCenter.log("频道消息推送失败 -> MessageTransmitter", Level.WARN) + } + } + else -> LogCenter.log("不支持PUSH事件: ${record.chatType}") } } catch (e: Throwable) { @@ -190,7 +203,12 @@ internal object AioListener : IKernelMsgListener { || record.peerUin == TicketSvc.getLongUin() ) return@launch - val rawMsg = record.elements.toCQCode(record.chatType, record.peerUin.toString()) + val peerId = when(record.chatType) { + MsgConstant.KCHATTYPEGUILD -> record.guildId + else -> record.peerUin.toString() + } + + val rawMsg = record.elements.toCQCode(record.chatType, peerId, record.channelId ?: peerId) if (rawMsg.isEmpty()) return@launch LogCenter.log("自发消息(target = ${record.peerUin}, id = $msgHash, msg = $rawMsg)")