Merge pull request #310 from whitechi73/kritor

kritorをmasterブランチに設定する
This commit is contained in:
白池 2024-03-21 16:15:12 +08:00 committed by GitHub
commit 0faada7b5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
119 changed files with 11778 additions and 156 deletions

1
kritor Submodule

@ -0,0 +1 @@
Subproject commit e4aac653e14249cbb6f27567ffd463165f6deebe

View File

@ -15,7 +15,7 @@ dependencies {
implementation("com.google.devtools.ksp:symbol-processing-api:1.9.21-1.0.15")
implementation("com.squareup:kotlinpoet:1.14.2")
implementation(DEPENDENCY_PROTOBUF)
//implementation(DEPENDENCY_PROTOBUF)
implementation(kotlinx("serialization-protobuf", "1.6.2"))
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")

View File

@ -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()
}
}

View File

@ -32,7 +32,7 @@ class ProtobufProcessor(
}.toList()
if (actions.isNotEmpty()) {
actions.forEachIndexed { index, clz ->
actions.forEachIndexed { _, clz ->
if (clz.isInternal()) return@forEachIndexed
if (clz.isPrivate()) return@forEachIndexed

View File

@ -1,5 +1,5 @@
@file:Suppress("UNCHECKED_CAST", "LocalVariableName", "PrivatePropertyName")
@file:OptIn(KspExperimental::class)
@file:OptIn(KspExperimental::class, KspExperimental::class)
package moe.fuqiuluo.ksp.impl
@ -27,10 +27,14 @@ class XposedHookProcessor(
private val logger: KSPLogger
): SymbolProcessor {
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 actions = (symbols.filter {
it is KSClassDeclaration && it.validate() && it.classKind == ClassKind.CLASS
it is KSClassDeclaration && it.classKind == ClassKind.CLASS
} as Sequence<KSClassDeclaration>).toList()
if (actions.isNotEmpty()) {
@ -46,7 +50,7 @@ class XposedHookProcessor(
}
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")
.addParameter("ctx", context)
.apply {
@ -96,16 +100,6 @@ class XposedHookProcessor(
}
}
}
return unableToProcess.toList()
}
inner class ActionLoaderVisitor(
private val firstActions: List<KSClassDeclaration>,
private val serviceActions: List<KSClassDeclaration>,
): KSVisitorVoid() {
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
}
}
}

View File

@ -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
)
}
}

View File

