mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 13:12:17 +08:00
commit
0faada7b5a
1
kritor
Submodule
1
kritor
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit e4aac653e14249cbb6f27567ffd463165f6deebe
|
@ -15,7 +15,7 @@ dependencies {
|
|||||||
implementation("com.google.devtools.ksp:symbol-processing-api:1.9.21-1.0.15")
|
implementation("com.google.devtools.ksp:symbol-processing-api:1.9.21-1.0.15")
|
||||||
implementation("com.squareup:kotlinpoet:1.14.2")
|
implementation("com.squareup:kotlinpoet:1.14.2")
|
||||||
|
|
||||||
implementation(DEPENDENCY_PROTOBUF)
|
//implementation(DEPENDENCY_PROTOBUF)
|
||||||
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
||||||
|
|
||||||
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
|
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
|
||||||
|
@ -0,0 +1,95 @@
|
|||||||
|
@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 {
|
||||||
|
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.*")
|
||||||
|
.addStatement("import io.kritor.developer.*")
|
||||||
|
.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()
|
||||||
|
}
|
||||||
|
}
|
@ -32,7 +32,7 @@ class ProtobufProcessor(
|
|||||||
}.toList()
|
}.toList()
|
||||||
|
|
||||||
if (actions.isNotEmpty()) {
|
if (actions.isNotEmpty()) {
|
||||||
actions.forEachIndexed { index, clz ->
|
actions.forEachIndexed { _, clz ->
|
||||||
if (clz.isInternal()) return@forEachIndexed
|
if (clz.isInternal()) return@forEachIndexed
|
||||||
if (clz.isPrivate()) return@forEachIndexed
|
if (clz.isPrivate()) return@forEachIndexed
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
@file:Suppress("UNCHECKED_CAST", "LocalVariableName", "PrivatePropertyName")
|
@file:Suppress("UNCHECKED_CAST", "LocalVariableName", "PrivatePropertyName")
|
||||||
@file:OptIn(KspExperimental::class)
|
@file:OptIn(KspExperimental::class, KspExperimental::class)
|
||||||
|
|
||||||
package moe.fuqiuluo.ksp.impl
|
package moe.fuqiuluo.ksp.impl
|
||||||
|
|
||||||
@ -27,10 +27,14 @@ class XposedHookProcessor(
|
|||||||
private val logger: KSPLogger
|
private val logger: KSPLogger
|
||||||
): SymbolProcessor {
|
): SymbolProcessor {
|
||||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||||
val symbols = resolver.getSymbolsWithAnnotation(XposedHook::class.qualifiedName!!)
|
val symbols = resolver.getSymbolsWithAnnotation(
|
||||||
|
annotationName = XposedHook::class.qualifiedName!!,
|
||||||
|
inDepth = true
|
||||||
|
)
|
||||||
|
logger.warn("Found ${symbols.count()} classes annotated with XposedHook")
|
||||||
val unableToProcess = symbols.filterNot { it.validate() }
|
val unableToProcess = symbols.filterNot { it.validate() }
|
||||||
val actions = (symbols.filter {
|
val actions = (symbols.filter {
|
||||||
it is KSClassDeclaration && it.validate() && it.classKind == ClassKind.CLASS
|
it is KSClassDeclaration && it.classKind == ClassKind.CLASS
|
||||||
} as Sequence<KSClassDeclaration>).toList()
|
} as Sequence<KSClassDeclaration>).toList()
|
||||||
|
|
||||||
if (actions.isNotEmpty()) {
|
if (actions.isNotEmpty()) {
|
||||||
@ -46,7 +50,7 @@ class XposedHookProcessor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val context = ClassName("android.content", "Context")
|
val context = ClassName("android.content", "Context")
|
||||||
val packageName = "moe.fuqiuluo.shamrock.xposed.hooks"
|
val packageName = "moe.fuqiuluo.shamrock.xposed.actions"
|
||||||
val fileSpec = FileSpec.builder(packageName, "AutoActionLoader").addFunction(FunSpec.builder("runFirstActions")
|
val fileSpec = FileSpec.builder(packageName, "AutoActionLoader").addFunction(FunSpec.builder("runFirstActions")
|
||||||
.addParameter("ctx", context)
|
.addParameter("ctx", context)
|
||||||
.apply {
|
.apply {
|
||||||
@ -96,16 +100,6 @@ class XposedHookProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return unableToProcess.toList()
|
return unableToProcess.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ActionLoaderVisitor(
|
|
||||||
private val firstActions: List<KSClassDeclaration>,
|
|
||||||
private val serviceActions: List<KSClassDeclaration>,
|
|
||||||
): KSVisitorVoid() {
|
|
||||||
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -37,7 +37,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(DEPENDENCY_PROTOBUF)
|
//implementation(DEPENDENCY_PROTOBUF)
|
||||||
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
||||||
implementation(kotlinx("serialization-json", "1.6.2"))
|
implementation(kotlinx("serialization-json", "1.6.2"))
|
||||||
|
|
||||||
@ -47,5 +47,5 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile>().configureEach {
|
tasks.withType<KotlinCompile>().configureEach {
|
||||||
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
|
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
@file:OptIn(ExperimentalSerializationApi::class)
|
@file:OptIn(ExperimentalSerializationApi::class)
|
||||||
package protobuf.oidb.cmd0x11c5
|
package protobuf.oidb.cmd0x11c5
|
||||||
|
|
||||||
import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY
|
import moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
@ -9,7 +9,7 @@ import moe.fuqiuluo.symbols.Protobuf
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class NtV2RichMediaRsp(
|
data class NtV2RichMediaRsp(
|
||||||
@ProtoNumber(1) val head: RspHead,
|
@ProtoNumber(1) val head: RspHead?,
|
||||||
@ProtoNumber(2) val upload: UploadRsp?,
|
@ProtoNumber(2) val upload: UploadRsp?,
|
||||||
@ProtoNumber(3) val download: DownloadRsp?,
|
@ProtoNumber(3) val download: DownloadRsp?,
|
||||||
@ProtoNumber(4) val downloadRkeyRsp: DownloadRkeyRsp?,
|
@ProtoNumber(4) val downloadRkeyRsp: DownloadRkeyRsp?,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package protobuf.oidb.cmd0x388
|
package protobuf.oidb.cmd0x388
|
||||||
|
|
||||||
import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY
|
import moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
import moe.fuqiuluo.symbols.Protobuf
|
import moe.fuqiuluo.symbols.Protobuf
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
package protobuf.oidb.cmd0x388
|
package protobuf.oidb.cmd0x388
|
||||||
|
|
||||||
import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY
|
import moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
@ -6,9 +6,13 @@ import com.tencent.mobileqq.app.BusinessObserver;
|
|||||||
import com.tencent.mobileqq.app.MessageHandler;
|
import com.tencent.mobileqq.app.MessageHandler;
|
||||||
import com.tencent.qphone.base.remote.ToServiceMsg;
|
import com.tencent.qphone.base.remote.ToServiceMsg;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import mqq.app.AppRuntime;
|
import mqq.app.AppRuntime;
|
||||||
|
|
||||||
public abstract class AppInterface extends AppRuntime {
|
public abstract class AppInterface extends AppRuntime {
|
||||||
|
private final ConcurrentHashMap<String, BusinessHandler> allHandler = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public String getCurrentNickname() {
|
public String getCurrentNickname() {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,10 @@ public abstract class BaseBusinessHandler extends OidbWrapper {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addBusinessObserver(ToServiceMsg toServiceMsg, BusinessObserver businessObserver, boolean z) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public final <T> T decodePacket(byte[] data, String name, T obj) {
|
public final <T> T decodePacket(byte[] data, String name, T obj) {
|
||||||
UniPacket uniPacket = new UniPacket(true);
|
UniPacket uniPacket = new UniPacket(true);
|
||||||
try {
|
try {
|
||||||
@ -24,6 +28,10 @@ public abstract class BaseBusinessHandler extends OidbWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean msgCmdFilter(String str) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract Set<String> getCommandList();
|
protected abstract Set<String> getCommandList();
|
||||||
|
|
||||||
protected abstract Set<String> getPushCommandList();
|
protected abstract Set<String> getPushCommandList();
|
||||||
|
@ -8,6 +8,8 @@ public abstract class BusinessHandler extends BaseBusinessHandler {
|
|||||||
public BusinessHandler(AppInterface appInterface) {
|
public BusinessHandler(AppInterface appInterface) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract Class<? extends BusinessObserver> observerClass();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<String> getCommandList() {
|
public Set<String> getCommandList() {
|
||||||
return null;
|
return null;
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
package com.tencent.mobileqq.msf.sdk;
|
||||||
|
|
||||||
|
import com.tencent.qphone.base.remote.FromServiceMsg;
|
||||||
|
import com.tencent.qphone.base.remote.ToServiceMsg;
|
||||||
|
|
||||||
|
public class MsfMessagePair {
|
||||||
|
public FromServiceMsg fromServiceMsg;
|
||||||
|
public String sendProcess;
|
||||||
|
public ToServiceMsg toServiceMsg;
|
||||||
|
|
||||||
|
public MsfMessagePair(String str, ToServiceMsg toServiceMsg, FromServiceMsg fromServiceMsg) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public MsfMessagePair(ToServiceMsg toServiceMsg, FromServiceMsg fromServiceMsg) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,11 @@ public class MsgService {
|
|||||||
public void addMsgListener(IKernelMsgListener listener) {
|
public void addMsgListener(IKernelMsgListener listener) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeMsgListener(@NotNull IKernelMsgListener iKernelMsgListener) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public String getRichMediaFilePathForGuild(@NotNull RichMediaFilePathInfo richMediaFilePathInfo) {
|
public String getRichMediaFilePathForGuild(@NotNull RichMediaFilePathInfo richMediaFilePathInfo) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,13 @@ public abstract class AppRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MobileQQ getApplication() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startServlet(NewIntent newIntent) {
|
||||||
|
}
|
||||||
|
|
||||||
public <T extends IRuntimeService> T getRuntimeService(Class<T> cls, String namespace) {
|
public <T extends IRuntimeService> T getRuntimeService(Class<T> cls, String namespace) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
29
qqinterface/src/main/java/mqq/app/NewIntent.java
Normal file
29
qqinterface/src/main/java/mqq/app/NewIntent.java
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package mqq.app;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import com.tencent.mobileqq.app.BusinessObserver;
|
||||||
|
|
||||||
|
public class NewIntent extends Intent {
|
||||||
|
public boolean runNow;
|
||||||
|
|
||||||
|
public NewIntent(Context context, Class<? extends Servlet> cls) {
|
||||||
|
super(context, cls);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BusinessObserver getObserver() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isWithouLogin() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setObserver(BusinessObserver businessObserver) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWithouLogin(boolean z) {
|
||||||
|
}
|
||||||
|
}
|
@ -39,3 +39,6 @@ include(
|
|||||||
include(":protobuf")
|
include(":protobuf")
|
||||||
include(":processor")
|
include(":processor")
|
||||||
include(":annotations")
|
include(":annotations")
|
||||||
|
include(":kritor")
|
||||||
|
|
||||||
|
project(":kritor").projectDir = file("kritor/protos")
|
@ -5,6 +5,7 @@ plugins {
|
|||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("kotlin-kapt")
|
id("kotlin-kapt")
|
||||||
id("com.google.devtools.ksp") version "1.9.22-1.0.17"
|
id("com.google.devtools.ksp") version "1.9.22-1.0.17"
|
||||||
|
id("com.google.protobuf") version "0.9.4"
|
||||||
kotlin("plugin.serialization") version "1.9.22"
|
kotlin("plugin.serialization") version "1.9.22"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +64,8 @@ dependencies {
|
|||||||
compileOnly ("de.robv.android.xposed:api:82")
|
compileOnly ("de.robv.android.xposed:api:82")
|
||||||
compileOnly (project(":qqinterface"))
|
compileOnly (project(":qqinterface"))
|
||||||
|
|
||||||
|
protobuf(project(":kritor"))
|
||||||
|
|
||||||
implementation(project(":protobuf"))
|
implementation(project(":protobuf"))
|
||||||
implementation(project(":annotations"))
|
implementation(project(":annotations"))
|
||||||
ksp(project(":processor"))
|
ksp(project(":processor"))
|
||||||
@ -72,9 +75,7 @@ dependencies {
|
|||||||
DEPENDENCY_ANDROIDX.forEach {
|
DEPENDENCY_ANDROIDX.forEach {
|
||||||
implementation(it)
|
implementation(it)
|
||||||
}
|
}
|
||||||
implementation(DEPENDENCY_JAVA_WEBSOCKET)
|
//implementation(DEPENDENCY_PROTOBUF)
|
||||||
implementation(DEPENDENCY_PROTOBUF)
|
|
||||||
implementation(DEPENDENCY_JSON5K)
|
|
||||||
|
|
||||||
implementation(room("runtime"))
|
implementation(room("runtime"))
|
||||||
kapt(room("compiler"))
|
kapt(room("compiler"))
|
||||||
@ -83,16 +84,15 @@ dependencies {
|
|||||||
implementation(kotlinx("io-jvm", "0.1.16"))
|
implementation(kotlinx("io-jvm", "0.1.16"))
|
||||||
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
||||||
|
|
||||||
implementation(ktor("server", "core"))
|
|
||||||
implementation(ktor("server", "host-common"))
|
|
||||||
implementation(ktor("server", "status-pages"))
|
|
||||||
implementation(ktor("server", "netty"))
|
|
||||||
implementation(ktor("server", "content-negotiation"))
|
|
||||||
implementation(ktor("client", "core"))
|
implementation(ktor("client", "core"))
|
||||||
implementation(ktor("client", "content-negotiation"))
|
implementation(ktor("client", "okhttp"))
|
||||||
implementation(ktor("client", "cio"))
|
|
||||||
implementation(ktor("serialization", "kotlinx-json"))
|
implementation(ktor("serialization", "kotlinx-json"))
|
||||||
implementation(ktor("network", "tls-certificates"))
|
|
||||||
|
implementation("io.grpc:grpc-stub:1.62.2")
|
||||||
|
implementation("io.grpc:grpc-protobuf-lite:1.62.2")
|
||||||
|
implementation("com.google.protobuf:protobuf-kotlin-lite:3.25.3")
|
||||||
|
implementation("io.grpc:grpc-kotlin-stub:1.4.1")
|
||||||
|
implementation("io.grpc:grpc-okhttp:1.62.2")
|
||||||
|
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
@ -101,6 +101,45 @@ dependencies {
|
|||||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile>().configureEach {
|
tasks.withType<KotlinCompile>().all {
|
||||||
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
|
kotlinOptions {
|
||||||
|
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protobuf {
|
||||||
|
protoc {
|
||||||
|
artifact = "com.google.protobuf:protoc:3.25.3"
|
||||||
|
}
|
||||||
|
plugins {
|
||||||
|
create("java") {
|
||||||
|
artifact = "io.grpc:protoc-gen-grpc-java:1.62.2"
|
||||||
|
}
|
||||||
|
create("grpc") {
|
||||||
|
artifact = "io.grpc:protoc-gen-grpc-java:1.62.2"
|
||||||
|
}
|
||||||
|
create("grpckt") {
|
||||||
|
artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generateProtoTasks {
|
||||||
|
all().forEach {
|
||||||
|
it.plugins {
|
||||||
|
create("java") {
|
||||||
|
option("lite")
|
||||||
|
}
|
||||||
|
create("grpc") {
|
||||||
|
option("lite")
|
||||||
|
}
|
||||||
|
create("grpckt") {
|
||||||
|
option("lite")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.builtins {
|
||||||
|
create("kotlin") {
|
||||||
|
option("lite")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
43
xposed/src/main/assets/config.properties
Normal file
43
xposed/src/main/assets/config.properties
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Shamrock Config
|
||||||
|
|
||||||
|
# 资源上传群组
|
||||||
|
resource_group=883536416
|
||||||
|
|
||||||
|
# 强制使用平板模式
|
||||||
|
force_tablet=false
|
||||||
|
|
||||||
|
# 被动(反向)RPC开关
|
||||||
|
passive_rpc=false
|
||||||
|
# 被动(反向)RPC地址
|
||||||
|
rpc_address=
|
||||||
|
# 第一个被动RPC鉴权token
|
||||||
|
rpc_address.ticket=
|
||||||
|
# 如果有多个请使用
|
||||||
|
# 我是第二个地址
|
||||||
|
#rpc_address.1=
|
||||||
|
# 第二个被动RPC鉴权token
|
||||||
|
#rpc_address.1.ticket=
|
||||||
|
|
||||||
|
# 主动(正向)RPC开关
|
||||||
|
active_rpc=false
|
||||||
|
# 主动(正向)RPC端口
|
||||||
|
rpc_port=5700
|
||||||
|
# 主动RPC鉴权token
|
||||||
|
active_ticket=
|
||||||
|
# 多鉴权token支持
|
||||||
|
# 第二个主动RPC鉴权token
|
||||||
|
#active_ticket.1=
|
||||||
|
|
||||||
|
# 自回复开关
|
||||||
|
#alive_reply=false
|
||||||
|
# 自回复消息
|
||||||
|
enable_self_message=false
|
||||||
|
|
||||||
|
# 旧BDH兼容开关
|
||||||
|
enable_old_bdh=true
|
||||||
|
|
||||||
|
# 反JVM调用栈跟踪
|
||||||
|
anti_jvm_trace=true
|
||||||
|
|
||||||
|
# 调试模式
|
||||||
|
#debug=false
|
@ -173,7 +173,7 @@ NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) {
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_moe_fuqiuluo_shamrock_xposed_hooks_AntiDetection_antiNativeDetections(JNIEnv *env,
|
Java_moe_fuqiuluo_shamrock_xposed_actions_AntiDetection_antiNativeDetections(JNIEnv *env,
|
||||||
jobject thiz) {
|
jobject thiz) {
|
||||||
if (hook_function == nullptr) return false;
|
if (hook_function == nullptr) return false;
|
||||||
hook_function((void*) __system_property_get, (void *)fake_system_property_get, (void **) &backup_system_property_get);
|
hook_function((void*) __system_property_get, (void *)fake_system_property_get, (void **) &backup_system_property_get);
|
||||||
|
64
xposed/src/main/java/kritor/auth/AuthInterceptor.kt
Normal file
64
xposed/src/main/java/kritor/auth/AuthInterceptor.kt
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package kritor.auth
|
||||||
|
|
||||||
|
import io.grpc.ForwardingServerCallListener
|
||||||
|
import io.grpc.Metadata
|
||||||
|
import io.grpc.ServerCall
|
||||||
|
import io.grpc.ServerCallHandler
|
||||||
|
import io.grpc.ServerInterceptor
|
||||||
|
import moe.fuqiuluo.shamrock.config.ActiveTicket
|
||||||
|
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||||
|
|
||||||
|
object AuthInterceptor: ServerInterceptor {
|
||||||
|
/**
|
||||||
|
* Intercept [ServerCall] dispatch by the `next` [ServerCallHandler]. General
|
||||||
|
* semantics of [ServerCallHandler.startCall] apply and the returned
|
||||||
|
* [io.grpc.ServerCall.Listener] must not be `null`.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* If the implementation throws an exception, `call` will be closed with an error.
|
||||||
|
* Implementations must not throw an exception if they started processing that may use `call` on another thread.
|
||||||
|
*
|
||||||
|
* @param call object to receive response messages
|
||||||
|
* @param headers which can contain extra call metadata from [ClientCall.start],
|
||||||
|
* e.g. authentication credentials.
|
||||||
|
* @param next next processor in the interceptor chain
|
||||||
|
* @return listener for processing incoming messages for `call`, never `null`.
|
||||||
|
*/
|
||||||
|
override fun <ReqT : Any?, RespT : Any?> interceptCall(
|
||||||
|
call: ServerCall<ReqT, RespT>,
|
||||||
|
headers: Metadata?,
|
||||||
|
next: ServerCallHandler<ReqT, RespT>
|
||||||
|
): ServerCall.Listener<ReqT> {
|
||||||
|
val methodName = call.methodDescriptor.fullMethodName
|
||||||
|
val ticket = getAllTicket()
|
||||||
|
if (ticket.isNotEmpty() && !methodName.startsWith("Auth")) {
|
||||||
|
val ticketHeader = headers?.get(Metadata.Key.of("ticket", Metadata.ASCII_STRING_MARSHALLER))
|
||||||
|
if (ticketHeader == null || ticketHeader !in ticket) {
|
||||||
|
call.close(io.grpc.Status.UNAUTHENTICATED.withDescription("Invalid ticket"), Metadata())
|
||||||
|
return object: ServerCall.Listener<ReqT>() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object: ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(next.startCall(call, headers)) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllTicket(): List<String> {
|
||||||
|
val result = arrayListOf<String>()
|
||||||
|
val activeTicketName = ActiveTicket.name()
|
||||||
|
var index = 0
|
||||||
|
while (true) {
|
||||||
|
val ticket = ShamrockConfig.getProperty(activeTicketName + if (index == 0) "" else ".$index", null)
|
||||||
|
if (ticket.isNullOrEmpty()) {
|
||||||
|
if (index == 0) {
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.add(ticket)
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
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 {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
45
xposed/src/main/java/kritor/server/KritorServer.kt
Normal file
45
xposed/src/main/java/kritor/server/KritorServer.kt
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
package kritor.server
|
||||||
|
|
||||||
|
import io.grpc.Grpc
|
||||||
|
import io.grpc.InsecureServerCredentials
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.asExecutor
|
||||||
|
import kritor.auth.AuthInterceptor
|
||||||
|
import kritor.service.*
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
class KritorServer(
|
||||||
|
private val port: Int
|
||||||
|
): CoroutineScope {
|
||||||
|
private val server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create())
|
||||||
|
.executor(Dispatchers.IO.asExecutor())
|
||||||
|
.intercept(AuthInterceptor)
|
||||||
|
.addService(Authentication)
|
||||||
|
.addService(ContactService)
|
||||||
|
.addService(KritorService)
|
||||||
|
.addService(FriendService)
|
||||||
|
.addService(GroupService)
|
||||||
|
.addService(GroupFileService)
|
||||||
|
.addService(MessageService)
|
||||||
|
.addService(EventService)
|
||||||
|
.addService(ForwardMessageService)
|
||||||
|
.addService(WebService)
|
||||||
|
.addService(DeveloperService)
|
||||||
|
.build()!!
|
||||||
|
|
||||||
|
fun start(block: Boolean = false) {
|
||||||
|
LogCenter.log("KritorServer started at port $port.")
|
||||||
|
server.start()
|
||||||
|
|
||||||
|
if (block) {
|
||||||
|
server.awaitTermination()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val coroutineContext: CoroutineContext =
|
||||||
|
Dispatchers.IO.limitedParallelism(12)
|
||||||
|
}
|
66
xposed/src/main/java/kritor/service/Authentication.kt
Normal file
66
xposed/src/main/java/kritor/service/Authentication.kt
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
import io.grpc.Status
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
|
import io.kritor.AuthCode
|
||||||
|
import io.kritor.AuthReq
|
||||||
|
import io.kritor.AuthRsp
|
||||||
|
import io.kritor.AuthenticationGrpcKt
|
||||||
|
import io.kritor.GetAuthStateReq
|
||||||
|
import io.kritor.GetAuthStateRsp
|
||||||
|
import io.kritor.authRsp
|
||||||
|
import io.kritor.getAuthStateRsp
|
||||||
|
import kritor.auth.AuthInterceptor
|
||||||
|
import moe.fuqiuluo.shamrock.config.ActiveTicket
|
||||||
|
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
|
||||||
|
internal object Authentication: AuthenticationGrpcKt.AuthenticationCoroutineImplBase() {
|
||||||
|
@Grpc("Authentication", "Auth")
|
||||||
|
override suspend fun auth(request: AuthReq): AuthRsp {
|
||||||
|
if (QQInterfaces.app.account != request.account) {
|
||||||
|
return authRsp {
|
||||||
|
code = AuthCode.NO_ACCOUNT
|
||||||
|
msg = "No such account"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val activeTicketName = ActiveTicket.name()
|
||||||
|
var index = 0
|
||||||
|
while (true) {
|
||||||
|
val ticket = ShamrockConfig.getProperty(activeTicketName + if (index == 0) "" else ".$index", null)
|
||||||
|
if (ticket.isNullOrEmpty()) {
|
||||||
|
if (index == 0) {
|
||||||
|
return authRsp {
|
||||||
|
code = AuthCode.OK
|
||||||
|
msg = "OK"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if (ticket == request.ticket) {
|
||||||
|
return authRsp {
|
||||||
|
code = AuthCode.OK
|
||||||
|
msg = "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
return authRsp {
|
||||||
|
code = AuthCode.NO_TICKET
|
||||||
|
msg = "Invalid ticket"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("Authentication", "GetAuthState")
|
||||||
|
override suspend fun getAuthState(request: GetAuthStateReq): GetAuthStateRsp {
|
||||||
|
if (request.account != QQInterfaces.app.account) {
|
||||||
|
throw StatusRuntimeException(Status.CANCELLED.withDescription("No such account"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAuthStateRsp {
|
||||||
|
isRequiredAuth = AuthInterceptor.getAllTicket().isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
183
xposed/src/main/java/kritor/service/ContactService.kt
Normal file
183
xposed/src/main/java/kritor/service/ContactService.kt
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.tencent.mobileqq.profilecard.api.IProfileCardBlacklistApi
|
||||||
|
import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst.*
|
||||||
|
import com.tencent.mobileqq.profilecard.api.IProfileProtocolService
|
||||||
|
import com.tencent.mobileqq.qroute.QRoute
|
||||||
|
import io.grpc.Status
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
|
import io.kritor.contact.ContactServiceGrpcKt
|
||||||
|
import io.kritor.contact.GetUidRequest
|
||||||
|
import io.kritor.contact.GetUidResponse
|
||||||
|
import io.kritor.contact.GetUinByUidRequest
|
||||||
|
import io.kritor.contact.GetUinByUidResponse
|
||||||
|
import io.kritor.contact.IsBlackListUserRequest
|
||||||
|
import io.kritor.contact.IsBlackListUserResponse
|
||||||
|
import io.kritor.contact.ProfileCard
|
||||||
|
import io.kritor.contact.ProfileCardRequest
|
||||||
|
import io.kritor.contact.SetProfileCardRequest
|
||||||
|
import io.kritor.contact.SetProfileCardResponse
|
||||||
|
import io.kritor.contact.StrangerExt
|
||||||
|
import io.kritor.contact.StrangerInfo
|
||||||
|
import io.kritor.contact.StrangerInfoRequest
|
||||||
|
import io.kritor.contact.VoteUserRequest
|
||||||
|
import io.kritor.contact.VoteUserResponse
|
||||||
|
import io.kritor.contact.profileCard
|
||||||
|
import io.kritor.contact.strangerInfo
|
||||||
|
import io.kritor.contact.voteUserResponse
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
import qq.service.contact.ContactHelper
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
internal object ContactService: ContactServiceGrpcKt.ContactServiceCoroutineImplBase() {
|
||||||
|
@Grpc("ContactService", "VoteUser")
|
||||||
|
override suspend fun voteUser(request: VoteUserRequest): VoteUserResponse {
|
||||||
|
ContactHelper.voteUser(when(request.accountCase!!) {
|
||||||
|
VoteUserRequest.AccountCase.ACCOUNT_UIN -> request.accountUin
|
||||||
|
VoteUserRequest.AccountCase.ACCOUNT_UID -> ContactHelper.getUinByUidAsync(request.accountUid).toLong()
|
||||||
|
VoteUserRequest.AccountCase.ACCOUNT_NOT_SET -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("account not set")
|
||||||
|
)
|
||||||
|
}, request.voteCount).onFailure {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL
|
||||||
|
.withDescription(it.stackTraceToString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return voteUserResponse { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("ContactService", "GetProfileCard")
|
||||||
|
override suspend fun getProfileCard(request: ProfileCardRequest): ProfileCard {
|
||||||
|
val uin = when (request.accountCase!!) {
|
||||||
|
ProfileCardRequest.AccountCase.ACCOUNT_UIN -> request.accountUin
|
||||||
|
ProfileCardRequest.AccountCase.ACCOUNT_UID -> ContactHelper.getUinByUidAsync(request.accountUid).toLong()
|
||||||
|
ProfileCardRequest.AccountCase.ACCOUNT_NOT_SET -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("account not set")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val contact = ContactHelper.getProfileCard(uin)
|
||||||
|
|
||||||
|
contact.onFailure {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL
|
||||||
|
.withDescription(it.stackTraceToString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
contact.onSuccess {
|
||||||
|
return profileCard {
|
||||||
|
this.uin = it.uin.toLong()
|
||||||
|
this.uid = if (request.hasAccountUid()) request.accountUid
|
||||||
|
else ContactHelper.getUidByUinAsync(it.uin.toLong())
|
||||||
|
this.name = it.strNick ?: ""
|
||||||
|
this.remark = it.strReMark ?: ""
|
||||||
|
this.level = it.iQQLevel
|
||||||
|
this.birthday = it.lBirthday
|
||||||
|
this.loginDay = it.lLoginDays.toInt()
|
||||||
|
this.voteCnt = it.lVoteCount.toInt()
|
||||||
|
this.qid = it.qid ?: ""
|
||||||
|
this.isSchoolVerified = it.schoolVerifiedFlag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL
|
||||||
|
.withDescription("logic failed")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("ContactService", "GetStrangerInfo")
|
||||||
|
override suspend fun getStrangerInfo(request: StrangerInfoRequest): StrangerInfo {
|
||||||
|
val userId = request.uin
|
||||||
|
val info = ContactHelper.refreshAndGetProfileCard(userId).onFailure {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL
|
||||||
|
.withCause(it)
|
||||||
|
.withDescription("Unable to fetch stranger info")
|
||||||
|
)
|
||||||
|
}.getOrThrow()
|
||||||
|
|
||||||
|
return strangerInfo {
|
||||||
|
this.uid = ContactHelper.getUidByUinAsync(userId)
|
||||||
|
this.uin = (info.uin ?: "0").toLong()
|
||||||
|
this.name = info.strNick ?: ""
|
||||||
|
this.level = info.iQQLevel
|
||||||
|
this.loginDay = info.lLoginDays.toInt()
|
||||||
|
this.voteCnt = info.lVoteCount.toInt()
|
||||||
|
this.qid = info.qid ?: ""
|
||||||
|
this.isSchoolVerified = info.schoolVerifiedFlag
|
||||||
|
this.ext = StrangerExt.newBuilder()
|
||||||
|
.setBigVip(info.bBigClubVipOpen == 1.toByte())
|
||||||
|
.setHollywoodVip(info.bHollywoodVipOpen == 1.toByte())
|
||||||
|
.setQqVip(info.bQQVipOpen == 1.toByte())
|
||||||
|
.setSuperVip(info.bSuperQQOpen == 1.toByte())
|
||||||
|
.setVoted(info.bVoted == 1.toByte())
|
||||||
|
.build().toByteString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("ContactService", "GetUid")
|
||||||
|
override suspend fun getUid(request: GetUidRequest): GetUidResponse {
|
||||||
|
return GetUidResponse.newBuilder().apply {
|
||||||
|
request.uinList.forEach {
|
||||||
|
putUid(it, ContactHelper.getUidByUinAsync(it))
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("ContactService", "GetUinByUid")
|
||||||
|
override suspend fun getUinByUid(request: GetUinByUidRequest): GetUinByUidResponse {
|
||||||
|
return GetUinByUidResponse.newBuilder().apply {
|
||||||
|
request.uidList.forEach {
|
||||||
|
putUin(it, ContactHelper.getUinByUidAsync(it).toLong())
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("ContactService", "SetProfileCard")
|
||||||
|
override suspend fun setProfileCard(request: SetProfileCardRequest): SetProfileCardResponse {
|
||||||
|
val bundle = Bundle()
|
||||||
|
val service = QQInterfaces.app
|
||||||
|
.getRuntimeService(IProfileProtocolService::class.java, "all")
|
||||||
|
if (request.hasNickName()) {
|
||||||
|
bundle.putString(KEY_NICK, request.nickName)
|
||||||
|
}
|
||||||
|
if (request.hasCompany()) {
|
||||||
|
bundle.putString(KEY_COMPANY, request.company)
|
||||||
|
}
|
||||||
|
if (request.hasEmail()) {
|
||||||
|
bundle.putString(KEY_EMAIL, request.email)
|
||||||
|
}
|
||||||
|
if (request.hasCollege()) {
|
||||||
|
bundle.putString(KEY_COLLEGE, request.college)
|
||||||
|
}
|
||||||
|
if (request.hasPersonalNote()) {
|
||||||
|
bundle.putString(KEY_PERSONAL_NOTE, request.personalNote)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.hasBirthday()) {
|
||||||
|
bundle.putInt(KEY_BIRTHDAY, request.birthday)
|
||||||
|
}
|
||||||
|
if (request.hasAge()) {
|
||||||
|
bundle.putInt(KEY_AGE, request.age)
|
||||||
|
}
|
||||||
|
|
||||||
|
service.setProfileDetail(bundle)
|
||||||
|
return super.setProfileCard(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("ContactService", "IsBlackListUser")
|
||||||
|
override suspend fun isBlackListUser(request: IsBlackListUserRequest): IsBlackListUserResponse {
|
||||||
|
val blacklistApi = QRoute.api(IProfileCardBlacklistApi::class.java)
|
||||||
|
val isBlack = withTimeoutOrNull(5000) {
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
blacklistApi.isBlackOrBlackedUin(request.uin.toString()) {
|
||||||
|
continuation.resume(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: false
|
||||||
|
return IsBlackListUserResponse.newBuilder().setIsBlackListUser(isBlack).build()
|
||||||
|
}
|
||||||
|
}
|
50
xposed/src/main/java/kritor/service/DeveloperService.kt
Normal file
50
xposed/src/main/java/kritor/service/DeveloperService.kt
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import com.tencent.mobileqq.fe.FEKit
|
||||||
|
import com.tencent.mobileqq.qsec.qsecdandelionsdk.Dandelion
|
||||||
|
import io.kritor.developer.DeveloperServiceGrpcKt
|
||||||
|
import io.kritor.developer.EnergyRequest
|
||||||
|
import io.kritor.developer.EnergyResponse
|
||||||
|
import io.kritor.developer.SendPacketRequest
|
||||||
|
import io.kritor.developer.SendPacketResponse
|
||||||
|
import io.kritor.developer.SignRequest
|
||||||
|
import io.kritor.developer.SignResponse
|
||||||
|
import io.kritor.developer.energyResponse
|
||||||
|
import io.kritor.developer.sendPacketResponse
|
||||||
|
import io.kritor.developer.signResponse
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
|
||||||
|
internal object DeveloperService: DeveloperServiceGrpcKt.DeveloperServiceCoroutineImplBase() {
|
||||||
|
@Grpc("DeveloperService", "Sign")
|
||||||
|
override suspend fun sign(request: SignRequest): SignResponse {
|
||||||
|
return signResponse {
|
||||||
|
val result = FEKit.getInstance().getSign(request.command, request.buffer.toByteArray(), request.seq, request.uin)
|
||||||
|
this.sign = ByteString.copyFrom(result.sign)
|
||||||
|
this.token = ByteString.copyFrom(result.token)
|
||||||
|
this.extra = ByteString.copyFrom(result.extra)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("DeveloperService", "Energy")
|
||||||
|
override suspend fun energy(request: EnergyRequest): EnergyResponse {
|
||||||
|
return energyResponse {
|
||||||
|
this.result = ByteString.copyFrom(Dandelion.getInstance().fly(request.data, request.salt.toByteArray()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Grpc("DeveloperService", "SendPacket")
|
||||||
|
override suspend fun sendPacket(request: SendPacketRequest): SendPacketResponse {
|
||||||
|
return sendPacketResponse {
|
||||||
|
val fromServiceMsg = QQInterfaces.sendBufferAW(request.command, request.isProtobuf, request.requestBuffer.toByteArray())
|
||||||
|
if (fromServiceMsg?.wupBuffer == null) {
|
||||||
|
this.isSuccess = false
|
||||||
|
} else {
|
||||||
|
this.isSuccess = true
|
||||||
|
this.responseBuffer = ByteString.copyFrom(fromServiceMsg.wupBuffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
42
xposed/src/main/java/kritor/service/EventService.kt
Normal file
42
xposed/src/main/java/kritor/service/EventService.kt
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
import io.grpc.Status
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
|
import io.kritor.event.EventRequest
|
||||||
|
import io.kritor.event.EventServiceGrpcKt
|
||||||
|
import io.kritor.event.EventStructure
|
||||||
|
import io.kritor.event.EventType
|
||||||
|
import io.kritor.event.RequestPushEvent
|
||||||
|
import io.kritor.event.eventStructure
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter
|
||||||
|
|
||||||
|
internal object EventService: EventServiceGrpcKt.EventServiceCoroutineImplBase() {
|
||||||
|
override fun registerActiveListener(request: RequestPushEvent): Flow<EventStructure> {
|
||||||
|
return channelFlow {
|
||||||
|
when(request.type!!) {
|
||||||
|
EventType.EVENT_TYPE_CORE_EVENT -> {}
|
||||||
|
EventType.EVENT_TYPE_MESSAGE -> GlobalEventTransmitter.onMessageEvent {
|
||||||
|
send(eventStructure {
|
||||||
|
this.type = EventType.EVENT_TYPE_MESSAGE
|
||||||
|
this.message = it.second
|
||||||
|
})
|
||||||
|
}
|
||||||
|
EventType.EVENT_TYPE_NOTICE -> GlobalEventTransmitter.onRequestEvent {
|
||||||
|
send(eventStructure {
|
||||||
|
this.type = EventType.EVENT_TYPE_NOTICE
|
||||||
|
this.request = it
|
||||||
|
})
|
||||||
|
}
|
||||||
|
EventType.EVENT_TYPE_REQUEST -> GlobalEventTransmitter.onNoticeEvent {
|
||||||
|
send(eventStructure {
|
||||||
|
this.type = EventType.EVENT_TYPE_NOTICE
|
||||||
|
this.notice = it
|
||||||
|
})
|
||||||
|
}
|
||||||
|
EventType.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
xposed/src/main/java/kritor/service/FriendService.kt
Normal file
41
xposed/src/main/java/kritor/service/FriendService.kt
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
import io.grpc.Status
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
|
import io.kritor.friend.FriendServiceGrpcKt
|
||||||
|
import io.kritor.friend.GetFriendListRequest
|
||||||
|
import io.kritor.friend.GetFriendListResponse
|
||||||
|
import io.kritor.friend.friendData
|
||||||
|
import io.kritor.friend.friendExt
|
||||||
|
import io.kritor.friend.getFriendListResponse
|
||||||
|
import qq.service.contact.ContactHelper
|
||||||
|
import qq.service.friend.FriendHelper
|
||||||
|
|
||||||
|
internal object FriendService: FriendServiceGrpcKt.FriendServiceCoroutineImplBase() {
|
||||||
|
@Grpc("FriendService", "GetFriendList")
|
||||||
|
override suspend fun getFriendList(request: GetFriendListRequest): GetFriendListResponse {
|
||||||
|
val friendList = FriendHelper.getFriendList(if(request.hasRefresh()) request.refresh else false).onFailure {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL
|
||||||
|
.withDescription(it.stackTraceToString())
|
||||||
|
)
|
||||||
|
}.getOrThrow()
|
||||||
|
|
||||||
|
return getFriendListResponse {
|
||||||
|
friendList.forEach {
|
||||||
|
this.friendList.add(friendData {
|
||||||
|
uin = it.uin.toLong()
|
||||||
|
uid = ContactHelper.getUidByUinAsync(uin)
|
||||||
|
qid = ""
|
||||||
|
nick = it.name ?: ""
|
||||||
|
remark = it.remark ?: ""
|
||||||
|
age = it.age
|
||||||
|
level = 0
|
||||||
|
gender = it.gender.toInt()
|
||||||
|
groupId = it.groupid
|
||||||
|
ext = friendExt {}.toByteString()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
139
xposed/src/main/java/kritor/service/GroupFileService.kt
Normal file
139
xposed/src/main/java/kritor/service/GroupFileService.kt
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
import io.grpc.Status
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
|
import io.kritor.file.*
|
||||||
|
import moe.fuqiuluo.shamrock.tools.slice
|
||||||
|
import moe.fuqiuluo.symbols.decodeProtobuf
|
||||||
|
import protobuf.auto.toByteArray
|
||||||
|
import protobuf.oidb.cmd0x6d7.CreateFolderReq
|
||||||
|
import protobuf.oidb.cmd0x6d7.DeleteFolderReq
|
||||||
|
import protobuf.oidb.cmd0x6d7.Oidb0x6d7ReqBody
|
||||||
|
import protobuf.oidb.cmd0x6d7.Oidb0x6d7RespBody
|
||||||
|
import protobuf.oidb.cmd0x6d7.RenameFolderReq
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
import qq.service.file.GroupFileHelper
|
||||||
|
import qq.service.file.GroupFileHelper.getGroupFileSystemInfo
|
||||||
|
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
|
||||||
|
import tencent.im.oidb.cmd0x6d8.oidb_0x6d8
|
||||||
|
import tencent.im.oidb.oidb_sso
|
||||||
|
|
||||||
|
internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCoroutineImplBase() {
|
||||||
|
@Grpc("GroupFileService", "CreateFolder")
|
||||||
|
override suspend fun createFolder(request: CreateFolderRequest): CreateFolderResponse {
|
||||||
|
val data = Oidb0x6d7ReqBody(
|
||||||
|
createFolder = CreateFolderReq(
|
||||||
|
groupCode = request.groupId.toULong(),
|
||||||
|
appId = 3u,
|
||||||
|
parentFolderId = "/",
|
||||||
|
folderName = request.name
|
||||||
|
)
|
||||||
|
).toByteArray()
|
||||||
|
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_0", 1751, 0, data)
|
||||||
|
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||||
|
if (fromServiceMsg.wupBuffer == null) {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||||
|
}
|
||||||
|
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||||
|
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
val rsp = oidbPkg.bytes_bodybuffer.get()
|
||||||
|
.toByteArray()
|
||||||
|
.decodeProtobuf<Oidb0x6d7RespBody>()
|
||||||
|
if (rsp.createFolder?.retCode != 0) {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to create folder: ${rsp.createFolder?.retCode}"))
|
||||||
|
}
|
||||||
|
return createFolderResponse {
|
||||||
|
this.id = rsp.createFolder?.folderInfo?.folderId ?: ""
|
||||||
|
this.usedSpace = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupFileService", "DeleteFolder")
|
||||||
|
override suspend fun deleteFolder(request: DeleteFolderRequest): DeleteFolderResponse {
|
||||||
|
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_1", 1751, 1, Oidb0x6d7ReqBody(
|
||||||
|
deleteFolder = DeleteFolderReq(
|
||||||
|
groupCode = request.groupId.toULong(),
|
||||||
|
appId = 3u,
|
||||||
|
folderId = request.folderId
|
||||||
|
)
|
||||||
|
).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||||
|
if (fromServiceMsg.wupBuffer == null) {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||||
|
}
|
||||||
|
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||||
|
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
|
||||||
|
if (rsp.deleteFolder?.retCode != 0) {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to delete folder: ${rsp.deleteFolder?.retCode}"))
|
||||||
|
}
|
||||||
|
return deleteFolderResponse { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupFileService", "DeleteFile")
|
||||||
|
override suspend fun deleteFile(request: DeleteFileRequest): DeleteFileResponse {
|
||||||
|
val oidb0x6d6ReqBody = oidb_0x6d6.ReqBody().apply {
|
||||||
|
delete_file_req.set(oidb_0x6d6.DeleteFileReqBody().apply {
|
||||||
|
uint64_group_code.set(request.groupId)
|
||||||
|
uint32_app_id.set(3)
|
||||||
|
uint32_bus_id.set(request.busId)
|
||||||
|
str_parent_folder_id.set("/")
|
||||||
|
str_file_id.set(request.fileId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d6_3", 1750, 3, oidb0x6d6ReqBody.toByteArray())
|
||||||
|
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||||
|
if (fromServiceMsg.wupBuffer == null) {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||||
|
}
|
||||||
|
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||||
|
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
val rsp = oidb_0x6d6.RspBody().apply {
|
||||||
|
mergeFrom(oidbPkg.bytes_bodybuffer.get().toByteArray())
|
||||||
|
}
|
||||||
|
if (rsp.delete_file_rsp.int32_ret_code.get() != 0) {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to delete file: ${rsp.delete_file_rsp.int32_ret_code.get()}"))
|
||||||
|
}
|
||||||
|
return deleteFileResponse { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupFileService", "RenameFolder")
|
||||||
|
override suspend fun renameFolder(request: RenameFolderRequest): RenameFolderResponse {
|
||||||
|
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_3", 1751, 3, Oidb0x6d7ReqBody(
|
||||||
|
renameFolder = RenameFolderReq(
|
||||||
|
groupCode = request.groupId.toULong(),
|
||||||
|
appId = 3u,
|
||||||
|
folderId = request.folderId,
|
||||||
|
folderName = request.name
|
||||||
|
)
|
||||||
|
).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||||
|
if (fromServiceMsg.wupBuffer == null) {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||||
|
}
|
||||||
|
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||||
|
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
|
||||||
|
if (rsp.renameFolder?.retCode != 0) {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to rename folder: ${rsp.renameFolder?.retCode}"))
|
||||||
|
}
|
||||||
|
return renameFolderResponse { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupFileService", "GetFileSystemInfo")
|
||||||
|
override suspend fun getFileSystemInfo(request: GetFileSystemInfoRequest): GetFileSystemInfoResponse {
|
||||||
|
return getGroupFileSystemInfo(request.groupId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupFileService", "GetRootFiles")
|
||||||
|
override suspend fun getRootFiles(request: GetRootFilesRequest): GetRootFilesResponse {
|
||||||
|
return getRootFilesResponse {
|
||||||
|
val response = GroupFileHelper.getGroupFiles(request.groupId)
|
||||||
|
this.files.addAll(response.filesList)
|
||||||
|
this.folders.addAll(response.foldersList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupFileService", "GetFiles")
|
||||||
|
override suspend fun getFiles(request: GetFilesRequest): GetFilesResponse {
|
||||||
|
return GroupFileHelper.getGroupFiles(request.groupId, request.folderId)
|
||||||
|
}
|
||||||
|
}
|
405
xposed/src/main/java/kritor/service/GroupService.kt
Normal file
405
xposed/src/main/java/kritor/service/GroupService.kt
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
import io.grpc.Status
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
|
import io.kritor.group.BanMemberRequest
|
||||||
|
import io.kritor.group.BanMemberResponse
|
||||||
|
import io.kritor.group.GetGroupHonorRequest
|
||||||
|
import io.kritor.group.GetGroupHonorResponse
|
||||||
|
import io.kritor.group.GetGroupInfoRequest
|
||||||
|
import io.kritor.group.GetGroupInfoResponse
|
||||||
|
import io.kritor.group.GetGroupListRequest
|
||||||
|
import io.kritor.group.GetGroupListResponse
|
||||||
|
import io.kritor.group.GetGroupMemberInfoRequest
|
||||||
|
import io.kritor.group.GetGroupMemberInfoResponse
|
||||||
|
import io.kritor.group.GetGroupMemberListRequest
|
||||||
|
import io.kritor.group.GetGroupMemberListResponse
|
||||||
|
import io.kritor.group.GetNotJoinedGroupInfoRequest
|
||||||
|
import io.kritor.group.GetNotJoinedGroupInfoResponse
|
||||||
|
import io.kritor.group.GetProhibitedUserListRequest
|
||||||
|
import io.kritor.group.GetProhibitedUserListResponse
|
||||||
|
import io.kritor.group.GetRemainCountAtAllRequest
|
||||||
|
import io.kritor.group.GetRemainCountAtAllResponse
|
||||||
|
import io.kritor.group.GroupServiceGrpcKt
|
||||||
|
import io.kritor.group.KickMemberRequest
|
||||||
|
import io.kritor.group.KickMemberResponse
|
||||||
|
import io.kritor.group.LeaveGroupRequest
|
||||||
|
import io.kritor.group.LeaveGroupResponse
|
||||||
|
import io.kritor.group.ModifyGroupNameRequest
|
||||||
|
import io.kritor.group.ModifyGroupNameResponse
|
||||||
|
import io.kritor.group.ModifyGroupRemarkRequest
|
||||||
|
import io.kritor.group.ModifyGroupRemarkResponse
|
||||||
|
import io.kritor.group.ModifyMemberCardRequest
|
||||||
|
import io.kritor.group.ModifyMemberCardResponse
|
||||||
|
import io.kritor.group.PokeMemberRequest
|
||||||
|
import io.kritor.group.PokeMemberResponse
|
||||||
|
import io.kritor.group.SetGroupAdminRequest
|
||||||
|
import io.kritor.group.SetGroupAdminResponse
|
||||||
|
import io.kritor.group.SetGroupUniqueTitleRequest
|
||||||
|
import io.kritor.group.SetGroupUniqueTitleResponse
|
||||||
|
import io.kritor.group.SetGroupWholeBanRequest
|
||||||
|
import io.kritor.group.SetGroupWholeBanResponse
|
||||||
|
import io.kritor.group.banMemberResponse
|
||||||
|
import io.kritor.group.getGroupHonorResponse
|
||||||
|
import io.kritor.group.getGroupInfoResponse
|
||||||
|
import io.kritor.group.getGroupListResponse
|
||||||
|
import io.kritor.group.getGroupMemberInfoResponse
|
||||||
|
import io.kritor.group.getGroupMemberListResponse
|
||||||
|
import io.kritor.group.getNotJoinedGroupInfoResponse
|
||||||
|
import io.kritor.group.getProhibitedUserListResponse
|
||||||
|
import io.kritor.group.getRemainCountAtAllResponse
|
||||||
|
import io.kritor.group.groupHonorInfo
|
||||||
|
import io.kritor.group.groupMemberInfo
|
||||||
|
import io.kritor.group.kickMemberResponse
|
||||||
|
import io.kritor.group.leaveGroupResponse
|
||||||
|
import io.kritor.group.modifyGroupNameResponse
|
||||||
|
import io.kritor.group.modifyGroupRemarkResponse
|
||||||
|
import io.kritor.group.modifyMemberCardResponse
|
||||||
|
import io.kritor.group.notJoinedGroupInfo
|
||||||
|
import io.kritor.group.pokeMemberResponse
|
||||||
|
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.decodeHonor
|
||||||
|
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
||||||
|
import qq.service.contact.ContactHelper
|
||||||
|
import qq.service.group.GroupHelper
|
||||||
|
|
||||||
|
internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase() {
|
||||||
|
@Grpc("GroupService", "BanMember")
|
||||||
|
override suspend fun banMember(request: BanMemberRequest): BanMemberResponse {
|
||||||
|
if (!GroupHelper.isAdmin(request.groupId.toString())) {
|
||||||
|
throw StatusRuntimeException(Status.PERMISSION_DENIED
|
||||||
|
.withDescription("You are not admin of this group")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupHelper.banMember(request.groupId, when(request.targetCase!!) {
|
||||||
|
BanMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||||
|
BanMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
|
||||||
|
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("target not set")
|
||||||
|
)
|
||||||
|
}, request.duration)
|
||||||
|
|
||||||
|
return banMemberResponse {
|
||||||
|
groupId = request.groupId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "PokeMember", )
|
||||||
|
override suspend fun pokeMember(request: PokeMemberRequest): PokeMemberResponse {
|
||||||
|
GroupHelper.pokeMember(request.groupId, when(request.targetCase!!) {
|
||||||
|
PokeMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||||
|
PokeMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
|
||||||
|
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("target not set")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return pokeMemberResponse { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "KickMember")
|
||||||
|
override suspend fun kickMember(request: KickMemberRequest): KickMemberResponse {
|
||||||
|
if (!GroupHelper.isAdmin(request.groupId.toString())) {
|
||||||
|
throw StatusRuntimeException(Status.PERMISSION_DENIED
|
||||||
|
.withDescription("You are not admin of this group")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
GroupHelper.kickMember(request.groupId, request.rejectAddRequest, if (request.hasKickReason()) request.kickReason else "", when(request.targetCase!!) {
|
||||||
|
KickMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||||
|
KickMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
|
||||||
|
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("target not set")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return kickMemberResponse { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "LeaveGroup")
|
||||||
|
override suspend fun leaveGroup(request: LeaveGroupRequest): LeaveGroupResponse {
|
||||||
|
GroupHelper.resignTroop(request.groupId.toString())
|
||||||
|
return leaveGroupResponse { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "ModifyMemberCard")
|
||||||
|
override suspend fun modifyMemberCard(request: ModifyMemberCardRequest): ModifyMemberCardResponse {
|
||||||
|
if (!GroupHelper.isAdmin(request.groupId.toString())) {
|
||||||
|
throw StatusRuntimeException(Status.PERMISSION_DENIED
|
||||||
|
.withDescription("You are not admin of this group")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
GroupHelper.modifyGroupMemberCard(request.groupId, when(request.targetCase!!) {
|
||||||
|
ModifyMemberCardRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||||
|
ModifyMemberCardRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
|
||||||
|
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("target not set")
|
||||||
|
)
|
||||||
|
}, request.card)
|
||||||
|
return modifyMemberCardResponse { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "ModifyGroupName")
|
||||||
|
override suspend fun modifyGroupName(request: ModifyGroupNameRequest): ModifyGroupNameResponse {
|
||||||
|
if (!GroupHelper.isAdmin(request.groupId.toString())) {
|
||||||
|
throw StatusRuntimeException(Status.PERMISSION_DENIED
|
||||||
|
.withDescription("You are not admin of this group")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupHelper.modifyTroopName(request.groupId.toString(), request.groupName)
|
||||||
|
|
||||||
|
return modifyGroupNameResponse { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "ModifyGroupRemark")
|
||||||
|
override suspend fun modifyGroupRemark(request: ModifyGroupRemarkRequest): ModifyGroupRemarkResponse {
|
||||||
|
GroupHelper.modifyGroupRemark(request.groupId, request.remark)
|
||||||
|
|
||||||
|
return modifyGroupRemarkResponse { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "SetGroupAdmin")
|
||||||
|
override suspend fun setGroupAdmin(request: SetGroupAdminRequest): SetGroupAdminResponse {
|
||||||
|
if (!GroupHelper.isOwner(request.groupId.toString())) {
|
||||||
|
throw StatusRuntimeException(Status.PERMISSION_DENIED
|
||||||
|
.withDescription("You are not admin of this group")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupHelper.setGroupAdmin(request.groupId, when(request.targetCase!!) {
|
||||||
|
SetGroupAdminRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||||
|
SetGroupAdminRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
|
||||||
|
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("target not set")
|
||||||
|
)
|
||||||
|
}, request.isAdmin)
|
||||||
|
|
||||||
|
return setGroupAdminResponse { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "SetGroupUniqueTitle")
|
||||||
|
override suspend fun setGroupUniqueTitle(request: SetGroupUniqueTitleRequest): SetGroupUniqueTitleResponse {
|
||||||
|
if (!GroupHelper.isAdmin(request.groupId.toString())) {
|
||||||
|
throw StatusRuntimeException(Status.PERMISSION_DENIED
|
||||||
|
.withDescription("You are not admin of this group")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupHelper.setGroupUniqueTitle(request.groupId.toString(), when(request.targetCase!!) {
|
||||||
|
SetGroupUniqueTitleRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||||
|
SetGroupUniqueTitleRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
|
||||||
|
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("target not set")
|
||||||
|
)
|
||||||
|
}.toString(), request.uniqueTitle)
|
||||||
|
|
||||||
|
return setGroupUniqueTitleResponse { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "SetGroupWholeBan")
|
||||||
|
override suspend fun setGroupWholeBan(request: SetGroupWholeBanRequest): SetGroupWholeBanResponse {
|
||||||
|
if (!GroupHelper.isAdmin(request.groupId.toString())) {
|
||||||
|
throw StatusRuntimeException(Status.PERMISSION_DENIED
|
||||||
|
.withDescription("You are not admin of this group")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupHelper.setGroupWholeBan(request.groupId, request.isBan)
|
||||||
|
return setGroupWholeBanResponse { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "GetGroupInfo")
|
||||||
|
override suspend fun getGroupInfo(request: GetGroupInfoRequest): GetGroupInfoResponse {
|
||||||
|
val groupInfo = GroupHelper.getGroupInfo(request.groupId.toString(), true).onFailure {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group info").withCause(it))
|
||||||
|
}.getOrThrow()
|
||||||
|
return getGroupInfoResponse {
|
||||||
|
this.groupInfo = io.kritor.group.groupInfo {
|
||||||
|
groupId = groupInfo.troopcode.toLong()
|
||||||
|
groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName } ?: ""
|
||||||
|
groupRemark = groupInfo.troopRemark ?: ""
|
||||||
|
owner = groupInfo.troopowneruin?.toLong() ?: 0
|
||||||
|
admins.addAll(GroupHelper.getAdminList(groupId))
|
||||||
|
maxMemberCount = groupInfo.wMemberMax
|
||||||
|
memberCount = groupInfo.wMemberNum
|
||||||
|
groupUin = groupInfo.troopuin?.toLong() ?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "GetGroupList")
|
||||||
|
override suspend fun getGroupList(request: GetGroupListRequest): GetGroupListResponse {
|
||||||
|
val groupList = GroupHelper.getGroupList(if (request.hasRefresh()) request.refresh else false).onFailure {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group list").withCause(it))
|
||||||
|
}.getOrThrow()
|
||||||
|
return getGroupListResponse {
|
||||||
|
groupList.forEach { groupInfo ->
|
||||||
|
this.groupInfo.add(io.kritor.group.groupInfo {
|
||||||
|
groupId = groupInfo.troopcode.toLong()
|
||||||
|
groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName } ?: ""
|
||||||
|
groupRemark = groupInfo.troopRemark ?: ""
|
||||||
|
owner = groupInfo.troopowneruin?.toLong() ?: 0
|
||||||
|
admins.addAll(GroupHelper.getAdminList(groupId))
|
||||||
|
maxMemberCount = groupInfo.wMemberMax
|
||||||
|
memberCount = groupInfo.wMemberNum
|
||||||
|
groupUin = groupInfo.troopuin?.toLong() ?: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "GetGroupMemberInfo")
|
||||||
|
override suspend fun getGroupMemberInfo(request: GetGroupMemberInfoRequest): GetGroupMemberInfoResponse {
|
||||||
|
val memberInfo = GroupHelper.getTroopMemberInfoByUin(request.groupId.toString(), when(request.targetCase!!) {
|
||||||
|
GetGroupMemberInfoRequest.TargetCase.UIN -> request.uin
|
||||||
|
GetGroupMemberInfoRequest.TargetCase.UID -> ContactHelper.getUinByUidAsync(request.uid).toLong()
|
||||||
|
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("target not set")
|
||||||
|
)
|
||||||
|
}.toString()).onFailure {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member info").withCause(it))
|
||||||
|
}.getOrThrow()
|
||||||
|
return getGroupMemberInfoResponse {
|
||||||
|
groupMemberInfo = groupMemberInfo {
|
||||||
|
uid = if (request.targetCase == GetGroupMemberInfoRequest.TargetCase.UID) request.uid else ContactHelper.getUidByUinAsync(request.uin)
|
||||||
|
uin = memberInfo.memberuin?.toLong() ?: 0
|
||||||
|
nick = memberInfo.troopnick
|
||||||
|
.ifNullOrEmpty { memberInfo.hwName }
|
||||||
|
.ifNullOrEmpty { memberInfo.troopColorNick }
|
||||||
|
.ifNullOrEmpty { memberInfo.friendnick } ?: ""
|
||||||
|
age = memberInfo.age.toInt()
|
||||||
|
uniqueTitle = memberInfo.mUniqueTitle ?: ""
|
||||||
|
uniqueTitleExpireTime = memberInfo.mUniqueTitleExpire
|
||||||
|
card = memberInfo.troopnick.ifNullOrEmpty { memberInfo.friendnick } ?: ""
|
||||||
|
joinTime = memberInfo.join_time
|
||||||
|
lastActiveTime = memberInfo.last_active_time
|
||||||
|
level = memberInfo.level
|
||||||
|
shutUpTimestamp = memberInfo.gagTimeStamp
|
||||||
|
|
||||||
|
distance = memberInfo.distance
|
||||||
|
honor.addAll((memberInfo.honorList ?: "")
|
||||||
|
.split("|")
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.map { it.toInt() })
|
||||||
|
unfriendly = false
|
||||||
|
cardChangeable = GroupHelper.isAdmin(request.groupId.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "GetGroupMemberList")
|
||||||
|
override suspend fun getGroupMemberList(request: GetGroupMemberListRequest): GetGroupMemberListResponse {
|
||||||
|
val memberList = GroupHelper.getGroupMemberList(request.groupId.toString(), if (request.hasRefresh()) request.refresh else false).onFailure {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member list").withCause(it))
|
||||||
|
}.getOrThrow()
|
||||||
|
return getGroupMemberListResponse {
|
||||||
|
memberList.forEach { memberInfo ->
|
||||||
|
this.groupMemberInfo.add(groupMemberInfo {
|
||||||
|
uid = ContactHelper.getUidByUinAsync(memberInfo.memberuin?.toLong() ?: 0)
|
||||||
|
uin = memberInfo.memberuin?.toLong() ?: 0
|
||||||
|
nick = memberInfo.troopnick
|
||||||
|
.ifNullOrEmpty { memberInfo.hwName }
|
||||||
|
.ifNullOrEmpty { memberInfo.troopColorNick }
|
||||||
|
.ifNullOrEmpty { memberInfo.friendnick } ?: ""
|
||||||
|
age = memberInfo.age.toInt()
|
||||||
|
uniqueTitle = memberInfo.mUniqueTitle ?: ""
|
||||||
|
uniqueTitleExpireTime = memberInfo.mUniqueTitleExpire
|
||||||
|
card = memberInfo.troopnick.ifNullOrEmpty { memberInfo.friendnick } ?: ""
|
||||||
|
joinTime = memberInfo.join_time
|
||||||
|
lastActiveTime = memberInfo.last_active_time
|
||||||
|
level = memberInfo.level
|
||||||
|
shutUpTimestamp = memberInfo.gagTimeStamp
|
||||||
|
|
||||||
|
distance = memberInfo.distance
|
||||||
|
honor.addAll((memberInfo.honorList ?: "")
|
||||||
|
.split("|")
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.map { it.toInt() })
|
||||||
|
unfriendly = false
|
||||||
|
cardChangeable = GroupHelper.isAdmin(request.groupId.toString())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "GetProhibitedUserList")
|
||||||
|
override suspend fun getProhibitedUserList(request: GetProhibitedUserListRequest): GetProhibitedUserListResponse {
|
||||||
|
val prohibitedList = GroupHelper.getProhibitedMemberList(request.groupId).onFailure {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get prohibited user list").withCause(it))
|
||||||
|
}.getOrThrow()
|
||||||
|
return getProhibitedUserListResponse {
|
||||||
|
prohibitedList.forEach {
|
||||||
|
this.prohibitedUserInfo.add(prohibitedUserInfo {
|
||||||
|
uid = ContactHelper.getUidByUinAsync(it.memberUin)
|
||||||
|
uin = it.memberUin
|
||||||
|
prohibitedTime = it.shutuptimestap
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "GetRemainCountAtAll")
|
||||||
|
override suspend fun getRemainCountAtAll(request: GetRemainCountAtAllRequest): GetRemainCountAtAllResponse {
|
||||||
|
val remainAtAllRsp = GroupHelper.getGroupRemainAtAllRemain(request.groupId).onFailure {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get remain count").withCause(it))
|
||||||
|
}.getOrThrow()
|
||||||
|
return getRemainCountAtAllResponse {
|
||||||
|
accessAtAll = remainAtAllRsp.bool_can_at_all.get()
|
||||||
|
remainCountForGroup = remainAtAllRsp.uint32_remain_at_all_count_for_group.get()
|
||||||
|
remainCountForSelf = remainAtAllRsp.uint32_remain_at_all_count_for_uin.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "GetNotJoinedGroupInfo")
|
||||||
|
override suspend fun getNotJoinedGroupInfo(request: GetNotJoinedGroupInfoRequest): GetNotJoinedGroupInfoResponse {
|
||||||
|
val groupInfo = GroupHelper.getNotJoinedGroupInfo(request.groupId).onFailure {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get not joined group info").withCause(it))
|
||||||
|
}.getOrThrow()
|
||||||
|
return getNotJoinedGroupInfoResponse {
|
||||||
|
this.groupInfo = notJoinedGroupInfo {
|
||||||
|
groupId = groupInfo.groupId
|
||||||
|
groupName = groupInfo.groupName
|
||||||
|
owner = groupInfo.owner
|
||||||
|
maxMemberCount = groupInfo.maxMember
|
||||||
|
memberCount = groupInfo.memberCount
|
||||||
|
groupDesc = groupInfo.groupDesc
|
||||||
|
createTime = groupInfo.createTime.toInt()
|
||||||
|
groupFlag = groupInfo.groupFlag
|
||||||
|
groupFlagExt = groupInfo.groupFlagExt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("GroupService", "GetGroupHonor")
|
||||||
|
override suspend fun getGroupHonor(request: GetGroupHonorRequest): GetGroupHonorResponse {
|
||||||
|
return getGroupHonorResponse {
|
||||||
|
GroupHelper.getGroupMemberList(request.groupId.toString(), true).onFailure {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member list").withCause(it))
|
||||||
|
}.onSuccess { memberList ->
|
||||||
|
memberList.forEach { member ->
|
||||||
|
(member.honorList ?: "").split("|")
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.map { it.toInt() }.forEach {
|
||||||
|
val honor = decodeHonor(member.memberuin.toLong(), it, member.mHonorRichFlag)
|
||||||
|
if (honor != null) {
|
||||||
|
groupHonorInfo.add(groupHonorInfo {
|
||||||
|
uid = ContactHelper.getUidByUinAsync(member.memberuin.toLong())
|
||||||
|
uin = member.memberuin.toLong()
|
||||||
|
nick = member.troopnick
|
||||||
|
.ifNullOrEmpty { member.hwName }
|
||||||
|
.ifNullOrEmpty { member.troopColorNick }
|
||||||
|
.ifNullOrEmpty { member.friendnick } ?: ""
|
||||||
|
honorName = honor.honorName
|
||||||
|
avatar = honor.honorIconUrl
|
||||||
|
id = honor.honorId
|
||||||
|
description = honor.honorUrl
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
136
xposed/src/main/java/kritor/service/KritorService.kt
Normal file
136
xposed/src/main/java/kritor/service/KritorService.kt
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import com.tencent.mobileqq.app.QQAppInterface
|
||||||
|
import io.grpc.Status
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
|
import io.kritor.core.ClearCacheRequest
|
||||||
|
import io.kritor.core.ClearCacheResponse
|
||||||
|
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
|
||||||
|
import io.kritor.core.SwitchAccountRequest
|
||||||
|
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
|
||||||
|
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
|
||||||
|
import qq.service.contact.ContactHelper
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
internal object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() {
|
||||||
|
@Grpc("KritorService", "GetVersion")
|
||||||
|
override suspend fun getVersion(request: GetVersionRequest): GetVersionResponse {
|
||||||
|
return getVersionResponse {
|
||||||
|
this.version = ShamrockVersion
|
||||||
|
this.appName = "Shamrock"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("KritorService", "ClearCache")
|
||||||
|
override suspend fun clearCache(request: ClearCacheRequest): ClearCacheResponse {
|
||||||
|
FileUtils.clearCache()
|
||||||
|
MMKVFetcher.mmkvWithId("audio2silk")
|
||||||
|
.clear()
|
||||||
|
return clearCacheResponse {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("KritorService", "GetCurrentAccount")
|
||||||
|
override suspend fun getCurrentAccount(request: GetCurrentAccountRequest): GetCurrentAccountResponse {
|
||||||
|
return getCurrentAccountResponse {
|
||||||
|
this.accountName = if (app is QQAppInterface) app.currentNickname else "unknown"
|
||||||
|
this.accountUid = app.currentUid ?: ""
|
||||||
|
this.accountUin = (app.currentUin ?: "0").toLong()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("KritorService", "DownloadFile")
|
||||||
|
override suspend fun downloadFile(request: DownloadFileRequest): DownloadFileResponse {
|
||||||
|
val headerMap = mutableMapOf(
|
||||||
|
"User-Agent" to "Shamrock"
|
||||||
|
)
|
||||||
|
if (request.hasHeaders()) {
|
||||||
|
request.headers.split("[\r\n]").forEach {
|
||||||
|
val pair = it.split("=")
|
||||||
|
if (pair.size >= 2) {
|
||||||
|
val (k, v) = pair
|
||||||
|
headerMap[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmp = FileUtils.getTmpFile("cache")
|
||||||
|
if (request.hasBase64()) {
|
||||||
|
val bytes = Base64.decode(request.base64, Base64.DEFAULT)
|
||||||
|
tmp.writeBytes(bytes)
|
||||||
|
} else if(request.hasUrl()) {
|
||||||
|
if(!DownloadUtils.download(
|
||||||
|
urlAdr = request.url,
|
||||||
|
dest = tmp,
|
||||||
|
headers = headerMap,
|
||||||
|
threadCount = if (request.hasThreadCnt()) request.threadCnt else 3
|
||||||
|
)) {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("download failed"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tmp = if (!request.hasFileName()) FileUtils.renameByMd5(tmp)
|
||||||
|
else tmp.parentFile!!.resolve(request.fileName).also {
|
||||||
|
tmp.renameTo(it)
|
||||||
|
}
|
||||||
|
if (request.hasRootPath()) {
|
||||||
|
tmp = File(request.rootPath).resolve(tmp.name).also {
|
||||||
|
tmp.renameTo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return downloadFileResponse {
|
||||||
|
this.fileMd5 = MD5.genFileMd5Hex(tmp.absolutePath)
|
||||||
|
this.fileAbsolutePath = tmp.absolutePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("KritorService", "SwitchAccount")
|
||||||
|
override suspend fun switchAccount(request: SwitchAccountRequest): SwitchAccountResponse {
|
||||||
|
val uin = when(request.accountCase!!) {
|
||||||
|
SwitchAccountRequest.AccountCase.ACCOUNT_UID -> ContactHelper.getUinByUidAsync(request.accountUid)
|
||||||
|
SwitchAccountRequest.AccountCase.ACCOUNT_UIN -> request.accountUin.toString()
|
||||||
|
SwitchAccountRequest.AccountCase.ACCOUNT_NOT_SET -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("account not found"))
|
||||||
|
}
|
||||||
|
val account = MobileQQ.getMobileQQ().allAccounts.firstOrNull { it.uin == uin }
|
||||||
|
?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("account not found"))
|
||||||
|
runCatching {
|
||||||
|
app.switchAccount(account, null)
|
||||||
|
}.onFailure {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withCause(it).withDescription("failed to switch account"))
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
469
xposed/src/main/java/kritor/service/MessageService.kt
Normal file
469
xposed/src/main/java/kritor/service/MessageService.kt
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
import com.tencent.mobileqq.qroute.QRoute
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||||
|
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.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
|
||||||
|
import io.kritor.message.GetHistoryMessageResponse
|
||||||
|
import io.kritor.message.GetMessageBySeqRequest
|
||||||
|
import io.kritor.message.GetMessageBySeqResponse
|
||||||
|
import io.kritor.message.GetMessageRequest
|
||||||
|
import io.kritor.message.GetMessageResponse
|
||||||
|
import io.kritor.message.MessageServiceGrpcKt
|
||||||
|
import io.kritor.message.RecallMessageRequest
|
||||||
|
import io.kritor.message.RecallMessageResponse
|
||||||
|
import io.kritor.message.Scene
|
||||||
|
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
|
||||||
|
import io.kritor.message.getMessageResponse
|
||||||
|
import io.kritor.message.messageBody
|
||||||
|
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
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import protobuf.auto.toByteArray
|
||||||
|
import protobuf.message.ContentHead
|
||||||
|
import protobuf.message.Elem
|
||||||
|
import protobuf.message.MsgBody
|
||||||
|
import protobuf.message.PbSendMsgReq
|
||||||
|
import protobuf.message.RichText
|
||||||
|
import protobuf.message.RoutingHead
|
||||||
|
import protobuf.message.element.GeneralFlags
|
||||||
|
import protobuf.message.routing.C2C
|
||||||
|
import protobuf.message.routing.Grp
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
import qq.service.contact.longPeer
|
||||||
|
import qq.service.internals.NTServiceFetcher
|
||||||
|
import qq.service.msg.MessageHelper
|
||||||
|
import qq.service.msg.NtMsgConvertor
|
||||||
|
import qq.service.msg.toKritorEventMessages
|
||||||
|
import qq.service.msg.toKritorReqMessages
|
||||||
|
import qq.service.msg.toKritorResponseMessages
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
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!!) {
|
||||||
|
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 uniseq = MessageHelper.generateMsgId(contact.chatType)
|
||||||
|
return sendMessageResponse {
|
||||||
|
this.messageId = MessageHelper.sendMessage(contact, NtMsgConvertor.convertToNtMsgs(contact, uniseq, request.elementsList), request.retryCount, uniseq).onFailure {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "SendMessageByResId")
|
||||||
|
override suspend fun sendMessageByResId(request: SendMessageByResIdRequest): SendMessageByResIdResponse {
|
||||||
|
val contact = request.contact
|
||||||
|
val req = PbSendMsgReq(
|
||||||
|
routingHead = when (request.contact.scene) {
|
||||||
|
Scene.GROUP -> RoutingHead(grp = Grp(contact.longPeer().toUInt()))
|
||||||
|
Scene.FRIEND -> RoutingHead(c2c = C2C(contact.longPeer().toUInt()))
|
||||||
|
else -> RoutingHead(grp = Grp(contact.longPeer().toUInt()))
|
||||||
|
},
|
||||||
|
contentHead = ContentHead(1, 0, 0, 0),
|
||||||
|
msgBody = MsgBody(
|
||||||
|
richText = RichText(
|
||||||
|
elements = arrayListOf(
|
||||||
|
Elem(
|
||||||
|
generalFlags = GeneralFlags(
|
||||||
|
longTextFlag = 1u,
|
||||||
|
longTextResid = request.resId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
msgSeq = Random.nextUInt(),
|
||||||
|
msgRand = Random.nextUInt(),
|
||||||
|
msgVia = 0u
|
||||||
|
)
|
||||||
|
QQInterfaces.sendBuffer("MessageSvc.PbSendMsg", true, req.toByteArray())
|
||||||
|
return sendMessageByResIdResponse { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "ClearMessages")
|
||||||
|
override suspend fun clearMessages(request: ClearMessagesRequest): ClearMessagesResponse {
|
||||||
|
val contact = request.contact
|
||||||
|
val kernelService = NTServiceFetcher.kernelService
|
||||||
|
val sessionService = kernelService.wrapperSession
|
||||||
|
val service = sessionService.msgService
|
||||||
|
val chatType = when(contact.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"))
|
||||||
|
}
|
||||||
|
service.clearMsgRecords(Contact(chatType, contact.peer, contact.subPeer), null)
|
||||||
|
return clearMessagesResponse { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "RecallMessage")
|
||||||
|
override suspend fun recallMessage(request: RecallMessageRequest): RecallMessageResponse {
|
||||||
|
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 kernelService = NTServiceFetcher.kernelService
|
||||||
|
val sessionService = kernelService.wrapperSession
|
||||||
|
val service = sessionService.msgService
|
||||||
|
service.recallMsg(contact, arrayListOf(request.messageId)) { code, msg ->
|
||||||
|
if (code != 0) {
|
||||||
|
LogCenter.log("消息撤回失败: $code:$msg", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recallMessageResponse {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "GetForwardMessages")
|
||||||
|
override suspend fun getForwardMessages(request: GetForwardMessagesRequest): GetForwardMessagesResponse {
|
||||||
|
return getForwardMessagesResponse {
|
||||||
|
MessageHelper.getForwardMsg(request.resId).onFailure {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
|
||||||
|
}.getOrThrow().forEach { detail ->
|
||||||
|
messages.add(messageBody {
|
||||||
|
val peer = when (scene) {
|
||||||
|
Scene.GROUP -> detail.groupId.toString()
|
||||||
|
Scene.FRIEND -> detail.sender.userId.toString()
|
||||||
|
else -> detail.peerId.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.time = detail.time
|
||||||
|
this.scene = when(detail.msgType) {
|
||||||
|
MsgConstant.KCHATTYPEC2C -> Scene.FRIEND
|
||||||
|
MsgConstant.KCHATTYPEGROUP -> Scene.GROUP
|
||||||
|
MsgConstant.KCHATTYPEGUILD -> Scene.GUILD
|
||||||
|
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> Scene.STRANGER_FROM_GROUP
|
||||||
|
MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN -> Scene.NEARBY
|
||||||
|
else -> Scene.STRANGER
|
||||||
|
}
|
||||||
|
this.messageId = detail.qqMsgId
|
||||||
|
this.messageSeq = detail.msgSeq
|
||||||
|
this.contact = contact {
|
||||||
|
this.scene = scene
|
||||||
|
this.peer = peer
|
||||||
|
}
|
||||||
|
this.sender = sender {
|
||||||
|
this.uin = detail.sender.userId
|
||||||
|
this.nick = detail.sender.nickName
|
||||||
|
this.uid = detail.sender.uid
|
||||||
|
}
|
||||||
|
detail.message?.elements?.toKritorResponseMessages(Contact(detail.msgType, peer, null))?.let {
|
||||||
|
this.elements.addAll(it)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "GetMessage")
|
||||||
|
override suspend fun getMessage(request: GetMessageRequest): GetMessageResponse {
|
||||||
|
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"))
|
||||||
|
|
||||||
|
return getMessageResponse {
|
||||||
|
this.message = messageBody {
|
||||||
|
this.messageId = msg.msgId
|
||||||
|
this.scene = request.contact.scene
|
||||||
|
this.contact = request.contact
|
||||||
|
this.sender = sender {
|
||||||
|
this.uin = msg.senderUin
|
||||||
|
this.nick = msg.sendNickName ?: ""
|
||||||
|
this.uid = msg.senderUid ?: ""
|
||||||
|
}
|
||||||
|
this.messageSeq = msg.msgSeq
|
||||||
|
this.elements.addAll(msg.elements.toKritorReqMessages(contact))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "GetMessageBySeq")
|
||||||
|
override suspend fun getMessageBySeq(request: GetMessageBySeqRequest): GetMessageBySeqResponse {
|
||||||
|
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.getMsgsBySeqAndCount(contact, request.messageSeq, 1, true) { 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"))
|
||||||
|
|
||||||
|
return getMessageBySeqResponse {
|
||||||
|
this.message = messageBody {
|
||||||
|
this.messageId = msg.msgId
|
||||||
|
this.scene = request.contact.scene
|
||||||
|
this.contact = request.contact
|
||||||
|
this.sender = sender {
|
||||||
|
this.uin = msg.senderUin
|
||||||
|
this.nick = msg.sendNickName ?: ""
|
||||||
|
this.uid = msg.senderUid ?: ""
|
||||||
|
}
|
||||||
|
this.messageSeq = msg.msgSeq
|
||||||
|
this.elements.addAll(msg.elements.toKritorReqMessages(contact))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "GetHistoryMessage")
|
||||||
|
override suspend fun getHistoryMessage(request: GetHistoryMessageRequest): GetHistoryMessageResponse {
|
||||||
|
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 msgs: List<MsgRecord> = withTimeoutOrNull(5000) {
|
||||||
|
val service = QRoute.api(IMsgService::class.java)
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
service.getMsgs(contact, request.startMessageId, request.count, true) { code, _, msgRecords ->
|
||||||
|
if (code == 0 && msgRecords.isNotEmpty()) {
|
||||||
|
continuation.resume(msgRecords)
|
||||||
|
} else {
|
||||||
|
continuation.resume(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
continuation.resume(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Messages not found"))
|
||||||
|
|
||||||
|
return getHistoryMessageResponse {
|
||||||
|
msgs.forEach {
|
||||||
|
messages.add(messageBody {
|
||||||
|
this.messageId = it.msgId
|
||||||
|
this.scene = request.contact.scene
|
||||||
|
this.contact = request.contact
|
||||||
|
this.sender = sender {
|
||||||
|
this.uin = it.senderUin
|
||||||
|
this.nick = it.sendNickName ?: ""
|
||||||
|
this.uid = it.senderUid ?: ""
|
||||||
|
}
|
||||||
|
this.messageSeq = it.msgSeq
|
||||||
|
this.elements.addAll(it.elements.toKritorReqMessages(contact))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
data object ActiveRPC: ConfigKey<Boolean>() {
|
||||||
|
override fun name(): String = "active_rpc"
|
||||||
|
|
||||||
|
override fun default(): Boolean = false
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
object ActiveTicket: ConfigKey<String>() {
|
||||||
|
override fun name(): String = "active_ticket"
|
||||||
|
|
||||||
|
override fun default(): String = ""
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
object AliveReply: ConfigKey<Boolean>() {
|
||||||
|
override fun name() = "alive_reply"
|
||||||
|
override fun default() = false
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
object AntiJvmTrace: ConfigKey<Boolean>() {
|
||||||
|
override fun default() = false
|
||||||
|
|
||||||
|
override fun name() = "anti_jvm_trace"
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
object B2Mode: ConfigKey<Boolean>() {
|
||||||
|
override fun name() = "b2_mode"
|
||||||
|
override fun default() = false
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
abstract class ConfigKey<T> {
|
||||||
|
abstract fun name(): String
|
||||||
|
|
||||||
|
abstract fun default(): T
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline fun <reified Type, reified T: ConfigKey<Type>> T.get(): Type {
|
||||||
|
return ShamrockConfig[this]
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
object DebugMode: ConfigKey<Boolean>() {
|
||||||
|
override fun name(): String = "debug"
|
||||||
|
|
||||||
|
override fun default(): Boolean = false
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
object EnableOldBDH: ConfigKey<Boolean>() {
|
||||||
|
override fun name() = "enable_old_bdh"
|
||||||
|
override fun default() = true
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
object EnableSelfMessage: ConfigKey<Boolean>() {
|
||||||
|
override fun name() = "enable_self_message"
|
||||||
|
override fun default() = false
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
data object ForceTablet: ConfigKey<Boolean>() {
|
||||||
|
override fun name(): String = "force_tablet"
|
||||||
|
|
||||||
|
override fun default(): Boolean = false
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
object IsInit: ConfigKey<Boolean>() {
|
||||||
|
override fun name(): String = "is_init"
|
||||||
|
|
||||||
|
override fun default(): Boolean = false
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
data object PassiveRPC: ConfigKey<Boolean>() {
|
||||||
|
override fun name(): String = "passive_rpc"
|
||||||
|
|
||||||
|
override fun default(): Boolean = false
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
data object RPCAddress: ConfigKey<String>() {
|
||||||
|
override fun name(): String = "rpc_address"
|
||||||
|
|
||||||
|
override fun default(): String = ""
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
data object RPCPort: ConfigKey<Int>() {
|
||||||
|
override fun name(): String = "rpc_port"
|
||||||
|
|
||||||
|
override fun default(): Int = 5700
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
data object ResourceGroup: ConfigKey<String>() {
|
||||||
|
override fun name(): String = "resource_group"
|
||||||
|
|
||||||
|
override fun default(): String = "883536416"
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.config
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader.moduleLoader
|
||||||
|
import mqq.app.MobileQQ
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
|
private val configDir = MobileQQ.getContext().getExternalFilesDir(null)!!
|
||||||
|
.parentFile!!.resolve("Tencent/Shamrock").also {
|
||||||
|
if (!it.exists()) it.mkdirs()
|
||||||
|
}
|
||||||
|
private val configFile = configDir.resolve("config.prop")
|
||||||
|
|
||||||
|
private val configKeys = setOf(
|
||||||
|
ActiveRPC,
|
||||||
|
AntiJvmTrace,
|
||||||
|
ForceTablet,
|
||||||
|
PassiveRPC,
|
||||||
|
ResourceGroup,
|
||||||
|
RPCAddress,
|
||||||
|
RPCPort,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal object ShamrockConfig: Properties() {
|
||||||
|
init {
|
||||||
|
if (!configFile.exists()) {
|
||||||
|
moduleLoader.getResourceAsStream("assets/config.properties")?.use {
|
||||||
|
configDir.resolve("default.prop").outputStream().use { output ->
|
||||||
|
it.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
moduleLoader.getResourceAsStream("assets/config.properties")?.use {
|
||||||
|
configFile.outputStream().use { output ->
|
||||||
|
it.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (configFile.exists()) configFile.inputStream().use {
|
||||||
|
load(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline operator fun <reified Type> get(key: ConfigKey<Type>): Type {
|
||||||
|
return when(Type::class) {
|
||||||
|
Int::class -> getProperty(key.name()).toInt() as Type ?: key.default()
|
||||||
|
Long::class -> getProperty(key.name()).toLong() as Type ?: key.default()
|
||||||
|
String::class -> getProperty(key.name()) as Type ?: key.default()
|
||||||
|
Boolean::class -> getProperty(key.name()).toBoolean() as Type ?: key.default()
|
||||||
|
else -> throw IllegalArgumentException("Unsupported type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateConfig(intent: Intent? = null) {
|
||||||
|
intent?.let {
|
||||||
|
for (key in configKeys) {
|
||||||
|
when (key.default()) {
|
||||||
|
is String -> {
|
||||||
|
val value = intent.getStringExtra(key.name())
|
||||||
|
if (value != null) setProperty(key.name(), value)
|
||||||
|
}
|
||||||
|
is Boolean -> {
|
||||||
|
val value = intent.getBooleanExtra(key.name(), key.default() as Boolean)
|
||||||
|
setProperty(key.name(), value.toString())
|
||||||
|
}
|
||||||
|
is Int -> {
|
||||||
|
val value = intent.getIntExtra(key.name(), key.default() as Int)
|
||||||
|
setProperty(key.name(), value.toString())
|
||||||
|
}
|
||||||
|
is Long -> {
|
||||||
|
val value = intent.getLongExtra(key.name(), key.default() as Long)
|
||||||
|
setProperty(key.name(), value.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getProperty(ActiveTicket.name()).isNullOrEmpty()) {
|
||||||
|
setProperty(ActiveTicket.name(), "") // 初始化ticket
|
||||||
|
}
|
||||||
|
|
||||||
|
setProperty(IsInit.name(), "true")
|
||||||
|
}
|
||||||
|
configFile.outputStream().use {
|
||||||
|
store(it, "Shamrock Config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readResolve(): Any = ShamrockConfig
|
||||||
|
}
|
@ -6,10 +6,14 @@ internal class ParamsException(key: String): InternalMessageMakerError("Lack of
|
|||||||
|
|
||||||
internal class IllegalParamsException(key: String): InternalMessageMakerError("Illegal param $key")
|
internal class IllegalParamsException(key: String): InternalMessageMakerError("Illegal param $key")
|
||||||
|
|
||||||
internal object ActionMsgException: InternalMessageMakerError("action msg")
|
internal object ActionMsgException: InternalMessageMakerError("action msg") {
|
||||||
|
private fun readResolve(): Any = ActionMsgException
|
||||||
|
}
|
||||||
|
|
||||||
internal class LogicException(why: String) : InternalMessageMakerError(why)
|
internal class LogicException(why: String) : InternalMessageMakerError(why)
|
||||||
|
|
||||||
internal object ErrorTokenException : InternalMessageMakerError("access_token error")
|
internal object ErrorTokenException : InternalMessageMakerError("access_token error") {
|
||||||
|
private fun readResolve(): Any = ErrorTokenException
|
||||||
|
}
|
||||||
|
|
||||||
internal class SendMsgException(why: String) : InternalMessageMakerError(why)
|
internal class SendMsgException(why: String) : InternalMessageMakerError(why)
|
||||||
|
@ -1,23 +1,2 @@
|
|||||||
package moe.fuqiuluo.shamrock.helper
|
package moe.fuqiuluo.shamrock.helper
|
||||||
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
|
|
||||||
import moe.fuqiuluo.shamrock.utils.FileUtils
|
|
||||||
import mqq.app.MobileQQ
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
internal object LocalCacheHelper: BaseSvc() {
|
|
||||||
// 获取外部储存data目录
|
|
||||||
private val dataDir = MobileQQ.getContext().getExternalFilesDir(null)!!
|
|
||||||
.parentFile!!.resolve("Tencent")
|
|
||||||
|
|
||||||
fun getCurrentPttPath(): File {
|
|
||||||
return dataDir.resolve("MobileQQ/${app.currentAccountUin}/ptt").also {
|
|
||||||
if (!it.exists()) it.mkdirs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCachePttFile(md5: String): File {
|
|
||||||
val file = FileUtils.getFileByMd5(md5)
|
|
||||||
return if (file.exists()) file else getCurrentPttPath().resolve("$md5.amr")
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,9 +8,10 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
import moe.fuqiuluo.shamrock.config.DebugMode
|
||||||
import moe.fuqiuluo.shamrock.xposed.hooks.toast
|
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.internal.DataRequester
|
import moe.fuqiuluo.shamrock.tools.toast
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.helper.AppTalker
|
||||||
import mqq.app.MobileQQ
|
import mqq.app.MobileQQ
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@ -51,19 +52,18 @@ internal object LogCenter {
|
|||||||
private val format = SimpleDateFormat("[HH:mm:ss] ")
|
private val format = SimpleDateFormat("[HH:mm:ss] ")
|
||||||
|
|
||||||
fun log(string: String, level: Level = Level.INFO, toast: Boolean = false) {
|
fun log(string: String, level: Level = Level.INFO, toast: Boolean = false) {
|
||||||
if (!ShamrockConfig.isDebug() && level == Level.DEBUG) {
|
if (!ShamrockConfig[DebugMode] && level == Level.DEBUG) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toast) {
|
if (toast) {
|
||||||
MobileQQ.getContext().toast(string)
|
MobileQQ.getContext().toast(string)
|
||||||
}
|
}
|
||||||
// 把日志广播到主进程
|
|
||||||
GlobalScope.launch(Dispatchers.Default) {
|
GlobalScope.launch(Dispatchers.Default) {
|
||||||
DataRequester.request("send_message", bodyBuilder = {
|
AppTalker.talk("send_message") {
|
||||||
put("string", string)
|
put("string", string)
|
||||||
put("level", level.id)
|
put("level", level.id)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!LogFile.exists()) {
|
if (!LogFile.exists()) {
|
||||||
@ -79,7 +79,7 @@ internal object LogCenter {
|
|||||||
level: Level = Level.INFO,
|
level: Level = Level.INFO,
|
||||||
toast: Boolean = false
|
toast: Boolean = false
|
||||||
) {
|
) {
|
||||||
if (!ShamrockConfig.isDebug() && level == Level.DEBUG) {
|
if (!ShamrockConfig[DebugMode] && level == Level.DEBUG) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,10 +89,10 @@ internal object LogCenter {
|
|||||||
}
|
}
|
||||||
// 把日志广播到主进程
|
// 把日志广播到主进程
|
||||||
GlobalScope.launch(Dispatchers.Default) {
|
GlobalScope.launch(Dispatchers.Default) {
|
||||||
DataRequester.request("send_message", bodyBuilder = {
|
AppTalker.talk("send_message") {
|
||||||
put("string", log)
|
put("string", log)
|
||||||
put("level", level.id)
|
put("level", level.id)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!LogFile.exists()) {
|
if (!LogFile.exists()) {
|
||||||
@ -103,10 +103,6 @@ internal object LogCenter {
|
|||||||
LogFile.appendText(format)
|
LogFile.appendText(format)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun getAllLog(): File {
|
|
||||||
// return LogFile
|
|
||||||
// }
|
|
||||||
|
|
||||||
fun getLogLines(start: Int, recent: Boolean = false): List<String> {
|
fun getLogLines(start: Int, recent: Boolean = false): List<String> {
|
||||||
val logData = LogFile.readLines()
|
val logData = LogFile.readLines()
|
||||||
val index = if(start > logData.size || start < 0) 0 else start
|
val index = if(start > logData.size || start < 0) 0 else start
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package moe.fuqiuluo.shamrock.helper
|
package moe.fuqiuluo.shamrock.helper
|
||||||
|
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.GroupMemberHonor
|
|
||||||
|
|
||||||
object TroopHonorHelper {
|
object TroopHonorHelper {
|
||||||
data class Honor(
|
data class Honor(
|
||||||
@ -60,4 +59,13 @@ object TroopHonorHelper {
|
|||||||
else -> flag shr 4
|
else -> flag shr 4
|
||||||
} and 3
|
} and 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class GroupMemberHonor(
|
||||||
|
val uin: Long,
|
||||||
|
val honorUrl: String,
|
||||||
|
val honorIconUrl: String,
|
||||||
|
val honorLevel: Int,
|
||||||
|
val honorId: Int,
|
||||||
|
val honorName: String
|
||||||
|
)
|
||||||
}
|
}
|
@ -0,0 +1,549 @@
|
|||||||
|
@file:OptIn(DelicateCoroutinesApi::class)
|
||||||
|
|
||||||
|
package moe.fuqiuluo.shamrock.internals
|
||||||
|
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
||||||
|
import io.kritor.event.GroupApplyType
|
||||||
|
import io.kritor.event.GroupMemberBanType
|
||||||
|
import io.kritor.event.GroupMemberDecreasedType
|
||||||
|
import io.kritor.event.GroupMemberIncreasedType
|
||||||
|
import io.kritor.event.MessageEvent
|
||||||
|
import io.kritor.event.NoticeEvent
|
||||||
|
import io.kritor.event.NoticeType
|
||||||
|
import io.kritor.event.RequestType
|
||||||
|
import io.kritor.event.RequestsEvent
|
||||||
|
import io.kritor.event.Scene
|
||||||
|
import io.kritor.event.contact
|
||||||
|
import io.kritor.event.essenceMessageNotice
|
||||||
|
import io.kritor.event.friendApplyRequest
|
||||||
|
import io.kritor.event.friendFileComeNotice
|
||||||
|
import io.kritor.event.friendPokeNotice
|
||||||
|
import io.kritor.event.friendRecallNotice
|
||||||
|
import io.kritor.event.groupAdminChangedNotice
|
||||||
|
import io.kritor.event.groupApplyRequest
|
||||||
|
import io.kritor.event.groupFileComeNotice
|
||||||
|
import io.kritor.event.groupMemberBannedNotice
|
||||||
|
import io.kritor.event.groupMemberDecreasedNotice
|
||||||
|
import io.kritor.event.groupMemberIncreasedNotice
|
||||||
|
import io.kritor.event.groupPokeNotice
|
||||||
|
import io.kritor.event.groupRecallNotice
|
||||||
|
import io.kritor.event.groupSignNotice
|
||||||
|
import io.kritor.event.groupUniqueTitleChangedNotice
|
||||||
|
import io.kritor.event.groupWholeBanNotice
|
||||||
|
import io.kritor.event.messageEvent
|
||||||
|
import io.kritor.event.noticeEvent
|
||||||
|
import io.kritor.event.requestsEvent
|
||||||
|
import io.kritor.event.sender
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
import qq.service.msg.toKritorEventMessages
|
||||||
|
|
||||||
|
internal object GlobalEventTransmitter: QQInterfaces() {
|
||||||
|
private val messageEventFlow by lazy {
|
||||||
|
MutableSharedFlow<Pair<MsgRecord, MessageEvent>>()
|
||||||
|
}
|
||||||
|
private val noticeEventFlow by lazy {
|
||||||
|
MutableSharedFlow<NoticeEvent>()
|
||||||
|
}
|
||||||
|
private val requestEventFlow by lazy {
|
||||||
|
MutableSharedFlow<RequestsEvent>()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun pushNotice(noticeEvent: NoticeEvent) = noticeEventFlow.emit(noticeEvent)
|
||||||
|
|
||||||
|
private suspend fun pushRequest(requestEvent: RequestsEvent) = requestEventFlow.emit(requestEvent)
|
||||||
|
|
||||||
|
private suspend fun transMessageEvent(record: MsgRecord, message: MessageEvent) = messageEventFlow.emit(record to message)
|
||||||
|
|
||||||
|
object MessageTransmitter {
|
||||||
|
suspend fun transGroupMessage(
|
||||||
|
record: MsgRecord,
|
||||||
|
elements: ArrayList<MsgElement>,
|
||||||
|
): Boolean {
|
||||||
|
transMessageEvent(record, messageEvent {
|
||||||
|
this.time = record.msgTime.toInt()
|
||||||
|
this.scene = Scene.GROUP
|
||||||
|
this.messageId = record.msgId
|
||||||
|
this.messageSeq = record.msgSeq
|
||||||
|
this.contact = contact {
|
||||||
|
this.scene = scene
|
||||||
|
this.peer = record.peerUin.toString()
|
||||||
|
this.subPeer = record.peerUid
|
||||||
|
}
|
||||||
|
this.sender = sender {
|
||||||
|
this.uin = record.senderUin
|
||||||
|
this.uid = record.senderUid
|
||||||
|
this.nick = record.sendNickName
|
||||||
|
}
|
||||||
|
this.elements.addAll(elements.toKritorEventMessages(record))
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transPrivateMessage(
|
||||||
|
record: MsgRecord,
|
||||||
|
elements: ArrayList<MsgElement>,
|
||||||
|
): Boolean {
|
||||||
|
transMessageEvent(record, messageEvent {
|
||||||
|
this.time = record.msgTime.toInt()
|
||||||
|
this.scene = Scene.FRIEND
|
||||||
|
this.messageId = record.msgId
|
||||||
|
this.messageSeq = record.msgSeq
|
||||||
|
this.contact = contact {
|
||||||
|
this.scene = scene
|
||||||
|
this.peer = record.senderUin.toString()
|
||||||
|
this.subPeer = record.senderUid
|
||||||
|
}
|
||||||
|
this.sender = sender {
|
||||||
|
this.uin = record.senderUin
|
||||||
|
this.uid = record.senderUid
|
||||||
|
this.nick = record.sendNickName
|
||||||
|
}
|
||||||
|
this.elements.addAll(elements.toKritorEventMessages(record))
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transTempMessage(
|
||||||
|
record: MsgRecord,
|
||||||
|
elements: ArrayList<MsgElement>,
|
||||||
|
groupCode: Long,
|
||||||
|
fromNick: String,
|
||||||
|
): Boolean {
|
||||||
|
transMessageEvent(record, messageEvent {
|
||||||
|
this.time = record.msgTime.toInt()
|
||||||
|
this.scene = Scene.FRIEND
|
||||||
|
this.messageId = record.msgId
|
||||||
|
this.messageSeq = record.msgSeq
|
||||||
|
this.contact = contact {
|
||||||
|
this.scene = scene
|
||||||
|
this.peer = record.senderUin.toString()
|
||||||
|
this.subPeer = groupCode.toString()
|
||||||
|
}
|
||||||
|
this.sender = sender {
|
||||||
|
this.uin = record.senderUin
|
||||||
|
this.uid = record.senderUid
|
||||||
|
this.nick = record.sendNickName
|
||||||
|
}
|
||||||
|
this.elements.addAll(elements.toKritorEventMessages(record))
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transGuildMessage(
|
||||||
|
record: MsgRecord,
|
||||||
|
elements: ArrayList<MsgElement>,
|
||||||
|
): Boolean {
|
||||||
|
transMessageEvent(record, messageEvent {
|
||||||
|
this.time = record.msgTime.toInt()
|
||||||
|
this.scene = Scene.GUILD
|
||||||
|
this.messageId = record.msgId
|
||||||
|
this.messageSeq = record.msgSeq
|
||||||
|
this.contact = contact {
|
||||||
|
this.scene = scene
|
||||||
|
this.peer = record.guildId ?: ""
|
||||||
|
this.subPeer = record.channelId ?: ""
|
||||||
|
}
|
||||||
|
this.sender = sender {
|
||||||
|
this.uin = record.senderUin
|
||||||
|
this.uid = record.senderUid
|
||||||
|
this.nick = record.sendNickName
|
||||||
|
}
|
||||||
|
this.elements.addAll(elements.toKritorEventMessages(record))
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件通知 通知器
|
||||||
|
**/
|
||||||
|
object FileNoticeTransmitter {
|
||||||
|
/**
|
||||||
|
* 推送私聊文件事件
|
||||||
|
*/
|
||||||
|
suspend fun transPrivateFileEvent(
|
||||||
|
msgTime: Long,
|
||||||
|
userId: Long,
|
||||||
|
fileId: String,
|
||||||
|
fileSubId: String,
|
||||||
|
fileName: String,
|
||||||
|
fileSize: Long,
|
||||||
|
expireTime: Long,
|
||||||
|
url: String
|
||||||
|
): Boolean {
|
||||||
|
pushNotice(noticeEvent {
|
||||||
|
this.type = NoticeType.FRIEND_FILE_COME
|
||||||
|
this.time = msgTime.toInt()
|
||||||
|
this.friendFileCome = friendFileComeNotice {
|
||||||
|
this.fileId = fileId
|
||||||
|
this.fileName = fileName
|
||||||
|
this.operator = userId
|
||||||
|
this.fileSize = fileSize
|
||||||
|
this.expireTime = expireTime.toInt()
|
||||||
|
this.fileSubId = fileSubId
|
||||||
|
this.url = url
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送私聊文件事件
|
||||||
|
*/
|
||||||
|
suspend fun transGroupFileEvent(
|
||||||
|
msgTime: Long,
|
||||||
|
userId: Long,
|
||||||
|
groupId: Long,
|
||||||
|
uuid: String,
|
||||||
|
fileName: String,
|
||||||
|
fileSize: Long,
|
||||||
|
bizId: Int,
|
||||||
|
url: String
|
||||||
|
): Boolean {
|
||||||
|
pushNotice(noticeEvent {
|
||||||
|
this.type = NoticeType.GROUP_FILE_COME
|
||||||
|
this.time = msgTime.toInt()
|
||||||
|
this.groupFileCome = groupFileComeNotice {
|
||||||
|
this.groupId = groupId
|
||||||
|
this.operator = userId
|
||||||
|
this.fileId = uuid
|
||||||
|
this.fileName = fileName
|
||||||
|
this.fileSize = fileSize
|
||||||
|
this.biz = bizId
|
||||||
|
this.url = url
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 群聊通知 通知器
|
||||||
|
*/
|
||||||
|
object GroupNoticeTransmitter {
|
||||||
|
suspend fun transGroupSign(time: Long, target: Long, action: String?, rankImg: String?, groupCode: Long): Boolean {
|
||||||
|
pushNotice(noticeEvent {
|
||||||
|
this.type = NoticeType.GROUP_SIGN
|
||||||
|
this.time = time.toInt()
|
||||||
|
this.groupSign = groupSignNotice {
|
||||||
|
this.groupId = groupCode
|
||||||
|
this.targetUin = target
|
||||||
|
this.action = action ?: ""
|
||||||
|
this.suffix = ""
|
||||||
|
this.rankImage = rankImg ?: ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transGroupPoke(time: Long, operator: Long, target: Long, action: String?, suffix: String?, actionImg: String?, groupCode: Long): Boolean {
|
||||||
|
pushNotice(noticeEvent {
|
||||||
|
this.type = NoticeType.GROUP_POKE
|
||||||
|
this.time = time.toInt()
|
||||||
|
this.groupPoke = groupPokeNotice {
|
||||||
|
this.action = action ?: ""
|
||||||
|
this.target = target
|
||||||
|
this.operator = operator
|
||||||
|
this.suffix = suffix ?: ""
|
||||||
|
this.actionImage = actionImg ?: ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transGroupMemberNumIncreased(
|
||||||
|
time: Long,
|
||||||
|
target: Long,
|
||||||
|
targetUid: String,
|
||||||
|
groupCode: Long,
|
||||||
|
operator: Long,
|
||||||
|
operatorUid: String,
|
||||||
|
type: GroupMemberIncreasedType
|
||||||
|
): Boolean {
|
||||||
|
pushNotice(noticeEvent {
|
||||||
|
this.type = NoticeType.GROUP_MEMBER_INCREASE
|
||||||
|
this.time = time.toInt()
|
||||||
|
this.groupMemberIncrease = groupMemberIncreasedNotice {
|
||||||
|
this.groupId = groupCode
|
||||||
|
this.operatorUid = operatorUid
|
||||||
|
this.operatorUin = operator
|
||||||
|
this.targetUid = targetUid
|
||||||
|
this.targetUin = target
|
||||||
|
this.type = type
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transGroupMemberNumDecreased(
|
||||||
|
time: Long,
|
||||||
|
target: Long,
|
||||||
|
targetUid: String,
|
||||||
|
groupCode: Long,
|
||||||
|
operator: Long,
|
||||||
|
operatorUid: String,
|
||||||
|
type: GroupMemberDecreasedType
|
||||||
|
): Boolean {
|
||||||
|
pushNotice(noticeEvent {
|
||||||
|
this.type = NoticeType.GROUP_MEMBER_INCREASE
|
||||||
|
this.time = time.toInt()
|
||||||
|
this.groupMemberDecrease = groupMemberDecreasedNotice {
|
||||||
|
this.groupId = groupCode
|
||||||
|
this.operatorUid = operatorUid
|
||||||
|
this.operatorUin = operator
|
||||||
|
this.targetUid = targetUid
|
||||||
|
this.targetUin = target
|
||||||
|
this.type = type
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transGroupAdminChanged(
|
||||||
|
msgTime: Long,
|
||||||
|
target: Long,
|
||||||
|
targetUid: String,
|
||||||
|
groupCode: Long,
|
||||||
|
setAdmin: Boolean
|
||||||
|
): Boolean {
|
||||||
|
pushNotice(noticeEvent {
|
||||||
|
this.type = NoticeType.GROUP_ADMIN_CHANGED
|
||||||
|
this.time = msgTime.toInt()
|
||||||
|
this.groupAdminChanged = groupAdminChangedNotice {
|
||||||
|
this.groupId = groupCode
|
||||||
|
this.targetUid = targetUid
|
||||||
|
this.targetUin = target
|
||||||
|
this.isAdmin = setAdmin
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transGroupWholeBan(
|
||||||
|
msgTime: Long,
|
||||||
|
operator: Long,
|
||||||
|
groupCode: Long,
|
||||||
|
isOpen: Boolean
|
||||||
|
): Boolean {
|
||||||
|
pushNotice(noticeEvent {
|
||||||
|
this.type = NoticeType.GROUP_WHOLE_BAN
|
||||||
|
this.time = msgTime.toInt()
|
||||||
|
this.groupWholeBan = groupWholeBanNotice {
|
||||||
|
this.groupId = groupCode
|
||||||
|
this.isWholeBan = isOpen
|
||||||
|
this.operator = operator
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transGroupBan(
|
||||||
|
msgTime: Long,
|
||||||
|
operator: Long,
|
||||||
|
operatorUid: String,
|
||||||
|
target: Long,
|
||||||
|
targetUid: String,
|
||||||
|
groupCode: Long,
|
||||||
|
duration: Int
|
||||||
|
): Boolean {
|
||||||
|
pushNotice(noticeEvent {
|
||||||
|
this.type = NoticeType.GROUP_MEMBER_BANNED
|
||||||
|
this.time = msgTime.toInt()
|
||||||
|
this.groupMemberBanned = groupMemberBannedNotice {
|
||||||
|
this.groupId = groupCode
|
||||||
|
this.operatorUid = operatorUid
|
||||||
|
this.operatorUin = operator
|
||||||
|
this.targetUid = targetUid
|
||||||
|
this.targetUin = target
|
||||||
|
this.duration = duration
|
||||||
|
this.type = if (duration > 0) GroupMemberBanType.BAN
|
||||||
|
else GroupMemberBanType.LIFT_BAN
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transGroupMsgRecall(
|
||||||
|
time: Long,
|
||||||
|
operator: Long,
|
||||||
|
operatorUid: String,
|
||||||
|
target: Long,
|
||||||
|
targetUid: String,
|
||||||
|
groupCode: Long,
|
||||||
|
msgId: Long,
|
||||||
|
tipText: String
|
||||||
|
): Boolean {
|
||||||
|
pushNotice(noticeEvent {
|
||||||
|
this.type = NoticeType.GROUP_RECALL
|
||||||
|
this.time = time.toInt()
|
||||||
|
this.groupRecall = groupRecallNotice {
|
||||||
|
this.groupId = groupCode
|
||||||
|
this.operatorUid = operatorUid
|
||||||
|
this.operatorUin = operator
|
||||||
|
this.targetUid = targetUid
|
||||||
|
this.targetUin = target
|
||||||
|
this.messageId = msgId
|
||||||
|
this.tipText = tipText
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transCardChange(
|
||||||
|
time: Long,
|
||||||
|
targetId: Long,
|
||||||
|
oldCard: String,
|
||||||
|
newCard: String,
|
||||||
|
groupId: Long
|
||||||
|
): Boolean {
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transTitleChange(
|
||||||
|
time: Long,
|
||||||
|
targetId: Long,
|
||||||
|
title: String,
|
||||||
|
groupId: Long
|
||||||
|
): Boolean {
|
||||||
|
pushNotice(noticeEvent {
|
||||||
|
this.type = NoticeType.GROUP_MEMBER_UNIQUE_TITLE_CHANGED
|
||||||
|
this.time = time.toInt()
|
||||||
|
this.groupMemberUniqueTitleChanged = groupUniqueTitleChangedNotice {
|
||||||
|
this.groupId = groupId
|
||||||
|
this.target = targetId
|
||||||
|
this.title = title
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transEssenceChange(
|
||||||
|
time: Long,
|
||||||
|
senderUin: Long,
|
||||||
|
operatorUin: Long,
|
||||||
|
msgId: Long,
|
||||||
|
groupId: Long,
|
||||||
|
subType: UInt
|
||||||
|
): Boolean {
|
||||||
|
pushNotice(noticeEvent {
|
||||||
|
this.type = NoticeType.GROUP_ESSENCE_CHANGED
|
||||||
|
this.time = time.toInt()
|
||||||
|
this.groupEssenceChanged = essenceMessageNotice {
|
||||||
|
this.groupId = groupId
|
||||||
|
this.messageId = msgId
|
||||||
|
this.sender = senderUin
|
||||||
|
this.operator = operatorUin
|
||||||
|
this.subType = subType.toInt()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 私聊通知 通知器
|
||||||
|
*/
|
||||||
|
object PrivateNoticeTransmitter {
|
||||||
|
suspend fun transPrivatePoke(msgTime: Long, operator: Long, target: Long, action: String?, suffix: String?, actionImg: String?): Boolean {
|
||||||
|
pushNotice(noticeEvent {
|
||||||
|
this.type = NoticeType.FRIEND_POKE
|
||||||
|
this.time = msgTime.toInt()
|
||||||
|
this.friendPoke = friendPokeNotice {
|
||||||
|
this.action = action ?: ""
|
||||||
|
this.target = target
|
||||||
|
this.operator = operator
|
||||||
|
this.suffix = suffix ?: ""
|
||||||
|
this.actionImage = actionImg ?: ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transPrivateRecall(time: Long, operator: Long, msgId: Long, tipText: String): Boolean {
|
||||||
|
pushNotice(noticeEvent {
|
||||||
|
this.type = NoticeType.FRIEND_RECALL
|
||||||
|
this.time = time.toInt()
|
||||||
|
this.friendRecall = friendRecallNotice {
|
||||||
|
this.operator = operator
|
||||||
|
this.messageId = msgId
|
||||||
|
this.tipText = tipText
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求 通知器
|
||||||
|
*/
|
||||||
|
object RequestTransmitter {
|
||||||
|
suspend fun transFriendApp(time: Long, operator: Long, tipText: String, flag: String): Boolean {
|
||||||
|
pushRequest(requestsEvent {
|
||||||
|
this.type = RequestType.FRIEND_APPLY
|
||||||
|
this.time = time.toInt()
|
||||||
|
this.friendApply = friendApplyRequest {
|
||||||
|
this.applierUin = operator
|
||||||
|
this.message = tipText
|
||||||
|
this.flag = flag
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transGroupApply(
|
||||||
|
time: Long,
|
||||||
|
applier: Long,
|
||||||
|
applierUid: String,
|
||||||
|
reason: String,
|
||||||
|
groupCode: Long,
|
||||||
|
flag: String,
|
||||||
|
type: GroupApplyType
|
||||||
|
): Boolean {
|
||||||
|
pushRequest(requestsEvent {
|
||||||
|
this.type = RequestType.GROUP_APPLY
|
||||||
|
this.time = time.toInt()
|
||||||
|
this.groupApply = groupApplyRequest {
|
||||||
|
this.applierUid = applierUid
|
||||||
|
this.applierUin = applier
|
||||||
|
this.groupId = groupCode
|
||||||
|
this.reason = reason
|
||||||
|
this.flag = flag
|
||||||
|
this.type = type
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun onMessageEvent(collector: FlowCollector<Pair<MsgRecord, MessageEvent>>) {
|
||||||
|
messageEventFlow.collect {
|
||||||
|
GlobalScope.launch {
|
||||||
|
collector.emit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun onNoticeEvent(collector: FlowCollector<NoticeEvent>) {
|
||||||
|
noticeEventFlow.collect {
|
||||||
|
GlobalScope.launch {
|
||||||
|
collector.emit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun onRequestEvent(collector: FlowCollector<RequestsEvent>) {
|
||||||
|
requestEventFlow.collect {
|
||||||
|
GlobalScope.launch {
|
||||||
|
collector.emit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
xposed/src/main/java/moe/fuqiuluo/shamrock/tools/AndroidX.kt
Normal file
16
xposed/src/main/java/moe/fuqiuluo/shamrock/tools/AndroidX.kt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.tools
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Handler
|
||||||
|
import android.widget.Toast
|
||||||
|
import de.robv.android.xposed.XposedBridge
|
||||||
|
|
||||||
|
lateinit var GlobalUi: Handler
|
||||||
|
|
||||||
|
internal fun Context.toast(msg: String, flag: Int = Toast.LENGTH_SHORT) {
|
||||||
|
XposedBridge.log(msg)
|
||||||
|
if (!::GlobalUi.isInitialized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
GlobalUi.post { Toast.makeText(this, msg, flag).show() }
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
package moe.fuqiuluo.shamrock.tools
|
package moe.fuqiuluo.shamrock.tools
|
||||||
|
|
||||||
import io.github.xn32.json5k.Json5
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
@ -27,15 +26,6 @@ val GlobalJson = Json {
|
|||||||
coerceInputValues = true // 强制输入值
|
coerceInputValues = true // 强制输入值
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val GlobalJson5 = Json5 {
|
|
||||||
prettyPrint = true
|
|
||||||
indentationWidth = 2
|
|
||||||
//useSingleQuotes = true
|
|
||||||
//quoteMemberNames = true
|
|
||||||
//encodeDefaults = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val String.asJson: JsonElement
|
val String.asJson: JsonElement
|
||||||
get() = Json.parseToJsonElement(this)
|
get() = Json.parseToJsonElement(this)
|
||||||
|
|
||||||
|
@ -47,8 +47,8 @@ fun ByteArray.slice(off: Int, length: Int = size - off): ByteArray {
|
|||||||
.let { s -> if (uppercase) s.lowercase(Locale.getDefault()) else s }
|
.let { s -> if (uppercase) s.lowercase(Locale.getDefault()) else s }
|
||||||
} ?: "null"
|
} ?: "null"
|
||||||
|
|
||||||
fun String?.ifNullOrEmpty(defaultValue: String?): String? {
|
fun String?.ifNullOrEmpty(defaultValue: () -> String?): String? {
|
||||||
return if (this.isNullOrEmpty()) defaultValue else this
|
return if (this.isNullOrEmpty()) defaultValue() else this
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads fun String.hex2ByteArray(replace: Boolean = false): ByteArray {
|
@JvmOverloads fun String.hex2ByteArray(replace: Boolean = false): ByteArray {
|
||||||
|
@ -1,43 +1,15 @@
|
|||||||
package moe.fuqiuluo.shamrock.tools
|
package moe.fuqiuluo.shamrock.tools
|
||||||
|
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.engine.okhttp.OkHttp
|
||||||
import io.ktor.client.plugins.HttpTimeout
|
import io.ktor.client.plugins.HttpTimeout
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
|
|
||||||
val GlobalClient: HttpClient by lazy {
|
val GlobalClient by lazy {
|
||||||
HttpClient {
|
HttpClient(OkHttp) {
|
||||||
//install(HttpCookies)
|
|
||||||
install(HttpTimeout) {
|
install(HttpTimeout) {
|
||||||
requestTimeoutMillis = 15000
|
requestTimeoutMillis = 15000
|
||||||
connectTimeoutMillis = 15000
|
connectTimeoutMillis = 15000
|
||||||
socketTimeoutMillis = 15000
|
socketTimeoutMillis = 15000
|
||||||
}
|
}
|
||||||
install(ContentNegotiation) {
|
|
||||||
json(Json {
|
|
||||||
prettyPrint = true
|
|
||||||
isLenient = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val GlobalClientNoRedirect: HttpClient by lazy {
|
|
||||||
HttpClient {
|
|
||||||
//install(HttpCookies)
|
|
||||||
followRedirects = false
|
|
||||||
|
|
||||||
install(HttpTimeout) {
|
|
||||||
requestTimeoutMillis = 15000
|
|
||||||
connectTimeoutMillis = 15000
|
|
||||||
socketTimeoutMillis = 15000
|
|
||||||
}
|
|
||||||
install(ContentNegotiation) {
|
|
||||||
json(Json {
|
|
||||||
prettyPrint = true
|
|
||||||
isLenient = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,9 +2,7 @@ package moe.fuqiuluo.shamrock.tools
|
|||||||
|
|
||||||
import mqq.app.MobileQQ
|
import mqq.app.MobileQQ
|
||||||
|
|
||||||
private val context = MobileQQ.getContext()
|
val ShamrockVersion: String by lazy {
|
||||||
private val packageManager = context.packageManager
|
MobileQQ.getContext().packageManager
|
||||||
|
.getPackageInfo("moe.fuqiuluo.shamrock.hided", 0).versionName
|
||||||
private fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(packageName, 0)
|
}
|
||||||
|
|
||||||
val ShamrockVersion: String = getPackageInfo("moe.fuqiuluo.shamrock.hided").versionName
|
|
||||||
|
@ -8,7 +8,7 @@ import com.arthenica.ffmpegkit.FFmpegKit
|
|||||||
import com.arthenica.ffmpegkit.FFprobeKit
|
import com.arthenica.ffmpegkit.FFprobeKit
|
||||||
import com.arthenica.ffmpegkit.ReturnCode
|
import com.arthenica.ffmpegkit.ReturnCode
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.QQNTWrapperUtil
|
import com.tencent.qqnt.kernel.nativeinterface.QQNTWrapperUtil
|
||||||
import moe.fuqiuluo.shamrock.helper.LocalCacheHelper
|
import qq.service.internals.LocalCacheHelper
|
||||||
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 java.io.File
|
import java.io.File
|
||||||
|
@ -33,7 +33,7 @@ object DownloadUtils {
|
|||||||
threadCount: Int = MAX_THREAD,
|
threadCount: Int = MAX_THREAD,
|
||||||
headers: Map<String, String> = mapOf()
|
headers: Map<String, String> = mapOf()
|
||||||
): Boolean {
|
): Boolean {
|
||||||
var threadCnt = if(threadCount == 0) MAX_THREAD else threadCount
|
var threadCnt = if(threadCount == 0 || threadCount < 0) MAX_THREAD else threadCount
|
||||||
val url = URL(urlAdr)
|
val url = URL(urlAdr)
|
||||||
val connection = withContext(Dispatchers.IO) { url.openConnection() } as HttpURLConnection
|
val connection = withContext(Dispatchers.IO) { url.openConnection() } as HttpURLConnection
|
||||||
headers.forEach { (k, v) ->
|
headers.forEach { (k, v) ->
|
||||||
|
@ -1,32 +1,28 @@
|
|||||||
package moe.fuqiuluo.shamrock.xposed
|
package moe.fuqiuluo.shamrock.xposed
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Process
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
import de.robv.android.xposed.IXposedHookLoadPackage
|
import de.robv.android.xposed.IXposedHookLoadPackage
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import de.robv.android.xposed.XposedBridge.log
|
import de.robv.android.xposed.XposedBridge.log
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
|
||||||
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
|
||||||
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
|
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
|
||||||
import moe.fuqiuluo.shamrock.xposed.loader.KeepAlive
|
import moe.fuqiuluo.shamrock.xposed.helper.KeepAlive
|
||||||
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
|
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
|
||||||
import moe.fuqiuluo.shamrock.tools.FuzzySearchClass
|
import moe.fuqiuluo.shamrock.tools.FuzzySearchClass
|
||||||
|
import moe.fuqiuluo.shamrock.tools.GlobalUi
|
||||||
import moe.fuqiuluo.shamrock.tools.afterHook
|
import moe.fuqiuluo.shamrock.tools.afterHook
|
||||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||||
import moe.fuqiuluo.shamrock.xposed.hooks.runFirstActions
|
|
||||||
import mqq.app.MobileQQ
|
import mqq.app.MobileQQ
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.Modifier
|
import java.lang.reflect.Modifier
|
||||||
import kotlin.system.exitProcess
|
import moe.fuqiuluo.shamrock.xposed.actions.runFirstActions
|
||||||
|
|
||||||
private const val PACKAGE_NAME_QQ = "com.tencent.mobileqq"
|
private const val PACKAGE_NAME_QQ = "com.tencent.mobileqq"
|
||||||
private const val PACKAGE_NAME_QQ_INTERNATIONAL = "com.tencent.mobileqqi"
|
|
||||||
private const val PACKAGE_NAME_QQ_LITE = "com.tencent.qqlite"
|
|
||||||
private const val PACKAGE_NAME_TIM = "com.tencent.tim"
|
private const val PACKAGE_NAME_TIM = "com.tencent.tim"
|
||||||
|
|
||||||
private val uselessProcess = listOf("peak", "tool", "mini", "qzone")
|
|
||||||
|
|
||||||
internal class XposedEntry: IXposedHookLoadPackage {
|
internal class XposedEntry: IXposedHookLoadPackage {
|
||||||
companion object {
|
companion object {
|
||||||
@ -36,7 +32,6 @@ internal class XposedEntry: IXposedHookLoadPackage {
|
|||||||
var secStaticNativehookInited = false
|
var secStaticNativehookInited = false
|
||||||
|
|
||||||
external fun injected(): Boolean
|
external fun injected(): Boolean
|
||||||
|
|
||||||
external fun hasEnv(): Boolean
|
external fun hasEnv(): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,22 +135,14 @@ internal class XposedEntry: IXposedHookLoadPackage {
|
|||||||
MMKVFetcher.initMMKV(ctx)
|
MMKVFetcher.initMMKV(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
runCatching {
|
|
||||||
if (ShamrockConfig.forbidUselessProcess()) {
|
|
||||||
if(uselessProcess.any {
|
|
||||||
processName.contains(it, ignoreCase = true)
|
|
||||||
}) {
|
|
||||||
log("[Shamrock] Useless process detected: $processName, exit.")
|
|
||||||
Process.killProcess(Process.myPid())
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log("[Shamrock] Useless process detection is disabled.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log("Process Name = $processName")
|
log("Process Name = $processName")
|
||||||
|
|
||||||
|
GlobalUi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
Handler.createAsync(ctx.mainLooper)
|
||||||
|
} else {
|
||||||
|
Handler(ctx.mainLooper)
|
||||||
|
}
|
||||||
|
|
||||||
runFirstActions(ctx)
|
runFirstActions(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,265 @@
|
|||||||
|
@file:Suppress("UNCHECKED_CAST", "LocalVariableName")
|
||||||
|
package moe.fuqiuluo.shamrock.xposed.actions
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.VersionedPackage
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Looper
|
||||||
|
import com.tencent.qphone.base.remote.ToServiceMsg
|
||||||
|
import de.robv.android.xposed.XC_MethodReplacement
|
||||||
|
import de.robv.android.xposed.XSharedPreferences
|
||||||
|
import de.robv.android.xposed.XposedHelpers
|
||||||
|
import moe.fuqiuluo.shamrock.config.AntiJvmTrace
|
||||||
|
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||||
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import moe.fuqiuluo.shamrock.tools.MethodHooker
|
||||||
|
import moe.fuqiuluo.shamrock.tools.hookMethod
|
||||||
|
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.XposedEntry
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader
|
||||||
|
import moe.fuqiuluo.symbols.XposedHook
|
||||||
|
import mqq.app.MobileQQ
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
|
||||||
|
@XposedHook(priority = 0)
|
||||||
|
class AntiDetection: IAction {
|
||||||
|
private external fun antiNativeDetections(): Boolean
|
||||||
|
|
||||||
|
override fun invoke(ctx: Context) {
|
||||||
|
try {
|
||||||
|
antiFindPackage(ctx)
|
||||||
|
}catch(_:Throwable){ } //某个大聪明在外面隐藏了shamrock,导致这个代码抛出异常,俺不说是谁>_<
|
||||||
|
antiGetPackageGidsDetection(ctx)
|
||||||
|
antiProviderDetection()
|
||||||
|
antiNativeDetection()
|
||||||
|
if (ShamrockConfig[AntiJvmTrace])
|
||||||
|
antiTrace()
|
||||||
|
antiMemoryWalking()
|
||||||
|
antiO3Report()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun antiO3Report() {
|
||||||
|
QQInterfaces.app.javaClass.hookMethod("sendToService").before {
|
||||||
|
val toServiceMsg = it.args[0] as ToServiceMsg?
|
||||||
|
if (toServiceMsg != null && toServiceMsg.serviceCmd.startsWith("trpc.o3")) {
|
||||||
|
LogCenter.log("拦截trpc.o3环境上报包", Level.WARN)
|
||||||
|
it.result = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun antiGetPackageGidsDetection(ctx: Context) {
|
||||||
|
ctx.packageManager::class.java.hookMethod("getPackageGids").before {
|
||||||
|
val packageName = it.args[0] as String
|
||||||
|
if (packageName == "moe.fuqiuluo.shamrock") {
|
||||||
|
it.result = null
|
||||||
|
it.throwable = PackageManager.NameNotFoundException(packageName)
|
||||||
|
LogCenter.log("AntiDetection: 检测到对Shamrock的检测,欺骗GetPackageGids")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun antiProviderDetection() {
|
||||||
|
ContentResolver::class.java.hookMethod("acquireContentProviderClient").before {
|
||||||
|
val uri = it.args[0] as String
|
||||||
|
if (uri == "moe.fuqiuluo.108.provider" || uri == "moe.fuqiuluo.xqbot.provider") {
|
||||||
|
it.result = null
|
||||||
|
LogCenter.log("AntiDetection: 检测到对Shamrock的检测,欺骗ContentResolver", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isModuleStack = fun String.(): Boolean {
|
||||||
|
return contains("fuqiuluo") || contains("shamrock") || contains("whitechi") || contains("lsposed") || contains("xposed")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isModuleStack(): Boolean {
|
||||||
|
Thread.currentThread().stackTrace.forEach {
|
||||||
|
if (it.className.isModuleStack()) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun antiNativeDetection() {
|
||||||
|
try {
|
||||||
|
val pref = XSharedPreferences("moe.fuqiuluo.shamrock", "shared_config")
|
||||||
|
if (!pref.file.canRead()) {
|
||||||
|
LogCenter.log("[Shamrock] unable to load XSharedPreferences", Level.WARN)
|
||||||
|
return
|
||||||
|
} else if (!pref.getBoolean("super_anti", false)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
NativeLoader.load("clover")
|
||||||
|
val env = XposedEntry.hasEnv()
|
||||||
|
val injected = XposedEntry.injected()
|
||||||
|
if (!env || !injected) {
|
||||||
|
LogCenter.log("[Shamrock] Shamrock反检测启动失败(env=$env, injected=$injected)", Level.ERROR)
|
||||||
|
} else {
|
||||||
|
XposedEntry.secStaticNativehookInited = true
|
||||||
|
if (PlatformUtils.isMainProcess()) {
|
||||||
|
LogCenter.log(
|
||||||
|
"[Shamrock] Shamrock反检测启动成功: ${antiNativeDetections()}",
|
||||||
|
Level.INFO
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
LogCenter.log("[Shamrock] Shamrock反检测启动失败,请检查LSPosed版本使用大于100: ${e.message}", Level.ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun antiFindPackage(context: Context) {
|
||||||
|
if (isAntiFindPackage) return
|
||||||
|
|
||||||
|
val packageManager = context.packageManager
|
||||||
|
val applicationInfo = packageManager.getApplicationInfo("moe.fuqiuluo.shamrock", 0)
|
||||||
|
val packageInfo = packageManager.getPackageInfo("moe.fuqiuluo.shamrock", 0)
|
||||||
|
|
||||||
|
packageManager.javaClass.hookMethod("getApplicationInfo").before {
|
||||||
|
val packageName = it.args[0] as String
|
||||||
|
if(packageName == "moe.fuqiuluo.shamrock") {
|
||||||
|
LogCenter.log("AntiDetection: 检测到对Shamrock的检测,欺骗PackageManager(GA)", Level.WARN)
|
||||||
|
it.throwable = PackageManager.NameNotFoundException("Hided")
|
||||||
|
} else if (packageName == "moe.fuqiuluo.shamrock.hided") {
|
||||||
|
it.result = applicationInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packageManager.javaClass.hookMethod("getPackageInfo").before {
|
||||||
|
when(val packageName = it.args[0]) {
|
||||||
|
is String -> {
|
||||||
|
if(packageName == "moe.fuqiuluo.shamrock") {
|
||||||
|
LogCenter.log("AntiDetection: 检测到对Shamrock的检测,欺骗PackageManager(GP)", Level.WARN)
|
||||||
|
it.throwable = PackageManager.NameNotFoundException()
|
||||||
|
} else if (packageName == "moe.fuqiuluo.shamrock.hided") {
|
||||||
|
it.result = packageInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && packageName is VersionedPackage) {
|
||||||
|
if(packageName.packageName == "moe.fuqiuluo.shamrock") {
|
||||||
|
LogCenter.log("AntiDetection: 检测到对Shamrock的检测,欺骗PackageManager(GPV)", Level.WARN)
|
||||||
|
it.throwable = PackageManager.NameNotFoundException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isAntiFindPackage = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun antiMemoryWalking() {
|
||||||
|
val c = Class.forName("dalvik.system.VMDebug")
|
||||||
|
//val startMethodTracingMethod = c.getDeclaredMethod(
|
||||||
|
// "startMethodTracing", String::class.java,
|
||||||
|
// Integer.TYPE, Integer.TYPE, java.lang.Boolean.TYPE, Integer.TYPE
|
||||||
|
//)
|
||||||
|
//val stopMethodTracingMethod = c.getDeclaredMethod("stopMethodTracing")
|
||||||
|
//val getMethodTracingModeMethod = c.getDeclaredMethod("getMethodTracingMode")
|
||||||
|
//val getRuntimeStatMethod = c.getDeclaredMethod("getRuntimeStat", String::class.java)
|
||||||
|
//val getRuntimeStatsMethod = c.getDeclaredMethod("getRuntimeStats")
|
||||||
|
val VMClassLoader = LuoClassloader.load("java/lang/VMClassLoader")
|
||||||
|
if (VMClassLoader != null) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin.runCatching {
|
||||||
|
XposedHelpers.findAndHookMethod(XposedHelpers.findClass("com.tencent.bugly.agent.CrashReport", LuoClassloader.hostClassLoader),
|
||||||
|
"initCrashReport", object: XC_MethodReplacement() {
|
||||||
|
override fun replaceHookedMethod(param: MethodHookParam): Any? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.hookMethod("countInstancesOfClass").before {
|
||||||
|
val clz = it.args[0] as Class<*>
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
if(clz.packageName.isModuleStack()) {
|
||||||
|
it.result = 0L
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(clz.canonicalName?.isModuleStack() == true) {
|
||||||
|
it.result = 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.hookMethod("countInstancesOfClasses").before {
|
||||||
|
val clzs = it.args[0] as Array<Class<*>>
|
||||||
|
clzs.forEach { clz ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
if(clz.packageName.isModuleStack()) {
|
||||||
|
it.result = 0L
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(clz.canonicalName?.isModuleStack() == true) {
|
||||||
|
it.result = 0L
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.hookMethod("getInstancesOfClasses").after {
|
||||||
|
val clzs = it.args[0] as Array<Class<*>>
|
||||||
|
clzs.forEachIndexed { _, clz ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
if(clz.packageName.isModuleStack()) {
|
||||||
|
it.result = Array<Any>(0) { }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(clz.canonicalName?.isModuleStack() == true) {
|
||||||
|
it.result = Array<Any>(0) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun antiTrace() {
|
||||||
|
val isModuleStack = fun StackTraceElement.(): Boolean {
|
||||||
|
return className.isModuleStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
val stackTraceHooker: MethodHooker = {
|
||||||
|
val result = it.result as Array<StackTraceElement>
|
||||||
|
var zygote = false
|
||||||
|
val newResult = result.filter {
|
||||||
|
if (it.className == ZYGOTE_NAME) {
|
||||||
|
zygote = true
|
||||||
|
}
|
||||||
|
!it.isModuleStack()
|
||||||
|
}.toTypedArray()
|
||||||
|
if (!zygote && Thread.currentThread() == Looper.getMainLooper().thread) {
|
||||||
|
it.result = arrayListOf(StackTraceElement(ZYGOTE_NAME, "main", ZYGOTE_NAME, 0), *newResult)
|
||||||
|
} else {
|
||||||
|
it.result = newResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread::class.java.hookMethod("getName").after {
|
||||||
|
val result = it.result as String
|
||||||
|
if (result.contains("fuqiuluo") || result.contains("shamrock") || result.contains("whitechi")) {
|
||||||
|
it.result = "android"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread::class.java.hookMethod("getStackTrace").after(stackTraceHooker)
|
||||||
|
Throwable::class.java.hookMethod("getStackTrace").after(stackTraceHooker)
|
||||||
|
Throwable::class.java.hookMethod("getOurStackTrace").after(stackTraceHooker)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
var isAntiFindPackage = false
|
||||||
|
|
||||||
|
const val ZYGOTE_NAME = "com.android.internal.os.ZygoteInit"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.xposed.actions
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.Build
|
||||||
|
import de.robv.android.xposed.XposedBridge
|
||||||
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.actions.interacts.SwitchStatus
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.actions.interacts.Init
|
||||||
|
import moe.fuqiuluo.symbols.Process
|
||||||
|
import moe.fuqiuluo.symbols.XposedHook
|
||||||
|
import mqq.app.MobileQQ
|
||||||
|
|
||||||
|
@XposedHook(priority = 1, process = Process.MAIN)
|
||||||
|
class DynamicBroadcast: IAction {
|
||||||
|
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||||
|
override fun invoke(ctx: Context) {
|
||||||
|
val intentFilter = IntentFilter()
|
||||||
|
intentFilter.addAction("moe.fuqiuluo.kritor.dynamic")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
MobileQQ.getMobileQQ().registerReceiver(
|
||||||
|
DynamicReceiver, intentFilter,
|
||||||
|
Context.RECEIVER_EXPORTED
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
MobileQQ.getMobileQQ().registerReceiver(DynamicReceiver, intentFilter)
|
||||||
|
}
|
||||||
|
XposedBridge.log("Register Main::Broadcast successfully.")
|
||||||
|
}
|
||||||
|
|
||||||
|
private object DynamicReceiver: BroadcastReceiver() {
|
||||||
|
private val handlers = mapOf(
|
||||||
|
"init" to Init,
|
||||||
|
"switch_status" to SwitchStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val cmd = intent.getStringExtra("__cmd") ?: ""
|
||||||
|
val handler = handlers[cmd]
|
||||||
|
if (handler == null) {
|
||||||
|
LogCenter.log("DynamicReceiver.onReceive: unknown cmd=$cmd", Level.ERROR)
|
||||||
|
} else {
|
||||||
|
handler(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
@file:OptIn(DelicateCoroutinesApi::class)
|
||||||
|
package moe.fuqiuluo.shamrock.xposed.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.tencent.qqnt.kernel.api.IKernelService
|
||||||
|
import com.tencent.qqnt.kernel.api.impl.KernelServiceImpl
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import moe.fuqiuluo.shamrock.tools.hookMethod
|
||||||
|
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||||
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import qq.service.internals.NTServiceFetcher
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader
|
||||||
|
import moe.fuqiuluo.symbols.XposedHook
|
||||||
|
|
||||||
|
@XposedHook(priority = 2)
|
||||||
|
internal class FetchService: IAction {
|
||||||
|
override fun invoke(ctx: Context) {
|
||||||
|
NativeLoader.load("shamrock")
|
||||||
|
|
||||||
|
if (PlatformUtils.isMqq()) {
|
||||||
|
KernelServiceImpl::class.java.hookMethod("initService").after {
|
||||||
|
val service = it.thisObject as IKernelService
|
||||||
|
LogCenter.log("NTKernel try to init service: $service", Level.DEBUG)
|
||||||
|
GlobalScope.launch {
|
||||||
|
NTServiceFetcher.onFetch(service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (PlatformUtils.isTim()) {
|
||||||
|
// TIM 尚未进入 NTKernel
|
||||||
|
LogCenter.log("NTKernel init failed: tim not support NT", Level.ERROR)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.xposed.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import moe.fuqiuluo.shamrock.tools.hookMethod
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader
|
||||||
|
import moe.fuqiuluo.symbols.XposedHook
|
||||||
|
|
||||||
|
@XposedHook(priority = 0)
|
||||||
|
internal class FixAudioLibraryLoader: IAction {
|
||||||
|
private val redirectedLibrary =arrayOf(
|
||||||
|
"ffmpegkit_abidetect",
|
||||||
|
"avutil",
|
||||||
|
"swscale",
|
||||||
|
"swresample",
|
||||||
|
"avcodec",
|
||||||
|
"avformat",
|
||||||
|
"avfilter",
|
||||||
|
"avdevice",
|
||||||
|
"ffmpegkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun invoke(ctx: Context) {
|
||||||
|
com.arthenica.ffmpegkit.NativeLoader::class.java.hookMethod("loadLibrary").before {
|
||||||
|
val name: String = it.args[0] as String
|
||||||
|
if (name in redirectedLibrary) {
|
||||||
|
redirectedLibrary.forEach {
|
||||||
|
NativeLoader.load(it)
|
||||||
|
}
|
||||||
|
NativeLoader.load(name)
|
||||||
|
}
|
||||||
|
it.result = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
@file:Suppress("UNUSED_VARIABLE", "LocalVariableName")
|
||||||
|
package moe.fuqiuluo.shamrock.xposed.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.tencent.common.config.pad.DeviceType
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.InitSessionConfig
|
||||||
|
import de.robv.android.xposed.XposedBridge
|
||||||
|
import moe.fuqiuluo.shamrock.config.ForceTablet
|
||||||
|
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||||
|
import moe.fuqiuluo.shamrock.tools.FuzzySearchClass
|
||||||
|
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import moe.fuqiuluo.shamrock.tools.afterHook
|
||||||
|
import moe.fuqiuluo.shamrock.tools.hookMethod
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
|
||||||
|
import moe.fuqiuluo.symbols.XposedHook
|
||||||
|
|
||||||
|
@XposedHook(priority = 0)
|
||||||
|
internal class ForceTablet: IAction {
|
||||||
|
override fun invoke(ctx: Context) {
|
||||||
|
if (ShamrockConfig[ForceTablet]) {
|
||||||
|
if (PlatformUtils.isMainProcess()) {
|
||||||
|
LogCenter.log("强制协议类型 (PAD)", toast = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val returnTablet = afterHook {
|
||||||
|
it.result = DeviceType.TABLET
|
||||||
|
}
|
||||||
|
|
||||||
|
FuzzySearchClass.findAllClassByMethod(
|
||||||
|
LuoClassloader.hostClassLoader, "com.tencent.common.config.pad"
|
||||||
|
) { _, method ->
|
||||||
|
method.returnType == DeviceType::class.java
|
||||||
|
}.forEach { clazz ->
|
||||||
|
//log("Inject to tablet mode in ${clazz.name}")
|
||||||
|
val method = clazz.declaredMethods.first { it.returnType == DeviceType::class.java }
|
||||||
|
XposedBridge.hookMethod(method, returnTablet)
|
||||||
|
}
|
||||||
|
|
||||||
|
val PadUtil = LuoClassloader.load("com.tencent.common.config.pad.PadUtil")
|
||||||
|
PadUtil?.declaredMethods?.filter {
|
||||||
|
it.returnType == DeviceType::class.java
|
||||||
|
}?.forEach {
|
||||||
|
XposedBridge.hookMethod(it, returnTablet)
|
||||||
|
}
|
||||||
|
|
||||||
|
val deviceTypeField = InitSessionConfig::class.java.declaredFields.firstOrNull {
|
||||||
|
it.type == com.tencent.qqnt.kernel.nativeinterface.DeviceType::class.java
|
||||||
|
}
|
||||||
|
if (deviceTypeField != null) {
|
||||||
|
XposedBridge.hookAllConstructors(InitSessionConfig::class.java, afterHook {
|
||||||
|
if (!deviceTypeField.isAccessible) deviceTypeField.isAccessible = true
|
||||||
|
deviceTypeField.set(it.thisObject, com.tencent.qqnt.kernel.nativeinterface.DeviceType.KPAD)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
InitSessionConfig::class.java.hookMethod("getDeviceType").after {
|
||||||
|
it.result = com.tencent.qqnt.kernel.nativeinterface.DeviceType.KPAD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.xposed.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
internal interface IAction {
|
||||||
|
|
||||||
|
operator fun invoke(ctx: Context)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
@file:OptIn(DelicateCoroutinesApi::class)
|
||||||
|
|
||||||
|
package moe.fuqiuluo.shamrock.xposed.actions
|
||||||
|
|
||||||
|
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
|
||||||
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
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 {
|
||||||
|
override fun invoke(ctx: Context) {
|
||||||
|
GlobalScope.launch {
|
||||||
|
runCatching {
|
||||||
|
if (ActiveRPC.get()) {
|
||||||
|
if (!::server.isInitialized) {
|
||||||
|
server = KritorServer(RPCPort.get())
|
||||||
|
server.start()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.xposed.actions
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.Process
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import moe.fuqiuluo.symbols.XposedHook
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
@XposedHook(priority = 20)
|
||||||
|
internal class ListenShamrockUpdate: IAction {
|
||||||
|
override fun invoke(ctx: Context) {
|
||||||
|
val intent = IntentFilter()
|
||||||
|
intent.addAction("android.intent.action.PACKAGE_ADDED")
|
||||||
|
intent.addAction("android.intent.action.PACKAGE_REMOVED")
|
||||||
|
intent.addAction("android.intent.action.PACKAGE_REPLACED")
|
||||||
|
intent.addDataScheme("package")
|
||||||
|
|
||||||
|
ctx.registerReceiver(Companion, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object: BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
when (intent.action) {
|
||||||
|
"android.intent.action.PACKAGE_ADDED",
|
||||||
|
"android.intent.action.PACKAGE_REMOVED",
|
||||||
|
"android.intent.action.PACKAGE_REPLACED" -> {
|
||||||
|
val packageName = intent.data?.schemeSpecificPart
|
||||||
|
if (packageName == "moe.fuqiuluo.shamrock") {
|
||||||
|
LogCenter.log("Shamrock更新, QQ已经自我销毁。")
|
||||||
|
Process.killProcess(Process.myPid())
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.xposed.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import de.robv.android.xposed.XposedHelpers
|
||||||
|
import moe.fuqiuluo.shamrock.tools.hookMethod
|
||||||
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
|
||||||
|
import moe.fuqiuluo.symbols.XposedHook
|
||||||
|
import mqq.app.MobileQQ
|
||||||
|
|
||||||
|
@XposedHook(priority = 10)
|
||||||
|
internal class NoBackGround: IAction {
|
||||||
|
override fun invoke(ctx: Context) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
XposedHelpers.findClass("com.tencent.mobileqq.activity.miniaio.MiniMsgUser", LuoClassloader)
|
||||||
|
}.onSuccess {
|
||||||
|
it.hookMethod("onBackground").before {
|
||||||
|
it.result = null
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
LogCenter.log("Keeping MiniMsgUser alive failed: ${it.message}", Level.WARN)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val application = MobileQQ.getMobileQQ()
|
||||||
|
application.javaClass.hookMethod("onActivityFocusChanged").before {
|
||||||
|
it.args[1] = true
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
LogCenter.log("Keeping MSF alive failed: ${e.message}", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
@file:Suppress("UNCHECKED_CAST", "UNUSED_VARIABLE", "LocalVariableName")
|
||||||
|
package moe.fuqiuluo.shamrock.xposed.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.tencent.common.app.AppInterface
|
||||||
|
import com.tencent.mobileqq.msf.sdk.MsfMessagePair
|
||||||
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import moe.fuqiuluo.shamrock.tools.hookMethod
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
|
||||||
|
import moe.fuqiuluo.symbols.XposedHook
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
import qq.service.internals.MSFHandler.onPush
|
||||||
|
import qq.service.internals.MSFHandler.onResp
|
||||||
|
|
||||||
|
|
||||||
|
@XposedHook(priority = 10)
|
||||||
|
class PatchMsfCore: IAction {
|
||||||
|
override fun invoke(ctx: Context) {
|
||||||
|
val app = QQInterfaces.app
|
||||||
|
require(app is AppInterface) { "QQInterface.app must be AppInterface" }
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
val MSFRespHandleTask = LuoClassloader.load("mqq.app.msghandle.MSFRespHandleTask")
|
||||||
|
if (MSFRespHandleTask == null) {
|
||||||
|
LogCenter.log("无法注入MSFRespHandleTask!", Level.ERROR)
|
||||||
|
} else {
|
||||||
|
val msfPair = MSFRespHandleTask.declaredFields.first {
|
||||||
|
it.type == MsfMessagePair::class.java
|
||||||
|
}
|
||||||
|
msfPair.isAccessible = true
|
||||||
|
MSFRespHandleTask.hookMethod("run").before {
|
||||||
|
val pair = msfPair.get(it.thisObject) as MsfMessagePair
|
||||||
|
if (pair.toServiceMsg == null) {
|
||||||
|
onPush(pair.fromServiceMsg)
|
||||||
|
} else {
|
||||||
|
onResp(pair.toServiceMsg, pair.fromServiceMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
LogCenter.log(it.stackTraceToString(), Level.ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
@file:OptIn(DelicateCoroutinesApi::class)
|
||||||
|
package moe.fuqiuluo.shamrock.xposed.actions
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import moe.fuqiuluo.shamrock.config.IsInit
|
||||||
|
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||||
|
import moe.fuqiuluo.shamrock.tools.toast
|
||||||
|
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.helper.AppTalker
|
||||||
|
import moe.fuqiuluo.symbols.Process
|
||||||
|
import moe.fuqiuluo.symbols.XposedHook
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@XposedHook(Process.MAIN, priority = 1)
|
||||||
|
class PullConfig: IAction {
|
||||||
|
override fun invoke(ctx: Context) {
|
||||||
|
if (!PlatformUtils.isMainProcess()) return
|
||||||
|
|
||||||
|
val isInit = ShamrockConfig[IsInit]
|
||||||
|
AppTalker.talk("init", onFailure = {
|
||||||
|
if (isInit) {
|
||||||
|
ctx.toast("Shamrock主进程未启动,将不会同步配置!")
|
||||||
|
} else {
|
||||||
|
ctx.toast("Shamrock主进程未启动,初始化失败!")
|
||||||
|
GlobalScope.launch {
|
||||||
|
delay(3.seconds)
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ctx.toast("同步配置中...")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.xposed.actions.interacts
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
interface IInteract {
|
||||||
|
operator fun invoke(intent: Intent)
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.xposed.actions.interacts
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||||
|
import moe.fuqiuluo.shamrock.tools.toast
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.actions.runServiceActions
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader
|
||||||
|
import mqq.app.MobileQQ
|
||||||
|
|
||||||
|
internal object Init: IInteract {
|
||||||
|
private external fun testNativeLibrary(): String
|
||||||
|
|
||||||
|
override fun invoke(intent: Intent) {
|
||||||
|
ShamrockConfig.updateConfig(intent)
|
||||||
|
initAppService(MobileQQ.getMobileQQ())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initAppService(ctx: Context) {
|
||||||
|
NativeLoader.load("shamrock")
|
||||||
|
ctx.toast(testNativeLibrary())
|
||||||
|
runServiceActions(ctx)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.xposed.actions.interacts
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import com.tencent.mobileqq.app.QQAppInterface
|
||||||
|
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.helper.AppTalker
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
|
||||||
|
object SwitchStatus: IInteract, QQInterfaces() {
|
||||||
|
override fun invoke(intent: Intent) {
|
||||||
|
AppTalker.talk("switch_status") {
|
||||||
|
put("account", app.currentAccountUin)
|
||||||
|
put("nickname", if (app is QQAppInterface) app.currentNickname else "unknown")
|
||||||
|
put("voice", NativeLoader.isVoiceLoaded)
|
||||||
|
put("core_version", ShamrockVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,11 +3,10 @@ package moe.fuqiuluo.shamrock.xposed.helper
|
|||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import mqq.app.MobileQQ
|
import mqq.app.MobileQQ
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
internal object AppTalker {
|
internal object AppTalker {
|
||||||
val uriName = "content://moe.fuqiuluo.108.provider" // 你是真的闲,这都上个检测
|
private const val uriName = "content://moe.fuqiuluo.108.provider" // 你是真的闲,这都上个检测
|
||||||
val URI = Uri.parse(uriName)
|
private val URI = Uri.parse(uriName)
|
||||||
|
|
||||||
fun talk(values: ContentValues, onFailure: ((Throwable) -> Unit)? = null) {
|
fun talk(values: ContentValues, onFailure: ((Throwable) -> Unit)? = null) {
|
||||||
val ctx = MobileQQ.getContext()
|
val ctx = MobileQQ.getContext()
|
||||||
@ -17,4 +16,20 @@ internal object AppTalker {
|
|||||||
onFailure?.invoke(e)
|
onFailure?.invoke(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun talk(action: String, bodyBuilder: ContentValues.() -> Unit) {
|
||||||
|
val values = ContentValues()
|
||||||
|
values.put("__cmd", action)
|
||||||
|
values.put("__hash", 0)
|
||||||
|
bodyBuilder.invoke(values)
|
||||||
|
talk(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun talk(action: String, onFailure: ((Throwable) -> Unit)? = null, bodyBuilder: ContentValues.() -> Unit = {}) {
|
||||||
|
val values = ContentValues()
|
||||||
|
values.put("__cmd", action)
|
||||||
|
values.put("__hash", 0)
|
||||||
|
bodyBuilder.invoke(values)
|
||||||
|
talk(values, onFailure)
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,180 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.xposed.helper
|
||||||
|
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.os.Build
|
||||||
|
import de.robv.android.xposed.XSharedPreferences
|
||||||
|
import de.robv.android.xposed.XposedBridge
|
||||||
|
import de.robv.android.xposed.XposedHelpers
|
||||||
|
import moe.fuqiuluo.shamrock.tools.hookMethod
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
|
internal object KeepAlive {
|
||||||
|
private val KeepPackage = arrayOf(
|
||||||
|
"com.tencent.mobileqq", "moe.fuqiuluo.shamrock"
|
||||||
|
)
|
||||||
|
private val KeepRecords = arrayListOf<Any>()
|
||||||
|
private lateinit var KeepThread: Thread
|
||||||
|
|
||||||
|
private lateinit var METHOD_IS_KILLED: Method
|
||||||
|
private var allowPersistent: Boolean = false
|
||||||
|
|
||||||
|
operator fun invoke(loader: ClassLoader) {
|
||||||
|
val pref = XSharedPreferences("moe.fuqiuluo.shamrock", "shared_config")
|
||||||
|
hookAMS(pref, loader)
|
||||||
|
hookDoze(pref, loader)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hookDoze(pref: XSharedPreferences, loader: ClassLoader) {
|
||||||
|
if (pref.file.canRead() && pref.getBoolean("hook_doze", false)) {
|
||||||
|
val result = runCatching {
|
||||||
|
val DeviceIdleController = XposedHelpers.findClass(
|
||||||
|
"com.android.server.DeviceIdleController",
|
||||||
|
loader
|
||||||
|
)
|
||||||
|
?: return@runCatching -1
|
||||||
|
val becomeActiveLocked = XposedHelpers.findMethodBestMatch(
|
||||||
|
DeviceIdleController,
|
||||||
|
"becomeActiveLocked",
|
||||||
|
String::class.java,
|
||||||
|
Integer.TYPE
|
||||||
|
)
|
||||||
|
?: return@runCatching -2
|
||||||
|
if (!becomeActiveLocked.isAccessible) {
|
||||||
|
becomeActiveLocked.isAccessible = true
|
||||||
|
}
|
||||||
|
DeviceIdleController.hookMethod("onStart").after {
|
||||||
|
XposedBridge.log("[Shamrock] DeviceIdleController onStart")
|
||||||
|
}
|
||||||
|
DeviceIdleController.hookMethod("becomeInactiveIfAppropriateLocked").before {
|
||||||
|
XposedBridge.log("[Shamrock] DeviceIdleController becomeInactiveIfAppropriateLocked")
|
||||||
|
it.result = Unit
|
||||||
|
}
|
||||||
|
DeviceIdleController.hookMethod("stepIdleStateLocked").before {
|
||||||
|
XposedBridge.log("[Shamrock] DeviceIdleController stepIdleStateLocked")
|
||||||
|
it.result = Unit
|
||||||
|
}
|
||||||
|
return@runCatching 0
|
||||||
|
}.getOrElse { -5 }
|
||||||
|
if(result < 0) {
|
||||||
|
XposedBridge.log("[Shamrock] Unable to hookDoze: $result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hookAMS(pref: XSharedPreferences, loader: ClassLoader) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
val ActivityManagerService =
|
||||||
|
XposedHelpers.findClass("com.android.server.am.ActivityManagerService", loader)
|
||||||
|
ActivityManagerService.hookMethod("newProcessRecordLocked").after {
|
||||||
|
increaseAdj(it.result)
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
XposedBridge.log("[Shamrock] Plan A failed: ${it.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pref.file.canRead()) {
|
||||||
|
allowPersistent = pref.getBoolean("persistent", false)
|
||||||
|
XposedBridge.log("[Shamrock] allowPersistent = $allowPersistent")
|
||||||
|
} else {
|
||||||
|
XposedBridge.log("[Shamrock] unable to load XSharedPreferences")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin.runCatching {
|
||||||
|
val ProcessList = XposedHelpers.findClass("com.android.server.am.ProcessList", loader)
|
||||||
|
ProcessList.hookMethod("newProcessRecordLocked").after {
|
||||||
|
increaseAdj(it.result)
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
XposedBridge.log("[Shamrock] Plan B failed: ${it.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkThread() {
|
||||||
|
if (!::KeepThread.isInitialized || !KeepThread.isAlive) {
|
||||||
|
KeepThread = Thread {
|
||||||
|
val deletedList = mutableSetOf<Any>()
|
||||||
|
while (true) {
|
||||||
|
Thread.sleep(100)
|
||||||
|
KeepRecords.forEach {
|
||||||
|
val isKilled = if (::METHOD_IS_KILLED.isInitialized) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
METHOD_IS_KILLED.invoke(it) as Boolean
|
||||||
|
}.getOrElse { false }
|
||||||
|
} else false
|
||||||
|
if (isKilled) {
|
||||||
|
deletedList.add(it)
|
||||||
|
XposedBridge.log("Process Closed: $it")
|
||||||
|
} else {
|
||||||
|
keepByAdj(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (deletedList.isNotEmpty()) {
|
||||||
|
KeepRecords.removeAll(deletedList)
|
||||||
|
deletedList.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.also {
|
||||||
|
it.isDaemon = true
|
||||||
|
it.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun increaseAdj(record: Any) {
|
||||||
|
if (record.toString().contains("system", ignoreCase = true)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val applicationInfo = record.javaClass.getDeclaredField("info").also {
|
||||||
|
if (!it.isAccessible) it.isAccessible = true
|
||||||
|
}.get(record) as ApplicationInfo
|
||||||
|
if(applicationInfo.processName in KeepPackage) {
|
||||||
|
XposedBridge.log("[Shamrock] Process is keeping: $record")
|
||||||
|
KeepRecords.add(record)
|
||||||
|
keepByAdj(record)
|
||||||
|
// Error
|
||||||
|
if (allowPersistent) {
|
||||||
|
XposedBridge.log("[Shamrock] Open NoDied Mode!!!")
|
||||||
|
keepByPersistent(record)
|
||||||
|
}
|
||||||
|
checkThread()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun keepByAdj(record: Any) {
|
||||||
|
val clazz = record.javaClass
|
||||||
|
if (!::METHOD_IS_KILLED.isInitialized) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
METHOD_IS_KILLED = clazz.getDeclaredMethod("isKilled").also {
|
||||||
|
if (!it.isAccessible) it.isAccessible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kotlin.runCatching {
|
||||||
|
val newState = clazz.getDeclaredField("mState").also {
|
||||||
|
if (!it.isAccessible) it.isAccessible = true
|
||||||
|
}.get(record)
|
||||||
|
newState.javaClass.getDeclaredMethod("setMaxAdj", Int::class.java).also {
|
||||||
|
if (!it.isAccessible) it.isAccessible = true
|
||||||
|
}.invoke(newState, 1)
|
||||||
|
}.onFailure {
|
||||||
|
clazz.getDeclaredField("maxAdj").also {
|
||||||
|
if (!it.isAccessible) it.isAccessible = true
|
||||||
|
}.set(record, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun keepByPersistent(record: Any) {
|
||||||
|
val clazz = record.javaClass
|
||||||
|
kotlin.runCatching {
|
||||||
|
if (Build.VERSION.SDK_INT < 29) {
|
||||||
|
clazz.getDeclaredField("persistent").also {
|
||||||
|
if (!it.isAccessible) it.isAccessible = true
|
||||||
|
}.set(record, true)
|
||||||
|
} else {
|
||||||
|
clazz.getDeclaredMethod("setPersistent", Boolean::class.java).also {
|
||||||
|
if (!it.isAccessible) it.isAccessible = true
|
||||||
|
}.invoke(record, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -34,8 +34,6 @@ internal object NativeLoader {
|
|||||||
XposedBridge.log("[Shamrock] 反射检测到 Android x86")
|
XposedBridge.log("[Shamrock] 反射检测到 Android x86")
|
||||||
true
|
true
|
||||||
} else false
|
} else false
|
||||||
}.onFailure {
|
|
||||||
XposedBridge.log("[Shamrock] ${it.stackTraceToString()}")
|
|
||||||
}.getOrElse { false }
|
}.getOrElse { false }
|
||||||
|
|
||||||
private fun getLibFilePath(name: String): String {
|
private fun getLibFilePath(name: String): String {
|
||||||
|
140
xposed/src/main/java/qq/service/QQInterfaces.kt
Normal file
140
xposed/src/main/java/qq/service/QQInterfaces.kt
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package qq.service
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import com.tencent.common.app.AppInterface
|
||||||
|
import com.tencent.mobileqq.pb.ByteStringMicro
|
||||||
|
import com.tencent.qphone.base.remote.FromServiceMsg
|
||||||
|
import com.tencent.qphone.base.remote.ToServiceMsg
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||||
|
import mqq.app.MobileQQ
|
||||||
|
import protobuf.auto.toByteArray
|
||||||
|
import protobuf.oidb.TrpcOidb
|
||||||
|
import qq.service.internals.MSFHandler
|
||||||
|
import tencent.im.oidb.oidb_sso
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
abstract class QQInterfaces {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val app = (if (PlatformUtils.isMqqPackage())
|
||||||
|
MobileQQ.getMobileQQ().waitAppRuntime()
|
||||||
|
else
|
||||||
|
MobileQQ.getMobileQQ().waitAppRuntime(null)) as AppInterface
|
||||||
|
|
||||||
|
fun sendToServiceMsg(to: ToServiceMsg) {
|
||||||
|
app.sendToService(to)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendToServiceMsgAW(
|
||||||
|
to: ToServiceMsg,
|
||||||
|
timeout: Duration = 30.seconds
|
||||||
|
): FromServiceMsg? {
|
||||||
|
val seq = MSFHandler.nextSeq()
|
||||||
|
to.addAttribute("shamrock_uid", seq)
|
||||||
|
val resp: Pair<ToServiceMsg, FromServiceMsg>? = withTimeoutOrNull(timeout) {
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
GlobalScope.launch {
|
||||||
|
MSFHandler.registerResp(seq, continuation)
|
||||||
|
sendToServiceMsg(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (resp == null) {
|
||||||
|
MSFHandler.unregisterResp(seq)
|
||||||
|
}
|
||||||
|
return resp?.second
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendExtra(cmd: String, builder: (Bundle) -> Unit) {
|
||||||
|
val toServiceMsg = createToServiceMsg(cmd)
|
||||||
|
builder(toServiceMsg.extraData)
|
||||||
|
app.sendToService(toServiceMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createToServiceMsg(cmd: String): ToServiceMsg {
|
||||||
|
return ToServiceMsg("mobileqq.service", app.currentAccountUin, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendOidb(cmd: String, command: Int, service: Int, data: ByteArray, trpc: Boolean = false) {
|
||||||
|
val to = createToServiceMsg(cmd)
|
||||||
|
if (trpc) {
|
||||||
|
val oidb = TrpcOidb(
|
||||||
|
cmd = command,
|
||||||
|
service = service,
|
||||||
|
buffer = data,
|
||||||
|
flag = 1
|
||||||
|
)
|
||||||
|
to.putWupBuffer(oidb.toByteArray())
|
||||||
|
} else {
|
||||||
|
val oidb = oidb_sso.OIDBSSOPkg()
|
||||||
|
oidb.uint32_command.set(command)
|
||||||
|
oidb.uint32_service_type.set(service)
|
||||||
|
oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(data))
|
||||||
|
oidb.str_client_version.set(PlatformUtils.getClientVersion(MobileQQ.getContext()))
|
||||||
|
to.putWupBuffer(oidb.toByteArray())
|
||||||
|
}
|
||||||
|
to.addAttribute("req_pb_protocol_flag", true)
|
||||||
|
app.sendToService(to)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendBuffer(
|
||||||
|
cmd: String,
|
||||||
|
isProto: Boolean,
|
||||||
|
data: ByteArray,
|
||||||
|
) {
|
||||||
|
val toServiceMsg = createToServiceMsg(cmd)
|
||||||
|
toServiceMsg.putWupBuffer(data)
|
||||||
|
toServiceMsg.addAttribute("req_pb_protocol_flag", isProto)
|
||||||
|
sendToServiceMsg(toServiceMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
@DelicateCoroutinesApi
|
||||||
|
suspend fun sendBufferAW(
|
||||||
|
cmd: String,
|
||||||
|
isProto: Boolean,
|
||||||
|
data: ByteArray,
|
||||||
|
timeout: Duration = 30.seconds
|
||||||
|
): FromServiceMsg? {
|
||||||
|
val toServiceMsg = createToServiceMsg(cmd)
|
||||||
|
toServiceMsg.putWupBuffer(data)
|
||||||
|
toServiceMsg.addAttribute("req_pb_protocol_flag", isProto)
|
||||||
|
return sendToServiceMsgAW(toServiceMsg, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
@DelicateCoroutinesApi
|
||||||
|
suspend fun sendOidbAW(
|
||||||
|
cmd: String,
|
||||||
|
command: Int,
|
||||||
|
service: Int,
|
||||||
|
data: ByteArray,
|
||||||
|
trpc: Boolean = false,
|
||||||
|
timeout: Duration = 30.seconds
|
||||||
|
): FromServiceMsg? {
|
||||||
|
val to = createToServiceMsg(cmd)
|
||||||
|
if (trpc) {
|
||||||
|
val oidb = TrpcOidb(
|
||||||
|
cmd = command,
|
||||||
|
service = service,
|
||||||
|
buffer = data,
|
||||||
|
flag = 1
|
||||||
|
)
|
||||||
|
to.putWupBuffer(oidb.toByteArray())
|
||||||
|
} else {
|
||||||
|
val oidb = oidb_sso.OIDBSSOPkg()
|
||||||
|
oidb.uint32_command.set(command)
|
||||||
|
oidb.uint32_service_type.set(service)
|
||||||
|
oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(data))
|
||||||
|
oidb.str_client_version.set(PlatformUtils.getClientVersion(MobileQQ.getContext()))
|
||||||
|
to.putWupBuffer(oidb.toByteArray())
|
||||||
|
}
|
||||||
|
to.addAttribute("req_pb_protocol_flag", true)
|
||||||
|
return sendToServiceMsgAW(to, timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
190
xposed/src/main/java/qq/service/bdh/FileTransfer.kt
Normal file
190
xposed/src/main/java/qq/service/bdh/FileTransfer.kt
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
@file:OptIn(DelicateCoroutinesApi::class)
|
||||||
|
|
||||||
|
package qq.service.bdh
|
||||||
|
|
||||||
|
import com.tencent.mobileqq.transfile.BaseTransProcessor
|
||||||
|
import com.tencent.mobileqq.transfile.FileMsg
|
||||||
|
import com.tencent.mobileqq.transfile.TransferRequest
|
||||||
|
import com.tencent.mobileqq.transfile.api.ITransFileController
|
||||||
|
import com.tencent.mobileqq.utils.httputils.IHttpCommunicatorListener
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import moe.fuqiuluo.shamrock.utils.MD5
|
||||||
|
import mqq.app.AppRuntime
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
import java.io.File
|
||||||
|
import java.lang.Math.abs
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
internal abstract class FileTransfer {
|
||||||
|
suspend fun transC2CResource(
|
||||||
|
peerId: String,
|
||||||
|
file: File,
|
||||||
|
fileType: Int, busiType: Int,
|
||||||
|
wait: Boolean = true,
|
||||||
|
builder: (TransferRequest) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
val runtime = QQInterfaces.app
|
||||||
|
val transferRequest = TransferRequest()
|
||||||
|
transferRequest.needSendMsg = false
|
||||||
|
transferRequest.mSelfUin = runtime.account
|
||||||
|
transferRequest.mPeerUin = peerId
|
||||||
|
transferRequest.mSecondId = runtime.currentAccountUin
|
||||||
|
transferRequest.mUinType = FileMsg.UIN_BUDDY
|
||||||
|
transferRequest.mFileType = fileType
|
||||||
|
transferRequest.mUniseq = createMessageUniseq()
|
||||||
|
transferRequest.mIsUp = true
|
||||||
|
builder(transferRequest)
|
||||||
|
transferRequest.mBusiType = busiType
|
||||||
|
transferRequest.mMd5 = MD5.genFileMd5Hex(file.absolutePath)
|
||||||
|
transferRequest.mLocalPath = file.absolutePath
|
||||||
|
return transAndWait(runtime, transferRequest, wait)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun transTroopResource(
|
||||||
|
groupId: String,
|
||||||
|
file: File,
|
||||||
|
fileType: Int, busiType: Int,
|
||||||
|
wait: Boolean = true,
|
||||||
|
builder: (TransferRequest) -> Unit
|
||||||
|
): Boolean {
|
||||||
|
val runtime = QQInterfaces.app
|
||||||
|
val transferRequest = TransferRequest()
|
||||||
|
transferRequest.needSendMsg = false
|
||||||
|
transferRequest.mSelfUin = runtime.account
|
||||||
|
transferRequest.mPeerUin = groupId
|
||||||
|
transferRequest.mSecondId = runtime.currentAccountUin
|
||||||
|
transferRequest.mUinType = FileMsg.UIN_TROOP
|
||||||
|
transferRequest.mFileType = fileType
|
||||||
|
transferRequest.mUniseq = createMessageUniseq()
|
||||||
|
transferRequest.mIsUp = true
|
||||||
|
builder(transferRequest)
|
||||||
|
transferRequest.mBusiType = busiType
|
||||||
|
transferRequest.mMd5 = MD5.genFileMd5Hex(file.absolutePath)
|
||||||
|
transferRequest.mLocalPath = file.absolutePath
|
||||||
|
return transAndWait(runtime, transferRequest, wait)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun transAndWait(
|
||||||
|
runtime: AppRuntime,
|
||||||
|
transferRequest: TransferRequest,
|
||||||
|
wait: Boolean
|
||||||
|
): Boolean {
|
||||||
|
return withTimeoutOrNull(60_000) {
|
||||||
|
val service = runtime.getRuntimeService(ITransFileController::class.java, "all")
|
||||||
|
if(service.transferAsync(transferRequest)) {
|
||||||
|
if (!wait) { // 如果无需等待直接返回
|
||||||
|
return@withTimeoutOrNull true
|
||||||
|
}
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
GlobalScope.launch {
|
||||||
|
lateinit var processor: IHttpCommunicatorListener
|
||||||
|
while (
|
||||||
|
//service.findProcessor(
|
||||||
|
// transferRequest.keyForTransfer // uin + uniseq
|
||||||
|
//) != null
|
||||||
|
service.containsProcessor(runtime.currentAccountUin, transferRequest.mUniseq)
|
||||||
|
// 如果上传处理器依旧存在,说明没有上传成功
|
||||||
|
&& service.isWorking.get()
|
||||||
|
) {
|
||||||
|
processor = service.findProcessor(runtime.currentAccountUin, transferRequest.mUniseq)
|
||||||
|
delay(100)
|
||||||
|
}
|
||||||
|
if (processor is BaseTransProcessor && processor.file != null) {
|
||||||
|
val fileMsg = processor.file
|
||||||
|
LogCenter.log("[OldBDH] 资源上传结束(fileId = ${fileMsg.fileID}, fileKey = ${fileMsg.fileKey}, path = ${fileMsg.filePath})")
|
||||||
|
}
|
||||||
|
continuation.resume(true)
|
||||||
|
}
|
||||||
|
// 实现取消上传器
|
||||||
|
// 目前没什么用
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
continuation.resume(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else true
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_AIO_ALBUM_PIC = 1031
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_AIO_KEY_WORD_PIC = 1046
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_AIO_QZONE_PIC = 1045
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_ALBUM_PIC = 1007
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_BLESS = 1056
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_CAPTURE_PIC = 1008
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_COMMEN_FALSH_PIC = 1040
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_CUSTOM = 1006
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_DOUTU_PIC = 1044
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_FALSH_PIC = 1039
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_FAST_IMAGE = 1037
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_FORWARD_EDIT = 1048
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_FORWARD_PIC = 1009
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_FULL_SCREEN_ESSENCE = 1057
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_GALEERY_PIC = 1041
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_GAME_CENTER_STRATEGY = 1058
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_HOT_PIC = 1042
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_MIXED_PICS = 1043
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_PIC_AIO_ALBUM = 1052
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_PIC_CAMERA = 1050
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_PIC_FAV = 1053
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_PIC_SCREEN = 1027
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_PIC_SHARE = 1030
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_PIC_TAB_CAMERA = 1051
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_QQPINYIN_SEND_PIC = 1038
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_RECOMMENDED_STICKER = 1047
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_RELATED_EMOTION = 1054
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_SHOWLOVE = 1036
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_SOGOU_SEND_PIC = 1034
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_TROOP_BAR = 1035
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_WLAN_RECV_NOTIFY = 1055
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_ZHITU_PIC = 1049
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_ZPLAN_EMOTICON_GIF = 1060
|
||||||
|
const val SEND_MSG_BUSINESS_TYPE_ZPLAN_PIC = 1059
|
||||||
|
|
||||||
|
const val VIDEO_FORMAT_AFS = 7
|
||||||
|
const val VIDEO_FORMAT_AVI = 1
|
||||||
|
const val VIDEO_FORMAT_MKV = 4
|
||||||
|
const val VIDEO_FORMAT_MOD = 9
|
||||||
|
const val VIDEO_FORMAT_MOV = 8
|
||||||
|
const val VIDEO_FORMAT_MP4 = 2
|
||||||
|
const val VIDEO_FORMAT_MTS = 11
|
||||||
|
const val VIDEO_FORMAT_RM = 6
|
||||||
|
const val VIDEO_FORMAT_RMVB = 5
|
||||||
|
const val VIDEO_FORMAT_TS = 10
|
||||||
|
const val VIDEO_FORMAT_WMV = 3
|
||||||
|
|
||||||
|
const val BUSI_TYPE_GUILD_VIDEO = 4601
|
||||||
|
const val BUSI_TYPE_MULTI_FORWARD_VIDEO = 1010
|
||||||
|
const val BUSI_TYPE_PUBACCOUNT_PERM_VIDEO = 1009
|
||||||
|
const val BUSI_TYPE_PUBACCOUNT_TEMP_VIDEO = 1007
|
||||||
|
const val BUSI_TYPE_SHORT_VIDEO = 1
|
||||||
|
const val BUSI_TYPE_SHORT_VIDEO_PTV = 2
|
||||||
|
const val BUSI_TYPE_VIDEO = 0
|
||||||
|
const val BUSI_TYPE_VIDEO_EMOTICON_PIC = 1022
|
||||||
|
const val BUSI_TYPE_VIDEO_EMOTICON_VIDEO = 1021
|
||||||
|
|
||||||
|
const val TRANSFILE_TYPE_PIC = 1
|
||||||
|
const val TRANSFILE_TYPE_PIC_EMO = 65538
|
||||||
|
const val TRANSFILE_TYPE_PIC_THUMB = 65537
|
||||||
|
const val TRANSFILE_TYPE_PISMA = 49
|
||||||
|
const val TRANSFILE_TYPE_RAWPIC = 131075
|
||||||
|
|
||||||
|
const val TRANSFILE_TYPE_PROFILE_COVER = 35
|
||||||
|
const val TRANSFILE_TYPE_PTT = 2
|
||||||
|
const val TRANSFILE_TYPE_PTT_SLICE_TO_TEXT = 327696
|
||||||
|
const val TRANSFILE_TYPE_QQHEAD_PIC = 131074
|
||||||
|
|
||||||
|
internal fun createMessageUniseq(time: Long = System.currentTimeMillis()): Long {
|
||||||
|
var uniseq = (time / 1000).toInt().toLong()
|
||||||
|
uniseq = uniseq shl 32 or kotlin.math.abs(Random.nextInt()).toLong()
|
||||||
|
return uniseq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
495
xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt
Normal file
495
xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt
Normal file
@ -0,0 +1,495 @@
|
|||||||
|
package qq.service.bdh
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import androidx.exifinterface.media.ExifInterface
|
||||||
|
import com.tencent.mobileqq.qroute.QRoute
|
||||||
|
import com.tencent.qqnt.aio.adapter.api.IAIOPttApi
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.CommonFileInfo
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.PicElement
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.PttElement
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.QQNTWrapperUtil
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.VideoElement
|
||||||
|
import com.tencent.qqnt.msg.api.IMsgService
|
||||||
|
import kotlinx.atomicfu.atomic
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import moe.fuqiuluo.shamrock.config.ResourceGroup
|
||||||
|
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||||
|
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
|
||||||
|
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
||||||
|
import moe.fuqiuluo.shamrock.tools.slice
|
||||||
|
import moe.fuqiuluo.shamrock.utils.AudioUtils
|
||||||
|
import moe.fuqiuluo.shamrock.utils.FileUtils
|
||||||
|
import moe.fuqiuluo.shamrock.utils.MediaType
|
||||||
|
import moe.fuqiuluo.symbols.decodeProtobuf
|
||||||
|
import protobuf.auto.toByteArray
|
||||||
|
import protobuf.oidb.TrpcOidb
|
||||||
|
import protobuf.oidb.cmd0x11c5.ClientMeta
|
||||||
|
import protobuf.oidb.cmd0x11c5.CodecConfigReq
|
||||||
|
import protobuf.oidb.cmd0x11c5.CommonHead
|
||||||
|
import protobuf.oidb.cmd0x11c5.DownloadExt
|
||||||
|
import protobuf.oidb.cmd0x11c5.DownloadReq
|
||||||
|
import protobuf.oidb.cmd0x11c5.FileInfo
|
||||||
|
import protobuf.oidb.cmd0x11c5.FileType
|
||||||
|
import protobuf.oidb.cmd0x11c5.IndexNode
|
||||||
|
import protobuf.oidb.cmd0x11c5.MultiMediaReqHead
|
||||||
|
import protobuf.oidb.cmd0x11c5.NtV2RichMediaReq
|
||||||
|
import protobuf.oidb.cmd0x11c5.NtV2RichMediaRsp
|
||||||
|
import protobuf.oidb.cmd0x11c5.SceneInfo
|
||||||
|
import protobuf.oidb.cmd0x11c5.UploadInfo
|
||||||
|
import protobuf.oidb.cmd0x11c5.UploadReq
|
||||||
|
import protobuf.oidb.cmd0x11c5.UploadRsp
|
||||||
|
import protobuf.oidb.cmd0x11c5.VideoDownloadExt
|
||||||
|
import protobuf.oidb.cmd0x388.Cmd0x388ReqBody
|
||||||
|
import protobuf.oidb.cmd0x388.Cmd0x388RspBody
|
||||||
|
import protobuf.oidb.cmd0x388.TryUpImgReq
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
import qq.service.internals.NTServiceFetcher
|
||||||
|
import qq.service.internals.msgService
|
||||||
|
import qq.service.kernel.SimpleKernelMsgListener
|
||||||
|
import qq.service.msg.MessageHelper
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.random.Random
|
||||||
|
import kotlin.random.nextUInt
|
||||||
|
import kotlin.random.nextULong
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
internal object NtV2RichMediaSvc: QQInterfaces() {
|
||||||
|
private val requestIdSeq = atomic(1L)
|
||||||
|
|
||||||
|
fun fetchGroupResUploadTo(): String {
|
||||||
|
return ShamrockConfig[ResourceGroup].ifNullOrEmpty { "100000000" }!!
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun tryUploadResourceByNt(
|
||||||
|
chatType: Int,
|
||||||
|
elementType: Int,
|
||||||
|
resources: ArrayList<File>,
|
||||||
|
timeout: Duration,
|
||||||
|
retryCnt: Int = 5
|
||||||
|
): Result<MutableList<CommonFileInfo>> {
|
||||||
|
return internalTryUploadResourceByNt(chatType, elementType, resources, timeout).onFailure {
|
||||||
|
if (retryCnt > 0) {
|
||||||
|
return tryUploadResourceByNt(chatType, elementType, resources, timeout, retryCnt - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量上传图片
|
||||||
|
*/
|
||||||
|
private suspend fun internalTryUploadResourceByNt(
|
||||||
|
chatType: Int,
|
||||||
|
elementType: Int,
|
||||||
|
resources: ArrayList<File>,
|
||||||
|
timeout: Duration
|
||||||
|
): Result<MutableList<CommonFileInfo>> {
|
||||||
|
require(resources.size in 1 .. 10) { "imageFiles.size() must be in 1 .. 10" }
|
||||||
|
|
||||||
|
val messages = ArrayList(resources.map { file ->
|
||||||
|
val elem = MsgElement()
|
||||||
|
elem.elementType = elementType
|
||||||
|
when(elementType) {
|
||||||
|
MsgConstant.KELEMTYPEPIC -> {
|
||||||
|
val pic = PicElement()
|
||||||
|
pic.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath)
|
||||||
|
val msgService = NTServiceFetcher.kernelService.msgService!!
|
||||||
|
val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
|
||||||
|
RichMediaFilePathInfo(
|
||||||
|
2, 0, pic.md5HexStr, file.name, 1, 0, null, "", true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
|
||||||
|
originalPath
|
||||||
|
) != file.length()
|
||||||
|
) {
|
||||||
|
val thumbPath = msgService.getRichMediaFilePathForMobileQQSend(
|
||||||
|
RichMediaFilePathInfo(
|
||||||
|
2, 0, pic.md5HexStr, file.name, 2, 720, null, "", true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
|
||||||
|
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, thumbPath)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
)
|
||||||
|
if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) {
|
||||||
|
pic.picWidth = options.outWidth
|
||||||
|
pic.picHeight = options.outHeight
|
||||||
|
} else {
|
||||||
|
pic.picWidth = options.outHeight
|
||||||
|
pic.picHeight = options.outWidth
|
||||||
|
}
|
||||||
|
pic.sourcePath = file.absolutePath
|
||||||
|
pic.fileSize = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath)
|
||||||
|
pic.original = true
|
||||||
|
pic.picType = FileUtils.getPicType(file)
|
||||||
|
elem.picElement = pic
|
||||||
|
}
|
||||||
|
MsgConstant.KELEMTYPEPTT -> {
|
||||||
|
require(resources.size == 1) // 语音只能单个上传
|
||||||
|
var pttFile = file
|
||||||
|
val ptt = PttElement()
|
||||||
|
when (AudioUtils.getMediaType(pttFile)) {
|
||||||
|
MediaType.Silk -> {
|
||||||
|
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
|
||||||
|
ptt.duration = QRoute.api(IAIOPttApi::class.java)
|
||||||
|
.getPttFileDuration(pttFile.absolutePath)
|
||||||
|
}
|
||||||
|
MediaType.Amr -> {
|
||||||
|
ptt.duration = AudioUtils.getDurationSec(pttFile)
|
||||||
|
ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR
|
||||||
|
}
|
||||||
|
MediaType.Pcm -> {
|
||||||
|
val result = AudioUtils.pcmToSilk(pttFile)
|
||||||
|
ptt.duration = (result.second * 0.001).roundToInt()
|
||||||
|
pttFile = result.first
|
||||||
|
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val result = AudioUtils.audioToSilk(pttFile)
|
||||||
|
ptt.duration = runCatching {
|
||||||
|
QRoute.api(IAIOPttApi::class.java)
|
||||||
|
.getPttFileDuration(result.second.absolutePath)
|
||||||
|
}.getOrElse {
|
||||||
|
result.first
|
||||||
|
}
|
||||||
|
pttFile = result.second
|
||||||
|
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ptt.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(pttFile.absolutePath)
|
||||||
|
val msgService = NTServiceFetcher.kernelService.msgService!!
|
||||||
|
val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
|
||||||
|
RichMediaFilePathInfo(
|
||||||
|
MsgConstant.KELEMTYPEPTT, 0, ptt.md5HexStr, file.name, 1, 0, null, "", true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(originalPath) != pttFile.length()) {
|
||||||
|
QQNTWrapperUtil.CppProxy.copyFile(pttFile.absolutePath, originalPath)
|
||||||
|
}
|
||||||
|
if (originalPath != null) {
|
||||||
|
ptt.filePath = originalPath
|
||||||
|
} else {
|
||||||
|
ptt.filePath = pttFile.absolutePath
|
||||||
|
}
|
||||||
|
ptt.canConvert2Text = true
|
||||||
|
ptt.fileId = 0
|
||||||
|
ptt.fileUuid = ""
|
||||||
|
ptt.text = ""
|
||||||
|
ptt.voiceType = MsgConstant.KPTTVOICETYPESOUNDRECORD
|
||||||
|
ptt.voiceChangeType = MsgConstant.KPTTVOICECHANGETYPENONE
|
||||||
|
elem.pttElement = ptt
|
||||||
|
}
|
||||||
|
MsgConstant.KELEMTYPEVIDEO -> {
|
||||||
|
require(resources.size == 1) // 视频只能单个上传
|
||||||
|
val video = VideoElement()
|
||||||
|
video.videoMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath)
|
||||||
|
val msgService = NTServiceFetcher.kernelService.msgService!!
|
||||||
|
val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
|
||||||
|
RichMediaFilePathInfo(
|
||||||
|
5, 2, video.videoMd5, file.name, 1, 0, null, "", true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val thumbPath = msgService.getRichMediaFilePathForMobileQQSend(
|
||||||
|
RichMediaFilePathInfo(
|
||||||
|
5, 1, video.videoMd5, file.name, 2, 0, null, "", true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
|
||||||
|
originalPath
|
||||||
|
) != file.length()
|
||||||
|
) {
|
||||||
|
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
|
||||||
|
AudioUtils.obtainVideoCover(file.absolutePath, thumbPath!!)
|
||||||
|
}
|
||||||
|
video.fileTime = AudioUtils.getVideoTime(file)
|
||||||
|
video.fileSize = file.length()
|
||||||
|
video.fileName = file.name
|
||||||
|
video.fileFormat = FileTransfer.VIDEO_FORMAT_MP4
|
||||||
|
video.thumbSize = QQNTWrapperUtil.CppProxy.getFileSize(thumbPath).toInt()
|
||||||
|
val options = BitmapFactory.Options()
|
||||||
|
BitmapFactory.decodeFile(thumbPath, options)
|
||||||
|
video.thumbWidth = options.outWidth
|
||||||
|
video.thumbHeight = options.outHeight
|
||||||
|
video.thumbMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(thumbPath)
|
||||||
|
video.thumbPath = hashMapOf(0 to thumbPath)
|
||||||
|
elem.videoElement = video
|
||||||
|
}
|
||||||
|
else -> throw IllegalArgumentException("unsupported elementType: $elementType")
|
||||||
|
}
|
||||||
|
return@map elem
|
||||||
|
})
|
||||||
|
if (messages.isEmpty()) {
|
||||||
|
return Result.failure(Exception("no valid image files"))
|
||||||
|
}
|
||||||
|
val contact = when(chatType) {
|
||||||
|
MsgConstant.KCHATTYPEC2C -> MessageHelper.generateContact(chatType, app.currentAccountUin)
|
||||||
|
else -> Contact(chatType, fetchGroupResUploadTo(), null)
|
||||||
|
}
|
||||||
|
val result = mutableListOf<CommonFileInfo>()
|
||||||
|
val msgService = NTServiceFetcher.kernelService.msgService
|
||||||
|
?: return Result.failure(Exception("kernelService.msgService is null"))
|
||||||
|
withTimeoutOrNull(timeout) {
|
||||||
|
val uniseq = MessageHelper.generateMsgId(chatType)
|
||||||
|
suspendCancellableCoroutine {
|
||||||
|
val listener = object: SimpleKernelMsgListener() {
|
||||||
|
override fun onRichMediaUploadComplete(fileTransNotifyInfo: FileTransNotifyInfo) {
|
||||||
|
if (fileTransNotifyInfo.msgId == uniseq) {
|
||||||
|
result.add(fileTransNotifyInfo.commonFileInfo)
|
||||||
|
}
|
||||||
|
if (result.size == resources.size) {
|
||||||
|
msgService.removeMsgListener(this)
|
||||||
|
it.resume(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msgService.addMsgListener(listener)
|
||||||
|
|
||||||
|
QRoute.api(IMsgService::class.java).sendMsg(contact, uniseq, messages) { _, _ ->
|
||||||
|
if (contact.chatType == MsgConstant.KCHATTYPEGROUP && contact.peerUid == "100000000") {
|
||||||
|
val kernelService = NTServiceFetcher.kernelService
|
||||||
|
val sessionService = kernelService.wrapperSession
|
||||||
|
val service = sessionService.msgService
|
||||||
|
service.deleteMsg(contact, arrayListOf(uniseq), null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it.invokeOnCancellation {
|
||||||
|
msgService.removeMsgListener(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isEmpty()) {
|
||||||
|
return Result.failure(Exception("upload failed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.success(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取NT图片的RKEY
|
||||||
|
*/
|
||||||
|
suspend fun getNtPicRKey(
|
||||||
|
fileId: String,
|
||||||
|
md5: String,
|
||||||
|
sha: String,
|
||||||
|
fileSize: ULong,
|
||||||
|
width: UInt,
|
||||||
|
height: UInt,
|
||||||
|
sceneBuilder: suspend SceneInfo.() -> Unit
|
||||||
|
): Result<String> {
|
||||||
|
runCatching {
|
||||||
|
val req = NtV2RichMediaReq(
|
||||||
|
head = MultiMediaReqHead(
|
||||||
|
commonHead = CommonHead(
|
||||||
|
requestId = requestIdSeq.incrementAndGet().toULong(),
|
||||||
|
cmd = 200u
|
||||||
|
),
|
||||||
|
sceneInfo = SceneInfo(
|
||||||
|
requestType = 2u,
|
||||||
|
businessType = 1u,
|
||||||
|
).apply {
|
||||||
|
sceneBuilder()
|
||||||
|
},
|
||||||
|
clientMeta = ClientMeta(2u)
|
||||||
|
),
|
||||||
|
download = DownloadReq(
|
||||||
|
IndexNode(
|
||||||
|
FileInfo(
|
||||||
|
fileSize = fileSize,
|
||||||
|
md5 = md5.lowercase(),
|
||||||
|
sha1 = sha.lowercase(),
|
||||||
|
name = "${md5}.jpg",
|
||||||
|
fileType = FileType(
|
||||||
|
fileType = 1u,
|
||||||
|
picFormat = 1000u,
|
||||||
|
videoFormat = 0u,
|
||||||
|
voiceFormat = 0u
|
||||||
|
),
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
time = 0u,
|
||||||
|
original = 1u
|
||||||
|
),
|
||||||
|
fileUuid = fileId,
|
||||||
|
storeId = 1u,
|
||||||
|
uploadTime = 0u,
|
||||||
|
ttl = 0u,
|
||||||
|
subType = 0u,
|
||||||
|
storeAppId = 0u
|
||||||
|
),
|
||||||
|
DownloadExt(
|
||||||
|
video = VideoDownloadExt(
|
||||||
|
busiType = 0u,
|
||||||
|
subBusiType = 0u,
|
||||||
|
msgCodecConfig = CodecConfigReq(
|
||||||
|
platformChipinfo = "",
|
||||||
|
osVer = "",
|
||||||
|
deviceName = ""
|
||||||
|
),
|
||||||
|
flag = 1u
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).toByteArray()
|
||||||
|
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x11c5_200", 4549, 200, req, true)
|
||||||
|
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
||||||
|
return Result.failure(Exception("unable to get multimedia pic info: ${fromServiceMsg?.wupBuffer}"))
|
||||||
|
}
|
||||||
|
fromServiceMsg.wupBuffer.slice(4).decodeProtobuf<TrpcOidb>().buffer.decodeProtobuf<NtV2RichMediaRsp>().download?.rkeyParam?.let {
|
||||||
|
return Result.success(it)
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
return Result.failure(it)
|
||||||
|
}
|
||||||
|
return Result.failure(Exception("unable to get c2c nt pic"))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun requestUploadNtPic(
|
||||||
|
file: File,
|
||||||
|
md5: String,
|
||||||
|
sha: String,
|
||||||
|
name: String,
|
||||||
|
width: UInt,
|
||||||
|
height: UInt,
|
||||||
|
retryCnt: Int,
|
||||||
|
sceneBuilder: suspend SceneInfo.() -> Unit
|
||||||
|
): Result<UploadRsp> {
|
||||||
|
return runCatching {
|
||||||
|
requestUploadNtPic(file, md5, sha, name, width, height, sceneBuilder).getOrThrow()
|
||||||
|
}.onFailure {
|
||||||
|
if (retryCnt > 0) {
|
||||||
|
return requestUploadNtPic(file, md5, sha, name, width, height, retryCnt - 1, sceneBuilder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun requestUploadNtPic(
|
||||||
|
file: File,
|
||||||
|
md5: String,
|
||||||
|
sha: String,
|
||||||
|
name: String,
|
||||||
|
width: UInt,
|
||||||
|
height: UInt,
|
||||||
|
sceneBuilder: suspend SceneInfo.() -> Unit
|
||||||
|
): Result<UploadRsp> {
|
||||||
|
val req = NtV2RichMediaReq(
|
||||||
|
head = MultiMediaReqHead(
|
||||||
|
commonHead = CommonHead(
|
||||||
|
requestId = requestIdSeq.incrementAndGet().toULong(),
|
||||||
|
cmd = 100u
|
||||||
|
),
|
||||||
|
sceneInfo = SceneInfo(
|
||||||
|
requestType = 2u,
|
||||||
|
businessType = 1u,
|
||||||
|
).apply {
|
||||||
|
sceneBuilder()
|
||||||
|
},
|
||||||
|
clientMeta = ClientMeta(2u)
|
||||||
|
),
|
||||||
|
upload = UploadReq(
|
||||||
|
listOf(UploadInfo(
|
||||||
|
FileInfo(
|
||||||
|
fileSize = file.length().toULong(),
|
||||||
|
md5 = md5,
|
||||||
|
sha1 = sha,
|
||||||
|
name = name,
|
||||||
|
fileType = FileType(
|
||||||
|
fileType = 1u,
|
||||||
|
picFormat = 1000u,
|
||||||
|
videoFormat = 0u,
|
||||||
|
voiceFormat = 0u
|
||||||
|
),
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
time = 0u,
|
||||||
|
original = 1u
|
||||||
|
),
|
||||||
|
subFileType = 0u
|
||||||
|
)),
|
||||||
|
tryFastUploadCompleted = true,
|
||||||
|
srvSendMsg = false,
|
||||||
|
clientRandomId = Random.nextULong(),
|
||||||
|
compatQMsgSceneType = 1u,
|
||||||
|
clientSeq = Random.nextUInt(),
|
||||||
|
noNeedCompatMsg = false
|
||||||
|
)
|
||||||
|
).toByteArray()
|
||||||
|
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x11c5_100", 4549, 100, req, true, timeout = 3.seconds)
|
||||||
|
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
||||||
|
return Result.failure(Exception("unable to request upload nt pic"))
|
||||||
|
}
|
||||||
|
val rspBuffer = fromServiceMsg.wupBuffer.slice(4).decodeProtobuf<TrpcOidb>().buffer
|
||||||
|
val rsp = rspBuffer.decodeProtobuf<NtV2RichMediaRsp>()
|
||||||
|
if (rsp.upload == null) {
|
||||||
|
return Result.failure(Exception("unable to request upload nt pic: ${rsp.head}"))
|
||||||
|
}
|
||||||
|
return Result.success(rsp.upload!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用OldBDH获取图片上传状态以及图片上传服务器
|
||||||
|
*/
|
||||||
|
suspend fun requestUploadGroupPic(
|
||||||
|
groupId: ULong,
|
||||||
|
md5: String,
|
||||||
|
fileSize: ULong,
|
||||||
|
width: UInt,
|
||||||
|
height: UInt,
|
||||||
|
): Result<TryUpPicData> {
|
||||||
|
return runCatching {
|
||||||
|
val fromServiceMsg = sendBufferAW("ImgStore.GroupPicUp", true, Cmd0x388ReqBody(
|
||||||
|
netType = 3,
|
||||||
|
subCmd = 1,
|
||||||
|
msgTryUpImg = arrayListOf(
|
||||||
|
TryUpImgReq(
|
||||||
|
groupCode = groupId.toLong(),
|
||||||
|
srcUin = app.longAccountUin,
|
||||||
|
fileMd5 = md5.hex2ByteArray(),
|
||||||
|
fileSize = fileSize.toLong(),
|
||||||
|
fileName = "$md5.jpg",
|
||||||
|
srcTerm = 2,
|
||||||
|
platformType = 9,
|
||||||
|
buType = 212,
|
||||||
|
picWidth = width.toInt(),
|
||||||
|
picHeight = height.toInt(),
|
||||||
|
picType = 1000,
|
||||||
|
buildVer = "1.0.0",
|
||||||
|
originalPic = 1,
|
||||||
|
fileIndex = byteArrayOf(),
|
||||||
|
srvUpload = 0
|
||||||
|
)
|
||||||
|
),
|
||||||
|
).toByteArray())
|
||||||
|
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
||||||
|
return Result.failure(Exception("unable to request upload group pic"))
|
||||||
|
}
|
||||||
|
val rsp = fromServiceMsg.wupBuffer.slice(4).decodeProtobuf<Cmd0x388RspBody>()
|
||||||
|
.msgTryUpImgRsp!!.first()
|
||||||
|
TryUpPicData(
|
||||||
|
uKey = rsp.ukey,
|
||||||
|
exist = rsp.fileExist,
|
||||||
|
fileId = rsp.fileId.toULong(),
|
||||||
|
upIp = rsp.upIp,
|
||||||
|
upPort = rsp.upPort
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
58
xposed/src/main/java/qq/service/bdh/ResourceData.kt
Normal file
58
xposed/src/main/java/qq/service/bdh/ResourceData.kt
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package qq.service.bdh
|
||||||
|
|
||||||
|
import com.tencent.mobileqq.data.MessageRecord
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
internal enum class ContactType {
|
||||||
|
TROOP,
|
||||||
|
PRIVATE,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface TransTarget {
|
||||||
|
val id: String
|
||||||
|
val type: ContactType
|
||||||
|
|
||||||
|
val mRec: MessageRecord?
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Troop(
|
||||||
|
override val id: String,
|
||||||
|
override val mRec: MessageRecord? = null
|
||||||
|
): TransTarget {
|
||||||
|
override val type: ContactType = ContactType.TROOP
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Private(
|
||||||
|
override val id: String,
|
||||||
|
override val mRec: MessageRecord? = null
|
||||||
|
): TransTarget {
|
||||||
|
override val type: ContactType = ContactType.PRIVATE
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum class ResourceType {
|
||||||
|
Picture,
|
||||||
|
Video,
|
||||||
|
Voice
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface Resource {
|
||||||
|
val type: ResourceType
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class PictureResource(
|
||||||
|
val src: File
|
||||||
|
): Resource {
|
||||||
|
override val type = ResourceType.Picture
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class VideoResource(
|
||||||
|
val src: File, val thumb: File
|
||||||
|
): Resource {
|
||||||
|
override val type = ResourceType.Video
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class VoiceResource(
|
||||||
|
val src: File
|
||||||
|
): Resource {
|
||||||
|
override val type = ResourceType.Voice
|
||||||
|
}
|
429
xposed/src/main/java/qq/service/bdh/RichProtoSvc.kt
Normal file
429
xposed/src/main/java/qq/service/bdh/RichProtoSvc.kt
Normal file
@ -0,0 +1,429 @@
|
|||||||
|
@file:OptIn(ExperimentalSerializationApi::class)
|
||||||
|
package qq.service.bdh
|
||||||
|
|
||||||
|
import com.tencent.mobileqq.pb.ByteStringMicro
|
||||||
|
import com.tencent.mobileqq.transfile.FileMsg
|
||||||
|
import com.tencent.mobileqq.transfile.api.IProtoReqManager
|
||||||
|
import com.tencent.mobileqq.transfile.protohandler.RichProto
|
||||||
|
import com.tencent.mobileqq.transfile.protohandler.RichProtoProc
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import moe.fuqiuluo.shamrock.tools.slice
|
||||||
|
import moe.fuqiuluo.shamrock.tools.toHexString
|
||||||
|
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||||
|
import moe.fuqiuluo.symbols.decodeProtobuf
|
||||||
|
import mqq.app.MobileQQ
|
||||||
|
import protobuf.auto.toByteArray
|
||||||
|
import protobuf.oidb.cmd0x11c5.C2CUserInfo
|
||||||
|
import protobuf.oidb.cmd0x11c5.ChannelUserInfo
|
||||||
|
import protobuf.oidb.cmd0x11c5.GroupUserInfo
|
||||||
|
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo
|
||||||
|
import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq
|
||||||
|
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody
|
||||||
|
import protobuf.oidb.cmd0xfc2.Oidb0xfc2RspBody
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
import qq.service.contact.ContactHelper
|
||||||
|
import tencent.im.cs.cmd0x346.cmd0x346
|
||||||
|
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
|
||||||
|
import tencent.im.oidb.cmd0xe37.cmd0xe37
|
||||||
|
import tencent.im.oidb.oidb_sso
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
private const val GPRO_PIC = "gchat.qpic.cn"
|
||||||
|
private const val MULTIMEDIA_DOMAIN = "multimedia.nt.qq.com.cn"
|
||||||
|
private const val C2C_PIC = "c2cpicdw.qpic.cn"
|
||||||
|
|
||||||
|
internal object RichProtoSvc: QQInterfaces() {
|
||||||
|
suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String {
|
||||||
|
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, Oidb0xfc2ReqBody(
|
||||||
|
msgCmd = 1200,
|
||||||
|
msgBusType = 4202,
|
||||||
|
msgChannelInfo = Oidb0xfc2ChannelInfo(
|
||||||
|
guildId = peerId.toULong(),
|
||||||
|
channelId = channelId.toULong()
|
||||||
|
),
|
||||||
|
msgTerminalType = 2,
|
||||||
|
msgApplyDownloadReq = Oidb0xfc2MsgApplyDownloadReq(
|
||||||
|
fieldId = fileId,
|
||||||
|
supportEncrypt = 0
|
||||||
|
)
|
||||||
|
).toByteArray())
|
||||||
|
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
val body = oidb_sso.OIDBSSOPkg()
|
||||||
|
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
body.bytes_bodybuffer
|
||||||
|
.get().toByteArray()
|
||||||
|
.decodeProtobuf<Oidb0xfc2RspBody>()
|
||||||
|
.msgApplyDownloadRsp?.let {
|
||||||
|
it.msgDownloadInfo?.let {
|
||||||
|
return "https://${it.downloadDomain}${it.downloadUrl}&fname=$fileId&isthumb=0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getGroupFileDownUrl(
|
||||||
|
peerId: Long,
|
||||||
|
fileId: String,
|
||||||
|
bizId: Int = 102
|
||||||
|
): String {
|
||||||
|
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x6d6_2", 1750, 2, oidb_0x6d6.ReqBody().apply {
|
||||||
|
download_file_req.set(oidb_0x6d6.DownloadFileReqBody().apply {
|
||||||
|
uint64_group_code.set(peerId)
|
||||||
|
uint32_app_id.set(3)
|
||||||
|
uint32_bus_id.set(bizId)
|
||||||
|
str_file_id.set(fileId)
|
||||||
|
})
|
||||||
|
}.toByteArray())
|
||||||
|
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
val body = oidb_sso.OIDBSSOPkg()
|
||||||
|
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
val result = oidb_0x6d6.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
|
||||||
|
if (body.uint32_result.get() != 0
|
||||||
|
|| result.download_file_rsp.int32_ret_code.get() != 0) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val domain = if (!result.download_file_rsp.str_download_dns.has())
|
||||||
|
("https://" + result.download_file_rsp.str_download_ip.get())
|
||||||
|
else ("http://" + result.download_file_rsp.str_download_dns.get().toByteArray().decodeToString())
|
||||||
|
val downloadUrl = result.download_file_rsp.bytes_download_url.get().toByteArray().toHexString()
|
||||||
|
val appId = MobileQQ.getMobileQQ().appId
|
||||||
|
val version = PlatformUtils.getQQVersion(MobileQQ.getContext())
|
||||||
|
|
||||||
|
return "$domain/ftn_handler/$downloadUrl/?fname=$fileId&client_proto=qq&client_appid=$appId&client_type=android&client_ver=$version&client_down_type=auto&client_aio_type=unk"
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getC2CFileDownUrl(
|
||||||
|
fileId: String,
|
||||||
|
subId: String,
|
||||||
|
retryCnt: Int = 0
|
||||||
|
): String {
|
||||||
|
val fromServiceMsg = sendOidbAW("OidbSvc.0xe37_1200", 3639, 1200, cmd0xe37.Req0xe37().apply {
|
||||||
|
bytes_cmd_0x346_req_body.set(ByteStringMicro.copyFrom(cmd0x346.ReqBody().apply {
|
||||||
|
uint32_cmd.set(1200)
|
||||||
|
uint32_seq.set(1)
|
||||||
|
msg_apply_download_req.set(cmd0x346.ApplyDownloadReq().apply {
|
||||||
|
uint64_uin.set(app.longAccountUin)
|
||||||
|
bytes_uuid.set(ByteStringMicro.copyFrom(fileId.toByteArray()))
|
||||||
|
uint32_owner_type.set(2)
|
||||||
|
str_fileidcrc.set(subId)
|
||||||
|
|
||||||
|
})
|
||||||
|
uint32_business_id.set(3)
|
||||||
|
uint32_client_type.set(104)
|
||||||
|
uint32_flag_support_mediaplatform.set(1)
|
||||||
|
msg_extension_req.set(cmd0x346.ExtensionReq().apply {
|
||||||
|
uint32_download_url_type.set(1)
|
||||||
|
})
|
||||||
|
}.toByteArray()))
|
||||||
|
}.toByteArray())
|
||||||
|
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
||||||
|
if (retryCnt < 5) {
|
||||||
|
return getC2CFileDownUrl(fileId, subId, retryCnt + 1)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
val body = oidb_sso.OIDBSSOPkg()
|
||||||
|
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
val result = cmd0x346.RspBody().mergeFrom(cmd0xe37.Resp0xe37().mergeFrom(
|
||||||
|
body.bytes_bodybuffer.get().toByteArray()
|
||||||
|
).bytes_cmd_0x346_rsp_body.get().toByteArray())
|
||||||
|
if (body.uint32_result.get() != 0 ||
|
||||||
|
result.msg_apply_download_rsp.int32_ret_code.has() && result.msg_apply_download_rsp.int32_ret_code.get() != 0) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val oldData = result.msg_apply_download_rsp.msg_download_info
|
||||||
|
//val newData = result[14, 40] NTQQ 文件信息
|
||||||
|
|
||||||
|
val domain = if (oldData.str_download_dns.has()) ("https://" + oldData.str_download_dns.get()) else ("http://" + oldData.rpt_str_downloadip_list.get().first())
|
||||||
|
val params = oldData.str_download_url.get()
|
||||||
|
val appId = MobileQQ.getMobileQQ().appId
|
||||||
|
val version = PlatformUtils.getQQVersion(MobileQQ.getContext())
|
||||||
|
|
||||||
|
return "$domain$params&isthumb=0&client_proto=qq&client_appid=$appId&client_type=android&client_ver=$version&client_down_type=auto&client_aio_type=unk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getGroupPicDownUrl(
|
||||||
|
originalUrl: String,
|
||||||
|
md5: String,
|
||||||
|
peer: String = "",
|
||||||
|
fileId: String = "",
|
||||||
|
sha: String = "",
|
||||||
|
fileSize: ULong = 0uL,
|
||||||
|
width: UInt = 0u,
|
||||||
|
height: UInt = 0u
|
||||||
|
): String {
|
||||||
|
val isNtServer = originalUrl.startsWith("/download")
|
||||||
|
val domain = if (isNtServer) MULTIMEDIA_DOMAIN else GPRO_PIC
|
||||||
|
if (originalUrl.isNotEmpty()) {
|
||||||
|
if (isNtServer && !originalUrl.contains("rkey=")) {
|
||||||
|
NtV2RichMediaSvc.getNtPicRKey(
|
||||||
|
fileId = fileId,
|
||||||
|
md5 = md5,
|
||||||
|
sha = sha,
|
||||||
|
fileSize = fileSize,
|
||||||
|
width = width,
|
||||||
|
height = height
|
||||||
|
) {
|
||||||
|
sceneType = 2u
|
||||||
|
grp = GroupUserInfo(peer.toULong())
|
||||||
|
}.onSuccess {
|
||||||
|
return "https://$domain$originalUrl$it"
|
||||||
|
}.onFailure {
|
||||||
|
LogCenter.log("getGroupPicDownUrl: ${it.stackTraceToString()}", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "https://$domain$originalUrl"
|
||||||
|
}
|
||||||
|
return "https://$domain/gchatpic_new/0/0-0-${md5.uppercase()}/0?term=2"
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getC2CPicDownUrl(
|
||||||
|
originalUrl: String,
|
||||||
|
md5: String,
|
||||||
|
peer: String = "",
|
||||||
|
fileId: String = "",
|
||||||
|
sha: String = "",
|
||||||
|
fileSize: ULong = 0uL,
|
||||||
|
width: UInt = 0u,
|
||||||
|
height: UInt = 0u,
|
||||||
|
storeId: Int = 0
|
||||||
|
): String {
|
||||||
|
val isNtServer = storeId == 1 || originalUrl.startsWith("/download")
|
||||||
|
val domain = if (isNtServer) MULTIMEDIA_DOMAIN else C2C_PIC
|
||||||
|
if (originalUrl.isNotEmpty()) {
|
||||||
|
if (fileId.isNotEmpty()) NtV2RichMediaSvc.getNtPicRKey(
|
||||||
|
fileId = fileId,
|
||||||
|
md5 = md5,
|
||||||
|
sha = sha,
|
||||||
|
fileSize = fileSize,
|
||||||
|
width = width,
|
||||||
|
height = height
|
||||||
|
) {
|
||||||
|
sceneType = 1u
|
||||||
|
c2c = C2CUserInfo(
|
||||||
|
accountType = 2u,
|
||||||
|
uid = ContactHelper.getUidByUinAsync(peer.toLong())
|
||||||
|
)
|
||||||
|
}.onSuccess {
|
||||||
|
if (isNtServer && !originalUrl.contains("rkey=")) {
|
||||||
|
return "https://$domain$originalUrl$it"
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
LogCenter.log("getC2CPicDownUrl: ${it.stackTraceToString()}", Level.WARN)
|
||||||
|
}
|
||||||
|
if (isNtServer && !originalUrl.contains("rkey=")) {
|
||||||
|
return "https://$domain$originalUrl&rkey="
|
||||||
|
}
|
||||||
|
return "https://$domain$originalUrl"
|
||||||
|
}
|
||||||
|
return "https://$domain/offpic_new/0/0-0-${md5}/0?term=2"
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getGuildPicDownUrl(
|
||||||
|
originalUrl: String,
|
||||||
|
md5: String,
|
||||||
|
peer: String = "",
|
||||||
|
subPeer: String = "",
|
||||||
|
fileId: String = "",
|
||||||
|
sha: String = "",
|
||||||
|
fileSize: ULong = 0uL,
|
||||||
|
width: UInt = 0u,
|
||||||
|
height: UInt = 0u
|
||||||
|
): String {
|
||||||
|
val isNtServer = originalUrl.startsWith("/download")
|
||||||
|
val domain = if (isNtServer) MULTIMEDIA_DOMAIN else GPRO_PIC
|
||||||
|
if (originalUrl.isNotEmpty()) {
|
||||||
|
if (isNtServer && !originalUrl.contains("rkey=")) {
|
||||||
|
NtV2RichMediaSvc.getNtPicRKey(
|
||||||
|
fileId = fileId,
|
||||||
|
md5 = md5,
|
||||||
|
sha = sha,
|
||||||
|
fileSize = fileSize,
|
||||||
|
width = width,
|
||||||
|
height = height
|
||||||
|
) {
|
||||||
|
sceneType = 3u
|
||||||
|
channel = ChannelUserInfo(peer.toULong(), subPeer.toULong(), 1u)
|
||||||
|
}.onSuccess {
|
||||||
|
return "https://$domain$originalUrl$it"
|
||||||
|
}.onFailure {
|
||||||
|
LogCenter.log("getGuildPicDownUrl: ${it.stackTraceToString()}", Level.WARN)
|
||||||
|
}
|
||||||
|
return "https://$domain$originalUrl&rkey="
|
||||||
|
}
|
||||||
|
return "https://$domain$originalUrl"
|
||||||
|
}
|
||||||
|
return "https://$domain/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2"
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getC2CVideoDownUrl(
|
||||||
|
peerId: String,
|
||||||
|
md5: ByteArray,
|
||||||
|
fileUUId: String
|
||||||
|
): String {
|
||||||
|
return suspendCancellableCoroutine {
|
||||||
|
val richProtoReq = RichProto.RichProtoReq()
|
||||||
|
val downReq: RichProto.RichProtoReq.ShortVideoDownReq = RichProto.RichProtoReq.ShortVideoDownReq()
|
||||||
|
downReq.selfUin = app.currentAccountUin
|
||||||
|
downReq.peerUin = peerId
|
||||||
|
downReq.secondUin = peerId
|
||||||
|
downReq.uinType = FileMsg.UIN_BUDDY
|
||||||
|
downReq.agentType = 0
|
||||||
|
downReq.chatType = 1
|
||||||
|
downReq.troopUin = peerId
|
||||||
|
downReq.clientType = 2
|
||||||
|
downReq.fileId = fileUUId
|
||||||
|
downReq.md5 = md5
|
||||||
|
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
|
||||||
|
downReq.subBusiType = 0
|
||||||
|
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4
|
||||||
|
downReq.downType = 1
|
||||||
|
downReq.sceneType = 1
|
||||||
|
richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp ->
|
||||||
|
if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) {
|
||||||
|
LogCenter.log("requestDownPrivateVideo: ${resp.resps.firstOrNull()?.errCode}", Level.WARN)
|
||||||
|
it.resume("")
|
||||||
|
} else {
|
||||||
|
val videoDownResp = resp.resps.first() as RichProto.RichProtoResp.ShortVideoDownResp
|
||||||
|
val url = StringBuilder()
|
||||||
|
url.append(videoDownResp.mIpList.random().getServerUrl("http://"))
|
||||||
|
url.append(videoDownResp.mUrl)
|
||||||
|
it.resume(url.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
richProtoReq.protoKey = RichProtoProc.SHORT_VIDEO_DW
|
||||||
|
richProtoReq.reqs.add(downReq)
|
||||||
|
richProtoReq.protoReqMgr = app.getRuntimeService(IProtoReqManager::class.java, "all")
|
||||||
|
RichProtoProc.procRichProtoReq(richProtoReq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getGroupVideoDownUrl(
|
||||||
|
peerId: String,
|
||||||
|
md5: ByteArray,
|
||||||
|
fileUUId: String
|
||||||
|
): String {
|
||||||
|
return suspendCancellableCoroutine {
|
||||||
|
val richProtoReq = RichProto.RichProtoReq()
|
||||||
|
val downReq: RichProto.RichProtoReq.ShortVideoDownReq = RichProto.RichProtoReq.ShortVideoDownReq()
|
||||||
|
downReq.selfUin = app.currentAccountUin
|
||||||
|
downReq.peerUin = peerId
|
||||||
|
downReq.secondUin = peerId
|
||||||
|
downReq.uinType = FileMsg.UIN_TROOP
|
||||||
|
downReq.agentType = 0
|
||||||
|
downReq.chatType = 1
|
||||||
|
downReq.troopUin = peerId
|
||||||
|
downReq.clientType = 2
|
||||||
|
downReq.fileId = fileUUId
|
||||||
|
downReq.md5 = md5
|
||||||
|
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
|
||||||
|
downReq.subBusiType = 0
|
||||||
|
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4
|
||||||
|
downReq.downType = 1
|
||||||
|
downReq.sceneType = 1
|
||||||
|
richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp ->
|
||||||
|
if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) {
|
||||||
|
LogCenter.log("requestDownGroupVideo: ${resp.resps.firstOrNull()?.errCode}", Level.WARN)
|
||||||
|
it.resume("")
|
||||||
|
} else {
|
||||||
|
val videoDownResp = resp.resps.first() as RichProto.RichProtoResp.ShortVideoDownResp
|
||||||
|
val url = StringBuilder()
|
||||||
|
url.append(videoDownResp.mIpList.random().getServerUrl("http://"))
|
||||||
|
url.append(videoDownResp.mUrl)
|
||||||
|
it.resume(url.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
richProtoReq.protoKey = RichProtoProc.SHORT_VIDEO_DW
|
||||||
|
richProtoReq.reqs.add(downReq)
|
||||||
|
richProtoReq.protoReqMgr = app.getRuntimeService(IProtoReqManager::class.java, "all")
|
||||||
|
RichProtoProc.procRichProtoReq(richProtoReq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getC2CPttDownUrl(
|
||||||
|
peerId: String,
|
||||||
|
fileUUId: String
|
||||||
|
): String {
|
||||||
|
return suspendCancellableCoroutine {
|
||||||
|
val richProtoReq = RichProto.RichProtoReq()
|
||||||
|
val pttDownReq: RichProto.RichProtoReq.C2CPttDownReq = RichProto.RichProtoReq.C2CPttDownReq()
|
||||||
|
pttDownReq.selfUin = app.currentAccountUin
|
||||||
|
pttDownReq.peerUin = peerId
|
||||||
|
pttDownReq.secondUin = peerId
|
||||||
|
pttDownReq.uinType = FileMsg.UIN_BUDDY
|
||||||
|
pttDownReq.busiType = 1002
|
||||||
|
pttDownReq.uuid = fileUUId
|
||||||
|
pttDownReq.storageSource = "pttcenter"
|
||||||
|
pttDownReq.isSelfSend = false
|
||||||
|
|
||||||
|
pttDownReq.voiceType = 1
|
||||||
|
pttDownReq.downType = 1
|
||||||
|
richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp ->
|
||||||
|
if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) {
|
||||||
|
LogCenter.log("requestDownPrivateVoice: ${resp.resps.firstOrNull()?.errCode}", Level.WARN)
|
||||||
|
it.resume("")
|
||||||
|
} else {
|
||||||
|
val pttDownResp = resp.resps.first() as RichProto.RichProtoResp.C2CPttDownResp
|
||||||
|
val url = StringBuilder()
|
||||||
|
url.append(pttDownResp.downloadUrl)
|
||||||
|
url.append("&client_proto=qq&client_appid=${MobileQQ.getMobileQQ().appId}&client_type=android&client_ver=${PlatformUtils.getQQVersion(MobileQQ.getContext())}&client_down_type=auto&client_aio_type=unk")
|
||||||
|
it.resume(url.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
richProtoReq.protoKey = RichProtoProc.C2C_PTT_DW
|
||||||
|
richProtoReq.reqs.add(pttDownReq)
|
||||||
|
richProtoReq.protoReqMgr = app.getRuntimeService(IProtoReqManager::class.java, "all")
|
||||||
|
RichProtoProc.procRichProtoReq(richProtoReq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getGroupPttDownUrl(
|
||||||
|
peerId: String,
|
||||||
|
md5: ByteArray,
|
||||||
|
groupFileKey: String
|
||||||
|
): String {
|
||||||
|
return suspendCancellableCoroutine {
|
||||||
|
val richProtoReq = RichProto.RichProtoReq()
|
||||||
|
val groupPttDownReq: RichProto.RichProtoReq.GroupPttDownReq = RichProto.RichProtoReq.GroupPttDownReq()
|
||||||
|
groupPttDownReq.selfUin = app.currentAccountUin
|
||||||
|
groupPttDownReq.peerUin = peerId
|
||||||
|
groupPttDownReq.secondUin = peerId
|
||||||
|
groupPttDownReq.uinType = FileMsg.UIN_TROOP
|
||||||
|
groupPttDownReq.groupFileID = 0
|
||||||
|
groupPttDownReq.groupFileKey = groupFileKey
|
||||||
|
groupPttDownReq.md5 = md5
|
||||||
|
groupPttDownReq.voiceType = 1
|
||||||
|
groupPttDownReq.downType = 1
|
||||||
|
richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp ->
|
||||||
|
if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) {
|
||||||
|
LogCenter.log("requestDownGroupVoice: ${resp.resps.firstOrNull()?.errCode}", Level.WARN)
|
||||||
|
it.resume("")
|
||||||
|
} else {
|
||||||
|
val pttDownResp = resp.resps.first() as RichProto.RichProtoResp.GroupPttDownResp
|
||||||
|
val url = StringBuilder()
|
||||||
|
url.append("http://")
|
||||||
|
url.append(pttDownResp.domainV4V6)
|
||||||
|
url.append(pttDownResp.urlPath)
|
||||||
|
url.append("&client_proto=qq&client_appid=${MobileQQ.getMobileQQ().appId}&client_type=android&client_ver=${
|
||||||
|
PlatformUtils.getQQVersion(
|
||||||
|
MobileQQ.getContext())}&client_down_type=auto&client_aio_type=unk")
|
||||||
|
it.resume(url.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
richProtoReq.protoKey = RichProtoProc.GRP_PTT_DW
|
||||||
|
richProtoReq.reqs.add(groupPttDownReq)
|
||||||
|
richProtoReq.protoReqMgr = app.getRuntimeService(IProtoReqManager::class.java, "all")
|
||||||
|
RichProtoProc.procRichProtoReq(richProtoReq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
135
xposed/src/main/java/qq/service/bdh/Transfer.kt
Normal file
135
xposed/src/main/java/qq/service/bdh/Transfer.kt
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package qq.service.bdh
|
||||||
|
|
||||||
|
import com.tencent.mobileqq.data.MessageForShortVideo
|
||||||
|
import com.tencent.mobileqq.data.MessageRecord
|
||||||
|
import com.tencent.mobileqq.transfile.FileMsg
|
||||||
|
import com.tencent.mobileqq.transfile.TransferRequest
|
||||||
|
import moe.fuqiuluo.shamrock.utils.MD5
|
||||||
|
import qq.service.bdh.ResourceType.*
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
internal object Transfer: FileTransfer() {
|
||||||
|
private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>(
|
||||||
|
ContactType.TROOP to mapOf(
|
||||||
|
Picture to { uploadGroupPic(id, (it as PictureResource).src, mRec) },
|
||||||
|
Voice to { uploadGroupVoice(id, (it as VoiceResource).src) },
|
||||||
|
Video to { uploadGroupVideo(id, (it as VideoResource).src, it.thumb) },
|
||||||
|
|
||||||
|
),
|
||||||
|
ContactType.PRIVATE to mapOf(
|
||||||
|
Picture to { uploadC2CPic(id, (it as PictureResource).src, mRec) },
|
||||||
|
Voice to { uploadC2CVoice(id, (it as VoiceResource).src) },
|
||||||
|
Video to { uploadC2CVideo(id, (it as VideoResource).src, it.thumb) },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun uploadC2CVideo(
|
||||||
|
userId: String,
|
||||||
|
file: File,
|
||||||
|
thumb: File,
|
||||||
|
wait: Boolean = true
|
||||||
|
): Boolean {
|
||||||
|
return transC2CResource(userId, file, FileMsg.TRANSFILE_TYPE_SHORT_VIDEO_C2C, BUSI_TYPE_SHORT_VIDEO, wait) {
|
||||||
|
it.mSourceVideoCodecFormat = VIDEO_FORMAT_MP4
|
||||||
|
it.mRec = MessageForShortVideo().also {
|
||||||
|
it.busiType = BUSI_TYPE_SHORT_VIDEO
|
||||||
|
}
|
||||||
|
it.mThumbPath = thumb.absolutePath
|
||||||
|
it.mThumbMd5 = MD5.genFileMd5Hex(thumb.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun uploadGroupVideo(
|
||||||
|
groupId: String,
|
||||||
|
file: File,
|
||||||
|
thumb: File,
|
||||||
|
wait: Boolean = true
|
||||||
|
): Boolean {
|
||||||
|
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_SHORT_VIDEO_TROOP, BUSI_TYPE_SHORT_VIDEO, wait) {
|
||||||
|
it.mSourceVideoCodecFormat = VIDEO_FORMAT_MP4
|
||||||
|
it.mRec = MessageForShortVideo().also {
|
||||||
|
it.busiType = BUSI_TYPE_SHORT_VIDEO
|
||||||
|
}
|
||||||
|
it.mThumbPath = thumb.absolutePath
|
||||||
|
it.mThumbMd5 = MD5.genFileMd5Hex(thumb.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun uploadC2CVoice(
|
||||||
|
userId: String,
|
||||||
|
file: File,
|
||||||
|
wait: Boolean = true
|
||||||
|
): Boolean {
|
||||||
|
return transC2CResource(userId, file, FileMsg.TRANSFILE_TYPE_PTT, 1002, wait) {
|
||||||
|
it.mPttUploadPanel = 3
|
||||||
|
it.mPttCompressFinish = true
|
||||||
|
it.mIsPttPreSend = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun uploadGroupVoice(
|
||||||
|
groupId: String,
|
||||||
|
file: File,
|
||||||
|
wait: Boolean = true
|
||||||
|
): Boolean {
|
||||||
|
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PTT, 1002, wait) {
|
||||||
|
it.mPttUploadPanel = 3
|
||||||
|
it.mPttCompressFinish = true
|
||||||
|
it.mIsPttPreSend = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun uploadC2CPic(
|
||||||
|
peerId: String,
|
||||||
|
file: File,
|
||||||
|
record: MessageRecord? = null,
|
||||||
|
wait: Boolean = true
|
||||||
|
): Boolean {
|
||||||
|
return transC2CResource(peerId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
|
||||||
|
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
|
||||||
|
picUpExtraInfo.mIsRaw = false
|
||||||
|
picUpExtraInfo.mUinType = FileMsg.UIN_BUDDY
|
||||||
|
it.mPicSendSource = 8
|
||||||
|
it.mExtraObj = picUpExtraInfo
|
||||||
|
it.mIsPresend = true
|
||||||
|
it.delayShowProgressTimeInMs = 2000
|
||||||
|
it.mRec = record
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun uploadGroupPic(
|
||||||
|
groupId: String,
|
||||||
|
file: File,
|
||||||
|
record: MessageRecord? = null,
|
||||||
|
wait: Boolean = true
|
||||||
|
): Boolean {
|
||||||
|
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
|
||||||
|
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
|
||||||
|
picUpExtraInfo.mIsRaw = false
|
||||||
|
picUpExtraInfo.mUinType = FileMsg.UIN_TROOP
|
||||||
|
it.mPicSendSource = 8
|
||||||
|
it.delayShowProgressTimeInMs = 2000
|
||||||
|
it.mExtraObj = picUpExtraInfo
|
||||||
|
it.mRec = record
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun get(contactType: ContactType, resourceType: ResourceType): suspend TransTarget.(Resource) -> Boolean {
|
||||||
|
return (ROUTE[contactType] ?: error("unsupported contact type: $contactType"))[resourceType]
|
||||||
|
?: error("Unsupported resource type: $resourceType")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal suspend infix fun TransferTaskBuilder.trans(res: Resource): Boolean {
|
||||||
|
return Transfer[contact.type, res.type](contact, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class TransferTaskBuilder {
|
||||||
|
lateinit var contact: TransTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
internal infix fun Transfer.with(contact: TransTarget): TransferTaskBuilder {
|
||||||
|
return TransferTaskBuilder().also {
|
||||||
|
it.contact = contact
|
||||||
|
}
|
||||||
|
}
|
13
xposed/src/main/java/qq/service/bdh/TryUpPicData.kt
Normal file
13
xposed/src/main/java/qq/service/bdh/TryUpPicData.kt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package qq.service.bdh
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TryUpPicData(
|
||||||
|
@SerialName("ukey") val uKey: ByteArray,
|
||||||
|
@SerialName("exist") val exist: Boolean,
|
||||||
|
@SerialName("file_id") val fileId: ULong,
|
||||||
|
@SerialName("up_ip") var upIp: ArrayList<Long>? = null,
|
||||||
|
@SerialName("up_port") var upPort: ArrayList<Int>? = null,
|
||||||
|
)
|
21
xposed/src/main/java/qq/service/contact/ContactExt.kt
Normal file
21
xposed/src/main/java/qq/service/contact/ContactExt.kt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package qq.service.contact
|
||||||
|
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||||
|
import io.kritor.message.Scene
|
||||||
|
|
||||||
|
suspend fun Contact.longPeer(): Long {
|
||||||
|
return when(this.chatType) {
|
||||||
|
MsgConstant.KCHATTYPEGROUP -> peerUid.toLong()
|
||||||
|
MsgConstant.KCHATTYPETEMPC2CFROMGROUP, MsgConstant.KCHATTYPEC2C -> if (peerUid.startsWith("u_")) ContactHelper.getUinByUidAsync(peerUid).toLong() else peerUid.toLong()
|
||||||
|
else -> 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun io.kritor.message.Contact.longPeer(): Long {
|
||||||
|
return when(this.scene) {
|
||||||
|
Scene.GROUP -> peer.toLong()
|
||||||
|
Scene.FRIEND, Scene.STRANGER, Scene.STRANGER_FROM_GROUP -> if (peer.startsWith("u_")) ContactHelper.getUinByUidAsync(peer).toLong() else peer.toLong()
|
||||||
|
else -> 0L
|
||||||
|
}
|
||||||
|
}
|
211
xposed/src/main/java/qq/service/contact/ContactHelper.kt
Normal file
211
xposed/src/main/java/qq/service/contact/ContactHelper.kt
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
package qq.service.contact
|
||||||
|
|
||||||
|
import com.tencent.common.app.AppInterface
|
||||||
|
import com.tencent.mobileqq.data.Card
|
||||||
|
import com.tencent.mobileqq.profilecard.api.IProfileDataService
|
||||||
|
import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst.PARAM_SELF_UIN
|
||||||
|
import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst.PARAM_TARGET_UIN
|
||||||
|
import com.tencent.mobileqq.profilecard.api.IProfileProtocolService
|
||||||
|
import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver
|
||||||
|
import com.tencent.protofile.join_group_link.join_group_link
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import moe.fuqiuluo.shamrock.tools.slice
|
||||||
|
import qq.service.internals.NTServiceFetcher
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
import tencent.im.oidb.cmd0x11b2.oidb_0x11b2
|
||||||
|
import tencent.im.oidb.oidb_sso
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
internal object ContactHelper: QQInterfaces() {
|
||||||
|
const val FROM_C2C_AIO = 2
|
||||||
|
const val FROM_CONDITION_SEARCH = 9
|
||||||
|
const val FROM_CONTACTS_TAB = 5
|
||||||
|
const val FROM_FACE_2_FACE_ADD_FRIEND = 11
|
||||||
|
const val FROM_MAYKNOW_FRIEND = 3
|
||||||
|
const val FROM_QCIRCLE = 4
|
||||||
|
const val FROM_QQ_TROOP = 1
|
||||||
|
const val FROM_QZONE = 7
|
||||||
|
const val FROM_SCAN = 6
|
||||||
|
const val FROM_SEARCH = 8
|
||||||
|
const val FROM_SETTING_ME = 12
|
||||||
|
const val FROM_SHARE_CARD = 10
|
||||||
|
|
||||||
|
const val PROFILE_CARD_IS_BLACK = 2
|
||||||
|
const val PROFILE_CARD_IS_BLACKED = 1
|
||||||
|
const val PROFILE_CARD_NOT_BLACK = 3
|
||||||
|
|
||||||
|
const val SUB_FROM_C2C_AIO = 21
|
||||||
|
const val SUB_FROM_C2C_INTERACTIVE_LOGO = 25
|
||||||
|
const val SUB_FROM_C2C_LEFT_SLIDE = 23
|
||||||
|
const val SUB_FROM_C2C_OTHER = 24
|
||||||
|
const val SUB_FROM_C2C_SETTING = 22
|
||||||
|
const val SUB_FROM_C2C_TOFU = 26
|
||||||
|
const val SUB_FROM_CONDITION_SEARCH_OTHER = 99
|
||||||
|
const val SUB_FROM_CONDITION_SEARCH_RESULT = 91
|
||||||
|
const val SUB_FROM_CONTACTS_FRIEND_TAB = 51
|
||||||
|
const val SUB_FROM_CONTACTS_TAB = 55
|
||||||
|
const val SUB_FROM_FACE_2_FACE_ADD_FRIEND_RESULT_AVATAR = 111
|
||||||
|
const val SUB_FROM_FACE_2_FACE_OTHER = 119
|
||||||
|
const val SUB_FROM_FRIEND_APPLY = 56
|
||||||
|
const val SUB_FROM_FRIEND_NOTIFY_MORE = 57
|
||||||
|
const val SUB_FROM_FRIEND_NOTIFY_TAB = 54
|
||||||
|
const val SUB_FROM_GROUPING_TAB = 52
|
||||||
|
const val SUB_FROM_MAYKNOW_FRIEND_CONTACT_TAB = 31
|
||||||
|
const val SUB_FROM_MAYKNOW_FRIEND_CONTACT_TAB_MORE = 37
|
||||||
|
const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE = 34
|
||||||
|
const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE_MORE = 39
|
||||||
|
const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE_SEARCH = 36
|
||||||
|
const val SUB_FROM_MAYKNOW_FRIEND_NEW_FRIEND_PAGE = 32
|
||||||
|
const val SUB_FROM_MAYKNOW_FRIEND_OTHER = 35
|
||||||
|
const val SUB_FROM_MAYKNOW_FRIEND_SEARCH = 33
|
||||||
|
const val SUB_FROM_MAYKNOW_FRIEND_SEARCH_MORE = 38
|
||||||
|
const val SUB_FROM_PHONE_LIST_TAB = 53
|
||||||
|
const val SUB_FROM_QCIRCLE_OTHER = 42
|
||||||
|
const val SUB_FROM_QCIRCLE_PROFILE = 41
|
||||||
|
const val SUB_FROM_QQ_TROOP_ACTIVE_MEMBER = 15
|
||||||
|
const val SUB_FROM_QQ_TROOP_ADMIN = 16
|
||||||
|
const val SUB_FROM_QQ_TROOP_AIO = 11
|
||||||
|
const val SUB_FROM_QQ_TROOP_MEMBER = 12
|
||||||
|
const val SUB_FROM_QQ_TROOP_OTHER = 14
|
||||||
|
const val SUB_FROM_QQ_TROOP_SETTING_MEMBER_LIST = 17
|
||||||
|
const val SUB_FROM_QQ_TROOP_TEMP_SESSION = 13
|
||||||
|
const val SUB_FROM_QRCODE_SCAN_DRAWER = 64
|
||||||
|
const val SUB_FROM_QRCODE_SCAN_NEW = 61
|
||||||
|
const val SUB_FROM_QRCODE_SCAN_OLD = 62
|
||||||
|
const val SUB_FROM_QRCODE_SCAN_OTHER = 69
|
||||||
|
const val SUB_FROM_QRCODE_SCAN_PROFILE = 63
|
||||||
|
const val SUB_FROM_QZONE_HOME = 71
|
||||||
|
const val SUB_FROM_QZONE_OTHER = 79
|
||||||
|
const val SUB_FROM_SEARCH_CONTACT_TAB_MORE_FIND_PROFILE = 83
|
||||||
|
const val SUB_FROM_SEARCH_FIND_PROFILE_TAB = 82
|
||||||
|
const val SUB_FROM_SEARCH_MESSAGE_TAB_MORE_FIND_PROFILE = 84
|
||||||
|
const val SUB_FROM_SEARCH_NEW_FRIEND_MORE_FIND_PROFILE = 85
|
||||||
|
const val SUB_FROM_SEARCH_OTHER = 89
|
||||||
|
const val SUB_FROM_SEARCH_TAB = 81
|
||||||
|
const val SUB_FROM_SETTING_ME_AVATAR = 121
|
||||||
|
const val SUB_FROM_SETTING_ME_OTHER = 129
|
||||||
|
const val SUB_FROM_SHARE_CARD_C2C = 101
|
||||||
|
const val SUB_FROM_SHARE_CARD_OTHER = 109
|
||||||
|
const val SUB_FROM_SHARE_CARD_TROOP = 102
|
||||||
|
const val SUB_FROM_TYPE_DEFAULT = 0
|
||||||
|
|
||||||
|
private val refreshCardLock by lazy { Mutex() }
|
||||||
|
|
||||||
|
suspend fun voteUser(target: Long, count: Int): Result<Unit> {
|
||||||
|
if(count !in 1 .. 20) {
|
||||||
|
return Result.failure(IllegalArgumentException("vote count must be in 1 .. 20"))
|
||||||
|
}
|
||||||
|
val card = getProfileCard(target).onFailure {
|
||||||
|
return Result.failure(RuntimeException("unable to fetch contact info"))
|
||||||
|
}.getOrThrow()
|
||||||
|
sendExtra("VisitorSvc.ReqFavorite") {
|
||||||
|
it.putLong(PARAM_SELF_UIN, app.longAccountUin)
|
||||||
|
it.putLong(PARAM_TARGET_UIN, target)
|
||||||
|
it.putByteArray("vCookies", card.vCookies)
|
||||||
|
it.putBoolean("nearby_people", true)
|
||||||
|
it.putInt("favoriteSource", FROM_CONTACTS_TAB)
|
||||||
|
it.putInt("iCount", count)
|
||||||
|
it.putInt("from", FROM_CONTACTS_TAB)
|
||||||
|
}
|
||||||
|
return Result.success(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getProfileCard(uin: Long): Result<Card> {
|
||||||
|
return getProfileCardFromCache(uin).onFailure {
|
||||||
|
return refreshAndGetProfileCard(uin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getProfileCardFromCache(uin: Long): Result<Card> {
|
||||||
|
val profileDataService = app
|
||||||
|
.getRuntimeService(IProfileDataService::class.java, "all")
|
||||||
|
val card = profileDataService.getProfileCard(uin.toString(), true)
|
||||||
|
return if (card == null || card.strNick.isNullOrEmpty()) {
|
||||||
|
Result.failure(Exception("unable to fetch profile card"))
|
||||||
|
} else {
|
||||||
|
Result.success(card)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun refreshAndGetProfileCard(uin: Long): Result<Card> {
|
||||||
|
require(app is AppInterface)
|
||||||
|
val dataService = app
|
||||||
|
.getRuntimeService(IProfileDataService::class.java, "all")
|
||||||
|
val card = refreshCardLock.withLock {
|
||||||
|
suspendCancellableCoroutine {
|
||||||
|
app.addObserver(object: ProfileCardObserver() {
|
||||||
|
override fun onGetProfileCard(success: Boolean, obj: Any) {
|
||||||
|
app.removeObserver(this)
|
||||||
|
if (!success || obj !is Card) {
|
||||||
|
it.resume(null)
|
||||||
|
} else {
|
||||||
|
dataService.saveProfileCard(obj)
|
||||||
|
it.resume(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
app.getRuntimeService(IProfileProtocolService::class.java, "all")
|
||||||
|
.requestProfileCard(app.currentUin, uin.toString(), 12, 0L, 0.toByte(), 0L, 0L, null, "", 0L, 10004, null, 0.toByte())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (card == null || card.strNick.isNullOrEmpty()) {
|
||||||
|
Result.failure(Exception("unable to fetch profile card"))
|
||||||
|
} else {
|
||||||
|
Result.success(card)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getUinByUidAsync(uid: String): String {
|
||||||
|
if (uid.isBlank() || uid == "0") {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
val kernelService = NTServiceFetcher.kernelService
|
||||||
|
val sessionService = kernelService.wrapperSession
|
||||||
|
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
sessionService.uixConvertService.getUin(hashSetOf(uid)) {
|
||||||
|
continuation.resume(it)
|
||||||
|
}
|
||||||
|
}[uid]?.toString() ?: "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getUidByUinAsync(peerId: Long): String {
|
||||||
|
val kernelService = NTServiceFetcher.kernelService
|
||||||
|
val sessionService = kernelService.wrapperSession
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
sessionService.uixConvertService.getUid(hashSetOf(peerId)) {
|
||||||
|
continuation.resume(it)
|
||||||
|
}
|
||||||
|
}[peerId]!!
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getSharePrivateArkMsg(peerId: Long): String {
|
||||||
|
val reqBody = oidb_0x11b2.BusinessCardV3Req()
|
||||||
|
reqBody.uin.set(peerId)
|
||||||
|
reqBody.jump_url.set("mqqapi://card/show_pslcard?src_type=internal&source=sharecard&version=1&uin=$peerId")
|
||||||
|
|
||||||
|
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x11ca_0", 4790, 0, reqBody.toByteArray())
|
||||||
|
?: error("unable to fetch contact ark_json_text")
|
||||||
|
|
||||||
|
val body = oidb_sso.OIDBSSOPkg()
|
||||||
|
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
val rsp = oidb_0x11b2.BusinessCardV3Rsp()
|
||||||
|
rsp.mergeFrom(body.bytes_bodybuffer.get().toByteArray())
|
||||||
|
return rsp.signed_ark_msg.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getShareTroopArkMsg(groupId: Long): String {
|
||||||
|
val reqBody = join_group_link.ReqBody()
|
||||||
|
reqBody.get_ark.set(true)
|
||||||
|
reqBody.type.set(1)
|
||||||
|
reqBody.group_code.set(groupId)
|
||||||
|
val fromServiceMsg = sendBufferAW("GroupSvc.JoinGroupLink", true, reqBody.toByteArray())
|
||||||
|
?: error("unable to fetch contact ark_json_text")
|
||||||
|
val body = join_group_link.RspBody()
|
||||||
|
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
return body.signed_ark.get().toStringUtf8()
|
||||||
|
}
|
||||||
|
}
|
162
xposed/src/main/java/qq/service/file/GroupFileHelper.kt
Normal file
162
xposed/src/main/java/qq/service/file/GroupFileHelper.kt
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
@file:OptIn(ExperimentalStdlibApi::class)
|
||||||
|
|
||||||
|
package qq.service.file
|
||||||
|
|
||||||
|
import com.tencent.mobileqq.pb.ByteStringMicro
|
||||||
|
import io.grpc.Status
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
|
import io.kritor.file.File
|
||||||
|
import io.kritor.file.Folder
|
||||||
|
import io.kritor.file.GetFileSystemInfoResponse
|
||||||
|
import io.kritor.file.GetFilesRequest
|
||||||
|
import io.kritor.file.GetFilesResponse
|
||||||
|
import io.kritor.file.folder
|
||||||
|
import io.kritor.file.getFileSystemInfoResponse
|
||||||
|
import io.kritor.file.getFilesRequest
|
||||||
|
import io.kritor.file.getFilesResponse
|
||||||
|
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.slice
|
||||||
|
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
import tencent.im.oidb.cmd0x6d8.oidb_0x6d8
|
||||||
|
import tencent.im.oidb.oidb_sso
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
internal object GroupFileHelper: QQInterfaces() {
|
||||||
|
suspend fun getGroupFileSystemInfo(groupId: Long): GetFileSystemInfoResponse {
|
||||||
|
val fromServiceMsg = sendOidbAW("OidbSvc.0x6d8_1", 1752, 2, oidb_0x6d8.ReqBody().also {
|
||||||
|
it.group_file_cnt_req.set(oidb_0x6d8.GetFileCountReqBody().also {
|
||||||
|
it.uint64_group_code.set(groupId)
|
||||||
|
it.uint32_app_id.set(3)
|
||||||
|
it.uint32_bus_id.set(0)
|
||||||
|
})
|
||||||
|
}.toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||||
|
if (fromServiceMsg.wupBuffer == null) {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||||
|
}
|
||||||
|
val fileCnt: Int
|
||||||
|
val limitCnt: Int
|
||||||
|
if (fromServiceMsg.wupBuffer != null) {
|
||||||
|
oidb_0x6d8.RspBody().mergeFrom(
|
||||||
|
oidb_sso.OIDBSSOPkg()
|
||||||
|
.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
.bytes_bodybuffer.get()
|
||||||
|
.toByteArray()
|
||||||
|
).group_file_cnt_rsp.apply {
|
||||||
|
fileCnt = uint32_all_file_count.get()
|
||||||
|
limitCnt = uint32_limit_count.get()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val fromServiceMsg2 = sendOidbAW("OidbSvc.0x6d8_1", 1752, 3, oidb_0x6d8.ReqBody().also {
|
||||||
|
it.group_space_req.set(oidb_0x6d8.GetSpaceReqBody().apply {
|
||||||
|
uint64_group_code.set(groupId)
|
||||||
|
uint32_app_id.set(3)
|
||||||
|
})
|
||||||
|
}.toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||||
|
val totalSpace: Long
|
||||||
|
val usedSpace: Long
|
||||||
|
if (fromServiceMsg2.isSuccess && fromServiceMsg2.wupBuffer != null) {
|
||||||
|
oidb_0x6d8.RspBody().mergeFrom(
|
||||||
|
oidb_sso.OIDBSSOPkg()
|
||||||
|
.mergeFrom(fromServiceMsg2.wupBuffer.slice(4))
|
||||||
|
.bytes_bodybuffer.get()
|
||||||
|
.toByteArray()).group_space_rsp.apply {
|
||||||
|
totalSpace = uint64_total_space.get()
|
||||||
|
usedSpace = uint64_used_space.get()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response x2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFileSystemInfoResponse {
|
||||||
|
this.fileCount = fileCnt
|
||||||
|
this.totalCount = limitCnt
|
||||||
|
this.totalSpace = totalSpace.toInt()
|
||||||
|
this.usedSpace = usedSpace.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getGroupFiles(groupId: Long, folderId: String = "/"): GetFilesResponse {
|
||||||
|
val fileSystemInfo = getGroupFileSystemInfo(groupId)
|
||||||
|
val fromServiceMsg = sendOidbAW("OidbSvc.0x6d8_1", 1752, 1, oidb_0x6d8.ReqBody().also {
|
||||||
|
it.file_list_info_req.set(oidb_0x6d8.GetFileListReqBody().apply {
|
||||||
|
uint64_group_code.set(groupId)
|
||||||
|
uint32_app_id.set(3)
|
||||||
|
str_folder_id.set(folderId)
|
||||||
|
|
||||||
|
uint32_file_count.set(fileSystemInfo.fileCount)
|
||||||
|
uint32_all_file_count.set(0)
|
||||||
|
uint32_req_from.set(3)
|
||||||
|
uint32_sort_by.set(oidb_0x6d8.GetFileListReqBody.SORT_BY_TIMESTAMP)
|
||||||
|
|
||||||
|
uint32_filter_code.set(0)
|
||||||
|
uint64_uin.set(0)
|
||||||
|
|
||||||
|
uint32_start_index.set(0)
|
||||||
|
|
||||||
|
bytes_context.set(ByteStringMicro.copyFrom(EMPTY_BYTE_ARRAY))
|
||||||
|
|
||||||
|
uint32_show_onlinedoc_folder.set(0)
|
||||||
|
})
|
||||||
|
}.toByteArray(), timeout = 15.seconds) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||||
|
if (fromServiceMsg.wupBuffer == null) {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||||
|
}
|
||||||
|
val files = arrayListOf<File>()
|
||||||
|
val dirs = arrayListOf<Folder>()
|
||||||
|
if (fromServiceMsg.wupBuffer != null) {
|
||||||
|
val oidb = oidb_sso.OIDBSSOPkg().mergeFrom(fromServiceMsg.wupBuffer.slice(4).let {
|
||||||
|
if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it
|
||||||
|
})
|
||||||
|
|
||||||
|
oidb_0x6d8.RspBody().mergeFrom(oidb.bytes_bodybuffer.get().toByteArray())
|
||||||
|
.file_list_info_rsp.apply {
|
||||||
|
rpt_item_list.get().forEach { file ->
|
||||||
|
if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FILE) {
|
||||||
|
val fileInfo = file.file_info
|
||||||
|
files.add(io.kritor.file.file {
|
||||||
|
this.fileId = fileInfo.str_file_id.get()
|
||||||
|
this.fileName = fileInfo.str_file_name.get()
|
||||||
|
this.fileSize = fileInfo.uint64_file_size.get()
|
||||||
|
this.busId = fileInfo.uint32_bus_id.get()
|
||||||
|
this.uploadTime = fileInfo.uint32_upload_time.get()
|
||||||
|
this.deadTime = fileInfo.uint32_dead_time.get()
|
||||||
|
this.modifyTime = fileInfo.uint32_modify_time.get()
|
||||||
|
this.downloadTimes = fileInfo.uint32_download_times.get()
|
||||||
|
this.uploader = fileInfo.uint64_uploader_uin.get()
|
||||||
|
this.uploaderName = fileInfo.str_uploader_name.get()
|
||||||
|
this.sha = fileInfo.bytes_sha.get().toByteArray().toHexString()
|
||||||
|
this.sha3 = fileInfo.bytes_sha3.get().toByteArray().toHexString()
|
||||||
|
this.md5 = fileInfo.bytes_md5.get().toByteArray().toHexString()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FOLDER) {
|
||||||
|
val folderInfo = file.folder_info
|
||||||
|
dirs.add(folder {
|
||||||
|
this.folderId = folderInfo.str_folder_id.get()
|
||||||
|
this.folderName = folderInfo.str_folder_name.get()
|
||||||
|
this.totalFileCount = folderInfo.uint32_total_file_count.get()
|
||||||
|
this.createTime = folderInfo.uint32_create_time.get()
|
||||||
|
this.creator = folderInfo.uint64_create_uin.get()
|
||||||
|
this.creatorName = folderInfo.str_creator_name.get()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
LogCenter.log("未知文件类型: ${file.uint32_type.get()}", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFilesResponse {
|
||||||
|
this.files.addAll(files)
|
||||||
|
this.folders.addAll(folders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
107
xposed/src/main/java/qq/service/friend/FriendHelper.kt
Normal file
107
xposed/src/main/java/qq/service/friend/FriendHelper.kt
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package qq.service.friend
|
||||||
|
|
||||||
|
import com.tencent.mobileqq.data.Friends
|
||||||
|
import com.tencent.mobileqq.friend.api.IFriendDataService
|
||||||
|
import com.tencent.mobileqq.friend.api.IFriendHandlerService
|
||||||
|
import com.tencent.mobileqq.qroute.QRoute
|
||||||
|
import com.tencent.mobileqq.relation.api.IAddFriendTempApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import moe.fuqiuluo.shamrock.tools.slice
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
import tencent.mobileim.structmsg.structmsg
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
internal object FriendHelper: QQInterfaces() {
|
||||||
|
suspend fun getFriendList(refresh: Boolean): Result<List<Friends>> {
|
||||||
|
val service = app.getRuntimeService(IFriendDataService::class.java, "all")
|
||||||
|
if(refresh || !service.isInitFinished) {
|
||||||
|
if(!requestFriendList(service)) {
|
||||||
|
return Result.failure(Exception("获取好友列表失败"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result.success(service.allFriends)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProfileService.Pb.ReqSystemMsgAction.Friend
|
||||||
|
fun requestFriendRequest(msgSeq: Long, uin: Long, remark: String = "", approve: Boolean? = true, notSee: Boolean? = false) {
|
||||||
|
val service = QRoute.api(IAddFriendTempApi::class.java)
|
||||||
|
val action = structmsg.SystemMsgActionInfo()
|
||||||
|
action.type.set(if (approve != false) 2 else 3)
|
||||||
|
action.group_id.set(0)
|
||||||
|
action.remark.set(remark)
|
||||||
|
val snInfo = structmsg.AddFrdSNInfo()
|
||||||
|
snInfo.uint32_not_see_dynamic.set(if (notSee != false) 1 else 0)
|
||||||
|
snInfo.uint32_set_sn.set(0)
|
||||||
|
action.addFrdSNInfo.set(snInfo)
|
||||||
|
service.sendFriendSystemMsgAction(1, msgSeq, uin, 1, 2004, 11, 0, action, 0, structmsg.StructMsg(), false, app)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun requestFriendSystemMsgNew(msgNum: Int, latestFriendSeq: Long = 0, latestGroupSeq: Long = 0, retryCnt: Int = 3): List<structmsg.StructMsg>? {
|
||||||
|
if (retryCnt < 0) {
|
||||||
|
return ArrayList()
|
||||||
|
}
|
||||||
|
val req = structmsg.ReqSystemMsgNew()
|
||||||
|
req.msg_num.set(msgNum)
|
||||||
|
req.latest_friend_seq.set(latestFriendSeq)
|
||||||
|
req.latest_group_seq.set(latestGroupSeq)
|
||||||
|
req.version.set(1000)
|
||||||
|
req.checktype.set(2)
|
||||||
|
val flag = structmsg.FlagInfo()
|
||||||
|
// flag.GrpMsg_Kick_Admin.set(1)
|
||||||
|
// flag.GrpMsg_HiddenGrp.set(1)
|
||||||
|
// flag.GrpMsg_WordingDown.set(1)
|
||||||
|
flag.FrdMsg_GetBusiCard.set(1)
|
||||||
|
// flag.GrpMsg_GetOfficialAccount.set(1)
|
||||||
|
// flag.GrpMsg_GetPayInGroup.set(1)
|
||||||
|
flag.FrdMsg_Discuss2ManyChat.set(1)
|
||||||
|
// flag.GrpMsg_NotAllowJoinGrp_InviteNotFrd.set(1)
|
||||||
|
flag.FrdMsg_NeedWaitingMsg.set(1)
|
||||||
|
flag.FrdMsg_uint32_need_all_unread_msg.set(1)
|
||||||
|
// flag.GrpMsg_NeedAutoAdminWording.set(1)
|
||||||
|
// flag.GrpMsg_get_transfer_group_msg_flag.set(1)
|
||||||
|
// flag.GrpMsg_get_quit_pay_group_msg_flag.set(1)
|
||||||
|
// flag.GrpMsg_support_invite_auto_join.set(1)
|
||||||
|
// flag.GrpMsg_mask_invite_auto_join.set(1)
|
||||||
|
// flag.GrpMsg_GetDisbandedByAdmin.set(1)
|
||||||
|
flag.GrpMsg_GetC2cInviteJoinGroup.set(1)
|
||||||
|
req.flag.set(flag)
|
||||||
|
req.is_get_frd_ribbon.set(false)
|
||||||
|
req.is_get_grp_ribbon.set(false)
|
||||||
|
req.friend_msg_type_flag.set(1)
|
||||||
|
req.uint32_req_msg_type.set(1)
|
||||||
|
req.uint32_need_uid.set(1)
|
||||||
|
val fromServiceMsg = sendBufferAW("ProfileService.Pb.ReqSystemMsgNew.Friend", true, req.toByteArray())
|
||||||
|
return if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
||||||
|
ArrayList()
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
val msg = structmsg.RspSystemMsgNew()
|
||||||
|
msg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
return msg.friendmsgs.get()
|
||||||
|
} catch (err: Throwable) {
|
||||||
|
requestFriendSystemMsgNew(msgNum, latestFriendSeq, latestGroupSeq, retryCnt - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun requestFriendList(dataService: IFriendDataService): Boolean {
|
||||||
|
val service = app.getRuntimeService(IFriendHandlerService::class.java, "all")
|
||||||
|
service.requestFriendList(true, 0)
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
val waiter = GlobalScope.launch {
|
||||||
|
while (!dataService.isInitFinished) {
|
||||||
|
delay(200)
|
||||||
|
}
|
||||||
|
continuation.resume(true)
|
||||||
|
}
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
waiter.cancel()
|
||||||
|
continuation.resume(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
772
xposed/src/main/java/qq/service/group/GroupHelper.kt
Normal file
772
xposed/src/main/java/qq/service/group/GroupHelper.kt
Normal file
@ -0,0 +1,772 @@
|
|||||||
|
package qq.service.group
|
||||||
|
|
||||||
|
import KQQ.RespBatchProcess
|
||||||
|
import com.qq.jce.wup.UniPacket
|
||||||
|
import com.tencent.mobileqq.app.BusinessHandlerFactory
|
||||||
|
import com.tencent.mobileqq.data.troop.TroopInfo
|
||||||
|
import com.tencent.mobileqq.data.troop.TroopMemberInfo
|
||||||
|
import com.tencent.mobileqq.pb.ByteStringMicro
|
||||||
|
import com.tencent.mobileqq.troop.api.ITroopInfoService
|
||||||
|
import com.tencent.mobileqq.troop.api.ITroopMemberInfoService
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.MemberInfo
|
||||||
|
import friendlist.stUinInfo
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import qq.service.internals.NTServiceFetcher
|
||||||
|
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
||||||
|
import moe.fuqiuluo.shamrock.tools.putBuf32Long
|
||||||
|
import moe.fuqiuluo.shamrock.tools.slice
|
||||||
|
import protobuf.auto.toByteArray
|
||||||
|
import protobuf.oidb.cmd0xf16.Oidb0xf16
|
||||||
|
import protobuf.oidb.cmd0xf16.SetGroupRemarkReq
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
import tencent.im.group.group_member_info
|
||||||
|
import tencent.im.oidb.cmd0x88d.oidb_0x88d
|
||||||
|
import tencent.im.oidb.cmd0x899.oidb_0x899
|
||||||
|
import tencent.im.oidb.cmd0x89a.oidb_0x89a
|
||||||
|
import tencent.im.oidb.cmd0x8a0.oidb_0x8a0
|
||||||
|
import tencent.im.oidb.cmd0x8a7.cmd0x8a7
|
||||||
|
import tencent.im.oidb.cmd0x8fc.Oidb_0x8fc
|
||||||
|
import tencent.im.oidb.cmd0xed3.oidb_cmd0xed3
|
||||||
|
import tencent.im.oidb.oidb_sso
|
||||||
|
import tencent.im.troop.honor.troop_honor
|
||||||
|
import tencent.mobileim.structmsg.structmsg
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
internal object GroupHelper: QQInterfaces() {
|
||||||
|
private val RefreshTroopMemberInfoLock by lazy { Mutex() }
|
||||||
|
private val RefreshTroopMemberListLock by lazy { Mutex() }
|
||||||
|
|
||||||
|
private lateinit var METHOD_REQ_MEMBER_INFO: Method
|
||||||
|
private lateinit var METHOD_REQ_MEMBER_INFO_V2: Method
|
||||||
|
private lateinit var METHOD_REQ_TROOP_LIST: Method
|
||||||
|
private lateinit var METHOD_REQ_TROOP_MEM_LIST: Method
|
||||||
|
private lateinit var METHOD_REQ_MODIFY_GROUP_NAME: Method
|
||||||
|
|
||||||
|
fun getGroupInfo(groupId: String): TroopInfo {
|
||||||
|
val service = app
|
||||||
|
.getRuntimeService(ITroopInfoService::class.java, "all")
|
||||||
|
|
||||||
|
return service.getTroopInfo(groupId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isAdmin(groupId: String): Boolean {
|
||||||
|
val groupInfo = getGroupInfo(groupId)
|
||||||
|
|
||||||
|
return groupInfo.isAdmin || groupInfo.troopowneruin == app.account
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isOwner(groupId: String): Boolean {
|
||||||
|
val groupInfo = getGroupInfo(groupId)
|
||||||
|
return groupInfo.troopowneruin == app.account
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAdminList(
|
||||||
|
groupId: Long,
|
||||||
|
withOwner: Boolean = false
|
||||||
|
): List<Long> {
|
||||||
|
val groupInfo = getGroupInfo(groupId.toString())
|
||||||
|
return (groupInfo.Administrator ?: "")
|
||||||
|
.split("|", ",")
|
||||||
|
.also {
|
||||||
|
if (withOwner && it is ArrayList<String>) {
|
||||||
|
it.add(0, groupInfo.troopowneruin)
|
||||||
|
}
|
||||||
|
}.mapNotNull { it.ifNullOrEmpty { null }?.toLong() }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getGroupList(refresh: Boolean): Result<List<TroopInfo>> {
|
||||||
|
val service = app.getRuntimeService(ITroopInfoService::class.java, "all")
|
||||||
|
|
||||||
|
var troopList = service.allTroopList
|
||||||
|
if(refresh || !service.isTroopCacheInited || troopList == null) {
|
||||||
|
if(!requestGroupInfo(service)) {
|
||||||
|
return Result.failure(Exception("获取群列表失败"))
|
||||||
|
} else {
|
||||||
|
troopList = service.allTroopList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result.success(troopList)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getTroopMemberInfoByUinV2(
|
||||||
|
groupId: String,
|
||||||
|
uin: String,
|
||||||
|
refresh: Boolean = false
|
||||||
|
): Result<TroopMemberInfo> {
|
||||||
|
val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all")
|
||||||
|
var info = service.getTroopMember(groupId, uin)
|
||||||
|
if (refresh || !service.isMemberInCache(groupId, uin) || info == null || info.troopnick == null) {
|
||||||
|
info = requestTroopMemberInfo(service, groupId, uin, timeout = 2000).getOrNull()
|
||||||
|
}
|
||||||
|
if (info == null) {
|
||||||
|
info = getTroopMemberInfoByUinViaNt(groupId, uin, timeout = 2000L).getOrNull()?.let {
|
||||||
|
TroopMemberInfo().apply {
|
||||||
|
troopnick = it.cardName
|
||||||
|
friendnick = it.nick
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (info != null && (info.alias == null || info.alias.isBlank())) {
|
||||||
|
val req = group_member_info.ReqBody()
|
||||||
|
req.uint64_group_code.set(groupId.toLong())
|
||||||
|
req.uint64_uin.set(uin.toLong())
|
||||||
|
req.bool_new_client.set(true)
|
||||||
|
req.uint32_client_type.set(1)
|
||||||
|
req.uint32_rich_card_name_ver.set(1)
|
||||||
|
val fromServiceMsg = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray(), timeout = 2.seconds)
|
||||||
|
if (fromServiceMsg != null && fromServiceMsg.wupBuffer != null) {
|
||||||
|
val rsp = group_member_info.RspBody()
|
||||||
|
rsp.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
if (rsp.msg_meminfo.str_location.has()) {
|
||||||
|
info.alias = rsp.msg_meminfo.str_location.get().toStringUtf8()
|
||||||
|
}
|
||||||
|
if (rsp.msg_meminfo.uint32_age.has()) {
|
||||||
|
info.age = rsp.msg_meminfo.uint32_age.get().toByte()
|
||||||
|
}
|
||||||
|
if (rsp.msg_meminfo.bytes_group_honor.has()) {
|
||||||
|
val honorBytes = rsp.msg_meminfo.bytes_group_honor.get().toByteArray()
|
||||||
|
val honor = troop_honor.GroupUserCardHonor()
|
||||||
|
honor.mergeFrom(honorBytes)
|
||||||
|
info.level = honor.level.get()
|
||||||
|
// 10315: medal_id not real group level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: Throwable) {
|
||||||
|
LogCenter.log(err.stackTraceToString(), Level.WARN)
|
||||||
|
}
|
||||||
|
return if (info != null) {
|
||||||
|
Result.success(info)
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("获取群成员信息失败"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun requestGroupInfo(
|
||||||
|
service: ITroopInfoService
|
||||||
|
): Boolean {
|
||||||
|
refreshTroopList()
|
||||||
|
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
val waiter = GlobalScope.launch {
|
||||||
|
do {
|
||||||
|
delay(1000)
|
||||||
|
} while (
|
||||||
|
!service.isTroopCacheInited
|
||||||
|
)
|
||||||
|
continuation.resume(true)
|
||||||
|
}
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
waiter.cancel()
|
||||||
|
continuation.resume(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun banMember(groupId: Long, memberUin: Long, time: Int) {
|
||||||
|
val buffer = ByteBuffer.allocate(1 * 8 + 7)
|
||||||
|
buffer.putBuf32Long(groupId)
|
||||||
|
buffer.put(32.toByte())
|
||||||
|
buffer.putShort(1)
|
||||||
|
buffer.putBuf32Long(memberUin)
|
||||||
|
buffer.putInt(time)
|
||||||
|
val array = buffer.array()
|
||||||
|
sendOidb("OidbSvc.0x570_8", 1392, 8, array)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pokeMember(groupId: Long, memberUin: Long) {
|
||||||
|
val req = oidb_cmd0xed3.ReqBody().apply {
|
||||||
|
uint64_group_code.set(groupId)
|
||||||
|
uint64_to_uin.set(memberUin)
|
||||||
|
uint32_msg_seq.set(0)
|
||||||
|
}
|
||||||
|
sendOidb("OidbSvc.0xed3", 3795, 1, req.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun kickMember(groupId: Long, rejectAddRequest: Boolean, kickMsg: String, vararg memberUin: Long) {
|
||||||
|
val reqBody = oidb_0x8a0.ReqBody()
|
||||||
|
reqBody.opt_uint64_group_code.set(groupId)
|
||||||
|
memberUin.forEach {
|
||||||
|
val memberInfo = oidb_0x8a0.KickMemberInfo()
|
||||||
|
memberInfo.opt_uint32_operate.set(5)
|
||||||
|
memberInfo.opt_uint64_member_uin.set(it)
|
||||||
|
memberInfo.opt_uint32_flag.set(if (rejectAddRequest) 1 else 0)
|
||||||
|
reqBody.rpt_msg_kick_list.add(memberInfo)
|
||||||
|
}
|
||||||
|
if (kickMsg.isNotEmpty()) {
|
||||||
|
reqBody.bytes_kick_msg.set(ByteStringMicro.copyFrom(kickMsg.toByteArray()))
|
||||||
|
}
|
||||||
|
sendOidb("OidbSvc.0x8a0_0", 2208, 0, reqBody.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resignTroop(groupId: String) {
|
||||||
|
sendExtra("ProfileService.GroupMngReq") {
|
||||||
|
it.putInt("groupreqtype", 2)
|
||||||
|
it.putString("troop_uin", groupId)
|
||||||
|
it.putString("uin", app.currentUin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun modifyGroupMemberCard(groupId: Long, userId: Long, name: String): Boolean {
|
||||||
|
val createToServiceMsg = createToServiceMsg("friendlist.ModifyGroupCardReq")
|
||||||
|
createToServiceMsg.extraData.putLong("dwZero", 0L)
|
||||||
|
createToServiceMsg.extraData.putLong("dwGroupCode", groupId)
|
||||||
|
val info = stUinInfo()
|
||||||
|
info.cGender = -1
|
||||||
|
info.dwuin = userId
|
||||||
|
info.sEmail = ""
|
||||||
|
info.sName = name
|
||||||
|
info.sPhone = ""
|
||||||
|
info.sRemark = ""
|
||||||
|
info.dwFlag = 1
|
||||||
|
createToServiceMsg.extraData.putSerializable("vecUinInfo", arrayListOf(info))
|
||||||
|
createToServiceMsg.extraData.putLong("dwNewSeq", 0L)
|
||||||
|
sendToServiceMsg(createToServiceMsg)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun modifyTroopName(groupId: String, name: String) {
|
||||||
|
val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MODIFY_HANDLER)
|
||||||
|
|
||||||
|
if (!::METHOD_REQ_MODIFY_GROUP_NAME.isInitialized) {
|
||||||
|
METHOD_REQ_MODIFY_GROUP_NAME = businessHandler.javaClass.declaredMethods.first {
|
||||||
|
it.parameterCount == 3
|
||||||
|
&& it.parameterTypes[0] == String::class.java
|
||||||
|
&& it.parameterTypes[1] == String::class.java
|
||||||
|
&& it.parameterTypes[2] == Boolean::class.java
|
||||||
|
&& !Modifier.isPrivate(it.modifiers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
METHOD_REQ_MODIFY_GROUP_NAME.invoke(businessHandler, groupId, name, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun modifyGroupRemark(groupId: Long, remark: String): Boolean {
|
||||||
|
sendOidb("OidbSvc.0xf16_1", 3862, 1, Oidb0xf16(
|
||||||
|
setGroupRemarkReq = SetGroupRemarkReq(
|
||||||
|
groupCode = groupId.toULong(),
|
||||||
|
groupUin = groupCode2GroupUin(groupId).toULong(),
|
||||||
|
groupRemark = remark
|
||||||
|
)
|
||||||
|
).toByteArray())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setGroupAdmin(groupId: Long, userId: Long, enable: Boolean) {
|
||||||
|
val buffer = ByteBuffer.allocate(9)
|
||||||
|
buffer.putBuf32Long(groupId)
|
||||||
|
buffer.putBuf32Long(userId)
|
||||||
|
buffer.put(if (enable) 1 else 0)
|
||||||
|
val array = buffer.array()
|
||||||
|
sendOidb("OidbSvc.0x55c_1", 1372, 1, array)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProfileService.Pb.ReqSystemMsgAction.Group
|
||||||
|
suspend fun requestGroupRequest(
|
||||||
|
msgSeq: Long,
|
||||||
|
uin: Long,
|
||||||
|
gid: Long,
|
||||||
|
msg: String? = "",
|
||||||
|
approve: Boolean? = true,
|
||||||
|
notSee: Boolean? = false,
|
||||||
|
subType: String
|
||||||
|
): Result<String>{
|
||||||
|
val req = structmsg.ReqSystemMsgAction().apply {
|
||||||
|
if (subType == "invite") {
|
||||||
|
msg_type.set(1)
|
||||||
|
src_id.set(3)
|
||||||
|
sub_src_id.set(10016)
|
||||||
|
group_msg_type.set(2)
|
||||||
|
} else {
|
||||||
|
msg_type.set(2)
|
||||||
|
src_id.set(2)
|
||||||
|
sub_src_id.set(30024)
|
||||||
|
group_msg_type.set(1)
|
||||||
|
}
|
||||||
|
msg_seq.set(msgSeq)
|
||||||
|
req_uin.set(uin)
|
||||||
|
sub_type.set(1)
|
||||||
|
action_info.set(structmsg.SystemMsgActionInfo().apply {
|
||||||
|
type.set(if (approve != false) 11 else 12)
|
||||||
|
group_code.set(gid)
|
||||||
|
if (subType == "add") {
|
||||||
|
this.msg.set(msg)
|
||||||
|
this.blacklist.set(notSee != false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
language.set(1000)
|
||||||
|
}
|
||||||
|
val fromServiceMsg = sendBufferAW("ProfileService.Pb.ReqSystemMsgAction.Group", true, req.toByteArray())
|
||||||
|
?: return Result.failure(Exception("ReqSystemMsgAction.Group: No Response"))
|
||||||
|
if (fromServiceMsg.wupBuffer == null) {
|
||||||
|
return Result.failure(Exception("ReqSystemMsgAction.Group: No WupBuffer"))
|
||||||
|
}
|
||||||
|
val rsp = structmsg.RspSystemMsgAction().mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
return if (rsp.head.result.has()) {
|
||||||
|
if (rsp.head.result.get() == 0) {
|
||||||
|
Result.success(rsp.msg_detail.get())
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception(rsp.head.msg_fail.get()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("操作失败"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun requestGroupSystemMsgNew(msgNum: Int, reqMsgType: Int = 1, latestFriendSeq: Long = 0, latestGroupSeq: Long = 0, retryCnt: Int = 5): List<structmsg.StructMsg> {
|
||||||
|
if (retryCnt < 0) {
|
||||||
|
return ArrayList()
|
||||||
|
}
|
||||||
|
val req = structmsg.ReqSystemMsgNew()
|
||||||
|
req.msg_num.set(msgNum)
|
||||||
|
req.latest_friend_seq.set(latestFriendSeq)
|
||||||
|
req.latest_group_seq.set(latestGroupSeq)
|
||||||
|
req.version.set(1000)
|
||||||
|
req.checktype.set(3)
|
||||||
|
val flag = structmsg.FlagInfo()
|
||||||
|
flag.GrpMsg_Kick_Admin.set(1)
|
||||||
|
flag.GrpMsg_HiddenGrp.set(1)
|
||||||
|
flag.GrpMsg_WordingDown.set(1)
|
||||||
|
// flag.FrdMsg_GetBusiCard.set(1)
|
||||||
|
flag.GrpMsg_GetOfficialAccount.set(1)
|
||||||
|
flag.GrpMsg_GetPayInGroup.set(1)
|
||||||
|
flag.FrdMsg_Discuss2ManyChat.set(1)
|
||||||
|
flag.GrpMsg_NotAllowJoinGrp_InviteNotFrd.set(1)
|
||||||
|
flag.FrdMsg_NeedWaitingMsg.set(1)
|
||||||
|
// flag.FrdMsg_uint32_need_all_unread_msg.set(1)
|
||||||
|
flag.GrpMsg_NeedAutoAdminWording.set(1)
|
||||||
|
flag.GrpMsg_get_transfer_group_msg_flag.set(1)
|
||||||
|
flag.GrpMsg_get_quit_pay_group_msg_flag.set(1)
|
||||||
|
flag.GrpMsg_support_invite_auto_join.set(1)
|
||||||
|
flag.GrpMsg_mask_invite_auto_join.set(1)
|
||||||
|
flag.GrpMsg_GetDisbandedByAdmin.set(1)
|
||||||
|
flag.GrpMsg_GetC2cInviteJoinGroup.set(1)
|
||||||
|
req.flag.set(flag)
|
||||||
|
req.is_get_frd_ribbon.set(false)
|
||||||
|
req.is_get_grp_ribbon.set(false)
|
||||||
|
req.friend_msg_type_flag.set(1)
|
||||||
|
req.uint32_req_msg_type.set(reqMsgType)
|
||||||
|
req.uint32_need_uid.set(1)
|
||||||
|
val fromServiceMsg = sendBufferAW("ProfileService.Pb.ReqSystemMsgNew.Group", true, req.toByteArray())
|
||||||
|
return if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
||||||
|
ArrayList()
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
val msg = structmsg.RspSystemMsgNew()
|
||||||
|
msg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
return msg.groupmsgs.get().orEmpty()
|
||||||
|
} catch (err: Throwable) {
|
||||||
|
requestGroupSystemMsgNew(msgNum, reqMsgType, latestFriendSeq, latestGroupSeq, retryCnt - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setGroupUniqueTitle(groupId: String, userId: String, title: String) {
|
||||||
|
val localMemberInfo = getTroopMemberInfoByUin(groupId, userId, true).getOrThrow()
|
||||||
|
val req = Oidb_0x8fc.ReqBody()
|
||||||
|
req.uint64_group_code.set(groupId.toLong())
|
||||||
|
val memberInfo = Oidb_0x8fc.MemberInfo()
|
||||||
|
memberInfo.uint64_uin.set(userId.toLong())
|
||||||
|
memberInfo.bytes_uin_name.set(ByteStringMicro.copyFromUtf8(localMemberInfo.troopnick.ifEmpty {
|
||||||
|
localMemberInfo.troopremark.ifNullOrEmpty { "" }
|
||||||
|
}))
|
||||||
|
memberInfo.bytes_special_title.set(ByteStringMicro.copyFromUtf8(title))
|
||||||
|
memberInfo.uint32_special_title_expire_time.set(-1)
|
||||||
|
req.rpt_mem_level_info.add(memberInfo)
|
||||||
|
sendOidb("OidbSvc.0x8fc_2", 2300, 2, req.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setGroupWholeBan(groupId: Long, enable: Boolean) {
|
||||||
|
val reqBody = oidb_0x89a.ReqBody()
|
||||||
|
reqBody.uint64_group_code.set(groupId)
|
||||||
|
reqBody.st_group_info.set(oidb_0x89a.groupinfo().apply {
|
||||||
|
uint32_shutup_time.set(if (enable) 268435455 else 0)
|
||||||
|
})
|
||||||
|
sendOidb("OidbSvc.0x89a_0", 2202, 0, reqBody.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getGroupMemberList(groupId: String, refresh: Boolean): Result<List<TroopMemberInfo>> {
|
||||||
|
val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all")
|
||||||
|
var memberList = service.getAllTroopMembers(groupId)
|
||||||
|
if (refresh || memberList == null) {
|
||||||
|
memberList = requestTroopMemberInfo(service, groupId).onFailure {
|
||||||
|
return Result.failure(Exception("获取群成员列表失败"))
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
getGroupInfo(groupId, true).onSuccess {
|
||||||
|
if(it.wMemberNum > memberList.size) {
|
||||||
|
return getGroupMemberList(groupId, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.success(memberList)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getProhibitedMemberList(groupId: Long): Result<List<ProhibitedMemberInfo>> {
|
||||||
|
val fromServiceMsg = sendOidbAW("OidbSvc.0x899_0", 2201, 0, oidb_0x899.ReqBody().apply {
|
||||||
|
uint64_group_code.set(groupId)
|
||||||
|
uint64_start_uin.set(0)
|
||||||
|
uint32_identify_flag.set(6)
|
||||||
|
memberlist_opt.set(oidb_0x899.memberlist().apply {
|
||||||
|
uint64_member_uin.set(0)
|
||||||
|
uint32_shutup_timestap.set(0)
|
||||||
|
})
|
||||||
|
}.toByteArray()) ?: return Result.failure(RuntimeException("[oidb] timeout"))
|
||||||
|
if (fromServiceMsg.wupBuffer == null) {
|
||||||
|
return Result.failure(RuntimeException("[oidb] failed"))
|
||||||
|
}
|
||||||
|
val body = oidb_sso.OIDBSSOPkg()
|
||||||
|
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
if(body.uint32_result.get() != 0) {
|
||||||
|
return Result.failure(RuntimeException(body.str_error_msg.get()))
|
||||||
|
}
|
||||||
|
val resp = oidb_0x899.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
|
||||||
|
return Result.success(resp.rpt_memberlist.get().map {
|
||||||
|
ProhibitedMemberInfo(it.uint64_member_uin.get(), it.uint32_shutup_timestap.get())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getGroupRemainAtAllRemain (groupId: Long): Result<cmd0x8a7.RspBody> {
|
||||||
|
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x8a7_0", 2215, 0, cmd0x8a7.ReqBody().apply {
|
||||||
|
uint32_sub_cmd.set(1)
|
||||||
|
uint32_limit_interval_type_for_uin.set(2)
|
||||||
|
uint32_limit_interval_type_for_group.set(1)
|
||||||
|
uint64_uin.set(app.longAccountUin)
|
||||||
|
uint64_group_code.set(groupId)
|
||||||
|
}.toByteArray(), trpc = true) ?: return Result.failure(RuntimeException("[oidb] timeout"))
|
||||||
|
if (fromServiceMsg.wupBuffer == null) {
|
||||||
|
return Result.failure(RuntimeException("[oidb] failed"))
|
||||||
|
}
|
||||||
|
val body = oidb_sso.OIDBSSOPkg()
|
||||||
|
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
if(body.uint32_result.get() != 0) {
|
||||||
|
return Result.failure(RuntimeException(body.str_error_msg.get()))
|
||||||
|
}
|
||||||
|
|
||||||
|
val resp = cmd0x8a7.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
|
||||||
|
return Result.success(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getNotJoinedGroupInfo(groupId: Long): Result<NotJoinedGroupInfo> {
|
||||||
|
return withTimeoutOrNull(5000) timeout@{
|
||||||
|
val toServiceMsg = createToServiceMsg("ProfileService.ReqBatchProcess")
|
||||||
|
toServiceMsg.extraData.putLong("troop_code", groupId)
|
||||||
|
toServiceMsg.extraData.putBoolean("is_admin", false)
|
||||||
|
toServiceMsg.extraData.putInt("from", 0)
|
||||||
|
val fromServiceMsg = sendToServiceMsgAW(toServiceMsg) ?: return@timeout Result.failure(Exception("获取群信息超时"))
|
||||||
|
if (fromServiceMsg.wupBuffer == null) {
|
||||||
|
return@timeout Result.failure(Exception("获取群信息失败"))
|
||||||
|
}
|
||||||
|
val uniPacket = UniPacket(true)
|
||||||
|
uniPacket.encodeName = "utf-8"
|
||||||
|
uniPacket.decode(fromServiceMsg.wupBuffer)
|
||||||
|
val respBatchProcess = uniPacket.getByClass("RespBatchProcess", RespBatchProcess())
|
||||||
|
val batchRespInfo = oidb_0x88d.RspBody().mergeFrom(oidb_sso.OIDBSSOPkg()
|
||||||
|
.mergeFrom(respBatchProcess.batch_response_list.first().buffer)
|
||||||
|
.bytes_bodybuffer.get().toByteArray()).stzrspgroupinfo.get().firstOrNull()
|
||||||
|
?: return@timeout Result.failure(Exception("获取群信息失败"))
|
||||||
|
val info = batchRespInfo.stgroupinfo
|
||||||
|
Result.success(NotJoinedGroupInfo(
|
||||||
|
groupId = batchRespInfo.uint64_group_code.get(),
|
||||||
|
maxMember = info.uint32_group_member_max_num.get(),
|
||||||
|
memberCount = info.uint32_group_member_num.get(),
|
||||||
|
groupName = info.string_group_name.get().toStringUtf8(),
|
||||||
|
groupDesc = info.string_group_finger_memo.get().toStringUtf8(),
|
||||||
|
owner = info.uint64_group_owner.get(),
|
||||||
|
createTime = info.uint32_group_create_time.get().toLong(),
|
||||||
|
groupFlag = info.uint32_group_flag.get(),
|
||||||
|
groupFlagExt = info.uint32_group_flag_ext.get()
|
||||||
|
))
|
||||||
|
} ?: Result.failure(Exception("获取群信息超时"))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getGroupInfo(groupId: String, refresh: Boolean): Result<TroopInfo> {
|
||||||
|
val service = app
|
||||||
|
.getRuntimeService(ITroopInfoService::class.java, "all")
|
||||||
|
|
||||||
|
val groupInfo = getGroupInfo(groupId)
|
||||||
|
|
||||||
|
return if(refresh || !service.isTroopCacheInited || groupInfo.troopuin.isNullOrBlank()) {
|
||||||
|
requestGroupInfo(service, groupId)
|
||||||
|
} else {
|
||||||
|
Result.success(groupInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun requestGroupInfo(dataService: ITroopInfoService, uin: String): Result<TroopInfo> {
|
||||||
|
val info = withTimeoutOrNull(1000) {
|
||||||
|
var troopInfo: TroopInfo?
|
||||||
|
do {
|
||||||
|
troopInfo = dataService.getTroopInfo(uin)
|
||||||
|
delay(100)
|
||||||
|
} while (troopInfo == null || troopInfo.troopuin.isNullOrBlank())
|
||||||
|
return@withTimeoutOrNull troopInfo
|
||||||
|
}
|
||||||
|
return if (info != null) {
|
||||||
|
Result.success(info)
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("获取群列表失败"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getTroopMemberInfoByUin(
|
||||||
|
groupId: String,
|
||||||
|
uin: String,
|
||||||
|
refresh: Boolean = false
|
||||||
|
): Result<TroopMemberInfo> {
|
||||||
|
val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all")
|
||||||
|
var info = service.getTroopMember(groupId, uin)
|
||||||
|
if (refresh || !service.isMemberInCache(groupId, uin) || info == null || info.troopnick == null) {
|
||||||
|
info = requestTroopMemberInfo(service, groupId, uin).getOrNull()
|
||||||
|
}
|
||||||
|
if (info == null) {
|
||||||
|
info = getTroopMemberInfoByUinViaNt(groupId, uin).getOrNull()?.let {
|
||||||
|
TroopMemberInfo().apply {
|
||||||
|
troopnick = it.cardName
|
||||||
|
friendnick = it.nick
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (info != null && (info.alias == null || info.alias.isBlank())) {
|
||||||
|
val req = group_member_info.ReqBody()
|
||||||
|
req.uint64_group_code.set(groupId.toLong())
|
||||||
|
req.uint64_uin.set(uin.toLong())
|
||||||
|
req.bool_new_client.set(true)
|
||||||
|
req.uint32_client_type.set(1)
|
||||||
|
req.uint32_rich_card_name_ver.set(1)
|
||||||
|
val fromServiceMsg = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray())
|
||||||
|
if (fromServiceMsg != null && fromServiceMsg.wupBuffer != null) {
|
||||||
|
val rsp = group_member_info.RspBody()
|
||||||
|
rsp.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||||
|
if (rsp.msg_meminfo.str_location.has()) {
|
||||||
|
info.alias = rsp.msg_meminfo.str_location.get().toStringUtf8()
|
||||||
|
}
|
||||||
|
if (rsp.msg_meminfo.uint32_age.has()) {
|
||||||
|
info.age = rsp.msg_meminfo.uint32_age.get().toByte()
|
||||||
|
}
|
||||||
|
if (rsp.msg_meminfo.bytes_group_honor.has()) {
|
||||||
|
val honorBytes = rsp.msg_meminfo.bytes_group_honor.get().toByteArray()
|
||||||
|
val honor = troop_honor.GroupUserCardHonor()
|
||||||
|
honor.mergeFrom(honorBytes)
|
||||||
|
info.level = honor.level.get()
|
||||||
|
// 10315: medal_id not real group level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: Throwable) {
|
||||||
|
LogCenter.log("getTroopMemberInfoByUin: " + err.stackTraceToString(), Level.WARN)
|
||||||
|
}
|
||||||
|
return if (info != null) {
|
||||||
|
Result.success(info)
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("获取群成员信息失败"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getTroopMemberInfoByUinViaNt(
|
||||||
|
groupId: String,
|
||||||
|
qq: String,
|
||||||
|
timeout: Long = 5000L
|
||||||
|
): Result<MemberInfo> {
|
||||||
|
return runCatching {
|
||||||
|
val kernelService = NTServiceFetcher.kernelService
|
||||||
|
val sessionService = kernelService.wrapperSession
|
||||||
|
val groupService = sessionService.groupService
|
||||||
|
val info = withTimeoutOrNull(timeout) {
|
||||||
|
suspendCancellableCoroutine {
|
||||||
|
groupService.getTransferableMemberInfo(groupId.toLong()) { code, _, data ->
|
||||||
|
if (code != 0) {
|
||||||
|
it.resume(null)
|
||||||
|
return@getTransferableMemberInfo
|
||||||
|
}
|
||||||
|
data.forEach { (_, info) ->
|
||||||
|
if (info.uin == qq.toLong()) {
|
||||||
|
it.resume(info)
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.resume(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (info != null) {
|
||||||
|
Result.success(info)
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("获取群成员信息失败"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: String, memberUin: String, timeout: Long = 10_000): Result<TroopMemberInfo> {
|
||||||
|
val info = RefreshTroopMemberInfoLock.withLock {
|
||||||
|
service.deleteTroopMember(groupId, memberUin)
|
||||||
|
|
||||||
|
requestMemberInfoV2(groupId, memberUin)
|
||||||
|
requestMemberInfo(groupId, memberUin)
|
||||||
|
|
||||||
|
withTimeoutOrNull(timeout) {
|
||||||
|
while (!service.isMemberInCache(groupId, memberUin)) {
|
||||||
|
delay(200)
|
||||||
|
}
|
||||||
|
return@withTimeoutOrNull service.getTroopMember(groupId, memberUin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (info != null) {
|
||||||
|
Result.success(info)
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("获取群成员信息失败"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestMemberInfo(groupId: String, memberUin: String) {
|
||||||
|
val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER)
|
||||||
|
|
||||||
|
if (!::METHOD_REQ_MEMBER_INFO.isInitialized) {
|
||||||
|
METHOD_REQ_MEMBER_INFO = businessHandler.javaClass.declaredMethods.first {
|
||||||
|
it.parameterCount == 2 &&
|
||||||
|
it.parameterTypes[0] == Long::class.java &&
|
||||||
|
it.parameterTypes[1] == Long::class.java &&
|
||||||
|
!Modifier.isPrivate(it.modifiers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
METHOD_REQ_MEMBER_INFO.invoke(businessHandler, groupId.toLong(), memberUin.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestMemberInfoV2(groupId: String, memberUin: String) {
|
||||||
|
val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER)
|
||||||
|
|
||||||
|
if (!::METHOD_REQ_MEMBER_INFO_V2.isInitialized) {
|
||||||
|
METHOD_REQ_MEMBER_INFO_V2 = businessHandler.javaClass.declaredMethods.first {
|
||||||
|
it.parameterCount == 3 &&
|
||||||
|
it.parameterTypes[0] == String::class.java &&
|
||||||
|
it.parameterTypes[1] == String::class.java &&
|
||||||
|
!Modifier.isPrivate(it.modifiers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
METHOD_REQ_MEMBER_INFO_V2.invoke(businessHandler,
|
||||||
|
groupId, groupUin2GroupCode(groupId.toLong()).toString(), arrayListOf(memberUin))
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: String): Result<List<TroopMemberInfo>> {
|
||||||
|
val info = RefreshTroopMemberListLock.withLock {
|
||||||
|
service.deleteTroopMembers(groupId)
|
||||||
|
refreshTroopMemberList(groupId)
|
||||||
|
|
||||||
|
withTimeoutOrNull(10000) {
|
||||||
|
var memberList: List<TroopMemberInfo>?
|
||||||
|
do {
|
||||||
|
delay(100)
|
||||||
|
memberList = service.getAllTroopMembers(groupId)
|
||||||
|
} while (memberList.isNullOrEmpty())
|
||||||
|
return@withTimeoutOrNull memberList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (info != null) {
|
||||||
|
Result.success(info)
|
||||||
|
} else {
|
||||||
|
Result.failure(Exception("获取群成员信息失败"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshTroopMemberList(groupId: String) {
|
||||||
|
val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_LIST_HANDLER)
|
||||||
|
|
||||||
|
// void C(boolean forceRefresh, String groupId, String troopcode, int reqType); // RequestedTroopList/refreshMemberListFromServer
|
||||||
|
if (!::METHOD_REQ_TROOP_MEM_LIST.isInitialized) {
|
||||||
|
METHOD_REQ_TROOP_MEM_LIST = businessHandler.javaClass.declaredMethods.first {
|
||||||
|
it.parameterCount == 4
|
||||||
|
&& it.parameterTypes[0] == Boolean::class.java
|
||||||
|
&& it.parameterTypes[1] == String::class.java
|
||||||
|
&& it.parameterTypes[2] == String::class.java
|
||||||
|
&& it.parameterTypes[3] == Int::class.java
|
||||||
|
&& !Modifier.isPrivate(it.modifiers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
METHOD_REQ_TROOP_MEM_LIST.invoke(businessHandler, true, groupId, groupUin2GroupCode(groupId.toLong()).toString(), 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshTroopList() {
|
||||||
|
val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_LIST_HANDLER)
|
||||||
|
|
||||||
|
if (!::METHOD_REQ_TROOP_LIST.isInitialized) {
|
||||||
|
METHOD_REQ_TROOP_LIST = businessHandler.javaClass.declaredMethods.first {
|
||||||
|
it.parameterCount == 0 && !Modifier.isPrivate(it.modifiers) && it.returnType == Void.TYPE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
METHOD_REQ_TROOP_LIST.invoke(businessHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun groupUin2GroupCode(groupuin: Long): Long {
|
||||||
|
var calc = groupuin / 1000000L
|
||||||
|
while (true) {
|
||||||
|
calc -= if (calc >= 0 + 202 && calc + 202 <= 10) {
|
||||||
|
(202 - 0).toLong()
|
||||||
|
} else if (calc >= 11 + 480 && calc <= 19 + 480) {
|
||||||
|
(480 - 11).toLong()
|
||||||
|
} else if (calc >= 20 + 2100 && calc <= 66 + 2100) {
|
||||||
|
(2100 - 20).toLong()
|
||||||
|
} else if (calc >= 67 + 2010 && calc <= 156 + 2010) {
|
||||||
|
(2010 - 67).toLong()
|
||||||
|
} else if (calc >= 157 + 2147 && calc <= 209 + 2147) {
|
||||||
|
(2147 - 157).toLong()
|
||||||
|
} else if (calc >= 210 + 4100 && calc <= 309 + 4100) {
|
||||||
|
(4100 - 210).toLong()
|
||||||
|
} else if (calc >= 310 + 3800 && calc <= 499 + 3800) {
|
||||||
|
(3800 - 310).toLong()
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return calc * 1000000L + groupuin % 1000000L
|
||||||
|
}
|
||||||
|
|
||||||
|
fun groupCode2GroupUin(groupcode: Long): Long {
|
||||||
|
var calc = groupcode / 1000000L
|
||||||
|
loop@ while (true) calc += when (calc) {
|
||||||
|
in 0..10 -> {
|
||||||
|
(202 - 0).toLong()
|
||||||
|
}
|
||||||
|
in 11..19 -> {
|
||||||
|
(480 - 11).toLong()
|
||||||
|
}
|
||||||
|
in 20..66 -> {
|
||||||
|
(2100 - 20).toLong()
|
||||||
|
}
|
||||||
|
in 67..156 -> {
|
||||||
|
(2010 - 67).toLong()
|
||||||
|
}
|
||||||
|
in 157..209 -> {
|
||||||
|
(2147 - 157).toLong()
|
||||||
|
}
|
||||||
|
in 210..309 -> {
|
||||||
|
(4100 - 210).toLong()
|
||||||
|
}
|
||||||
|
in 310..499 -> {
|
||||||
|
(3800 - 310).toLong()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
break@loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return calc * 1000000L + groupcode % 1000000L
|
||||||
|
}
|
||||||
|
}
|
17
xposed/src/main/java/qq/service/group/NotJoinedGroupInfo.kt
Normal file
17
xposed/src/main/java/qq/service/group/NotJoinedGroupInfo.kt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package qq.service.group
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class NotJoinedGroupInfo(
|
||||||
|
@SerialName("group_id") val groupId: Long,
|
||||||
|
@SerialName("max_member_cnt") val maxMember: Int,
|
||||||
|
@SerialName("member_count") val memberCount: Int,
|
||||||
|
@SerialName("group_name") val groupName: String,
|
||||||
|
@SerialName("group_desc") val groupDesc: String,
|
||||||
|
@SerialName("owner") val owner: Long,
|
||||||
|
@SerialName("create_time") val createTime: Long,
|
||||||
|
@SerialName("group_flag") val groupFlag: Int,
|
||||||
|
@SerialName("group_flag_ext") val groupFlagExt: Int,
|
||||||
|
)
|
@ -0,0 +1,10 @@
|
|||||||
|
package qq.service.group
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
internal data class ProhibitedMemberInfo(
|
||||||
|
@SerialName("user_id") val memberUin: Long,
|
||||||
|
@SerialName("time") val shutuptimestap: Int
|
||||||
|
)
|
140
xposed/src/main/java/qq/service/internals/AioListener.kt
Normal file
140
xposed/src/main/java/qq/service/internals/AioListener.kt
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
@file:OptIn(DelicateCoroutinesApi::class)
|
||||||
|
|
||||||
|
package qq.service.internals
|
||||||
|
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter
|
||||||
|
import qq.service.bdh.RichProtoSvc
|
||||||
|
import qq.service.kernel.SimpleKernelMsgListener
|
||||||
|
import qq.service.msg.MessageHelper
|
||||||
|
|
||||||
|
object AioListener: SimpleKernelMsgListener() {
|
||||||
|
override fun onRecvMsg(records: ArrayList<MsgRecord>) {
|
||||||
|
records.forEach {
|
||||||
|
GlobalScope.launch {
|
||||||
|
try {
|
||||||
|
onMsg(it)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogCenter.log("OnMessage: " + e.stackTraceToString(), Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun onMsg(record: MsgRecord) {
|
||||||
|
when (record.chatType) {
|
||||||
|
MsgConstant.KCHATTYPEGROUP -> {
|
||||||
|
if (record.senderUin == 0L) return
|
||||||
|
|
||||||
|
LogCenter.log("群消息(group = ${record.peerName}(${record.peerUin}), uin = ${record.senderUin}, id = ${record.msgId})")
|
||||||
|
|
||||||
|
if (!GlobalEventTransmitter.MessageTransmitter.transGroupMessage(record, record.elements)) {
|
||||||
|
LogCenter.log("群消息推送失败 -> 推送目标可能不存在", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MsgConstant.KCHATTYPEC2C -> {
|
||||||
|
LogCenter.log("私聊消息(private = ${record.senderUin}, id = [${record.msgId} | ${record.msgSeq}])")
|
||||||
|
|
||||||
|
if (!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
|
||||||
|
record, record.elements
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
LogCenter.log("私聊消息推送失败 -> MessageTransmitter", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
|
||||||
|
var groupCode = 0L
|
||||||
|
var fromNick = ""
|
||||||
|
MessageHelper.getTempChatInfo(record.chatType, record.senderUid).onSuccess {
|
||||||
|
groupCode = it.groupCode.toLong()
|
||||||
|
fromNick = it.fromNick
|
||||||
|
}
|
||||||
|
|
||||||
|
LogCenter.log("私聊临时消息(private = ${record.senderUin}, groupId=$groupCode)")
|
||||||
|
|
||||||
|
if (!GlobalEventTransmitter.MessageTransmitter.transTempMessage(record, record.elements, groupCode, fromNick)
|
||||||
|
) {
|
||||||
|
LogCenter.log("私聊临时消息推送失败 -> MessageTransmitter", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MsgConstant.KCHATTYPEGUILD -> {
|
||||||
|
LogCenter.log("频道消息(guildId = ${record.guildId}, sender = ${record.senderUid}, id = [${record.msgId}])")
|
||||||
|
if (!GlobalEventTransmitter.MessageTransmitter
|
||||||
|
.transGuildMessage(record, record.elements)
|
||||||
|
) {
|
||||||
|
LogCenter.log("频道消息推送失败 -> MessageTransmitter", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> LogCenter.log("不支持PUSH事件: ${record.chatType}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFileMsgCome(arrayList: ArrayList<MsgRecord>?) {
|
||||||
|
arrayList?.forEach { record ->
|
||||||
|
GlobalScope.launch {
|
||||||
|
when (record.chatType) {
|
||||||
|
MsgConstant.KCHATTYPEGROUP -> onGroupFileMsg(record)
|
||||||
|
MsgConstant.KCHATTYPEC2C -> onC2CFileMsg(record)
|
||||||
|
else -> LogCenter.log("不支持该来源的文件上传事件:${record}", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun onC2CFileMsg(record: MsgRecord) {
|
||||||
|
val userId = record.senderUin
|
||||||
|
val fileMsg = record.elements.firstOrNull {
|
||||||
|
it.elementType == MsgConstant.KELEMTYPEFILE
|
||||||
|
}?.fileElement ?: kotlin.run {
|
||||||
|
LogCenter.log("消息为私聊文件消息但不包含文件消息,来自:${record.peerUin}", Level.WARN)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileName = fileMsg.fileName
|
||||||
|
val fileSize = fileMsg.fileSize
|
||||||
|
val expireTime = fileMsg.expireTime ?: 0
|
||||||
|
val fileId = fileMsg.fileUuid
|
||||||
|
val fileSubId = fileMsg.fileSubId ?: ""
|
||||||
|
val url = RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
|
||||||
|
|
||||||
|
if (!GlobalEventTransmitter.FileNoticeTransmitter
|
||||||
|
.transPrivateFileEvent(record.msgTime, userId, fileId, fileSubId, fileName, fileSize, expireTime, url)
|
||||||
|
) {
|
||||||
|
LogCenter.log("私聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun onGroupFileMsg(record: MsgRecord) {
|
||||||
|
val groupId = record.peerUin
|
||||||
|
val userId = record.senderUin
|
||||||
|
val fileMsg = record.elements.firstOrNull {
|
||||||
|
it.elementType == MsgConstant.KELEMTYPEFILE
|
||||||
|
}?.fileElement ?: kotlin.run {
|
||||||
|
LogCenter.log("消息为群聊文件消息但不包含文件消息,来自:${record.peerUin}", Level.WARN)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//val fileMd5 = fileMsg.fileMd5
|
||||||
|
val fileName = fileMsg.fileName
|
||||||
|
val fileSize = fileMsg.fileSize
|
||||||
|
val uuid = fileMsg.fileUuid
|
||||||
|
val bizId = fileMsg.fileBizId
|
||||||
|
|
||||||
|
val url = RichProtoSvc.getGroupFileDownUrl(record.peerUin, uuid, bizId)
|
||||||
|
|
||||||
|
if (!GlobalEventTransmitter.FileNoticeTransmitter
|
||||||
|
.transGroupFileEvent(record.msgTime, userId, groupId, uuid, fileName, fileSize, bizId, url)
|
||||||
|
) {
|
||||||
|
LogCenter.log("群聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
xposed/src/main/java/qq/service/internals/LineDevListener.kt
Normal file
14
xposed/src/main/java/qq/service/internals/LineDevListener.kt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package qq.service.internals
|
||||||
|
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.DevInfo
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.KickedInfo
|
||||||
|
import qq.service.kernel.SimpleKernelMsgListener
|
||||||
|
import java.util.ArrayList
|
||||||
|
|
||||||
|
object LineDevListener: SimpleKernelMsgListener() {
|
||||||
|
override fun onKickedOffLine(kickedInfo: KickedInfo) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLineDev(devs: ArrayList<DevInfo>) {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package qq.service.internals
|
||||||
|
|
||||||
|
import moe.fuqiuluo.shamrock.utils.FileUtils
|
||||||
|
import mqq.app.MobileQQ
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
internal object LocalCacheHelper: QQInterfaces() {
|
||||||
|
// 获取外部储存data目录
|
||||||
|
private val dataDir = MobileQQ.getContext().getExternalFilesDir(null)!!
|
||||||
|
.parentFile!!.resolve("Tencent")
|
||||||
|
|
||||||
|
fun getCurrentPttPath(): File {
|
||||||
|
return dataDir.resolve("MobileQQ/${app.currentAccountUin}/ptt").also {
|
||||||
|
if (!it.exists()) it.mkdirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCachePttFile(md5: String): File {
|
||||||
|
val file = FileUtils.getFileByMd5(md5)
|
||||||
|
return if (file.exists()) file else getCurrentPttPath().resolve("$md5.amr")
|
||||||
|
}
|
||||||
|
}
|
73
xposed/src/main/java/qq/service/internals/MSFHandler.kt
Normal file
73
xposed/src/main/java/qq/service/internals/MSFHandler.kt
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package qq.service.internals
|
||||||
|
|
||||||
|
import com.tencent.qphone.base.remote.FromServiceMsg
|
||||||
|
import com.tencent.qphone.base.remote.ToServiceMsg
|
||||||
|
import kotlinx.atomicfu.atomic
|
||||||
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
typealias MsfPush = (FromServiceMsg) -> Unit
|
||||||
|
typealias MsfResp = CancellableContinuation<Pair<ToServiceMsg, FromServiceMsg>>
|
||||||
|
|
||||||
|
internal object MSFHandler {
|
||||||
|
private val mPushHandlers = hashMapOf<String, MsfPush>()
|
||||||
|
private val mRespHandler = hashMapOf<Int, MsfResp>()
|
||||||
|
private val mPushLock = Mutex()
|
||||||
|
private val mRespLock = Mutex()
|
||||||
|
|
||||||
|
private val seq = atomic(0)
|
||||||
|
|
||||||
|
fun nextSeq(): Int {
|
||||||
|
seq.compareAndSet(0xFFFFFFF, 0)
|
||||||
|
return seq.incrementAndGet()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun registerPush(cmd: String, push: MsfPush) {
|
||||||
|
mPushLock.withLock {
|
||||||
|
mPushHandlers[cmd] = push
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun unregisterPush(cmd: String) {
|
||||||
|
mPushLock.withLock {
|
||||||
|
mPushHandlers.remove(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun registerResp(cmd: Int, resp: MsfResp) {
|
||||||
|
mRespLock.withLock {
|
||||||
|
mRespHandler[cmd] = resp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun unregisterResp(cmd: Int) {
|
||||||
|
mRespLock.withLock {
|
||||||
|
mRespHandler.remove(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPush(fromServiceMsg: FromServiceMsg) {
|
||||||
|
val cmd = fromServiceMsg.serviceCmd
|
||||||
|
if (cmd == "trpc.msg.olpush.OlPushService.MsgPush") {
|
||||||
|
PrimitiveListener.onPush(fromServiceMsg)
|
||||||
|
} else {
|
||||||
|
val push = mPushHandlers[cmd]
|
||||||
|
push?.invoke(fromServiceMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onResp(toServiceMsg: ToServiceMsg, fromServiceMsg: FromServiceMsg) {
|
||||||
|
runCatching {
|
||||||
|
val cmd = toServiceMsg.getAttribute("shamrock_uid") as? Int?
|
||||||
|
?: return@runCatching
|
||||||
|
val resp = mRespHandler[cmd]
|
||||||
|
resp?.resume(toServiceMsg to fromServiceMsg)
|
||||||
|
}.onFailure {
|
||||||
|
LogCenter.log("MSF.onResp failed: ${it.message}", Level.ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user