diff --git a/annotations/src/main/java/kritor/service/Grpc.kt b/annotations/src/main/java/kritor/service/Grpc.kt index 0a66477e..2d6fe33d 100644 --- a/annotations/src/main/java/kritor/service/Grpc.kt +++ b/annotations/src/main/java/kritor/service/Grpc.kt @@ -1,7 +1,11 @@ package kritor.service +import kotlin.reflect.KClass + +@Retention(AnnotationRetention.SOURCE) @Target(AnnotationTarget.FUNCTION) annotation class Grpc( val serviceName: String, - val funcName: String -) \ No newline at end of file + val funcName: String, + +) diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt index 68daf10c..2b3bfa20 100644 --- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt +++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt @@ -118,9 +118,7 @@ private fun APIInfoCard( text = rpcAddress, hint = "请输入回调地址", error = "输入的地址不合法", - checker = { - it.isEmpty() || it.contains(":") - }, + checker = { true }, confirm = { ShamrockConfig[ctx, RPCAddress] = rpcAddress.value AppRuntime.log("设置回调RPC地址为[${rpcAddress.value}]。") diff --git a/processor/src/main/java/moe/fuqiuluo/ksp/impl/GrpcProcessor.kt b/processor/src/main/java/moe/fuqiuluo/ksp/impl/GrpcProcessor.kt new file mode 100644 index 00000000..adb69ec2 --- /dev/null +++ b/processor/src/main/java/moe/fuqiuluo/ksp/impl/GrpcProcessor.kt @@ -0,0 +1,96 @@ +@file:Suppress("UNCHECKED_CAST") +@file:OptIn(KspExperimental::class) + +package moe.fuqiuluo.ksp.impl + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getAnnotationsByType +import com.google.devtools.ksp.getClassDeclarationByName +import com.google.devtools.ksp.getJavaClassByName +import com.google.devtools.ksp.getKotlinClassByName +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSTypeParameter +import com.google.devtools.ksp.symbol.Modifier +import com.google.devtools.ksp.validate +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import kritor.service.Grpc + +class GrpcProcessor( + private val codeGenerator: CodeGenerator, + private val logger: KSPLogger +): SymbolProcessor { + private val subPackage = arrayOf("contact", "core", "file", "friend", "group", "message", "web") + + override fun process(resolver: Resolver): List { + val symbols = resolver.getSymbolsWithAnnotation(Grpc::class.qualifiedName!!) + val actions = (symbols as Sequence).toList() + + if (actions.isEmpty()) return emptyList() + + // 怎么返回nullable的结果 + val packageName = "kritor.handlers" + val funcBuilder = FunSpec.builder("handleGrpc") + .addModifiers(KModifier.SUSPEND) + .addParameter("cmd", String::class) + .addParameter("data", ByteArray::class) + .returns(ByteArray::class) + val fileSpec = FileSpec.scriptBuilder("AutoGrpcHandlers", packageName) + + logger.warn("Found ${actions.size} grpc-actions") + + //logger.error(resolver.getClassDeclarationByName("io.kritor.AuthReq").toString()) + //logger.error(resolver.getJavaClassByName("io.kritor.AuthReq").toString()) + //logger.error(resolver.getKotlinClassByName("io.kritor.AuthReq").toString()) + + actions.forEach { action -> + val methodName = action.qualifiedName?.asString()!! + val grpcMethod = action.getAnnotationsByType(Grpc::class).first() + val service = grpcMethod.serviceName + val funcName = grpcMethod.funcName + funcBuilder.addStatement("if (cmd == \"${service}.${funcName}\") {\t") + + val reqType = action.parameters[0].type.toString() + val rspType = action.returnType.toString() + funcBuilder.addStatement("val resp: $rspType = $methodName($reqType.parseFrom(data))") + funcBuilder.addStatement("return resp.toByteArray()") + + funcBuilder.addStatement("}") + } + funcBuilder.addStatement("return EMPTY_BYTE_ARRAY") + fileSpec + .addStatement("import io.kritor.*") + .addStatement("import io.kritor.core.*") + .addStatement("import io.kritor.contact.*") + .addStatement("import io.kritor.group.*") + .addStatement("import io.kritor.friend.*") + .addStatement("import io.kritor.file.*") + .addStatement("import io.kritor.message.*") + .addStatement("import io.kritor.web.*") + .addFunction(funcBuilder.build()) + .addImport("moe.fuqiuluo.symbols", "EMPTY_BYTE_ARRAY") + runCatching { + codeGenerator.createNewFile( + dependencies = Dependencies(aggregating = false), + packageName = packageName, + fileName = fileSpec.name + ).use { outputStream -> + outputStream.writer().use { + fileSpec.build().writeTo(it) + } + } + } + + return emptyList() + } +} \ No newline at end of file diff --git a/processor/src/main/java/moe/fuqiuluo/ksp/providers/GrpcProvider.kt b/processor/src/main/java/moe/fuqiuluo/ksp/providers/GrpcProvider.kt new file mode 100644 index 00000000..008110cb --- /dev/null +++ b/processor/src/main/java/moe/fuqiuluo/ksp/providers/GrpcProvider.kt @@ -0,0 +1,17 @@ +package moe.fuqiuluo.ksp.providers + +import com.google.auto.service.AutoService +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import moe.fuqiuluo.ksp.impl.GrpcProcessor + +@AutoService(SymbolProcessorProvider::class) +class GrpcProvider: SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return GrpcProcessor( + environment.codeGenerator, + environment.logger + ) + } +} \ No newline at end of file diff --git a/xposed/src/main/assets/config.properties b/xposed/src/main/assets/config.properties index 6505ef1f..b715d29e 100644 --- a/xposed/src/main/assets/config.properties +++ b/xposed/src/main/assets/config.properties @@ -34,7 +34,7 @@ active_ticket= enable_self_message=false # 旧BDH兼容开关 -enable_old_bdh=false +enable_old_bdh=true # 反JVM调用栈跟踪 anti_jvm_trace=true diff --git a/xposed/src/main/java/kritor/client/KritorClient.kt b/xposed/src/main/java/kritor/client/KritorClient.kt new file mode 100644 index 00000000..c31a0f11 --- /dev/null +++ b/xposed/src/main/java/kritor/client/KritorClient.kt @@ -0,0 +1,142 @@ +@file:OptIn(DelicateCoroutinesApi::class) +package kritor.client + +import com.google.protobuf.ByteString +import io.grpc.ManagedChannel +import io.grpc.ManagedChannelBuilder +import io.kritor.ReverseServiceGrpcKt +import io.kritor.event.EventServiceGrpcKt +import io.kritor.event.EventType +import io.kritor.event.eventStructure +import io.kritor.event.messageEvent +import io.kritor.reverse.ReqCode +import io.kritor.reverse.Request +import io.kritor.reverse.Response +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.launch +import kritor.handlers.handleGrpc +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter +import kotlin.time.Duration.Companion.seconds + +internal class KritorClient( + val host: String, + val port: Int +) { + private lateinit var channel: ManagedChannel + + private lateinit var channelJob: Job + private val senderChannel = MutableSharedFlow() + + fun start() { + runCatching { + if (::channel.isInitialized && isActive()){ + channel.shutdown() + } + channel = ManagedChannelBuilder + .forAddress(host, port) + .usePlaintext() + .enableRetry() // 允许尝试 + .executor(Dispatchers.IO.asExecutor()) // 使用协程的调度器 + .build() + }.onFailure { + LogCenter.log("KritorClient start failed: ${it.stackTraceToString()}", Level.ERROR) + } + } + + fun listen(retryCnt: Int = -1) { + if (::channelJob.isInitialized && channelJob.isActive) { + channelJob.cancel() + } + channelJob = GlobalScope.launch(Dispatchers.IO) { + runCatching { + val stub = ReverseServiceGrpcKt.ReverseServiceCoroutineStub(channel) + registerEvent(EventType.EVENT_TYPE_MESSAGE) + registerEvent(EventType.EVENT_TYPE_CORE_EVENT) + registerEvent(EventType.EVENT_TYPE_REQUEST) + registerEvent(EventType.EVENT_TYPE_NOTICE) + stub.reverseStream(channelFlow { + senderChannel.collect { send(it) } + }).collect { + onReceive(it) + } + }.onFailure { + LogCenter.log("KritorClient listen failed, retry after 15s: ${it.stackTraceToString()}", Level.WARN) + } + delay(15.seconds) + LogCenter.log("KritorClient listen retrying, retryCnt = $retryCnt", Level.WARN) + if (retryCnt != 0) listen(retryCnt - 1) + } + } + + fun registerEvent(eventType: EventType) { + GlobalScope.launch(Dispatchers.IO) { + runCatching { + EventServiceGrpcKt.EventServiceCoroutineStub(channel).registerPassiveListener(channelFlow { + when(eventType) { + EventType.EVENT_TYPE_MESSAGE -> GlobalEventTransmitter.onMessageEvent { + send(eventStructure { + this.type = EventType.EVENT_TYPE_MESSAGE + this.message = it.second + }) + } + EventType.EVENT_TYPE_CORE_EVENT -> {} + EventType.EVENT_TYPE_NOTICE -> GlobalEventTransmitter.onNoticeEvent { + send(eventStructure { + this.type = EventType.EVENT_TYPE_NOTICE + this.notice = it + }) + } + EventType.EVENT_TYPE_REQUEST -> GlobalEventTransmitter.onRequestEvent { + send(eventStructure { + this.type = EventType.EVENT_TYPE_REQUEST + this.request = it + }) + } + EventType.UNRECOGNIZED -> {} + } + }) + }.onFailure { + LogCenter.log("KritorClient registerEvent failed: ${it.stackTraceToString()}", Level.ERROR) + } + } + } + + private suspend fun onReceive(request: Request) = GlobalScope.launch { + //LogCenter.log("KritorClient onReceive: $request") + runCatching { + val rsp = handleGrpc(request.cmd, request.buf.toByteArray()) + senderChannel.emit(Response.newBuilder() + .setCmd(request.cmd) + .setCode(ReqCode.SUCCESS) + .setMsg("success") + .setSeq(request.seq) + .setBuf(ByteString.copyFrom(rsp)) + .build()) + }.onFailure { + senderChannel.emit(Response.newBuilder() + .setCmd(request.cmd) + .setCode(ReqCode.INTERNAL) + .setMsg(it.stackTraceToString()) + .setSeq(request.seq) + .setBuf(ByteString.EMPTY) + .build()) + } + } + + fun isActive(): Boolean { + return !channel.isShutdown + } + + fun close() { + channel.shutdown() + } +} \ No newline at end of file diff --git a/xposed/src/main/java/kritor/handlers/GrpcHandlers.kt b/xposed/src/main/java/kritor/handlers/GrpcHandlers.kt new file mode 100644 index 00000000..9bd3aa85 --- /dev/null +++ b/xposed/src/main/java/kritor/handlers/GrpcHandlers.kt @@ -0,0 +1,6 @@ +package kritor.handlers + +internal object GrpcHandlers { + + +} \ No newline at end of file diff --git a/xposed/src/main/java/kritor/server/KritorServer.kt b/xposed/src/main/java/kritor/server/KritorServer.kt index 94962869..c073ea8a 100644 --- a/xposed/src/main/java/kritor/server/KritorServer.kt +++ b/xposed/src/main/java/kritor/server/KritorServer.kt @@ -26,6 +26,8 @@ class KritorServer( .addService(GroupFileService) .addService(MessageService) .addService(EventService) + .addService(ForwardMessageService) + .addService(WebService) .build()!! fun start(block: Boolean = false) { diff --git a/xposed/src/main/java/kritor/service/ForwardMessageService.kt b/xposed/src/main/java/kritor/service/ForwardMessageService.kt new file mode 100644 index 00000000..a6b91f64 --- /dev/null +++ b/xposed/src/main/java/kritor/service/ForwardMessageService.kt @@ -0,0 +1,50 @@ +package kritor.service + +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.kernel.nativeinterface.MsgElement +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.message.Element +import io.kritor.message.ElementType +import io.kritor.message.ForwardMessageRequest +import io.kritor.message.ForwardMessageResponse +import io.kritor.message.ForwardMessageServiceGrpcKt +import io.kritor.message.Scene +import io.kritor.message.element +import io.kritor.message.forwardMessageResponse +import qq.service.contact.longPeer +import qq.service.msg.ForwardMessageHelper +import qq.service.msg.MessageHelper +import qq.service.msg.NtMsgConvertor + +internal object ForwardMessageService: ForwardMessageServiceGrpcKt.ForwardMessageServiceCoroutineImplBase() { + @Grpc("ForwardMessageService", "ForwardMessage") + override suspend fun forwardMessage(request: ForwardMessageRequest): ForwardMessageResponse { + val contact = request.contact.let { + MessageHelper.generateContact(when(it.scene!!) { + Scene.GROUP -> MsgConstant.KCHATTYPEGROUP + Scene.FRIEND -> MsgConstant.KCHATTYPEC2C + Scene.GUILD -> MsgConstant.KCHATTYPEGUILD + Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP + Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene")) + }, it.peer, it.subPeer) + } + + val forwardMessage = ForwardMessageHelper.uploadMultiMsg(contact.chatType, contact.longPeer().toString(), contact.guildId, request.messagesList).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withCause(it)) + }.getOrThrow() + + val uniseq = MessageHelper.generateMsgId(contact.chatType) + return forwardMessageResponse { + this.messageId = MessageHelper.sendMessage(contact, NtMsgConvertor.convertToNtMsgs(contact, uniseq, arrayListOf(element { + this.type = ElementType.FORWARD + this.forward = forwardMessage + })), request.retryCount, uniseq).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withCause(it)) + }.getOrThrow() + this.resId = forwardMessage.id + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/kritor/service/GroupFileService.kt b/xposed/src/main/java/kritor/service/GroupFileService.kt index 1daaa639..b67685a3 100644 --- a/xposed/src/main/java/kritor/service/GroupFileService.kt +++ b/xposed/src/main/java/kritor/service/GroupFileService.kt @@ -118,6 +118,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti return renameFolderResponse { } } + @Grpc("GroupFileService", "GetFileSystemInfo") override suspend fun getFileSystemInfo(request: GetFileSystemInfoRequest): GetFileSystemInfoResponse { return getGroupFileSystemInfo(request.groupId) } diff --git a/xposed/src/main/java/kritor/service/GroupService.kt b/xposed/src/main/java/kritor/service/GroupService.kt index 9563b50f..8f986273 100644 --- a/xposed/src/main/java/kritor/service/GroupService.kt +++ b/xposed/src/main/java/kritor/service/GroupService.kt @@ -61,7 +61,6 @@ import io.kritor.group.prohibitedUserInfo import io.kritor.group.setGroupAdminResponse import io.kritor.group.setGroupUniqueTitleResponse import io.kritor.group.setGroupWholeBanResponse -import moe.fuqiuluo.shamrock.helper.TroopHonorHelper import moe.fuqiuluo.shamrock.helper.TroopHonorHelper.decodeHonor import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty import qq.service.contact.ContactHelper @@ -89,7 +88,7 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase() } } - @Grpc("GroupService", "PokeMember") + @Grpc("GroupService", "PokeMember", ) override suspend fun pokeMember(request: PokeMemberRequest): PokeMemberResponse { GroupHelper.pokeMember(request.groupId, when(request.targetCase!!) { PokeMemberRequest.TargetCase.TARGET_UIN -> request.targetUin diff --git a/xposed/src/main/java/kritor/service/KritorService.kt b/xposed/src/main/java/kritor/service/KritorService.kt index 45eb251d..2190d3c8 100644 --- a/xposed/src/main/java/kritor/service/KritorService.kt +++ b/xposed/src/main/java/kritor/service/KritorService.kt @@ -10,6 +10,8 @@ import io.kritor.core.DownloadFileRequest import io.kritor.core.DownloadFileResponse import io.kritor.core.GetCurrentAccountRequest import io.kritor.core.GetCurrentAccountResponse +import io.kritor.core.GetDeviceBatteryRequest +import io.kritor.core.GetDeviceBatteryResponse import io.kritor.core.GetVersionRequest import io.kritor.core.GetVersionResponse import io.kritor.core.KritorServiceGrpcKt @@ -18,6 +20,7 @@ import io.kritor.core.SwitchAccountResponse import io.kritor.core.clearCacheResponse import io.kritor.core.downloadFileResponse import io.kritor.core.getCurrentAccountResponse +import io.kritor.core.getDeviceBatteryResponse import io.kritor.core.getVersionResponse import io.kritor.core.switchAccountResponse import moe.fuqiuluo.shamrock.tools.ShamrockVersion @@ -25,6 +28,7 @@ import moe.fuqiuluo.shamrock.utils.DownloadUtils import moe.fuqiuluo.shamrock.utils.FileUtils import moe.fuqiuluo.shamrock.utils.MD5 import moe.fuqiuluo.shamrock.utils.MMKVFetcher +import moe.fuqiuluo.shamrock.utils.PlatformUtils import mqq.app.MobileQQ import qq.service.QQInterfaces import qq.service.QQInterfaces.Companion.app @@ -118,4 +122,15 @@ object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() { } return switchAccountResponse { } } + + @Grpc("KritorService", "GetDeviceBattery") + override suspend fun getDeviceBattery(request: GetDeviceBatteryRequest): GetDeviceBatteryResponse { + return getDeviceBatteryResponse { + PlatformUtils.getDeviceBattery().let { + this.battery = it.battery + this.scale = it.scale + this.status = it.status + } + } + } } \ No newline at end of file diff --git a/xposed/src/main/java/kritor/service/MessageService.kt b/xposed/src/main/java/kritor/service/MessageService.kt index efb3e164..0d2ad7b9 100644 --- a/xposed/src/main/java/kritor/service/MessageService.kt +++ b/xposed/src/main/java/kritor/service/MessageService.kt @@ -9,6 +9,10 @@ import io.grpc.Status import io.grpc.StatusRuntimeException import io.kritor.message.ClearMessagesRequest import io.kritor.message.ClearMessagesResponse +import io.kritor.message.DeleteEssenceMsgRequest +import io.kritor.message.DeleteEssenceMsgResponse +import io.kritor.message.GetEssenceMessagesRequest +import io.kritor.message.GetEssenceMessagesResponse import io.kritor.message.GetForwardMessagesRequest import io.kritor.message.GetForwardMessagesResponse import io.kritor.message.GetHistoryMessageRequest @@ -25,8 +29,15 @@ import io.kritor.message.SendMessageByResIdRequest import io.kritor.message.SendMessageByResIdResponse import io.kritor.message.SendMessageRequest import io.kritor.message.SendMessageResponse +import io.kritor.message.SetEssenceMessageRequest +import io.kritor.message.SetEssenceMessageResponse +import io.kritor.message.SetMessageCommentEmojiRequest +import io.kritor.message.SetMessageCommentEmojiResponse import io.kritor.message.clearMessagesResponse import io.kritor.message.contact +import io.kritor.message.deleteEssenceMsgResponse +import io.kritor.message.essenceMessage +import io.kritor.message.getEssenceMessagesResponse import io.kritor.message.getForwardMessagesResponse import io.kritor.message.getHistoryMessageResponse import io.kritor.message.getMessageBySeqResponse @@ -36,6 +47,8 @@ import io.kritor.message.recallMessageResponse import io.kritor.message.sendMessageByResIdResponse import io.kritor.message.sendMessageResponse import io.kritor.message.sender +import io.kritor.message.setEssenceMessageResponse +import io.kritor.message.setMessageCommentEmojiResponse import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull import moe.fuqiuluo.shamrock.helper.Level @@ -63,6 +76,7 @@ import kotlin.random.Random import kotlin.random.nextUInt internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImplBase() { + @Grpc("MessageService", "SendMessage") override suspend fun sendMessage(request: SendMessageRequest): SendMessageResponse { val contact = request.contact.let { MessageHelper.generateContact(when(it.scene!!) { @@ -84,6 +98,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl } } + @Grpc("MessageService", "SendMessageByResId") override suspend fun sendMessageByResId(request: SendMessageByResIdRequest): SendMessageByResIdResponse { val contact = request.contact val req = PbSendMsgReq( @@ -113,6 +128,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl return sendMessageByResIdResponse { } } + @Grpc("MessageService", "ClearMessages") override suspend fun clearMessages(request: ClearMessagesRequest): ClearMessagesResponse { val contact = request.contact val kernelService = NTServiceFetcher.kernelService @@ -131,6 +147,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl return clearMessagesResponse { } } + @Grpc("MessageService", "RecallMessage") override suspend fun recallMessage(request: RecallMessageRequest): RecallMessageResponse { val contact = request.contact.let { MessageHelper.generateContact(when(it.scene!!) { @@ -155,6 +172,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl return recallMessageResponse {} } + @Grpc("MessageService", "GetForwardMessages") override suspend fun getForwardMessages(request: GetForwardMessagesRequest): GetForwardMessagesResponse { return getForwardMessagesResponse { MessageHelper.getForwardMsg(request.resId).onFailure { @@ -195,6 +213,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl } } + @Grpc("MessageService", "GetMessage") override suspend fun getMessage(request: GetMessageRequest): GetMessageResponse { val contact = request.contact.let { MessageHelper.generateContact(when(it.scene!!) { @@ -239,6 +258,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl } } + @Grpc("MessageService", "GetMessageBySeq") override suspend fun getMessageBySeq(request: GetMessageBySeqRequest): GetMessageBySeqResponse { val contact = request.contact.let { MessageHelper.generateContact(when(it.scene!!) { @@ -283,6 +303,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl } } + @Grpc("MessageService", "GetHistoryMessage") override suspend fun getHistoryMessage(request: GetHistoryMessageRequest): GetHistoryMessageResponse { val contact = request.contact.let { MessageHelper.generateContact(when(it.scene!!) { @@ -328,4 +349,121 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl } } } + + @Grpc("MessageService", "DeleteEssenceMsg") + override suspend fun deleteEssenceMsg(request: DeleteEssenceMsgRequest): DeleteEssenceMsgResponse { + val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString()) + val msg: MsgRecord = withTimeoutOrNull(5000) { + val service = QRoute.api(IMsgService::class.java) + suspendCancellableCoroutine { continuation -> + service.getMsgsByMsgId(contact, arrayListOf(request.messageId)) { code, _, msgRecords -> + if (code == 0 && msgRecords.isNotEmpty()) { + continuation.resume(msgRecords.first()) + } else { + continuation.resume(null) + } + } + continuation.invokeOnCancellation { + continuation.resume(null) + } + } + } ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found")) + if(MessageHelper.deleteEssenceMessage(request.groupId, msg.msgSeq, msg.msgRandom) == null) + throw StatusRuntimeException(Status.NOT_FOUND.withDescription("delete essence message failed")) + return deleteEssenceMsgResponse { } + } + + @Grpc("MessageService", "GetEssenceMessages") + override suspend fun getEssenceMessages(request: GetEssenceMessagesRequest): GetEssenceMessagesResponse { + val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString()) + return getEssenceMessagesResponse { + MessageHelper.getEssenceMessageList(request.groupId, request.page, request.pageSize).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withCause(it)) + }.getOrThrow().forEach { + essenceMessage.add(essenceMessage { + withTimeoutOrNull(5000) { + val service = QRoute.api(IMsgService::class.java) + suspendCancellableCoroutine { continuation -> + service.getMsgsBySeqAndCount(contact, it.messageSeq, 1, true) { code, _, msgRecords -> + if (code == 0 && msgRecords.isNotEmpty()) { + continuation.resume(msgRecords.first()) + } else { + continuation.resume(null) + } + } + continuation.invokeOnCancellation { + continuation.resume(null) + } + } + }?.let { + this.messageId = it.msgId + } + this.messageSeq = it.messageSeq + this.msgTime = it.senderTime.toInt() + this.senderNick = it.senderNick + this.senderUin = it.senderId + this.operationTime = it.operatorTime.toInt() + this.operatorNick = it.operatorNick + this.operatorUin = it.operatorId + this.jsonElements = it.messageContent.toString() + }) + } + } + } + + @Grpc("MessageService", "SetEssenceMessage") + override suspend fun setEssenceMessage(request: SetEssenceMessageRequest): SetEssenceMessageResponse { + val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString()) + val msg: MsgRecord = withTimeoutOrNull(5000) { + val service = QRoute.api(IMsgService::class.java) + suspendCancellableCoroutine { continuation -> + service.getMsgsByMsgId(contact, arrayListOf(request.messageId)) { code, _, msgRecords -> + if (code == 0 && msgRecords.isNotEmpty()) { + continuation.resume(msgRecords.first()) + } else { + continuation.resume(null) + } + } + continuation.invokeOnCancellation { + continuation.resume(null) + } + } + } ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found")) + if (MessageHelper.setEssenceMessage(request.groupId, msg.msgSeq, msg.msgRandom) == null) { + throw StatusRuntimeException(Status.NOT_FOUND.withDescription("set essence message failed")) + } + return setEssenceMessageResponse { } + } + + @Grpc("MessageService", "SetMessageCommentEmoji") + override suspend fun setMessageCommentEmoji(request: SetMessageCommentEmojiRequest): SetMessageCommentEmojiResponse { + val contact = request.contact.let { + MessageHelper.generateContact(when(it.scene!!) { + Scene.GROUP -> MsgConstant.KCHATTYPEGROUP + Scene.FRIEND -> MsgConstant.KCHATTYPEC2C + Scene.GUILD -> MsgConstant.KCHATTYPEGUILD + Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP + Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene")) + }, it.peer, it.subPeer) + } + val msg: MsgRecord = withTimeoutOrNull(5000) { + val service = QRoute.api(IMsgService::class.java) + suspendCancellableCoroutine { continuation -> + service.getMsgsByMsgId(contact, arrayListOf(request.messageId)) { code, _, msgRecords -> + if (code == 0 && msgRecords.isNotEmpty()) { + continuation.resume(msgRecords.first()) + } else { + continuation.resume(null) + } + } + continuation.invokeOnCancellation { + continuation.resume(null) + } + } + } ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found")) + MessageHelper.setGroupMessageCommentFace(request.contact.longPeer(), msg.msgSeq.toULong(), request.faceId.toString(), request.isComment) + return setMessageCommentEmojiResponse { } + } } \ No newline at end of file diff --git a/xposed/src/main/java/kritor/service/WebService.kt b/xposed/src/main/java/kritor/service/WebService.kt new file mode 100644 index 00000000..81e3deff --- /dev/null +++ b/xposed/src/main/java/kritor/service/WebService.kt @@ -0,0 +1,72 @@ +package kritor.service + +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.web.GetCSRFTokenRequest +import io.kritor.web.GetCSRFTokenResponse +import io.kritor.web.GetCookiesRequest +import io.kritor.web.GetCookiesResponse +import io.kritor.web.GetCredentialsRequest +import io.kritor.web.GetCredentialsResponse +import io.kritor.web.GetHttpCookiesRequest +import io.kritor.web.GetHttpCookiesResponse +import io.kritor.web.WebServiceGrpcKt +import io.kritor.web.getCSRFTokenResponse +import io.kritor.web.getCookiesResponse +import io.kritor.web.getCredentialsResponse +import io.kritor.web.getHttpCookiesResponse +import qq.service.ticket.TicketHelper + +internal object WebService: WebServiceGrpcKt.WebServiceCoroutineImplBase() { + @Grpc("WebService", "GetCookies") + override suspend fun getCookies(request: GetCookiesRequest): GetCookiesResponse { + return getCookiesResponse { + if (request.domain.isNullOrEmpty()) { + this.cookie = TicketHelper.getCookie() + } else { + this.cookie = TicketHelper.getCookie(request.domain) + } + } + } + + @Grpc("WebService", "GetCredentials") + override suspend fun getCredentials(request: GetCredentialsRequest): GetCredentialsResponse { + return getCredentialsResponse { + if (request.domain.isNullOrEmpty()) { + val uin = TicketHelper.getUin() + val skey = TicketHelper.getRealSkey(uin) + val pskey = TicketHelper.getPSKey(uin) + this.cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey;" + this.bkn = TicketHelper.getCSRF(pskey) + } else { + val uin = TicketHelper.getUin() + val skey = TicketHelper.getRealSkey(uin) + val pskey = TicketHelper.getPSKey(uin, request.domain) ?: "" + val pt4token = TicketHelper.getPt4Token(uin, request.domain) ?: "" + this.cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey; pt4_token=$pt4token;" + this.bkn = TicketHelper.getCSRF(pskey) + } + } + } + + @Grpc("WebService", "GetCSRFToken") + override suspend fun getCSRFToken(request: GetCSRFTokenRequest): GetCSRFTokenResponse { + return getCSRFTokenResponse { + if (request.domain.isNullOrEmpty()) { + this.bkn = TicketHelper.getCSRF() + } else { + this.bkn = TicketHelper.getCSRF(TicketHelper.getUin(), request.domain) + } + } + } + + @Grpc("WebService", "GetHttpCookies") + override suspend fun getHttpCookies(request: GetHttpCookiesRequest): GetHttpCookiesResponse { + return getHttpCookiesResponse { + this.cookie = TicketHelper.getHttpCookies(request.appid, request.daid, request.jumpUrl) + ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get http cookies")) + } + } + + +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableOldBDH.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableOldBDH.kt index e723c807..2af8dbcb 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableOldBDH.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/config/EnableOldBDH.kt @@ -2,5 +2,5 @@ package moe.fuqiuluo.shamrock.config object EnableOldBDH: ConfigKey() { override fun name() = "enable_old_bdh" - override fun default() = false + override fun default() = true } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt index 0be7f92f..8bce0230 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/InitRemoteService.kt @@ -6,8 +6,11 @@ import android.content.Context import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import kritor.client.KritorClient import kritor.server.KritorServer import moe.fuqiuluo.shamrock.config.ActiveRPC +import moe.fuqiuluo.shamrock.config.PassiveRPC +import moe.fuqiuluo.shamrock.config.RPCAddress import moe.fuqiuluo.shamrock.config.RPCPort import moe.fuqiuluo.shamrock.config.ShamrockConfig import moe.fuqiuluo.shamrock.config.get @@ -17,6 +20,7 @@ import moe.fuqiuluo.symbols.Process import moe.fuqiuluo.symbols.XposedHook private lateinit var server: KritorServer +private lateinit var client: KritorClient @XposedHook(Process.MAIN, priority = 10) internal class InitRemoteService : IAction { @@ -32,6 +36,21 @@ internal class InitRemoteService : IAction { LogCenter.log("ActiveRPC is disabled, KritorServer will not be started.") } + if (PassiveRPC.get()) { + if (!::client.isInitialized) { + val hostAndPort = RPCAddress.get().split(":").let { + it.first() to it.last().toInt() + } + LogCenter.log("Connect RPC to ${hostAndPort.first}:${hostAndPort.second}") + client = KritorClient(hostAndPort.first, hostAndPort.second) + client.start() + client.listen() + + } + } else { + LogCenter.log("PassiveRPC is disabled, KritorServer will not be started.") + } + }.onFailure { LogCenter.log("Start RPC failed: ${it.message}", Level.ERROR) diff --git a/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt b/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt index 233a55e5..e25ffd70 100644 --- a/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt +++ b/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt @@ -65,7 +65,7 @@ import kotlin.time.Duration.Companion.seconds internal object NtV2RichMediaSvc: QQInterfaces() { private val requestIdSeq = atomic(1L) - private fun fetchGroupResUploadTo(): String { + fun fetchGroupResUploadTo(): String { return ShamrockConfig[ResourceGroup].ifNullOrEmpty { "100000000" }!! } diff --git a/xposed/src/main/java/qq/service/msg/ForwardMessageHelper.kt b/xposed/src/main/java/qq/service/msg/ForwardMessageHelper.kt new file mode 100644 index 00000000..eb1c8a46 --- /dev/null +++ b/xposed/src/main/java/qq/service/msg/ForwardMessageHelper.kt @@ -0,0 +1,227 @@ +package qq.service.msg + +import com.tencent.mobileqq.qroute.QRoute +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.kernel.nativeinterface.MsgRecord +import com.tencent.qqnt.msg.api.IMsgService +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.message.Element +import io.kritor.message.ElementType +import io.kritor.message.ForwardElement +import io.kritor.message.ForwardMessageBody +import io.kritor.message.Scene +import io.kritor.message.forwardElement +import io.kritor.message.nodeOrNull +import io.kritor.message.senderOrNull +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.tools.slice +import moe.fuqiuluo.shamrock.utils.DeflateTools +import moe.fuqiuluo.symbols.decodeProtobuf +import protobuf.auto.toByteArray +import protobuf.message.* +import protobuf.message.longmsg.* +import qq.service.QQInterfaces +import qq.service.contact.ContactHelper +import qq.service.msg.MessageHelper.getMultiMsg +import qq.service.ticket.TicketHelper +import java.util.UUID +import kotlin.coroutines.resume +import kotlin.random.Random +import kotlin.time.Duration.Companion.seconds + +internal object ForwardMessageHelper: QQInterfaces() { + suspend fun uploadMultiMsg( + chatType: Int, + peerId: String, + fromId: String = peerId, + messages: List, + ): Result { + var i = -1 + val desc = MutableList(messages.size) { "" } + val forwardMsg = mutableMapOf() + + val msgs = messages.mapNotNull { msg -> + kotlin.runCatching { + val contact = msg.contact.let { + MessageHelper.generateContact(when(it.scene!!) { + Scene.GROUP -> MsgConstant.KCHATTYPEGROUP + Scene.FRIEND -> MsgConstant.KCHATTYPEC2C + Scene.GUILD -> MsgConstant.KCHATTYPEGUILD + Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP + Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN + Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene")) + }, it.peer, it.subPeer) + } + val node = msg.elementsList.find { it.type == ElementType.NODE }?.nodeOrNull + if (node != null) { + val msgId = node.messageId + val record: MsgRecord = withTimeoutOrNull(5000) { + val service = QRoute.api(IMsgService::class.java) + suspendCancellableCoroutine { continuation -> + service.getMsgsByMsgId(contact, arrayListOf(msgId)) { code, _, msgRecords -> + if (code == 0 && msgRecords.isNotEmpty()) { + continuation.resume(msgRecords.first()) + } else { + continuation.resume(null) + } + } + continuation.invokeOnCancellation { + continuation.resume(null) + } + } + } ?: error("合并转发消息节点消息(id = $msgId)获取失败") + PushMsgBody( + msgHead = ResponseHead( + peerUid = record.senderUid, + receiverUid = record.peerUid, + forward = ResponseForward( + friendName = record.sendNickName + ), + responseGrp = if (record.chatType == MsgConstant.KCHATTYPEGROUP) ResponseGrp( + groupCode = record.peerUin.toULong(), + memberCard = record.sendMemberName, + u1 = 2 + ) else null + ), + contentHead = ContentHead( + msgType = when (record.chatType) { + MsgConstant.KCHATTYPEC2C -> 9 + MsgConstant.KCHATTYPEGROUP -> 82 + else -> throw UnsupportedOperationException("Unsupported chatType: $chatType") + }, + msgSubType = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null, + divSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null, + msgViaRandom = record.msgId, + sequence = record.msgSeq, // idk what this is(i++) + msgTime = record.msgTime, + u2 = 1, + u6 = 0, + u7 = 0, + msgSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) record.msgSeq else null, // seq for dm + forwardHead = ForwardHead( + u1 = 0, + u2 = 0, + u3 = 0, + ub641 = "", + avatar = "" + ) + ), + body = MsgBody( + richText = record.elements.toKritorReqMessages(contact).toRichText(contact).onFailure { + error("消息合成失败: ${it.stackTraceToString()}") + }.onSuccess { + desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": " + it.first + }.getOrThrow().second + ) + ) + } else { + PushMsgBody( + msgHead = ResponseHead( + peer = msg.senderOrNull?.uin ?: TicketHelper.getUin().toLong(), + peerUid = msg.senderOrNull?.uid ?: TicketHelper.getUid(), + receiverUid = TicketHelper.getUid(), + forward = ResponseForward( + friendName = msg.senderOrNull?.nick ?: TicketHelper.getNickname() + ) + ), + contentHead = ContentHead( + msgType = 9, + msgSubType = 175, + divSeq = 175, + msgViaRandom = Random.nextLong(), + sequence = msg.messageSeq.toLong(), + msgTime = msg.messageTime.toLong(), + u2 = 1, + u6 = 0, + u7 = 0, + msgSeq = msg.messageSeq.toLong(), + forwardHead = ForwardHead( + u1 = 0, + u2 = 0, + u3 = 2, + ub641 = "", + avatar = "" + ) + ), + body = MsgBody( + richText = msg.elementsList.toRichText(contact).onSuccess { + desc[++i] = (msg.senderOrNull?.nick ?: TicketHelper.getNickname()) + ": " + it.first + }.onFailure { + error("消息合成失败: ${it.stackTraceToString()}") + }.getOrThrow().second + ) + ) + } + }.onFailure { + LogCenter.log("消息节点解析失败:${it.stackTraceToString()}", Level.WARN) + }.getOrNull() + }.ifEmpty { + return Result.failure(Exception("消息节点为空")) + } + + val payload = LongMsgPayload( + action = mutableListOf( + LongMsgAction( + command = "MultiMsg", + data = LongMsgContent( + body = msgs + ) + ) + ).apply { + forwardMsg.map { msg -> + addAll(getMultiMsg(msg.value).getOrElse { return Result.failure(Exception("无法获取嵌套转发消息: $it")) } + .map { action -> + if (action.command == "MultiMsg") LongMsgAction( + command = msg.key, + data = action.data + ) else action + }) + } + } + ) + + val req = LongMsgReq( + sendInfo = when (chatType) { + MsgConstant.KCHATTYPEC2C -> SendLongMsgInfo( + type = 1, + uid = LongMsgUid(if(peerId.startsWith("u_")) peerId else ContactHelper.getUidByUinAsync(peerId.toLong()) ), + payload = DeflateTools.gzip(payload.toByteArray()) + ) + MsgConstant.KCHATTYPEGROUP -> SendLongMsgInfo( + type = 3, + uid = LongMsgUid(fromId), + groupUin = fromId.toULong(), + payload = DeflateTools.gzip(payload.toByteArray()) + ) + else -> throw UnsupportedOperationException("Unsupported chatType: $chatType") + }, + setting = LongMsgSettings( + field1 = 4, + field2 = 2, + field3 = 9, + field4 = 0 + ) + ).toByteArray() + + val fromServiceMsg = sendBufferAW("trpc.group.long_msg_interface.MsgService.SsoSendLongMsg", true, req, timeout = 60.seconds) + ?: return Result.failure(Exception("unable to upload multi message, response timeout")) + val rsp = runCatching { + fromServiceMsg.wupBuffer.slice(4).decodeProtobuf() + }.getOrElse { + fromServiceMsg.wupBuffer.decodeProtobuf() + } + val resId = rsp.sendResult?.resId ?: return Result.failure(Exception("unable to upload multi message")) + + return Result.success(forwardElement { + this.id = resId + this.summary = summary + this.uniseq = UUID.randomUUID().toString() + this.description = desc.slice(0..if (i < 3) i else 3).joinToString("\n") + }) + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/msg/MessageData.kt b/xposed/src/main/java/qq/service/msg/MessageData.kt index 6df7a32e..15535301 100644 --- a/xposed/src/main/java/qq/service/msg/MessageData.kt +++ b/xposed/src/main/java/qq/service/msg/MessageData.kt @@ -64,8 +64,6 @@ internal data class EssenceMessage( @SerialName("operator_id") val operatorId: Long, @SerialName("operator_nick") val operatorNick: String, @SerialName("operator_time") val operatorTime: Long, - @SerialName("message_id") var messageId: Int, - @SerialName("message_seq") val messageSeq: Int, - @SerialName("real_id") val realId: Int, + @SerialName("message_seq") val messageSeq: Long, @SerialName("message_content") val messageContent: JsonElement, ) \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/msg/MessageHelper.kt b/xposed/src/main/java/qq/service/msg/MessageHelper.kt index 8f994972..a70edb3b 100644 --- a/xposed/src/main/java/qq/service/msg/MessageHelper.kt +++ b/xposed/src/main/java/qq/service/msg/MessageHelper.kt @@ -11,11 +11,26 @@ import com.tencent.qqnt.kernel.nativeinterface.TempChatGameSession import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo import com.tencent.qqnt.msg.api.IMsgService +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.header import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.jsonObject import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY +import moe.fuqiuluo.shamrock.tools.EmptyJsonArray +import moe.fuqiuluo.shamrock.tools.GlobalClient +import moe.fuqiuluo.shamrock.tools.asInt +import moe.fuqiuluo.shamrock.tools.asJsonArrayOrNull +import moe.fuqiuluo.shamrock.tools.asJsonObject +import moe.fuqiuluo.shamrock.tools.asLong +import moe.fuqiuluo.shamrock.tools.asString +import moe.fuqiuluo.shamrock.tools.asStringOrNull import moe.fuqiuluo.shamrock.tools.slice import moe.fuqiuluo.shamrock.tools.toHexString import moe.fuqiuluo.shamrock.utils.DeflateTools @@ -28,14 +43,104 @@ import protobuf.message.longmsg.LongMsgRsp import protobuf.message.longmsg.LongMsgSettings import protobuf.message.longmsg.LongMsgUid import protobuf.message.longmsg.RecvLongMsgInfo +import protobuf.oidb.cmd0x9082.Oidb0x9082 import qq.service.QQInterfaces import qq.service.contact.ContactHelper import qq.service.internals.msgService +import qq.service.ticket.TicketHelper +import tencent.im.oidb.cmd0xeac.oidb_0xeac +import tencent.im.oidb.oidb_sso import kotlin.coroutines.resume typealias MessageId = Long internal object MessageHelper: QQInterfaces() { + suspend fun getEssenceMessageList(groupId: Long, page: Int = 0, pageSize: Int = 20): Result>{ + val cookie = TicketHelper.getCookie("qun.qq.com") + val bkn = TicketHelper.getBkn(TicketHelper.getRealSkey(TicketHelper.getUin())) + val url = "https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=${bkn}&group_code=${groupId}&page_start=${page}&page_limit=${pageSize}" + val response = GlobalClient.get(url) { + header("Cookie", cookie) + } + val body = Json.decodeFromStream(response.body()) + if (body.jsonObject["retcode"].asInt == 0) { + val data = body.jsonObject["data"].asJsonObject + val list = data["msg_list"].asJsonArrayOrNull + ?: // is_end + return Result.success(ArrayList()) + return Result.success(list.map { + val obj = it.jsonObject + val msgSeq = obj["msg_seq"].asLong + EssenceMessage( + senderId = obj["sender_uin"].asString.toLong(), + senderNick = obj["sender_nick"].asString, + senderTime = obj["sender_time"].asLong, + operatorId = obj["add_digest_uin"].asString.toLong(), + operatorNick = obj["add_digest_nick"].asString, + operatorTime = obj["add_digest_time"].asLong, + messageSeq = msgSeq, + messageContent = obj["msg_content"] ?: EmptyJsonArray + ) + }) + } else { + return Result.failure(Exception(body.jsonObject["retmsg"].asStringOrNull)) + } + } + + fun setGroupMessageCommentFace(peer: Long, msgSeq: ULong, faceIndex: String, isSet: Boolean) { + val serviceId = if (isSet) 1 else 2 + sendOidb("OidbSvcTrpcTcp.0x9082_$serviceId", 36994, serviceId, Oidb0x9082( + peer = peer.toULong(), + msgSeq = msgSeq, + faceIndex = faceIndex, + flag = 1u, + u1 = 0u, + u2 = 0u + ).toByteArray()) + } + + suspend fun setEssenceMessage(groupId: Long, seq: Long, rand: Long): String? { + val fromServiceMsg = sendOidbAW("OidbSvc.0xeac_1", 3756, 1, oidb_0xeac.ReqBody().apply { + group_code.set(groupId) + msg_seq.set(seq.toInt()) + msg_random.set(rand.toInt()) + }.toByteArray()) + if (fromServiceMsg?.wupBuffer == null) { + return "no response" + } + val body = oidb_sso.OIDBSSOPkg() + body.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val result = oidb_0xeac.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) + return if (result.wording.has()) { + LogCenter.log("设置群精华失败: ${result.wording.get()}", Level.WARN) + "设置群精华失败: ${result.wording.get()}" + } else { + LogCenter.log("设置群精华 -> $groupId: $seq") + null + } + } + + suspend fun deleteEssenceMessage(groupId: Long, seq: Long, rand: Long): String? { + val fromServiceMsg = sendOidbAW("OidbSvc.0xeac_2", 3756, 2, oidb_0xeac.ReqBody().apply { + group_code.set(groupId) + msg_seq.set(seq.toInt()) + msg_random.set(rand.toInt()) + }.toByteArray()) + if (fromServiceMsg?.wupBuffer == null) { + return "no response" + } + val body = oidb_sso.OIDBSSOPkg() + body.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val result = oidb_0xeac.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) + return if (result.wording.has()) { + LogCenter.log("移除群精华失败: ${result.wording.get()}", Level.WARN) + "移除群精华失败: ${result.wording.get()}" + } else { + LogCenter.log("移除群精华 -> $groupId: $seq") + null + } + } + private suspend fun prepareTempChatFromGroup( groupId: String, peerId: String diff --git a/xposed/src/main/java/qq/service/msg/NtMsgConvertor.kt b/xposed/src/main/java/qq/service/msg/NtMsgConvertor.kt index 74e9581c..a943efe8 100644 --- a/xposed/src/main/java/qq/service/msg/NtMsgConvertor.kt +++ b/xposed/src/main/java/qq/service/msg/NtMsgConvertor.kt @@ -30,6 +30,7 @@ import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogicException import moe.fuqiuluo.shamrock.tools.asJsonObject import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty +import moe.fuqiuluo.shamrock.tools.json import moe.fuqiuluo.shamrock.utils.AudioUtils import moe.fuqiuluo.shamrock.utils.DownloadUtils import moe.fuqiuluo.shamrock.utils.FileUtils @@ -94,6 +95,7 @@ object NtMsgConvertor { SHARE to ::shareConvertor, CONTACT to ::contactConvertor, JSON to ::jsonConvertor, + FORWARD to ::forwardConvertor, MARKDOWN to ::markdownConvertor, BUTTON to ::buttonConvertor, ) @@ -835,4 +837,58 @@ object NtMsgConvertor { elem.inlineKeyboardElement = InlineKeyboardElement(rows, 0) return Result.success(elem) } + + private suspend fun forwardConvertor(contact: Contact, msgId: Long, sourceForward: Element): Result { + val resId = sourceForward.forward.id + val filename = sourceForward.forward.uniseq + var summary = sourceForward.forward.summary + val descriptions = sourceForward.forward.description + var news = descriptions?.split("\n")?.map { "text" to it } + + if (news == null || summary == null) { + val forwardMsg = MessageHelper.getForwardMsg(resId).getOrElse { return Result.failure(it) } + if (news == null) { + news = forwardMsg.map { + "text" to it.sender.nickName + ": " + descriptions + } + } + if (summary == null) { + summary = "查看${forwardMsg.size}条转发消息" + } + } + + val json = mapOf( + "app" to "com.tencent.multimsg", + "config" to mapOf( + "autosize" to 1, + "forward" to 1, + "round" to 1, + "type" to "normal", + "width" to 300 + ), + "desc" to "[聊天记录]", + "extra" to mapOf( + "filename" to filename, + "tsum" to 2 + ).json.toString(), + "meta" to mapOf( + "detail" to mapOf( + "news" to news, + "resid" to resId, + "source" to "群聊的聊天记录", + "summary" to summary, + "uniseq" to filename + ) + ), + "prompt" to "[聊天记录]", + "ver" to "0.0.0.5", + "view" to "contact" + ) + + val elem = MsgElement() + elem.elementType = MsgConstant.KELEMTYPEARKSTRUCT + val ark = ArkElement(json.json.toString(), null, null) + elem.arkElement = ark + return Result.success(elem) + } } \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/msg/ReqMultiConvertor.kt b/xposed/src/main/java/qq/service/msg/ReqMultiConvertor.kt new file mode 100644 index 00000000..0f2d833b --- /dev/null +++ b/xposed/src/main/java/qq/service/msg/ReqMultiConvertor.kt @@ -0,0 +1,575 @@ +package qq.service.msg + +import android.graphics.BitmapFactory +import android.util.Base64 +import androidx.exifinterface.media.ExifInterface +import com.tencent.mobileqq.qroute.QRoute +import com.tencent.qqnt.kernel.nativeinterface.Contact +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.msg.api.IMsgService +import io.kritor.message.AtElement +import io.kritor.message.Element +import io.kritor.message.ElementType +import io.kritor.message.ImageElement +import io.kritor.message.ImageType +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.helper.LogicException +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.json +import moe.fuqiuluo.shamrock.tools.putBuf32Long +import moe.fuqiuluo.shamrock.utils.DeflateTools +import moe.fuqiuluo.shamrock.utils.DownloadUtils +import moe.fuqiuluo.shamrock.utils.FileUtils +import protobuf.auto.toByteArray +import protobuf.message.Elem +import protobuf.message.RichText +import protobuf.message.element.* +import protobuf.message.element.commelem.Action +import protobuf.message.element.commelem.Button +import protobuf.message.element.commelem.ButtonExtra +import protobuf.message.element.commelem.MarkdownExtra +import protobuf.message.element.commelem.Object1 +import protobuf.message.element.commelem.Permission +import protobuf.message.element.commelem.PokeExtra +import protobuf.message.element.commelem.QFaceExtra +import protobuf.message.element.commelem.RenderData +import protobuf.message.element.commelem.Row +import protobuf.oidb.cmd0x11c5.C2CUserInfo +import protobuf.oidb.cmd0x11c5.GroupUserInfo +import qq.service.QQInterfaces +import qq.service.bdh.NtV2RichMediaSvc +import qq.service.bdh.NtV2RichMediaSvc.fetchGroupResUploadTo +import qq.service.contact.ContactHelper +import qq.service.contact.longPeer +import qq.service.group.GroupHelper +import qq.service.lightapp.WeatherHelper +import java.io.ByteArrayInputStream +import java.io.File +import java.nio.ByteBuffer +import java.util.UUID +import kotlin.coroutines.resume +import kotlin.random.Random +import kotlin.random.nextULong +import kotlin.time.Duration.Companion.seconds + +/** + * 请求消息(io.kritor.message.*)转换合并转发消息 + */ + +suspend fun List.toRichText(contact: Contact): Result> { + val summary = StringBuilder() + val elems = ArrayList() + forEach { + try { + when(it.type!!) { + ElementType.TEXT -> { + val text = it.text.text + val elem = Elem( + text = TextMsg(text) + ) + elems.add(elem) + summary.append(text) + } + ElementType.AT -> { + when (contact.chatType) { + MsgConstant.KCHATTYPEGROUP -> { + val qq = when (it.at.accountCase) { + AtElement.AccountCase.UIN -> it.at.uin.toString() + else -> ContactHelper.getUinByUidAsync(it.at.uid) + } + val type: Int + val nick = if (it.at.uid == "all" || it.at.uin == 0L) { + type = 1 + "@全体成员" + } else { + type = 0 + "@" + (GroupHelper.getTroopMemberInfoByUinV2(contact.longPeer().toString(), qq, true).let { + val info = it.getOrNull() + if (info == null) + LogCenter.log("无法获取群成员信息: $qq", Level.ERROR) + else info.troopnick + .ifNullOrEmpty { info.friendnick } + .ifNullOrEmpty { qq } + }) + } + val attr6 = ByteBuffer.allocate(6) + attr6.put(byteArrayOf(0, 1, 0, 0, 0)) + attr6.put(nick.length.toByte()) + attr6.putChar(type.toChar()) + attr6.putBuf32Long(qq.toLong()) + attr6.put(byteArrayOf(0, 0)) + val elem = Elem( + text = TextMsg(str = nick, attr6Buf = attr6.array()) + ) + elems.add(elem) + summary.append(nick) + } + + MsgConstant.KCHATTYPEC2C -> { + val qq = when (it.at.accountCase) { + AtElement.AccountCase.UIN -> it.at.uin.toString() + else -> ContactHelper.getUinByUidAsync(it.at.uid) + } + val display = "@" + (ContactHelper.getProfileCard(qq.toLong()).onSuccess { + it.strNick.ifNullOrEmpty { qq } + }.onFailure { + LogCenter.log("无法获取QQ信息: $qq", Level.WARN) + }) + val elem = Elem( + text = TextMsg(str = display) + ) + elems.add(elem) + summary.append(display) + } + else -> throw UnsupportedOperationException("Unsupported chatType($contact) for AtMsg") + } + } + ElementType.FACE -> { + val faceId = it.face.id + val elem = if (it.face.isBig) { + Elem( + commonElem = CommonElem( + serviceType = 37, + elem = QFaceExtra( + packId = "1", + stickerId = "1", + faceId = faceId, + field4 = 1, + field5 = 1, + result = "", + faceText = "", //todo 表情名字 + field9 = 1 + ).toByteArray(), + businessType = 1 + ) + ) + } else { + Elem( + face = FaceMsg( + index = faceId + ) + ) + } + elems.add(elem) + summary.append("[表情]") + } + ElementType.BUBBLE_FACE -> throw UnsupportedOperationException("Unsupported ElementType.BUBBLE_FACE") + ElementType.REPLY -> { + val msgId = it.reply.messageId + withTimeoutOrNull(3000) { + suspendCancellableCoroutine { + QRoute.api(IMsgService::class.java).getMsgsByMsgId(contact, arrayListOf(msgId)) { _, _, records -> + it.resume(records) + } + } + }?.firstOrNull()?.let { + val sourceContact = MessageHelper.generateContact(it) + elems.add(Elem( + srcMsg = SourceMsg( + origSeqs = listOf(it.msgSeq.toInt()), + senderUin = it.senderUin.toULong(), + time = it.msgTime.toULong(), + flag = 1u, + elems = it.elements + .toKritorReqMessages(sourceContact) + .toRichText(contact).getOrThrow().second.elements, + type = 0u, + pbReserve = SourceMsg.Companion.PbReserve( + msgRand = Random.nextULong(), + senderUid = it.senderUid, + receiverUid = QQInterfaces.app.currentUid, + field8 = Random.nextInt(0, 10000) + ), + ) + )) + } + summary.append("[回复消息]") + } + ElementType.IMAGE -> { + val type = it.image.type + val isOriginal = type == ImageType.ORIGIN + val file = when(it.image.dataCase!!) { + ImageElement.DataCase.FILE_NAME -> { + val fileMd5 = it.image.fileName.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase() + FileUtils.getFileByMd5(fileMd5) + } + ImageElement.DataCase.FILE_PATH -> { + val filePath = it.image.filePath + File(filePath).inputStream().use { + FileUtils.saveFileToCache(it) + } + } + ImageElement.DataCase.FILE_BASE64 -> { + FileUtils.saveFileToCache( + ByteArrayInputStream( + Base64.decode(it.image.fileBase64, Base64.DEFAULT) + ) + ) + } + ImageElement.DataCase.URL -> { + val tmp = FileUtils.getTmpFile() + if(DownloadUtils.download(it.image.url, tmp)) { + tmp.inputStream().use { + FileUtils.saveFileToCache(it) + }.also { + tmp.delete() + } + } else { + tmp.delete() + throw LogicException("图片资源下载失败: ${it.image.url}") + } + } + ImageElement.DataCase.DATA_NOT_SET -> throw IllegalArgumentException("ImageElement data is not set") + } + + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeFile(file.absolutePath, options) + val exifInterface = ExifInterface(file.absolutePath) + val orientation = exifInterface.getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_UNDEFINED + ) + val picWidth: Int + val picHeight: Int + if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) { + picWidth = options.outWidth + picHeight = options.outHeight + } else { + picWidth = options.outHeight + picHeight = options.outWidth + } + + val fileInfo = NtV2RichMediaSvc.tryUploadResourceByNt( + chatType = contact.chatType, + elementType = MsgConstant.KELEMTYPEPIC, + resources = arrayListOf(file), + timeout = 30.seconds + ).getOrThrow().first() + + runCatching { + fileInfo.uuid.toUInt() + }.onFailure { + NtV2RichMediaSvc.requestUploadNtPic(file, fileInfo.md5, fileInfo.sha, fileInfo.fileName, picWidth.toUInt(), picHeight.toUInt(), 5) { + when(contact.chatType) { + MsgConstant.KCHATTYPEGROUP -> { + sceneType = 2u + grp = GroupUserInfo(fetchGroupResUploadTo().toULong()) + } + MsgConstant.KCHATTYPEC2C -> { + sceneType = 1u + c2c = C2CUserInfo( + accountType = 2u, + uid = contact.peerUid + ) + } + else -> error("不支持的合并转发图片类型") + } + }.onFailure { + LogCenter.log("获取MultiMedia图片信息失败: $it", Level.ERROR) + }.onSuccess { + //LogCenter.log({ "获取MultiMedia图片信息成功: ${it.hashCode()}" }, Level.INFO) + elems.add(Elem( + commonElem = CommonElem( + serviceType = 48, + businessType = 10, + elem = it.msgInfo!!.toByteArray() + ) + )) + } + }.onSuccess { uuid -> + elems.add(when (contact.chatType) { + MsgConstant.KCHATTYPEGROUP -> Elem( + customFace = CustomFace( + filePath = fileInfo.fileName, + fileId = uuid, + serverIp = 0u, + serverPort = 0u, + fileType = FileUtils.getPicType(file).toUInt(), + useful = 1u, + md5 = fileInfo.md5.hex2ByteArray(), + bizType = 0u, + imageType = FileUtils.getPicType(file).toUInt(), + width = picWidth.toUInt(), + height = picHeight.toUInt(), + size = fileInfo.fileSize.toUInt(), + origin = isOriginal, + thumbWidth = 0u, + thumbHeight = 0u, + pbReserve = CustomFace.Companion.PbReserve( + field1 = 0, + field3 = 0, + field4 = 0, + field10 = 0, + field21 = CustomFace.Companion.Object1( + field1 = 0, + field2 = "", + field3 = 0, + field4 = 0, + field5 = 0, + md5Str = fileInfo.md5 + ) + ) + ) + ) + MsgConstant.KCHATTYPEC2C -> Elem( + notOnlineImage = NotOnlineImage( + filePath = fileInfo.fileName, + fileLen = fileInfo.fileSize.toUInt(), + downloadPath = fileInfo.uuid, + imgType = FileUtils.getPicType(file).toUInt(), + picMd5 = fileInfo.md5.hex2ByteArray(), + picHeight = picWidth.toUInt(), + picWidth = picHeight.toUInt(), + resId = fileInfo.uuid, + original = isOriginal, // true + pbReserve = NotOnlineImage.Companion.PbReserve( + field1 = 0, + field3 = 0, + field4 = 0, + field10 = 0, + field20 = NotOnlineImage.Companion.Object1( + field1 = 0, + field2 = "", + field3 = 0, + field4 = 0, + field5 = 0, + field7 = "", + ), + md5Str = fileInfo.md5 + ) + ) + ) + else -> throw LogicException("Not supported chatType($contact) for PictureMsg") + }) + } + + summary.append("[图片]") + } + ElementType.VOICE -> throw UnsupportedOperationException("Unsupported ElementType.VOICE") + ElementType.VIDEO -> throw UnsupportedOperationException("Unsupported ElementType.VIDEO") + ElementType.BASKETBALL -> throw UnsupportedOperationException("Unsupported ElementType.BASKETBALL") + ElementType.DICE -> { + val elem = Elem( + commonElem = CommonElem( + serviceType = 37, + elem = QFaceExtra( + packId = "1", + stickerId = "33", + faceId = 358, + field4 = 1, + field5 = 2, + result = "", + faceText = "/骰子", + field9 = 1 + ).toByteArray(), + businessType = 2 + ) + ) + elems.add(elem) + summary .append( "[骰子]" ) + } + ElementType.RPS -> { + val elem = Elem( + commonElem = CommonElem( + serviceType = 37, + elem = QFaceExtra( + packId = "1", + stickerId = "34", + faceId = 359, + field4 = 1, + field5 = 2, + result = "", + faceText = "/包剪锤", + field9 = 1 + ).toByteArray(), + businessType = 1 + ) + ) + elems.add(elem) + summary .append( "[包剪锤]" ) + } + ElementType.POKE -> { + val elem = Elem( + commonElem = CommonElem( + serviceType = 2, + elem = PokeExtra( + type = it.poke.type, + field7 = 0, + field8 = 0 + ).toByteArray(), + businessType = it.poke.id + ) + ) + elems.add(elem) + summary .append( "[戳一戳]" ) + } + ElementType.MUSIC -> throw UnsupportedOperationException("Unsupported ElementType.MUSIC") + ElementType.WEATHER -> { + var code = it.weather.code.toIntOrNull() + if (code == null) { + val city = it.weather.city + WeatherHelper.searchCity(city).onFailure { + LogCenter.log("无法获取城市天气: $city", Level.ERROR) + }.getOrNull()?.firstOrNull()?.let { + code = it.adcode + } + } + + if (code != null) { + val weatherCard = WeatherHelper.fetchWeatherCard(code!!).getOrThrow() + val elem = Elem( + lightApp = LightAppElem( + data = byteArrayOf(1) + DeflateTools.compress( + weatherCard["weekStore"] + .asJsonObject["share"].asString.toByteArray() + ) + ) + ) + elems.add(elem) + summary .append( "[天气卡片]" ) + } else { + throw LogicException("无法获取城市天气") + } + } + ElementType.LOCATION -> throw UnsupportedOperationException("Unsupported ElementType.LOCATION") + ElementType.SHARE -> throw UnsupportedOperationException("Unsupported ElementType.SHARE") + ElementType.GIFT -> throw UnsupportedOperationException("Unsupported ElementType.GIFT") + ElementType.MARKET_FACE -> throw UnsupportedOperationException("Unsupported ElementType.MARKET_FACE") + ElementType.FORWARD -> { + val resId = it.forward.id + val filename = UUID.randomUUID().toString().uppercase() + var content = it.forward.summary + val descriptions = it.forward.description + var news = descriptions?.split("\n")?.map { "text" to it } + + if (news == null || content == null) { + val forwardMsg = MessageHelper.getForwardMsg(resId).getOrThrow() + if (news == null) { + news = forwardMsg.map { + "text" to it.sender.nickName + ": " + descriptions + } + } + if (content == null) { + content = "查看${forwardMsg.size}条转发消息" + } + } + + val json = mapOf( + "app" to "com.tencent.multimsg", + "config" to mapOf( + "autosize" to 1, + "forward" to 1, + "round" to 1, + "type" to "normal", + "width" to 300 + ), + "desc" to "[聊天记录]", + "extra" to mapOf( + "filename" to filename, + "tsum" to 2 + ).json.toString(), + "meta" to mapOf( + "detail" to mapOf( + "news" to news, + "resid" to resId, + "source" to "群聊的聊天记录", + "summary" to content, + "uniseq" to filename + ) + ), + "prompt" to "[聊天记录]", + "ver" to "0.0.0.5", + "view" to "contact" + ) + val elem = Elem( + lightApp = LightAppElem( + data = byteArrayOf(1) + DeflateTools.compress(json.json.toString().toByteArray()) + ) + ) + elems.add(elem) + summary.append( "[聊天记录]" ) + } + ElementType.CONTACT -> throw UnsupportedOperationException("Unsupported ElementType.CONTACT") + ElementType.JSON -> { + val elem = Elem( + lightApp = LightAppElem( + data = byteArrayOf(1) + DeflateTools.compress(it.json.json.toByteArray()) + ) + ) + elems.add(elem) + summary .append( "[Json消息]" ) + } + ElementType.XML -> throw UnsupportedOperationException("Unsupported ElementType.XML") + ElementType.FILE -> throw UnsupportedOperationException("Unsupported ElementType.FILE") + ElementType.MARKDOWN -> { + val elem = Elem( + commonElem = CommonElem( + serviceType = 45, + elem = MarkdownExtra(it.markdown.markdown).toByteArray(), + businessType = 1 + ) + ) + elems.add(elem) + summary.append("[Markdown消息]") + } + ElementType.BUTTON -> { + val elem = Elem( + commonElem = CommonElem( + serviceType = 46, + elem = ButtonExtra( + field1 = Object1( + rows = it.button.rowsList.map { row -> + Row(buttons = row.buttonsList.map { button -> + val renderData = button.renderData + val action = button.action + val permission = action.permission + Button( + id = button.id, + renderData = RenderData( + label = renderData.label, + visitedLabel = renderData.visitedLabel, + style = renderData.style + ), + action = Action( + type = action.type, + permission = Permission( + type = permission.type, + specifyRoleIds = permission.roleIdsList, + specifyUserIds = permission.userIdsList + ), + unsupportTips = action.unsupportedTips, + data = action.data, + reply = action.reply, + enter = action.enter + ) + ) + }) + }, + appid = 0 + ) + ).toByteArray(), + businessType = 1 + ) + ) + elems.add(elem) + summary.append("[Button消息]") + } + ElementType.NODE -> throw UnsupportedOperationException("Unsupported ElementType.NODE") + ElementType.UNRECOGNIZED -> throw UnsupportedOperationException("Unsupported ElementType.UNRECOGNIZED") + } + } catch (e: Throwable) { + LogCenter.log("转换消息失败(Multi): ${e.stackTraceToString()}", Level.ERROR) + } + } + return Result.success(summary.toString() to RichText( + elements = elems + )) +} + diff --git a/xposed/src/main/java/qq/service/ticket/TicketHelper.kt b/xposed/src/main/java/qq/service/ticket/TicketHelper.kt index 70bb8239..955d583c 100644 --- a/xposed/src/main/java/qq/service/ticket/TicketHelper.kt +++ b/xposed/src/main/java/qq/service/ticket/TicketHelper.kt @@ -48,19 +48,15 @@ internal object TicketHelper: QQInterfaces() { ) } - fun getUin(): String { + inline fun getUin(): String { return app.currentUin.ifBlank { "0" } } - fun getLongUin(): Long { - return app.longAccountUin - } - fun getUid(): String { return app.currentUid.ifBlank { "u_" } } - fun getNickname(): String { + inline fun getNickname(): String { return app.currentNickname } @@ -123,7 +119,7 @@ internal object TicketHelper: QQInterfaces() { fun getSKey(uin: String): String { require(app is QQAppInterface) - return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getSkey(uin) + return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getRealSkey(uin) } fun getRealSkey(uin: String): String {