@ -37,7 +37,7 @@ android {
}
dependencies {
implementation(DEPENDENCY_PROTOBUF)
//implementation(DEPENDENCY_PROTOBUF)
implementation(kotlinx("serialization-protobuf", "1.6.2"))
implementation(kotlinx("serialization-json", "1.6.2"))
@ -47,5 +47,5 @@ dependencies {
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
}

View File

@ -1,7 +1,7 @@
@file:OptIn(ExperimentalSerializationApi::class)
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.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@ -9,7 +9,7 @@ import moe.fuqiuluo.symbols.Protobuf
@Serializable
data class NtV2RichMediaRsp(
@ProtoNumber(1) val head: RspHead,
@ProtoNumber(1) val head: RspHead?,
@ProtoNumber(2) val upload: UploadRsp?,
@ProtoNumber(3) val download: DownloadRsp?,
@ProtoNumber(4) val downloadRkeyRsp: DownloadRkeyRsp?,

View File

@ -1,6 +1,6 @@
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.protobuf.ProtoNumber
import moe.fuqiuluo.symbols.Protobuf

View File

@ -2,7 +2,7 @@
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.Serializable
import kotlinx.serialization.protobuf.ProtoNumber

View File

@ -6,9 +6,13 @@ import com.tencent.mobileqq.app.BusinessObserver;
import com.tencent.mobileqq.app.MessageHandler;
import com.tencent.qphone.base.remote.ToServiceMsg;
import java.util.concurrent.ConcurrentHashMap;
import mqq.app.AppRuntime;
public abstract class AppInterface extends AppRuntime {
private final ConcurrentHashMap<String, BusinessHandler> allHandler = new ConcurrentHashMap<>();
public String getCurrentNickname() {
return "";
}

View File

@ -13,6 +13,10 @@ public abstract class BaseBusinessHandler extends OidbWrapper {
return null;
}
public void addBusinessObserver(ToServiceMsg toServiceMsg, BusinessObserver businessObserver, boolean z) {
}
public final <T> T decodePacket(byte[] data, String name, T obj) {
UniPacket uniPacket = new UniPacket(true);
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> getPushCommandList();

View File

@ -8,6 +8,8 @@ public abstract class BusinessHandler extends BaseBusinessHandler {
public BusinessHandler(AppInterface appInterface) {
}
protected abstract Class<? extends BusinessObserver> observerClass();
@Override
public Set<String> getCommandList() {
return null;

View File

@ -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) {
}
}

View File

@ -18,6 +18,11 @@ public class MsgService {
public void addMsgListener(IKernelMsgListener listener) {
}
public void removeMsgListener(@NotNull IKernelMsgListener iKernelMsgListener) {
}
public String getRichMediaFilePathForGuild(@NotNull RichMediaFilePathInfo richMediaFilePathInfo) {
return null;
}

View File

@ -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) {
throw new UnsupportedOperationException();
}

View 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) {
}
}

View File

@ -39,3 +39,6 @@ include(
include(":protobuf")
include(":processor")
include(":annotations")
include(":kritor")
project(":kritor").projectDir = file("kritor/protos")

View File

@ -5,6 +5,7 @@ plugins {
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
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"
}
@ -63,6 +64,8 @@ dependencies {
compileOnly ("de.robv.android.xposed:api:82")
compileOnly (project(":qqinterface"))
protobuf(project(":kritor"))
implementation(project(":protobuf"))
implementation(project(":annotations"))
ksp(project(":processor"))
@ -72,9 +75,7 @@ dependencies {
DEPENDENCY_ANDROIDX.forEach {
implementation(it)
}
implementation(DEPENDENCY_JAVA_WEBSOCKET)
implementation(DEPENDENCY_PROTOBUF)
implementation(DEPENDENCY_JSON5K)
//implementation(DEPENDENCY_PROTOBUF)
implementation(room("runtime"))
kapt(room("compiler"))
@ -83,16 +84,15 @@ dependencies {
implementation(kotlinx("io-jvm", "0.1.16"))
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", "content-negotiation"))
implementation(ktor("client", "cio"))
implementation(ktor("client", "okhttp"))
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")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
@ -101,6 +101,45 @@ dependencies {
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
}
tasks.withType<KotlinCompile>().all {
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")
}
}
}
}
}

View 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

View File

@ -173,7 +173,7 @@ NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) {
extern "C"
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) {
if (hook_function == nullptr) return false;
hook_function((void*) __system_property_get, (void *)fake_system_property_get, (void **) &backup_system_property_get);

View 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
}
}

View 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()
}
}

View File

@ -0,0 +1,6 @@
package kritor.handlers
internal object GrpcHandlers {
}

View 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)
}

View 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()
}
}
}

View 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()
}
}

View 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)
}
}
}
}

View 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)
}
}
}
}

View 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
}
}
}

View 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()
})
}
}
}
}

View 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)
}
}

View 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
})
}
}
}
}
}
}
}

View 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
}
}
}
}

View 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 { }
}
}

View 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"))
}
}
}

View File

@ -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
}

View File

@ -0,0 +1,7 @@
package moe.fuqiuluo.shamrock.config
object ActiveTicket: ConfigKey<String>() {
override fun name(): String = "active_ticket"
override fun default(): String = ""
}

View File

@ -0,0 +1,6 @@
package moe.fuqiuluo.shamrock.config
object AliveReply: ConfigKey<Boolean>() {
override fun name() = "alive_reply"
override fun default() = false
}

View File

@ -0,0 +1,7 @@
package moe.fuqiuluo.shamrock.config
object AntiJvmTrace: ConfigKey<Boolean>() {
override fun default() = false
override fun name() = "anti_jvm_trace"
}

View File

@ -0,0 +1,6 @@
package moe.fuqiuluo.shamrock.config
object B2Mode: ConfigKey<Boolean>() {
override fun name() = "b2_mode"
override fun default() = false
}

View File

@ -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]
}

View File

@ -0,0 +1,7 @@
package moe.fuqiuluo.shamrock.config
object DebugMode: ConfigKey<Boolean>() {
override fun name(): String = "debug"
override fun default(): Boolean = false
}

View File

@ -0,0 +1,6 @@
package moe.fuqiuluo.shamrock.config
object EnableOldBDH: ConfigKey<Boolean>() {
override fun name() = "enable_old_bdh"
override fun default() = true
}

View File

@ -0,0 +1,6 @@
package moe.fuqiuluo.shamrock.config
object EnableSelfMessage: ConfigKey<Boolean>() {
override fun name() = "enable_self_message"
override fun default() = false
}

View File

@ -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
}

View File

@ -0,0 +1,7 @@
package moe.fuqiuluo.shamrock.config
object IsInit: ConfigKey<Boolean>() {
override fun name(): String = "is_init"
override fun default(): Boolean = false
}

View File

@ -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
}

View File

