mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 13:12:17 +08:00
Shamrock
: 精华消息支持
Signed-off-by: 白池 <whitechi73@outlook.com>
This commit is contained in:
parent
6c9b282c6a
commit
ee5fcc3403
@ -1,7 +1,11 @@
|
|||||||
package kritor.service
|
package kritor.service
|
||||||
|
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
@Target(AnnotationTarget.FUNCTION)
|
@Target(AnnotationTarget.FUNCTION)
|
||||||
annotation class Grpc(
|
annotation class Grpc(
|
||||||
val serviceName: String,
|
val serviceName: String,
|
||||||
val funcName: String
|
val funcName: String,
|
||||||
)
|
|
||||||
|
)
|
||||||
|
@ -118,9 +118,7 @@ private fun APIInfoCard(
|
|||||||
text = rpcAddress,
|
text = rpcAddress,
|
||||||
hint = "请输入回调地址",
|
hint = "请输入回调地址",
|
||||||
error = "输入的地址不合法",
|
error = "输入的地址不合法",
|
||||||
checker = {
|
checker = { true },
|
||||||
it.isEmpty() || it.contains(":")
|
|
||||||
},
|
|
||||||
confirm = {
|
confirm = {
|
||||||
ShamrockConfig[ctx, RPCAddress] = rpcAddress.value
|
ShamrockConfig[ctx, RPCAddress] = rpcAddress.value
|
||||||
AppRuntime.log("设置回调RPC地址为[${rpcAddress.value}]。")
|
AppRuntime.log("设置回调RPC地址为[${rpcAddress.value}]。")
|
||||||
|
@ -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<KSAnnotated> {
|
||||||
|
val symbols = resolver.getSymbolsWithAnnotation(Grpc::class.qualifiedName!!)
|
||||||
|
val actions = (symbols as Sequence<KSFunctionDeclaration>).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()
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -34,7 +34,7 @@ active_ticket=
|
|||||||
enable_self_message=false
|
enable_self_message=false
|
||||||
|
|
||||||
# 旧BDH兼容开关
|
# 旧BDH兼容开关
|
||||||
enable_old_bdh=false
|
enable_old_bdh=true
|
||||||
|
|
||||||
# 反JVM调用栈跟踪
|
# 反JVM调用栈跟踪
|
||||||
anti_jvm_trace=true
|
anti_jvm_trace=true
|
||||||
|
142
xposed/src/main/java/kritor/client/KritorClient.kt
Normal file
142
xposed/src/main/java/kritor/client/KritorClient.kt
Normal file
@ -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<Response>()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
6
xposed/src/main/java/kritor/handlers/GrpcHandlers.kt
Normal file
6
xposed/src/main/java/kritor/handlers/GrpcHandlers.kt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package kritor.handlers
|
||||||
|
|
||||||
|
internal object GrpcHandlers {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -26,6 +26,8 @@ class KritorServer(
|
|||||||
.addService(GroupFileService)
|
.addService(GroupFileService)
|
||||||
.addService(MessageService)
|
.addService(MessageService)
|
||||||
.addService(EventService)
|
.addService(EventService)
|
||||||
|
.addService(ForwardMessageService)
|
||||||
|
.addService(WebService)
|
||||||
.build()!!
|
.build()!!
|
||||||
|
|
||||||
fun start(block: Boolean = false) {
|
fun start(block: Boolean = false) {
|
||||||
|
50
xposed/src/main/java/kritor/service/ForwardMessageService.kt
Normal file
50
xposed/src/main/java/kritor/service/ForwardMessageService.kt
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -118,6 +118,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
|
|||||||
return renameFolderResponse { }
|
return renameFolderResponse { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupFileService", "GetFileSystemInfo")
|
||||||
override suspend fun getFileSystemInfo(request: GetFileSystemInfoRequest): GetFileSystemInfoResponse {
|
override suspend fun getFileSystemInfo(request: GetFileSystemInfoRequest): GetFileSystemInfoResponse {
|
||||||
return getGroupFileSystemInfo(request.groupId)
|
return getGroupFileSystemInfo(request.groupId)
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,6 @@ import io.kritor.group.prohibitedUserInfo
|
|||||||
import io.kritor.group.setGroupAdminResponse
|
import io.kritor.group.setGroupAdminResponse
|
||||||
import io.kritor.group.setGroupUniqueTitleResponse
|
import io.kritor.group.setGroupUniqueTitleResponse
|
||||||
import io.kritor.group.setGroupWholeBanResponse
|
import io.kritor.group.setGroupWholeBanResponse
|
||||||
import moe.fuqiuluo.shamrock.helper.TroopHonorHelper
|
|
||||||
import moe.fuqiuluo.shamrock.helper.TroopHonorHelper.decodeHonor
|
import moe.fuqiuluo.shamrock.helper.TroopHonorHelper.decodeHonor
|
||||||
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
||||||
import qq.service.contact.ContactHelper
|
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 {
|
override suspend fun pokeMember(request: PokeMemberRequest): PokeMemberResponse {
|
||||||
GroupHelper.pokeMember(request.groupId, when(request.targetCase!!) {
|
GroupHelper.pokeMember(request.groupId, when(request.targetCase!!) {
|
||||||
PokeMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
|
PokeMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||||
|
@ -10,6 +10,8 @@ import io.kritor.core.DownloadFileRequest
|
|||||||
import io.kritor.core.DownloadFileResponse
|
import io.kritor.core.DownloadFileResponse
|
||||||
import io.kritor.core.GetCurrentAccountRequest
|
import io.kritor.core.GetCurrentAccountRequest
|
||||||
import io.kritor.core.GetCurrentAccountResponse
|
import io.kritor.core.GetCurrentAccountResponse
|
||||||
|
import io.kritor.core.GetDeviceBatteryRequest
|
||||||
|
import io.kritor.core.GetDeviceBatteryResponse
|
||||||
import io.kritor.core.GetVersionRequest
|
import io.kritor.core.GetVersionRequest
|
||||||
import io.kritor.core.GetVersionResponse
|
import io.kritor.core.GetVersionResponse
|
||||||
import io.kritor.core.KritorServiceGrpcKt
|
import io.kritor.core.KritorServiceGrpcKt
|
||||||
@ -18,6 +20,7 @@ import io.kritor.core.SwitchAccountResponse
|
|||||||
import io.kritor.core.clearCacheResponse
|
import io.kritor.core.clearCacheResponse
|
||||||
import io.kritor.core.downloadFileResponse
|
import io.kritor.core.downloadFileResponse
|
||||||
import io.kritor.core.getCurrentAccountResponse
|
import io.kritor.core.getCurrentAccountResponse
|
||||||
|
import io.kritor.core.getDeviceBatteryResponse
|
||||||
import io.kritor.core.getVersionResponse
|
import io.kritor.core.getVersionResponse
|
||||||
import io.kritor.core.switchAccountResponse
|
import io.kritor.core.switchAccountResponse
|
||||||
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
|
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.FileUtils
|
||||||
import moe.fuqiuluo.shamrock.utils.MD5
|
import moe.fuqiuluo.shamrock.utils.MD5
|
||||||
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
|
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
|
||||||
|
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||||
import mqq.app.MobileQQ
|
import mqq.app.MobileQQ
|
||||||
import qq.service.QQInterfaces
|
import qq.service.QQInterfaces
|
||||||
import qq.service.QQInterfaces.Companion.app
|
import qq.service.QQInterfaces.Companion.app
|
||||||
@ -118,4 +122,15 @@ object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() {
|
|||||||
}
|
}
|
||||||
return switchAccountResponse { }
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -9,6 +9,10 @@ import io.grpc.Status
|
|||||||
import io.grpc.StatusRuntimeException
|
import io.grpc.StatusRuntimeException
|
||||||
import io.kritor.message.ClearMessagesRequest
|
import io.kritor.message.ClearMessagesRequest
|
||||||
import io.kritor.message.ClearMessagesResponse
|
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.GetForwardMessagesRequest
|
||||||
import io.kritor.message.GetForwardMessagesResponse
|
import io.kritor.message.GetForwardMessagesResponse
|
||||||
import io.kritor.message.GetHistoryMessageRequest
|
import io.kritor.message.GetHistoryMessageRequest
|
||||||
@ -25,8 +29,15 @@ import io.kritor.message.SendMessageByResIdRequest
|
|||||||
import io.kritor.message.SendMessageByResIdResponse
|
import io.kritor.message.SendMessageByResIdResponse
|
||||||
import io.kritor.message.SendMessageRequest
|
import io.kritor.message.SendMessageRequest
|
||||||
import io.kritor.message.SendMessageResponse
|
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.clearMessagesResponse
|
||||||
import io.kritor.message.contact
|
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.getForwardMessagesResponse
|
||||||
import io.kritor.message.getHistoryMessageResponse
|
import io.kritor.message.getHistoryMessageResponse
|
||||||
import io.kritor.message.getMessageBySeqResponse
|
import io.kritor.message.getMessageBySeqResponse
|
||||||
@ -36,6 +47,8 @@ import io.kritor.message.recallMessageResponse
|
|||||||
import io.kritor.message.sendMessageByResIdResponse
|
import io.kritor.message.sendMessageByResIdResponse
|
||||||
import io.kritor.message.sendMessageResponse
|
import io.kritor.message.sendMessageResponse
|
||||||
import io.kritor.message.sender
|
import io.kritor.message.sender
|
||||||
|
import io.kritor.message.setEssenceMessageResponse
|
||||||
|
import io.kritor.message.setMessageCommentEmojiResponse
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
@ -63,6 +76,7 @@ import kotlin.random.Random
|
|||||||
import kotlin.random.nextUInt
|
import kotlin.random.nextUInt
|
||||||
|
|
||||||
internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImplBase() {
|
internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImplBase() {
|
||||||
|
@Grpc("MessageService", "SendMessage")
|
||||||
override suspend fun sendMessage(request: SendMessageRequest): SendMessageResponse {
|
override suspend fun sendMessage(request: SendMessageRequest): SendMessageResponse {
|
||||||
val contact = request.contact.let {
|
val contact = request.contact.let {
|
||||||
MessageHelper.generateContact(when(it.scene!!) {
|
MessageHelper.generateContact(when(it.scene!!) {
|
||||||
@ -84,6 +98,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "SendMessageByResId")
|
||||||
override suspend fun sendMessageByResId(request: SendMessageByResIdRequest): SendMessageByResIdResponse {
|
override suspend fun sendMessageByResId(request: SendMessageByResIdRequest): SendMessageByResIdResponse {
|
||||||
val contact = request.contact
|
val contact = request.contact
|
||||||
val req = PbSendMsgReq(
|
val req = PbSendMsgReq(
|
||||||
@ -113,6 +128,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl
|
|||||||
return sendMessageByResIdResponse { }
|
return sendMessageByResIdResponse { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "ClearMessages")
|
||||||
override suspend fun clearMessages(request: ClearMessagesRequest): ClearMessagesResponse {
|
override suspend fun clearMessages(request: ClearMessagesRequest): ClearMessagesResponse {
|
||||||
val contact = request.contact
|
val contact = request.contact
|
||||||
val kernelService = NTServiceFetcher.kernelService
|
val kernelService = NTServiceFetcher.kernelService
|
||||||
@ -131,6 +147,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl
|
|||||||
return clearMessagesResponse { }
|
return clearMessagesResponse { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "RecallMessage")
|
||||||
override suspend fun recallMessage(request: RecallMessageRequest): RecallMessageResponse {
|
override suspend fun recallMessage(request: RecallMessageRequest): RecallMessageResponse {
|
||||||
val contact = request.contact.let {
|
val contact = request.contact.let {
|
||||||
MessageHelper.generateContact(when(it.scene!!) {
|
MessageHelper.generateContact(when(it.scene!!) {
|
||||||
@ -155,6 +172,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl
|
|||||||
return recallMessageResponse {}
|
return recallMessageResponse {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "GetForwardMessages")
|
||||||
override suspend fun getForwardMessages(request: GetForwardMessagesRequest): GetForwardMessagesResponse {
|
override suspend fun getForwardMessages(request: GetForwardMessagesRequest): GetForwardMessagesResponse {
|
||||||
return getForwardMessagesResponse {
|
return getForwardMessagesResponse {
|
||||||
MessageHelper.getForwardMsg(request.resId).onFailure {
|
MessageHelper.getForwardMsg(request.resId).onFailure {
|
||||||
@ -195,6 +213,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "GetMessage")
|
||||||
override suspend fun getMessage(request: GetMessageRequest): GetMessageResponse {
|
override suspend fun getMessage(request: GetMessageRequest): GetMessageResponse {
|
||||||
val contact = request.contact.let {
|
val contact = request.contact.let {
|
||||||
MessageHelper.generateContact(when(it.scene!!) {
|
MessageHelper.generateContact(when(it.scene!!) {
|
||||||
@ -239,6 +258,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "GetMessageBySeq")
|
||||||
override suspend fun getMessageBySeq(request: GetMessageBySeqRequest): GetMessageBySeqResponse {
|
override suspend fun getMessageBySeq(request: GetMessageBySeqRequest): GetMessageBySeqResponse {
|
||||||
val contact = request.contact.let {
|
val contact = request.contact.let {
|
||||||
MessageHelper.generateContact(when(it.scene!!) {
|
MessageHelper.generateContact(when(it.scene!!) {
|
||||||
@ -283,6 +303,7 @@ internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImpl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "GetHistoryMessage")
|
||||||
override suspend fun getHistoryMessage(request: GetHistoryMessageRequest): GetHistoryMessageResponse {
|
override suspend fun getHistoryMessage(request: GetHistoryMessageRequest): GetHistoryMessageResponse {
|
||||||
val contact = request.contact.let {
|
val contact = request.contact.let {
|
||||||
MessageHelper.generateContact(when(it.scene!!) {
|
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 { }
|
||||||
|
}
|
||||||
}
|
}
|
72
xposed/src/main/java/kritor/service/WebService.kt
Normal file
72
xposed/src/main/java/kritor/service/WebService.kt
Normal file
@ -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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -2,5 +2,5 @@ package moe.fuqiuluo.shamrock.config
|
|||||||
|
|
||||||
object EnableOldBDH: ConfigKey<Boolean>() {
|
object EnableOldBDH: ConfigKey<Boolean>() {
|
||||||
override fun name() = "enable_old_bdh"
|
override fun name() = "enable_old_bdh"
|
||||||
override fun default() = false
|
override fun default() = true
|
||||||
}
|
}
|
@ -6,8 +6,11 @@ import android.content.Context
|
|||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kritor.client.KritorClient
|
||||||
import kritor.server.KritorServer
|
import kritor.server.KritorServer
|
||||||
import moe.fuqiuluo.shamrock.config.ActiveRPC
|
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.RPCPort
|
||||||
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||||
import moe.fuqiuluo.shamrock.config.get
|
import moe.fuqiuluo.shamrock.config.get
|
||||||
@ -17,6 +20,7 @@ import moe.fuqiuluo.symbols.Process
|
|||||||
import moe.fuqiuluo.symbols.XposedHook
|
import moe.fuqiuluo.symbols.XposedHook
|
||||||
|
|
||||||
private lateinit var server: KritorServer
|
private lateinit var server: KritorServer
|
||||||
|
private lateinit var client: KritorClient
|
||||||
|
|
||||||
@XposedHook(Process.MAIN, priority = 10)
|
@XposedHook(Process.MAIN, priority = 10)
|
||||||
internal class InitRemoteService : IAction {
|
internal class InitRemoteService : IAction {
|
||||||
@ -32,6 +36,21 @@ internal class InitRemoteService : IAction {
|
|||||||
LogCenter.log("ActiveRPC is disabled, KritorServer will not be started.")
|
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 {
|
}.onFailure {
|
||||||
LogCenter.log("Start RPC failed: ${it.message}", Level.ERROR)
|
LogCenter.log("Start RPC failed: ${it.message}", Level.ERROR)
|
||||||
|
@ -65,7 +65,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
internal object NtV2RichMediaSvc: QQInterfaces() {
|
internal object NtV2RichMediaSvc: QQInterfaces() {
|
||||||
private val requestIdSeq = atomic(1L)
|
private val requestIdSeq = atomic(1L)
|
||||||
|
|
||||||
private fun fetchGroupResUploadTo(): String {
|
fun fetchGroupResUploadTo(): String {
|
||||||
return ShamrockConfig[ResourceGroup].ifNullOrEmpty { "100000000" }!!
|
return ShamrockConfig[ResourceGroup].ifNullOrEmpty { "100000000" }!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
227
xposed/src/main/java/qq/service/msg/ForwardMessageHelper.kt
Normal file
227
xposed/src/main/java/qq/service/msg/ForwardMessageHelper.kt
Normal file
@ -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<ForwardMessageBody>,
|
||||||
|
): Result<ForwardElement> {
|
||||||
|
var i = -1
|
||||||
|
val desc = MutableList(messages.size) { "" }
|
||||||
|
val forwardMsg = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
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<LongMsgRsp>()
|
||||||
|
}.getOrElse {
|
||||||
|
fromServiceMsg.wupBuffer.decodeProtobuf<LongMsgRsp>()
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -64,8 +64,6 @@ internal data class EssenceMessage(
|
|||||||
@SerialName("operator_id") val operatorId: Long,
|
@SerialName("operator_id") val operatorId: Long,
|
||||||
@SerialName("operator_nick") val operatorNick: String,
|
@SerialName("operator_nick") val operatorNick: String,
|
||||||
@SerialName("operator_time") val operatorTime: Long,
|
@SerialName("operator_time") val operatorTime: Long,
|
||||||
@SerialName("message_id") var messageId: Int,
|
@SerialName("message_seq") val messageSeq: Long,
|
||||||
@SerialName("message_seq") val messageSeq: Int,
|
|
||||||
@SerialName("real_id") val realId: Int,
|
|
||||||
@SerialName("message_content") val messageContent: JsonElement,
|
@SerialName("message_content") val messageContent: JsonElement,
|
||||||
)
|
)
|
@ -11,11 +11,26 @@ import com.tencent.qqnt.kernel.nativeinterface.TempChatGameSession
|
|||||||
import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo
|
import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo
|
import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo
|
||||||
import com.tencent.qqnt.msg.api.IMsgService
|
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.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
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.Level
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
|
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.slice
|
||||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
import moe.fuqiuluo.shamrock.tools.toHexString
|
||||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||||
@ -28,14 +43,104 @@ import protobuf.message.longmsg.LongMsgRsp
|
|||||||
import protobuf.message.longmsg.LongMsgSettings
|
import protobuf.message.longmsg.LongMsgSettings
|
||||||
import protobuf.message.longmsg.LongMsgUid
|
import protobuf.message.longmsg.LongMsgUid
|
||||||
import protobuf.message.longmsg.RecvLongMsgInfo
|
import protobuf.message.longmsg.RecvLongMsgInfo
|
||||||
|
import protobuf.oidb.cmd0x9082.Oidb0x9082
|
||||||
import qq.service.QQInterfaces
|
import qq.service.QQInterfaces
|
||||||
import qq.service.contact.ContactHelper
|
import qq.service.contact.ContactHelper
|
||||||
import qq.service.internals.msgService
|
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
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
typealias MessageId = Long
|
typealias MessageId = Long
|
||||||
|
|
||||||
internal object MessageHelper: QQInterfaces() {
|
internal object MessageHelper: QQInterfaces() {
|
||||||
|
suspend fun getEssenceMessageList(groupId: Long, page: Int = 0, pageSize: Int = 20): Result<List<EssenceMessage>>{
|
||||||
|
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<JsonElement>(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(
|
private suspend fun prepareTempChatFromGroup(
|
||||||
groupId: String,
|
groupId: String,
|
||||||
peerId: String
|
peerId: String
|
||||||
|
@ -30,6 +30,7 @@ import moe.fuqiuluo.shamrock.helper.LogCenter
|
|||||||
import moe.fuqiuluo.shamrock.helper.LogicException
|
import moe.fuqiuluo.shamrock.helper.LogicException
|
||||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||||||
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
||||||
|
import moe.fuqiuluo.shamrock.tools.json
|
||||||
import moe.fuqiuluo.shamrock.utils.AudioUtils
|
import moe.fuqiuluo.shamrock.utils.AudioUtils
|
||||||
import moe.fuqiuluo.shamrock.utils.DownloadUtils
|
import moe.fuqiuluo.shamrock.utils.DownloadUtils
|
||||||
import moe.fuqiuluo.shamrock.utils.FileUtils
|
import moe.fuqiuluo.shamrock.utils.FileUtils
|
||||||
@ -94,6 +95,7 @@ object NtMsgConvertor {
|
|||||||
SHARE to ::shareConvertor,
|
SHARE to ::shareConvertor,
|
||||||
CONTACT to ::contactConvertor,
|
CONTACT to ::contactConvertor,
|
||||||
JSON to ::jsonConvertor,
|
JSON to ::jsonConvertor,
|
||||||
|
FORWARD to ::forwardConvertor,
|
||||||
MARKDOWN to ::markdownConvertor,
|
MARKDOWN to ::markdownConvertor,
|
||||||
BUTTON to ::buttonConvertor,
|
BUTTON to ::buttonConvertor,
|
||||||
)
|
)
|
||||||
@ -835,4 +837,58 @@ object NtMsgConvertor {
|
|||||||
elem.inlineKeyboardElement = InlineKeyboardElement(rows, 0)
|
elem.inlineKeyboardElement = InlineKeyboardElement(rows, 0)
|
||||||
return Result.success(elem)
|
return Result.success(elem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun forwardConvertor(contact: Contact, msgId: Long, sourceForward: Element): Result<MsgElement> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
575
xposed/src/main/java/qq/service/msg/ReqMultiConvertor.kt
Normal file
575
xposed/src/main/java/qq/service/msg/ReqMultiConvertor.kt
Normal file
@ -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<Element>.toRichText(contact: Contact): Result<Pair<String, RichText>> {
|
||||||
|
val summary = StringBuilder()
|
||||||
|
val elems = ArrayList<Elem>()
|
||||||
|
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
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
@ -48,19 +48,15 @@ internal object TicketHelper: QQInterfaces() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUin(): String {
|
inline fun getUin(): String {
|
||||||
return app.currentUin.ifBlank { "0" }
|
return app.currentUin.ifBlank { "0" }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLongUin(): Long {
|
|
||||||
return app.longAccountUin
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getUid(): String {
|
fun getUid(): String {
|
||||||
return app.currentUid.ifBlank { "u_" }
|
return app.currentUid.ifBlank { "u_" }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNickname(): String {
|
inline fun getNickname(): String {
|
||||||
return app.currentNickname
|
return app.currentNickname
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +119,7 @@ internal object TicketHelper: QQInterfaces() {
|
|||||||
|
|
||||||
fun getSKey(uin: String): String {
|
fun getSKey(uin: String): String {
|
||||||
require(app is QQAppInterface)
|
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 {
|
fun getRealSkey(uin: String): String {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user