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
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
annotation class Grpc(
|
||||
val serviceName: String,
|
||||
val funcName: String
|
||||
)
|
||||
val funcName: String,
|
||||
|
||||
)
|
||||
|
@ -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}]。")
|
||||
|
@ -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
|
||||
|
||||
# 旧BDH兼容开关
|
||||
enable_old_bdh=false
|
||||
enable_old_bdh=true
|
||||
|
||||
# 反JVM调用栈跟踪
|
||||
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(MessageService)
|
||||
.addService(EventService)
|
||||
.addService(ForwardMessageService)
|
||||
.addService(WebService)
|
||||
.build()!!
|
||||
|
||||
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 { }
|
||||
}
|
||||
|
||||
@Grpc("GroupFileService", "GetFileSystemInfo")
|
||||
override suspend fun getFileSystemInfo(request: GetFileSystemInfoRequest): GetFileSystemInfoResponse {
|
||||
return getGroupFileSystemInfo(request.groupId)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 { }
|
||||
}
|
||||
}
|
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>() {
|
||||
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.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)
|
||||
|
@ -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" }!!
|
||||
}
|
||||
|
||||
|
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_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,
|
||||
)
|
@ -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<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(
|
||||
groupId: String,
|
||||
peerId: String
|
||||
|
@ -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<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" }
|
||||
}
|
||||
|
||||
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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user