@ -0,0 +1,7 @@
package moe.fuqiuluo.shamrock.config
data object RPCAddress: ConfigKey<String>() {
override fun name(): String = "rpc_address"
override fun default(): String = ""
}

View File

@ -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
}

View File

@ -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"
}

View File

@ -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
}

View File

@ -6,10 +6,14 @@ internal class ParamsException(key: String): InternalMessageMakerError("Lack of
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 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)

View File

@ -1,23 +1,2 @@
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")
}
}

View File

@ -8,9 +8,10 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.xposed.hooks.toast
import moe.fuqiuluo.shamrock.xposed.helper.internal.DataRequester
import moe.fuqiuluo.shamrock.config.DebugMode
import moe.fuqiuluo.shamrock.config.ShamrockConfig
import moe.fuqiuluo.shamrock.tools.toast
import moe.fuqiuluo.shamrock.xposed.helper.AppTalker
import mqq.app.MobileQQ
import java.io.File
import java.util.Date
@ -51,19 +52,18 @@ internal object LogCenter {
private val format = SimpleDateFormat("[HH:mm:ss] ")
fun log(string: String, level: Level = Level.INFO, toast: Boolean = false) {
if (!ShamrockConfig.isDebug() && level == Level.DEBUG) {
if (!ShamrockConfig[DebugMode] && level == Level.DEBUG) {
return
}
if (toast) {
MobileQQ.getContext().toast(string)
}
// 把日志广播到主进程
GlobalScope.launch(Dispatchers.Default) {
DataRequester.request("send_message", bodyBuilder = {
AppTalker.talk("send_message") {
put("string", string)
put("level", level.id)
})
}
}
if (!LogFile.exists()) {
@ -79,7 +79,7 @@ internal object LogCenter {
level: Level = Level.INFO,
toast: Boolean = false
) {
if (!ShamrockConfig.isDebug() && level == Level.DEBUG) {
if (!ShamrockConfig[DebugMode] && level == Level.DEBUG) {
return
}
@ -89,10 +89,10 @@ internal object LogCenter {
}
// 把日志广播到主进程
GlobalScope.launch(Dispatchers.Default) {
DataRequester.request("send_message", bodyBuilder = {
AppTalker.talk("send_message") {
put("string", log)
put("level", level.id)
})
}
}
if (!LogFile.exists()) {
@ -103,10 +103,6 @@ internal object LogCenter {
LogFile.appendText(format)
}
// fun getAllLog(): File {
// return LogFile
// }
fun getLogLines(start: Int, recent: Boolean = false): List<String> {
val logData = LogFile.readLines()
val index = if(start > logData.size || start < 0) 0 else start

View File

@ -1,6 +1,5 @@
package moe.fuqiuluo.shamrock.helper
import moe.fuqiuluo.shamrock.remote.service.data.GroupMemberHonor
object TroopHonorHelper {
data class Honor(
@ -60,4 +59,13 @@ object TroopHonorHelper {
else -> flag shr 4
} and 3
}
data class GroupMemberHonor(
val uin: Long,
val honorUrl: String,
val honorIconUrl: String,
val honorLevel: Int,
val honorId: Int,
val honorName: String
)
}

View File

@ -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)
}
}
}
}

View 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() }
}

View File

@ -1,6 +1,5 @@
package moe.fuqiuluo.shamrock.tools
import io.github.xn32.json5k.Json5
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
@ -27,15 +26,6 @@ val GlobalJson = Json {
coerceInputValues = true // 强制输入值
}
val GlobalJson5 = Json5 {
prettyPrint = true
indentationWidth = 2
//useSingleQuotes = true
//quoteMemberNames = true
//encodeDefaults = true
}
val String.asJson: JsonElement
get() = Json.parseToJsonElement(this)

View File

@ -47,8 +47,8 @@ fun ByteArray.slice(off: Int, length: Int = size - off): ByteArray {
.let { s -> if (uppercase) s.lowercase(Locale.getDefault()) else s }
} ?: "null"
fun String?.ifNullOrEmpty(defaultValue: String?): String? {
return if (this.isNullOrEmpty()) defaultValue else this
fun String?.ifNullOrEmpty(defaultValue: () -> String?): String? {
return if (this.isNullOrEmpty()) defaultValue() else this
}
@JvmOverloads fun String.hex2ByteArray(replace: Boolean = false): ByteArray {

View File

@ -1,43 +1,15 @@
package moe.fuqiuluo.shamrock.tools
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
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 {
HttpClient {
//install(HttpCookies)
val GlobalClient by lazy {
HttpClient(OkHttp) {
install(HttpTimeout) {
requestTimeoutMillis = 15000
connectTimeoutMillis = 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
})
}
}
}

View File

@ -2,9 +2,7 @@ package moe.fuqiuluo.shamrock.tools
import mqq.app.MobileQQ
private val context = MobileQQ.getContext()
private val packageManager = context.packageManager
private fun getPackageInfo(packageName: String) = packageManager.getPackageInfo(packageName, 0)
val ShamrockVersion: String = getPackageInfo("moe.fuqiuluo.shamrock.hided").versionName
val ShamrockVersion: String by lazy {
MobileQQ.getContext().packageManager
.getPackageInfo("moe.fuqiuluo.shamrock.hided", 0).versionName
}

View File

@ -8,7 +8,7 @@ import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.FFprobeKit
import com.arthenica.ffmpegkit.ReturnCode
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.LogCenter
import java.io.File

View File

@ -33,7 +33,7 @@ object DownloadUtils {
threadCount: Int = MAX_THREAD,
headers: Map<String, String> = mapOf()
): 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 connection = withContext(Dispatchers.IO) { url.openConnection() } as HttpURLConnection
headers.forEach { (k, v) ->

View File

@ -1,32 +1,28 @@
package moe.fuqiuluo.shamrock.xposed
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.XposedBridge
import de.robv.android.xposed.callbacks.XC_LoadPackage
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.xposed.loader.KeepAlive
import moe.fuqiuluo.shamrock.xposed.helper.KeepAlive
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
import moe.fuqiuluo.shamrock.tools.FuzzySearchClass
import moe.fuqiuluo.shamrock.tools.GlobalUi
import moe.fuqiuluo.shamrock.tools.afterHook
import moe.fuqiuluo.shamrock.utils.PlatformUtils
import moe.fuqiuluo.shamrock.xposed.hooks.runFirstActions
import mqq.app.MobileQQ
import java.lang.reflect.Field
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_INTERNATIONAL = "com.tencent.mobileqqi"
private const val PACKAGE_NAME_QQ_LITE = "com.tencent.qqlite"
private const val PACKAGE_NAME_TIM = "com.tencent.tim"
private val uselessProcess = listOf("peak", "tool", "mini", "qzone")
internal class XposedEntry: IXposedHookLoadPackage {
companion object {
@ -36,7 +32,6 @@ internal class XposedEntry: IXposedHookLoadPackage {
var secStaticNativehookInited = false
external fun injected(): Boolean
external fun hasEnv(): Boolean
}
@ -140,22 +135,14 @@ internal class XposedEntry: IXposedHookLoadPackage {
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")
GlobalUi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Handler.createAsync(ctx.mainLooper)
} else {
Handler(ctx.mainLooper)
}
runFirstActions(ctx)
}
}

View File

@ -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"
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}
}

View File

@ -0,0 +1,9 @@
package moe.fuqiuluo.shamrock.xposed.actions
import android.content.Context
internal interface IAction {
operator fun invoke(ctx: Context)
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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("同步配置中...")
}
}

View File

@ -0,0 +1,7 @@
package moe.fuqiuluo.shamrock.xposed.actions.interacts
import android.content.Intent
interface IInteract {
operator fun invoke(intent: Intent)
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -3,11 +3,10 @@ package moe.fuqiuluo.shamrock.xposed.helper
import android.content.ContentValues
import android.net.Uri
import mqq.app.MobileQQ
import kotlin.random.Random
internal object AppTalker {
val uriName = "content://moe.fuqiuluo.108.provider" // 你是真的闲,这都上个检测
val URI = Uri.parse(uriName)
private const val uriName = "content://moe.fuqiuluo.108.provider" // 你是真的闲,这都上个检测
private val URI = Uri.parse(uriName)
fun talk(values: ContentValues, onFailure: ((Throwable) -> Unit)? = null) {
val ctx = MobileQQ.getContext()
@ -17,4 +16,20 @@ internal object AppTalker {
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)
}
}

View File

@ -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)
}
}
}
}

View File

@ -34,8 +34,6 @@ internal object NativeLoader {
XposedBridge.log("[Shamrock] 反射检测到 Android x86")
true
} else false
}.onFailure {
XposedBridge.log("[Shamrock] ${it.stackTraceToString()}")
}.getOrElse { false }
private fun getLibFilePath(name: String): String {

View 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)
}
}
}

View 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
}
}
}

View 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
)
}
}
}

View 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
}

View 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)
}
}
}

View 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
}
}

View 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,
)

View 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
}
}

View 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()
}
}

View 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)
}
}
}

View 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)
}
}
}
}

View 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
}
}

View 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,
)

View File

@ -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
)

View 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)
}
}
}

View 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>) {
}
}

View File

@ -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")
}
}

View 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