mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 05:12:17 +00:00
Compare commits
51 Commits
3a07116093
...
1.1.0-ntrn
Author | SHA1 | Date | |
---|---|---|---|
59d762eecf | |||
e891bc8512 | |||
494c70f2f8 | |||
7baf459b2a | |||
36a09ca088 | |||
926c4659f6 | |||
cb7c68f36c | |||
72af39208c | |||
042f4bd330 | |||
9aef71b09f | |||
9cbe755520 | |||
df02f9f872 | |||
5cbb695a66 | |||
c014e85faa | |||
4a396b0935 | |||
d59fcf9f6a | |||
cdc664f44a | |||
ad313f384c | |||
b6a510ce05 | |||
bed5947909 | |||
fb6578d243 | |||
d33cace7aa | |||
659d4e5da4 | |||
ac2aee8c0e | |||
0faada7b5a | |||
680317da13 | |||
7782feb6ac | |||
d66358a1f3 | |||
824f280b3a | |||
6936262d62 | |||
0955267ee5 | |||
f3da62fa74 | |||
abbac6315c | |||
0cf10eabd6 | |||
8c33267887 | |||
f030104ff2 | |||
ee5fcc3403 | |||
5e819179b4 | |||
ea206faf4f | |||
5adfc544a2 | |||
bdb75841cf | |||
a3dc0d06b2 | |||
3664352f23 | |||
2770979fee | |||
6c9b282c6a | |||
be58c368e9 | |||
1d035fa378 | |||
7d0b60271e | |||
d38777d06a | |||
93c49953cf | |||
883e949cc1 |
2
.github/workflows/build-apk.yml
vendored
2
.github/workflows/build-apk.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
|
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -1,3 +1,3 @@
|
||||
[submodule "kritor"]
|
||||
path = kritor
|
||||
path = kritor/kritor
|
||||
url = https://github.com/KarinJS/kritor
|
||||
|
23
README.md
23
README.md
@ -16,20 +16,35 @@
|
||||
|
||||
## 简介
|
||||
|
||||
☘ 基于 Lsposed(**Non**-Riru) 实现 OneBot 标准的 QQ 机器人框架,原作者[**fuqiuluo**](https://github.com/fuqiuluo)已脱离开发,接下来由白池接手哦!本项目为OpenShamrock,不会有任何收费行为,欢迎大家的加入!
|
||||
☘ 基于 Lsposed(**Non**-Riru) 实现 Kritor 标准的 QQ 机器人框架!
|
||||
|
||||
> 本项目仅提供学习与交流用途,请在24小时内删除。
|
||||
> 本项目目的是研究 Xposed 和 LSPosed 框架的使用。 Epic 框架开发相关知识。
|
||||
> Riru可能导致封禁,请减少使用。
|
||||
> 如有违反法律,请联系删除。
|
||||
> 请勿在任何平台宣传,宣扬,转发本项目,请勿恶意修改企业安装包造成相关企业产生损失,如有违背,必将追责到底。
|
||||
> 官方论坛,[点我直达](https://forum.libfekit.so/)!
|
||||
|
||||
## 兼容|迁移|替代 说明
|
||||
|
||||
- 一键移植:本项目基于 go-cqhttp 的文档进行开发实现。
|
||||
- 平行部署:可多平台部署,未来将会支持 Docker 部署的教程。
|
||||
- 替代方案:[Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core)
|
||||
- 平行部署:可多平台部署。
|
||||
|
||||
## 相关项目
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><a href="https://github.com/LagrangeDev/Lagrange.Core">Lagrange.Core</a></td>
|
||||
<td>NTQQ 的协议实现</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/whitechi73/OpenShamrock">OpenShamrock</a></td>
|
||||
<td>基于 Xposed 实现 OneBot 标准的机器人框架(👈你在这里</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/chrononeko/chronocat">Chronocat</a></td>
|
||||
<td>基于 Electron 的、模块化的 Satori 框架</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 权限声明
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
package kritor.service
|
||||
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
annotation class Grpc(
|
||||
val serviceName: String,
|
||||
val funcName: String
|
||||
val funcName: String,
|
||||
|
||||
)
|
@ -1,8 +0,0 @@
|
||||
package moe.fuqiuluo.symbols
|
||||
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class OneBotHandler(
|
||||
val actionName: String,
|
||||
val alias: Array<String> = []
|
||||
)
|
@ -118,9 +118,7 @@ private fun APIInfoCard(
|
||||
text = rpcAddress,
|
||||
hint = "请输入回调地址",
|
||||
error = "输入的地址不合法",
|
||||
checker = {
|
||||
it.isEmpty() || it.contains(":")
|
||||
},
|
||||
checker = { true },
|
||||
confirm = {
|
||||
ShamrockConfig[ctx, RPCAddress] = rpcAddress.value
|
||||
AppRuntime.log("设置回调RPC地址为[${rpcAddress.value}]。")
|
||||
|
@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.9.21"
|
||||
kotlin("jvm") version "1.9.22"
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
@ -15,8 +15,9 @@ fun ktor(target: String, name: String): String {
|
||||
return "io.ktor:ktor-$target-$name:${Versions.ktorVersion}"
|
||||
}
|
||||
|
||||
fun grpc(name: String, version: String) = "io.grpc:grpc-$name:$version"
|
||||
|
||||
object Versions {
|
||||
const val roomVersion = "2.5.0"
|
||||
|
||||
const val ktorVersion = "2.3.3"
|
||||
}
|
1
kritor
1
kritor
Submodule kritor deleted from e4aac653e1
42
kritor/.gitignore
vendored
Normal file
42
kritor/.gitignore
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
76
kritor/build.gradle.kts
Normal file
76
kritor/build.gradle.kts
Normal file
@ -0,0 +1,76 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("com.google.protobuf") version "0.9.4"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "moe.whitechi73.kritor"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
protobuf(files("kritor/protos"))
|
||||
|
||||
implementation("com.google.protobuf:protobuf-java:4.26.0")
|
||||
|
||||
implementation(kotlinx("coroutines-core", "1.8.0"))
|
||||
|
||||
implementation(grpc("stub", "1.62.2"))
|
||||
implementation(grpc("protobuf", "1.62.2"))
|
||||
implementation(grpc("kotlin-stub", "1.4.1"))
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = "com.google.protobuf:protoc:4.26.0"
|
||||
}
|
||||
plugins {
|
||||
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("grpc")
|
||||
create("grpckt")
|
||||
}
|
||||
it.builtins {
|
||||
create("java")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
|
||||
}
|
0
kritor/consumer-rules.pro
Normal file
0
kritor/consumer-rules.pro
Normal file
1
kritor/kritor
Submodule
1
kritor/kritor
Submodule
Submodule kritor/kritor added at 27669a8f57
21
kritor/proguard-rules.pro
vendored
Normal file
21
kritor/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
@ -0,0 +1,83 @@
|
||||
@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.processing.*
|
||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
|
||||
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.authentication.*")
|
||||
.addStatement("import io.kritor.core.*")
|
||||
.addStatement("import io.kritor.customization.*")
|
||||
.addStatement("import io.kritor.developer.*")
|
||||
.addStatement("import io.kritor.file.*")
|
||||
.addStatement("import io.kritor.friend.*")
|
||||
.addStatement("import io.kritor.group.*")
|
||||
.addStatement("import io.kritor.guild.*")
|
||||
.addStatement("import io.kritor.message.*")
|
||||
.addStatement("import io.kritor.web.*")
|
||||
.addFunction(funcBuilder.build())
|
||||
.addImport("moe.fuqiuluo.symbols", "EMPTY_BYTE_ARRAY")
|
||||
runCatching {
|
||||
codeGenerator.createNewFile(
|
||||
dependencies = Dependencies(aggregating = false),
|
||||
packageName = packageName,
|
||||
fileName = fileSpec.name
|
||||
).use { outputStream ->
|
||||
outputStream.writer().use {
|
||||
fileSpec.build().writeTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return emptyList()
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package moe.fuqiuluo.ksp.providers
|
||||
|
||||
import com.google.auto.service.AutoService
|
||||
import com.google.devtools.ksp.processing.SymbolProcessor
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorProvider
|
||||
import moe.fuqiuluo.ksp.impl.GrpcProcessor
|
||||
|
||||
@AutoService(SymbolProcessorProvider::class)
|
||||
class GrpcProvider: SymbolProcessorProvider {
|
||||
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
|
||||
return GrpcProcessor(
|
||||
environment.codeGenerator,
|
||||
environment.logger
|
||||
)
|
||||
}
|
||||
}
|
@ -37,7 +37,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
//implementation(DEPENDENCY_PROTOBUF)
|
||||
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
||||
implementation(kotlinx("serialization-json", "1.6.2"))
|
||||
|
||||
|
@ -13,7 +13,7 @@ data class ButtonExtra(
|
||||
@Serializable
|
||||
data class Object1(
|
||||
@ProtoNumber(1) val rows: List<Row>? = null,
|
||||
@ProtoNumber(2) val appid: Int? = null,
|
||||
@ProtoNumber(2) val appid: ULong? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
@ -26,7 +26,7 @@ buildscript {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools:r8:8.2.47")
|
||||
classpath("com.android.tools:r8:8.3.37")
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,11 +34,9 @@ rootProject.name = "Shamrock"
|
||||
include(
|
||||
":app",
|
||||
":xposed",
|
||||
":qqinterface"
|
||||
":qqinterface",
|
||||
":protobuf",
|
||||
":processor",
|
||||
":annotations",
|
||||
":kritor"
|
||||
)
|
||||
include(":protobuf")
|
||||
include(":processor")
|
||||
include(":annotations")
|
||||
include(":kritor")
|
||||
|
||||
project(":kritor").projectDir = file("kritor/protos")
|
@ -5,7 +5,6 @@ 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"
|
||||
}
|
||||
|
||||
@ -61,11 +60,10 @@ kotlin {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly ("de.robv.android.xposed:api:82")
|
||||
compileOnly (project(":qqinterface"))
|
||||
|
||||
protobuf(project(":kritor"))
|
||||
compileOnly("de.robv.android.xposed:api:82")
|
||||
compileOnly(project(":qqinterface"))
|
||||
|
||||
implementation(project(":kritor"))
|
||||
implementation(project(":protobuf"))
|
||||
implementation(project(":annotations"))
|
||||
ksp(project(":processor"))
|
||||
@ -75,24 +73,20 @@ dependencies {
|
||||
DEPENDENCY_ANDROIDX.forEach {
|
||||
implementation(it)
|
||||
}
|
||||
//implementation(DEPENDENCY_PROTOBUF)
|
||||
|
||||
implementation(room("runtime"))
|
||||
kapt(room("compiler"))
|
||||
implementation(room("ktx"))
|
||||
|
||||
implementation(kotlinx("io-jvm", "0.1.16"))
|
||||
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
||||
|
||||
implementation(ktor("client", "core"))
|
||||
implementation(ktor("client", "okhttp"))
|
||||
implementation(ktor("serialization", "kotlinx-json"))
|
||||
|
||||
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")
|
||||
implementation(grpc("protobuf", "1.62.2"))
|
||||
implementation(grpc("kotlin-stub", "1.4.1"))
|
||||
implementation(grpc("okhttp", "1.62.2"))
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
@ -106,40 +100,3 @@ tasks.withType<KotlinCompile>().all {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ active_ticket=
|
||||
enable_self_message=false
|
||||
|
||||
# 旧BDH兼容开关
|
||||
enable_old_bdh=false
|
||||
enable_old_bdh=true
|
||||
|
||||
# 反JVM调用栈跟踪
|
||||
anti_jvm_trace=true
|
||||
|
164
xposed/src/main/java/kritor/client/KritorClient.kt
Normal file
164
xposed/src/main/java/kritor/client/KritorClient.kt
Normal file
@ -0,0 +1,164 @@
|
||||
@file:OptIn(DelicateCoroutinesApi::class)
|
||||
package kritor.client
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import io.grpc.CallOptions
|
||||
import io.grpc.Channel
|
||||
import io.grpc.ClientCall
|
||||
import io.grpc.ClientInterceptor
|
||||
import io.grpc.ForwardingClientCall
|
||||
import io.grpc.Metadata
|
||||
import io.grpc.ManagedChannel
|
||||
import io.grpc.ManagedChannelBuilder
|
||||
import io.grpc.MethodDescriptor
|
||||
import io.kritor.common.Request
|
||||
import io.kritor.common.Response
|
||||
import io.kritor.event.EventServiceGrpcKt
|
||||
import io.kritor.event.EventStructure
|
||||
import io.kritor.event.EventType
|
||||
import io.kritor.reverse.ReverseServiceGrpcKt
|
||||
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 moe.fuqiuluo.shamrock.tools.ShamrockVersion
|
||||
import qq.service.ticket.TicketHelper
|
||||
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()
|
||||
}
|
||||
val interceptor = object : ClientInterceptor {
|
||||
override fun <ReqT, RespT> interceptCall(method: MethodDescriptor<ReqT, RespT>, callOptions: CallOptions, next: Channel): ClientCall<ReqT, RespT> {
|
||||
return object : ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
|
||||
override fun start(responseListener: Listener<RespT>, headers: Metadata) {
|
||||
headers.merge(Metadata().apply {
|
||||
put(Metadata.Key.of("kritor-self-uin", Metadata.ASCII_STRING_MARSHALLER), TicketHelper.getUin())
|
||||
put(Metadata.Key.of("kritor-self-uid", Metadata.ASCII_STRING_MARSHALLER), TicketHelper.getUid())
|
||||
put(Metadata.Key.of("kritor-self-version", Metadata.ASCII_STRING_MARSHALLER), "OpenShamrock-$ShamrockVersion")
|
||||
})
|
||||
super.start(responseListener, headers)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
channel = ManagedChannelBuilder
|
||||
.forAddress(host, port)
|
||||
.usePlaintext()
|
||||
.enableRetry() // 允许尝试
|
||||
.executor(Dispatchers.IO.asExecutor()) // 使用协程的调度器
|
||||
.intercept(interceptor)
|
||||
.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.newBuilder().apply {
|
||||
this.type = EventType.EVENT_TYPE_MESSAGE
|
||||
this.message = it.second
|
||||
}.build())
|
||||
}
|
||||
EventType.EVENT_TYPE_CORE_EVENT -> {}
|
||||
EventType.EVENT_TYPE_NOTICE -> GlobalEventTransmitter.onNoticeEvent {
|
||||
send(EventStructure.newBuilder().apply {
|
||||
this.type = EventType.EVENT_TYPE_NOTICE
|
||||
this.notice = it
|
||||
}.build())
|
||||
}
|
||||
EventType.EVENT_TYPE_REQUEST -> GlobalEventTransmitter.onRequestEvent {
|
||||
send(EventStructure.newBuilder().apply {
|
||||
this.type = EventType.EVENT_TYPE_REQUEST
|
||||
this.request = it
|
||||
}.build())
|
||||
}
|
||||
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(Response.ResponseCode.SUCCESS)
|
||||
.setMsg("success")
|
||||
.setSeq(request.seq)
|
||||
.setBuf(ByteString.copyFrom(rsp))
|
||||
.build())
|
||||
}.onFailure {
|
||||
senderChannel.emit(Response.newBuilder()
|
||||
.setCmd(request.cmd)
|
||||
.setCode(Response.ResponseCode.INTERNAL)
|
||||
.setMsg(it.stackTraceToString())
|
||||
.setSeq(request.seq)
|
||||
.setBuf(ByteString.EMPTY)
|
||||
.build())
|
||||
}
|
||||
}
|
||||
|
||||
fun isActive(): Boolean {
|
||||
return !channel.isShutdown
|
||||
}
|
||||
|
||||
fun close() {
|
||||
channel.shutdown()
|
||||
}
|
||||
}
|
6
xposed/src/main/java/kritor/handlers/GrpcHandlers.kt
Normal file
6
xposed/src/main/java/kritor/handlers/GrpcHandlers.kt
Normal file
@ -0,0 +1,6 @@
|
||||
package kritor.handlers
|
||||
|
||||
internal object GrpcHandlers {
|
||||
|
||||
|
||||
}
|
@ -2,7 +2,12 @@
|
||||
package kritor.server
|
||||
|
||||
import io.grpc.Grpc
|
||||
import io.grpc.Metadata
|
||||
import io.grpc.InsecureServerCredentials
|
||||
import io.grpc.ServerCall
|
||||
import io.grpc.ServerCallHandler
|
||||
import io.grpc.ServerInterceptor
|
||||
import io.grpc.ForwardingServerCall.SimpleForwardingServerCall;
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@ -10,22 +15,45 @@ import kotlinx.coroutines.asExecutor
|
||||
import kritor.auth.AuthInterceptor
|
||||
import kritor.service.*
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
|
||||
import qq.service.ticket.TicketHelper
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class KritorServer(
|
||||
private val port: Int
|
||||
): CoroutineScope {
|
||||
|
||||
private val serverInterceptor = object : ServerInterceptor {
|
||||
override fun <ReqT, RespT> interceptCall(
|
||||
call: ServerCall<ReqT, RespT>, headers: Metadata, next: ServerCallHandler<ReqT, RespT>
|
||||
): ServerCall.Listener<ReqT> {
|
||||
return next.startCall(object : SimpleForwardingServerCall<ReqT, RespT>(call) {
|
||||
override fun sendHeaders(headers: Metadata?) {
|
||||
headers?.apply {
|
||||
put(Metadata.Key.of("kritor-self-uin", Metadata.ASCII_STRING_MARSHALLER), TicketHelper.getUin())
|
||||
put(Metadata.Key.of("kritor-self-uid", Metadata.ASCII_STRING_MARSHALLER), TicketHelper.getUid())
|
||||
put(Metadata.Key.of("kritor-self-version", Metadata.ASCII_STRING_MARSHALLER), "OpenShamrock-$ShamrockVersion")
|
||||
}
|
||||
super.sendHeaders(headers)
|
||||
}
|
||||
}, headers)
|
||||
}
|
||||
}
|
||||
|
||||
private val server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create())
|
||||
.executor(Dispatchers.IO.asExecutor())
|
||||
.intercept(AuthInterceptor)
|
||||
.addService(Authentication)
|
||||
.addService(ContactService)
|
||||
.addService(KritorService)
|
||||
.intercept(serverInterceptor)
|
||||
.addService(AuthenticationService)
|
||||
.addService(CoreService)
|
||||
.addService(FriendService)
|
||||
.addService(GroupService)
|
||||
.addService(GroupFileService)
|
||||
.addService(MessageService)
|
||||
.addService(EventService)
|
||||
.addService(WebService)
|
||||
.addService(DeveloperService)
|
||||
.addService(QsignService)
|
||||
.build()!!
|
||||
|
||||
fun start(block: Boolean = false) {
|
||||
|
@ -1,66 +0,0 @@
|
||||
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
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
60
xposed/src/main/java/kritor/service/AuthenticationService.kt
Normal file
60
xposed/src/main/java/kritor/service/AuthenticationService.kt
Normal file
@ -0,0 +1,60 @@
|
||||
package kritor.service
|
||||
|
||||
import io.grpc.Status
|
||||
import io.grpc.StatusRuntimeException
|
||||
import io.kritor.authentication.*
|
||||
import io.kritor.authentication.AuthenticateResponse.AuthenticateResponseCode
|
||||
import kritor.auth.AuthInterceptor
|
||||
import moe.fuqiuluo.shamrock.config.ActiveTicket
|
||||
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||
import qq.service.QQInterfaces
|
||||
|
||||
internal object AuthenticationService: AuthenticationServiceGrpcKt.AuthenticationServiceCoroutineImplBase() {
|
||||
@Grpc("AuthenticationService", "Authenticate")
|
||||
override suspend fun authenticate(request: AuthenticateRequest): AuthenticateResponse {
|
||||
if (QQInterfaces.app.account != request.account) {
|
||||
return AuthenticateResponse.newBuilder().apply {
|
||||
code = AuthenticateResponseCode.NO_ACCOUNT
|
||||
msg = "No such account"
|
||||
}.build()
|
||||
}
|
||||
|
||||
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 AuthenticateResponse.newBuilder().apply {
|
||||
code = AuthenticateResponseCode.OK
|
||||
msg = "OK"
|
||||
}.build()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else if (ticket == request.ticket) {
|
||||
return AuthenticateResponse.newBuilder().apply {
|
||||
code = AuthenticateResponseCode.OK
|
||||
msg = "OK"
|
||||
}.build()
|
||||
}
|
||||
index++
|
||||
}
|
||||
|
||||
return AuthenticateResponse.newBuilder().apply {
|
||||
code = AuthenticateResponseCode.NO_TICKET
|
||||
msg = "Invalid ticket"
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("AuthenticationService", "GetAuthenticationState")
|
||||
override suspend fun getAuthenticationState(request: GetAuthenticationStateRequest): GetAuthenticationStateResponse {
|
||||
if (request.account != QQInterfaces.app.account) {
|
||||
throw StatusRuntimeException(Status.CANCELLED.withDescription("No such account"))
|
||||
}
|
||||
|
||||
return GetAuthenticationStateResponse.newBuilder().apply {
|
||||
isRequired = AuthInterceptor.getAllTicket().isNotEmpty()
|
||||
}.build()
|
||||
}
|
||||
}
|
@ -1,183 +0,0 @@
|
||||
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
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
@ -4,60 +4,35 @@ 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.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.getVersionResponse
|
||||
import io.kritor.core.switchAccountResponse
|
||||
import io.kritor.core.*
|
||||
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 mqq.app.MobileQQ
|
||||
import qq.service.QQInterfaces
|
||||
import qq.service.QQInterfaces.Companion.app
|
||||
import qq.service.contact.ContactHelper
|
||||
import java.io.File
|
||||
|
||||
object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() {
|
||||
@Grpc("KritorService", "GetVersion")
|
||||
internal object CoreService : CoreServiceGrpcKt.CoreServiceCoroutineImplBase() {
|
||||
@Grpc("CoreService", "GetVersion")
|
||||
override suspend fun getVersion(request: GetVersionRequest): GetVersionResponse {
|
||||
return getVersionResponse {
|
||||
return GetVersionResponse.newBuilder().apply {
|
||||
this.version = ShamrockVersion
|
||||
this.appName = "Shamrock"
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("KritorService", "ClearCache")
|
||||
override suspend fun clearCache(request: ClearCacheRequest): ClearCacheResponse {
|
||||
FileUtils.clearCache()
|
||||
MMKVFetcher.mmkvWithId("audio2silk")
|
||||
.clear()
|
||||
return clearCacheResponse {}
|
||||
}
|
||||
|
||||
@Grpc("KritorService", "GetCurrentAccount")
|
||||
@Grpc("CoreService", "GetCurrentAccount")
|
||||
override suspend fun getCurrentAccount(request: GetCurrentAccountRequest): GetCurrentAccountResponse {
|
||||
return getCurrentAccountResponse {
|
||||
return GetCurrentAccountResponse.newBuilder().apply {
|
||||
this.accountName = if (app is QQAppInterface) app.currentNickname else "unknown"
|
||||
this.accountUid = app.currentUid ?: ""
|
||||
this.accountUin = (app.currentUin ?: "0").toLong()
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("KritorService", "DownloadFile")
|
||||
@Grpc("CoreService", "DownloadFile")
|
||||
override suspend fun downloadFile(request: DownloadFileRequest): DownloadFileResponse {
|
||||
val headerMap = mutableMapOf(
|
||||
"User-Agent" to "Shamrock"
|
||||
@ -76,13 +51,14 @@ object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() {
|
||||
if (request.hasBase64()) {
|
||||
val bytes = Base64.decode(request.base64, Base64.DEFAULT)
|
||||
tmp.writeBytes(bytes)
|
||||
} else if(request.hasUrl()) {
|
||||
if(!DownloadUtils.download(
|
||||
} 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"))
|
||||
}
|
||||
}
|
||||
@ -96,18 +72,22 @@ object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() {
|
||||
}
|
||||
}
|
||||
|
||||
return downloadFileResponse {
|
||||
return DownloadFileResponse.newBuilder().apply {
|
||||
this.fileMd5 = MD5.genFileMd5Hex(tmp.absolutePath)
|
||||
this.fileAbsolutePath = tmp.absolutePath
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("KritorService", "SwitchAccount")
|
||||
@Grpc("CoreService", "SwitchAccount")
|
||||
override suspend fun switchAccount(request: SwitchAccountRequest): SwitchAccountResponse {
|
||||
val uin = when(request.accountCase!!) {
|
||||
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"))
|
||||
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"))
|
||||
@ -116,6 +96,6 @@ object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() {
|
||||
}.onFailure {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withCause(it).withDescription("failed to switch account"))
|
||||
}
|
||||
return switchAccountResponse { }
|
||||
return SwitchAccountResponse.newBuilder().build()
|
||||
}
|
||||
}
|
67
xposed/src/main/java/kritor/service/DeveloperService.kt
Normal file
67
xposed/src/main/java/kritor/service/DeveloperService.kt
Normal file
@ -0,0 +1,67 @@
|
||||
package kritor.service
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import io.kritor.developer.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import moe.fuqiuluo.shamrock.utils.FileUtils
|
||||
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
|
||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||
import qq.service.QQInterfaces
|
||||
import java.io.File
|
||||
|
||||
internal object DeveloperService: DeveloperServiceGrpcKt.DeveloperServiceCoroutineImplBase() {
|
||||
@Grpc("DeveloperService", "Shell")
|
||||
override suspend fun shell(request: ShellRequest): ShellResponse {
|
||||
if (request.commandList.isEmpty()) return ShellResponse.newBuilder().setIsSuccess(false).build()
|
||||
val runtime = Runtime.getRuntime()
|
||||
val result = withTimeoutOrNull(5000L) {
|
||||
withContext(Dispatchers.IO) {
|
||||
runtime.exec(request.commandList.toTypedArray(), null, File(request.directory)).apply { waitFor() }
|
||||
}
|
||||
}
|
||||
return ShellResponse.newBuilder().apply {
|
||||
if (result == null) {
|
||||
isSuccess = false
|
||||
} else {
|
||||
isSuccess = true
|
||||
result.inputStream.use {
|
||||
data = it.readBytes().toString(Charsets.UTF_8)
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("DeveloperService", "ClearCache")
|
||||
override suspend fun clearCache(request: ClearCacheRequest): ClearCacheResponse {
|
||||
FileUtils.clearCache()
|
||||
MMKVFetcher.mmkvWithId("audio2silk")
|
||||
.clear()
|
||||
return ClearCacheResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("DeveloperService", "GetDeviceBattery")
|
||||
override suspend fun getDeviceBattery(request: GetDeviceBatteryRequest): GetDeviceBatteryResponse {
|
||||
return GetDeviceBatteryResponse.newBuilder().apply {
|
||||
PlatformUtils.getDeviceBattery().let {
|
||||
this.battery = it.battery
|
||||
this.scale = it.scale
|
||||
this.status = it.status
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("DeveloperService", "SendPacket")
|
||||
override suspend fun sendPacket(request: SendPacketRequest): SendPacketResponse {
|
||||
return SendPacketResponse.newBuilder().apply {
|
||||
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)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
@ -2,39 +2,40 @@ 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
|
||||
|
||||
object EventService: EventServiceGrpcKt.EventServiceCoroutineImplBase() {
|
||||
internal object EventService : EventServiceGrpcKt.EventServiceCoroutineImplBase() {
|
||||
override fun registerActiveListener(request: RequestPushEvent): Flow<EventStructure> {
|
||||
return channelFlow {
|
||||
when(request.type!!) {
|
||||
when (request.type!!) {
|
||||
EventType.EVENT_TYPE_CORE_EVENT -> {}
|
||||
EventType.EVENT_TYPE_MESSAGE -> GlobalEventTransmitter.onMessageEvent {
|
||||
send(eventStructure {
|
||||
send(EventStructure.newBuilder().apply {
|
||||
this.type = EventType.EVENT_TYPE_MESSAGE
|
||||
this.message = it.second
|
||||
})
|
||||
}.build())
|
||||
}
|
||||
|
||||
EventType.EVENT_TYPE_NOTICE -> GlobalEventTransmitter.onRequestEvent {
|
||||
send(eventStructure {
|
||||
send(EventStructure.newBuilder().apply {
|
||||
this.type = EventType.EVENT_TYPE_NOTICE
|
||||
this.request = it
|
||||
})
|
||||
}.build())
|
||||
}
|
||||
|
||||
EventType.EVENT_TYPE_REQUEST -> GlobalEventTransmitter.onNoticeEvent {
|
||||
send(eventStructure {
|
||||
send(EventStructure.newBuilder().apply {
|
||||
this.type = EventType.EVENT_TYPE_NOTICE
|
||||
this.notice = it
|
||||
})
|
||||
}.build())
|
||||
}
|
||||
|
||||
EventType.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT)
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,33 @@
|
||||
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.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 io.kritor.friend.*
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import qq.service.QQInterfaces
|
||||
import qq.service.contact.ContactHelper
|
||||
import qq.service.friend.FriendHelper
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
object FriendService: FriendServiceGrpcKt.FriendServiceCoroutineImplBase() {
|
||||
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())
|
||||
val friendList = FriendHelper.getFriendList(if (request.hasRefresh()) request.refresh else false).onFailure {
|
||||
throw StatusRuntimeException(
|
||||
Status.INTERNAL
|
||||
.withDescription(it.stackTraceToString())
|
||||
)
|
||||
}.getOrThrow()
|
||||
|
||||
return getFriendListResponse {
|
||||
return GetFriendListResponse.newBuilder().apply {
|
||||
friendList.forEach {
|
||||
this.friendList.add(friendData {
|
||||
this.addFriendsInfo(FriendInfo.newBuilder().apply {
|
||||
uin = it.uin.toLong()
|
||||
uid = ContactHelper.getUidByUinAsync(uin)
|
||||
qid = ""
|
||||
@ -32,10 +37,208 @@ object FriendService: FriendServiceGrpcKt.FriendServiceCoroutineImplBase() {
|
||||
level = 0
|
||||
gender = it.gender.toInt()
|
||||
groupId = it.groupid
|
||||
ext = friendExt {}.toByteString()
|
||||
|
||||
ext = ExtInfo.newBuilder().build()
|
||||
})
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("FriendService", "GetFriendProfileCard")
|
||||
override suspend fun getFriendProfileCard(request: GetFriendProfileCardRequest): GetFriendProfileCardResponse {
|
||||
return GetFriendProfileCardResponse.newBuilder().apply {
|
||||
request.targetUinsList.forEach {
|
||||
ContactHelper.getProfileCard(it).getOrThrow().let { info ->
|
||||
addFriendsProfileCard(ProfileCard.newBuilder().apply {
|
||||
this.uin = info.uin.toLong()
|
||||
this.uid = ContactHelper.getUidByUinAsync(info.uin.toLong())
|
||||
this.nick = info.strNick
|
||||
this.remark = info.strReMark
|
||||
this.level = info.iQQLevel
|
||||
this.birthday = info.lBirthday
|
||||
this.loginDay = info.lLoginDays.toInt()
|
||||
this.voteCnt = info.lVoteCount.toInt()
|
||||
this.qid = info.qid ?: ""
|
||||
this.isSchoolVerified = info.schoolVerifiedFlag
|
||||
|
||||
this.ext = ExtInfo.newBuilder().apply {
|
||||
this.bigVip = info.bBigClubVipOpen == 1.toByte()
|
||||
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
|
||||
this.qqVip = info.bQQVipOpen == 1.toByte()
|
||||
this.superVip = info.bSuperQQOpen == 1.toByte()
|
||||
this.voted = info.bVoted == 1.toByte()
|
||||
}.build()
|
||||
}.build())
|
||||
}
|
||||
}
|
||||
request.targetUidsList.forEach {
|
||||
ContactHelper.getProfileCard(ContactHelper.getUinByUidAsync(it).toLong()).getOrThrow().let { info ->
|
||||
addFriendsProfileCard(ProfileCard.newBuilder().apply {
|
||||
this.uin = info.uin.toLong()
|
||||
this.uid = it
|
||||
this.nick = info.strNick
|
||||
this.remark = info.strReMark
|
||||
this.level = info.iQQLevel
|
||||
this.birthday = info.lBirthday
|
||||
this.loginDay = info.lLoginDays.toInt()
|
||||
this.voteCnt = info.lVoteCount.toInt()
|
||||
this.qid = info.qid ?: ""
|
||||
this.isSchoolVerified = info.schoolVerifiedFlag
|
||||
|
||||
this.ext = ExtInfo.newBuilder().apply {
|
||||
this.bigVip = info.bBigClubVipOpen == 1.toByte()
|
||||
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
|
||||
this.qqVip = info.bQQVipOpen == 1.toByte()
|
||||
this.superVip = info.bSuperQQOpen == 1.toByte()
|
||||
this.voted = info.bVoted == 1.toByte()
|
||||
}.build()
|
||||
}.build())
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("FriendService", "GetStrangerProfileCard")
|
||||
override suspend fun getStrangerProfileCard(request: GetStrangerProfileCardRequest): GetStrangerProfileCardResponse {
|
||||
return GetStrangerProfileCardResponse.newBuilder().apply {
|
||||
request.targetUinsList.forEach {
|
||||
ContactHelper.refreshAndGetProfileCard(it).getOrThrow().let { info ->
|
||||
addStrangersProfileCard(ProfileCard.newBuilder().apply {
|
||||
this.uin = info.uin.toLong()
|
||||
this.uid = ContactHelper.getUidByUinAsync(info.uin.toLong())
|
||||
this.nick = info.strNick
|
||||
this.level = info.iQQLevel
|
||||
this.birthday = info.lBirthday
|
||||
this.loginDay = info.lLoginDays.toInt()
|
||||
this.voteCnt = info.lVoteCount.toInt()
|
||||
this.qid = info.qid ?: ""
|
||||
this.isSchoolVerified = info.schoolVerifiedFlag
|
||||
|
||||
this.ext = ExtInfo.newBuilder().apply {
|
||||
this.bigVip = info.bBigClubVipOpen == 1.toByte()
|
||||
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
|
||||
this.qqVip = info.bQQVipOpen == 1.toByte()
|
||||
this.superVip = info.bSuperQQOpen == 1.toByte()
|
||||
this.voted = info.bVoted == 1.toByte()
|
||||
}.build()
|
||||
}.build())
|
||||
}
|
||||
}
|
||||
request.targetUidsList.forEach {
|
||||
ContactHelper.refreshAndGetProfileCard(ContactHelper.getUinByUidAsync(it).toLong()).getOrThrow()
|
||||
.let { info ->
|
||||
addStrangersProfileCard(ProfileCard.newBuilder().apply {
|
||||
this.uin = info.uin.toLong()
|
||||
this.uid = it
|
||||
this.nick = info.strNick
|
||||
this.level = info.iQQLevel
|
||||
this.birthday = info.lBirthday
|
||||
this.loginDay = info.lLoginDays.toInt()
|
||||
this.voteCnt = info.lVoteCount.toInt()
|
||||
this.qid = info.qid ?: ""
|
||||
this.isSchoolVerified = info.schoolVerifiedFlag
|
||||
|
||||
this.ext = ExtInfo.newBuilder().apply {
|
||||
this.bigVip = info.bBigClubVipOpen == 1.toByte()
|
||||
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
|
||||
this.qqVip = info.bQQVipOpen == 1.toByte()
|
||||
this.superVip = info.bSuperQQOpen == 1.toByte()
|
||||
this.voted = info.bVoted == 1.toByte()
|
||||
}.build()
|
||||
}.build())
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("FriendService", "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(IProfileProtocolConst.KEY_NICK, request.nickName)
|
||||
}
|
||||
if (request.hasCompany()) {
|
||||
bundle.putString(IProfileProtocolConst.KEY_COMPANY, request.company)
|
||||
}
|
||||
if (request.hasEmail()) {
|
||||
bundle.putString(IProfileProtocolConst.KEY_EMAIL, request.email)
|
||||
}
|
||||
if (request.hasCollege()) {
|
||||
bundle.putString(IProfileProtocolConst.KEY_COLLEGE, request.college)
|
||||
}
|
||||
if (request.hasPersonalNote()) {
|
||||
bundle.putString(IProfileProtocolConst.KEY_PERSONAL_NOTE, request.personalNote)
|
||||
}
|
||||
|
||||
if (request.hasBirthday()) {
|
||||
bundle.putInt(IProfileProtocolConst.KEY_BIRTHDAY, request.birthday)
|
||||
}
|
||||
if (request.hasAge()) {
|
||||
bundle.putInt(IProfileProtocolConst.KEY_AGE, request.age)
|
||||
}
|
||||
|
||||
service.setProfileDetail(bundle)
|
||||
return super.setProfileCard(request)
|
||||
}
|
||||
|
||||
@Grpc("FriendService", "IsBlackListUser")
|
||||
override suspend fun isBlackListUser(request: IsBlackListUserRequest): IsBlackListUserResponse {
|
||||
val uin = when (request.targetCase!!) {
|
||||
IsBlackListUserRequest.TargetCase.TARGET_UIN -> request.targetUin.toString()
|
||||
IsBlackListUserRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid)
|
||||
IsBlackListUserRequest.TargetCase.TARGET_NOT_SET -> throw StatusRuntimeException(
|
||||
Status.INVALID_ARGUMENT
|
||||
.withDescription("account not set")
|
||||
)
|
||||
}
|
||||
val blacklistApi = QRoute.api(IProfileCardBlacklistApi::class.java)
|
||||
val isBlack = withTimeoutOrNull(5000) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
blacklistApi.isBlackOrBlackedUin(uin) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
}
|
||||
} ?: false
|
||||
return IsBlackListUserResponse.newBuilder().setIsBlackListUser(isBlack).build()
|
||||
}
|
||||
|
||||
@Grpc("FriendService", "VoteUser")
|
||||
override suspend fun voteUser(request: VoteUserRequest): VoteUserResponse {
|
||||
ContactHelper.voteUser(
|
||||
when (request.targetCase!!) {
|
||||
VoteUserRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||
VoteUserRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
|
||||
VoteUserRequest.TargetCase.TARGET_NOT_SET -> throw StatusRuntimeException(
|
||||
Status.INVALID_ARGUMENT
|
||||
.withDescription("account not set")
|
||||
)
|
||||
}, request.voteCount
|
||||
).onFailure {
|
||||
throw StatusRuntimeException(
|
||||
Status.INTERNAL
|
||||
.withDescription(it.stackTraceToString())
|
||||
)
|
||||
}
|
||||
return VoteUserResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("FriendService", "GetUidByUin")
|
||||
override suspend fun getUidByUin(request: GetUidByUinRequest): GetUidByUinResponse {
|
||||
return GetUidByUinResponse.newBuilder().apply {
|
||||
request.targetUinsList.forEach {
|
||||
putUidMap(it, ContactHelper.getUidByUinAsync(it))
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("FriendService", "GetUinByUid")
|
||||
override suspend fun getUinByUid(request: GetUinByUidRequest): GetUinByUidResponse {
|
||||
return GetUinByUidResponse.newBuilder().apply {
|
||||
request.targetUidsList.forEach {
|
||||
putUinMap(it, ContactHelper.getUinByUidAsync(it).toLong())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
@ -15,10 +15,9 @@ 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() {
|
||||
internal object GroupFileService : GroupFileServiceGrpcKt.GroupFileServiceCoroutineImplBase() {
|
||||
@Grpc("GroupFileService", "CreateFolder")
|
||||
override suspend fun createFolder(request: CreateFolderRequest): CreateFolderResponse {
|
||||
val data = Oidb0x6d7ReqBody(
|
||||
@ -42,21 +41,23 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
|
||||
if (rsp.createFolder?.retCode != 0) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to create folder: ${rsp.createFolder?.retCode}"))
|
||||
}
|
||||
return createFolderResponse {
|
||||
return CreateFolderResponse.newBuilder().apply {
|
||||
this.id = rsp.createFolder?.folderInfo?.folderId ?: ""
|
||||
this.usedSpace = 0
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@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"))
|
||||
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"))
|
||||
}
|
||||
@ -66,7 +67,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
|
||||
if (rsp.deleteFolder?.retCode != 0) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to delete folder: ${rsp.deleteFolder?.retCode}"))
|
||||
}
|
||||
return deleteFolderResponse { }
|
||||
return DeleteFolderResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupFileService", "DeleteFile")
|
||||
@ -93,19 +94,21 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
|
||||
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 { }
|
||||
return DeleteFileResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@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"))
|
||||
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"))
|
||||
}
|
||||
@ -115,24 +118,19 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
|
||||
if (rsp.renameFolder?.retCode != 0) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to rename folder: ${rsp.renameFolder?.retCode}"))
|
||||
}
|
||||
return renameFolderResponse { }
|
||||
return RenameFolderResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@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)
|
||||
@Grpc("GroupFileService", "GetFileList")
|
||||
override suspend fun getFileList(request: GetFileListRequest): GetFileListResponse {
|
||||
return if (request.hasFolderId())
|
||||
GroupFileHelper.getGroupFiles(request.groupId, request.folderId)
|
||||
else
|
||||
GroupFileHelper.getGroupFiles(request.groupId)
|
||||
}
|
||||
}
|
@ -2,213 +2,187 @@ 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
|
||||
import io.kritor.group.*
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
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() {
|
||||
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")
|
||||
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)
|
||||
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 {
|
||||
return BanMemberResponse.newBuilder().apply {
|
||||
groupId = request.groupId
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@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 { }
|
||||
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.newBuilder().build()
|
||||
}
|
||||
|
||||
@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")
|
||||
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 { }
|
||||
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.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "LeaveGroup")
|
||||
override suspend fun leaveGroup(request: LeaveGroupRequest): LeaveGroupResponse {
|
||||
GroupHelper.resignTroop(request.groupId.toString())
|
||||
return leaveGroupResponse { }
|
||||
return LeaveGroupResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@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")
|
||||
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 { }
|
||||
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.newBuilder().build()
|
||||
}
|
||||
|
||||
@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")
|
||||
throw StatusRuntimeException(
|
||||
Status.PERMISSION_DENIED
|
||||
.withDescription("You are not admin of this group")
|
||||
)
|
||||
}
|
||||
|
||||
GroupHelper.modifyTroopName(request.groupId.toString(), request.groupName)
|
||||
|
||||
return modifyGroupNameResponse { }
|
||||
return ModifyGroupNameResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "ModifyGroupRemark")
|
||||
override suspend fun modifyGroupRemark(request: ModifyGroupRemarkRequest): ModifyGroupRemarkResponse {
|
||||
GroupHelper.modifyGroupRemark(request.groupId, request.remark)
|
||||
|
||||
return modifyGroupRemarkResponse { }
|
||||
return ModifyGroupRemarkResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@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")
|
||||
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)
|
||||
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 { }
|
||||
return SetGroupAdminResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@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")
|
||||
throw StatusRuntimeException(
|
||||
Status.PERMISSION_DENIED
|
||||
.withDescription("You are not admin of this group")
|
||||
)
|
||||
}
|
||||
|
||||
GroupHelper.setGroupUniqueTitle(request.groupId, 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")
|
||||
)
|
||||
}, request.uniqueTitle)
|
||||
GroupHelper.setGroupUniqueTitle(
|
||||
request.groupId.toString(), when (request.targetCase!!) {
|
||||
SetGroupUniqueTitleRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||
SetGroupUniqueTitleRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid)
|
||||
.toLong()
|
||||
|
||||
return setGroupUniqueTitleResponse { }
|
||||
else -> throw StatusRuntimeException(
|
||||
Status.INVALID_ARGUMENT
|
||||
.withDescription("target not set")
|
||||
)
|
||||
}.toString(), request.uniqueTitle
|
||||
)
|
||||
|
||||
return SetGroupUniqueTitleResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@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")
|
||||
throw StatusRuntimeException(
|
||||
Status.PERMISSION_DENIED
|
||||
.withDescription("You are not admin of this group")
|
||||
)
|
||||
}
|
||||
|
||||
GroupHelper.setGroupWholeBan(request.groupId, request.isBan)
|
||||
return setGroupWholeBanResponse { }
|
||||
return SetGroupWholeBanResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "GetGroupInfo")
|
||||
@ -216,18 +190,20 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
|
||||
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 {
|
||||
return GetGroupInfoResponse.newBuilder().apply {
|
||||
this.groupInfo = GroupInfo.newBuilder().apply {
|
||||
groupId = groupInfo.troopcode.toLong()
|
||||
groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName } ?: ""
|
||||
groupName =
|
||||
groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName }
|
||||
?: ""
|
||||
groupRemark = groupInfo.troopRemark ?: ""
|
||||
owner = groupInfo.troopowneruin?.toLong() ?: 0
|
||||
admins.addAll(GroupHelper.getAdminList(groupId))
|
||||
addAllAdmins(GroupHelper.getAdminList(groupId))
|
||||
maxMemberCount = groupInfo.wMemberMax
|
||||
memberCount = groupInfo.wMemberNum
|
||||
groupUin = groupInfo.troopuin?.toLong() ?: 0
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "GetGroupList")
|
||||
@ -235,36 +211,46 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
|
||||
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 {
|
||||
return GetGroupListResponse.newBuilder().apply {
|
||||
groupList.forEach { groupInfo ->
|
||||
this.groupInfo.add(io.kritor.group.groupInfo {
|
||||
groupId = groupInfo.troopcode.toLong()
|
||||
groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName } ?: ""
|
||||
this.addGroupsInfo(GroupInfo.newBuilder().apply {
|
||||
groupId = groupInfo.troopcode.ifNullOrEmpty { groupInfo.uin }.ifNullOrEmpty { groupInfo.troopuin }?.toLong() ?: 0
|
||||
groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }
|
||||
.ifNullOrEmpty { groupInfo.newTroopName }
|
||||
?: ""
|
||||
groupRemark = groupInfo.troopRemark ?: ""
|
||||
owner = groupInfo.troopowneruin?.toLong() ?: 0
|
||||
admins.addAll(GroupHelper.getAdminList(groupId))
|
||||
addAllAdmins(GroupHelper.getAdminList(groupId))
|
||||
maxMemberCount = groupInfo.wMemberMax
|
||||
memberCount = groupInfo.wMemberNum
|
||||
groupUin = groupInfo.troopuin?.toLong() ?: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "GetGroupMemberInfo")
|
||||
override suspend fun getGroupMemberInfo(request: GetGroupMemberInfoRequest): GetGroupMemberInfoResponse {
|
||||
val memberInfo = GroupHelper.getTroopMemberInfoByUin(request.groupId, 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")
|
||||
val memberInfo = GroupHelper.getTroopMemberInfoByUin(
|
||||
request.groupId.toString(), when (request.targetCase!!) {
|
||||
GetGroupMemberInfoRequest.TargetCase.TARGET_UID -> request.targetUin
|
||||
GetGroupMemberInfoRequest.TargetCase.TARGET_UIN -> ContactHelper.getUinByUidAsync(request.targetUid).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)
|
||||
)
|
||||
}).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)
|
||||
return GetGroupMemberInfoResponse.newBuilder().apply {
|
||||
groupMemberInfo = GroupMemberInfo.newBuilder().apply {
|
||||
uid =
|
||||
if (request.targetCase == GetGroupMemberInfoRequest.TargetCase.TARGET_UID) request.targetUid else ContactHelper.getUidByUinAsync(
|
||||
request.targetUin
|
||||
)
|
||||
uin = memberInfo.memberuin?.toLong() ?: 0
|
||||
nick = memberInfo.troopnick
|
||||
.ifNullOrEmpty { memberInfo.hwName }
|
||||
@ -280,24 +266,29 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
|
||||
shutUpTimestamp = memberInfo.gagTimeStamp
|
||||
|
||||
distance = memberInfo.distance
|
||||
honor.addAll((memberInfo.honorList ?: "")
|
||||
addAllHonors((memberInfo.honorList ?: "")
|
||||
.split("|")
|
||||
.filter { it.isNotBlank() }
|
||||
.map { it.toInt() })
|
||||
unfriendly = false
|
||||
cardChangeable = GroupHelper.isAdmin(request.groupId.toString())
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
@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))
|
||||
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 {
|
||||
return GetGroupMemberListResponse.newBuilder().apply {
|
||||
memberList.forEach { memberInfo ->
|
||||
this.groupMemberInfo.add(groupMemberInfo {
|
||||
this.addGroupMembersInfo(GroupMemberInfo.newBuilder().apply {
|
||||
uid = ContactHelper.getUidByUinAsync(memberInfo.memberuin?.toLong() ?: 0)
|
||||
uin = memberInfo.memberuin?.toLong() ?: 0
|
||||
nick = memberInfo.troopnick
|
||||
@ -314,7 +305,7 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
|
||||
shutUpTimestamp = memberInfo.gagTimeStamp
|
||||
|
||||
distance = memberInfo.distance
|
||||
honor.addAll((memberInfo.honorList ?: "")
|
||||
addAllHonors((memberInfo.honorList ?: "")
|
||||
.split("|")
|
||||
.filter { it.isNotBlank() }
|
||||
.map { it.toInt() })
|
||||
@ -322,23 +313,25 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
|
||||
cardChangeable = GroupHelper.isAdmin(request.groupId.toString())
|
||||
})
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@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))
|
||||
throw StatusRuntimeException(
|
||||
Status.INTERNAL.withDescription("unable to get prohibited user list").withCause(it)
|
||||
)
|
||||
}.getOrThrow()
|
||||
return getProhibitedUserListResponse {
|
||||
return GetProhibitedUserListResponse.newBuilder().apply {
|
||||
prohibitedList.forEach {
|
||||
this.prohibitedUserInfo.add(prohibitedUserInfo {
|
||||
this.addProhibitedUsersInfo(ProhibitedUserInfo.newBuilder().apply {
|
||||
uid = ContactHelper.getUidByUinAsync(it.memberUin)
|
||||
uin = it.memberUin
|
||||
prohibitedTime = it.shutuptimestap
|
||||
})
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "GetRemainCountAtAll")
|
||||
@ -346,20 +339,22 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
|
||||
val remainAtAllRsp = GroupHelper.getGroupRemainAtAllRemain(request.groupId).onFailure {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get remain count").withCause(it))
|
||||
}.getOrThrow()
|
||||
return getRemainCountAtAllResponse {
|
||||
return GetRemainCountAtAllResponse.newBuilder().apply {
|
||||
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()
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@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))
|
||||
throw StatusRuntimeException(
|
||||
Status.INTERNAL.withDescription("unable to get not joined group info").withCause(it)
|
||||
)
|
||||
}.getOrThrow()
|
||||
return getNotJoinedGroupInfoResponse {
|
||||
this.groupInfo = notJoinedGroupInfo {
|
||||
return GetNotJoinedGroupInfoResponse.newBuilder().apply {
|
||||
this.groupInfo = NotJoinedGroupInfo.newBuilder().apply {
|
||||
groupId = groupInfo.groupId
|
||||
groupName = groupInfo.groupName
|
||||
owner = groupInfo.owner
|
||||
@ -369,15 +364,17 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
|
||||
createTime = groupInfo.createTime.toInt()
|
||||
groupFlag = groupInfo.groupFlag
|
||||
groupFlagExt = groupInfo.groupFlagExt
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "GetGroupHonor")
|
||||
override suspend fun getGroupHonor(request: GetGroupHonorRequest): GetGroupHonorResponse {
|
||||
return getGroupHonorResponse {
|
||||
return GetGroupHonorResponse.newBuilder().apply {
|
||||
GroupHelper.getGroupMemberList(request.groupId.toString(), true).onFailure {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member list").withCause(it))
|
||||
throw StatusRuntimeException(
|
||||
Status.INTERNAL.withDescription("unable to get group member list").withCause(it)
|
||||
)
|
||||
}.onSuccess { memberList ->
|
||||
memberList.forEach { member ->
|
||||
(member.honorList ?: "").split("|")
|
||||
@ -385,7 +382,7 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
|
||||
.map { it.toInt() }.forEach {
|
||||
val honor = decodeHonor(member.memberuin.toLong(), it, member.mHonorRichFlag)
|
||||
if (honor != null) {
|
||||
groupHonorInfo.add(groupHonorInfo {
|
||||
addGroupHonorsInfo(GroupHonorInfo.newBuilder().apply {
|
||||
uid = ContactHelper.getUidByUinAsync(member.memberuin.toLong())
|
||||
uin = member.memberuin.toLong()
|
||||
nick = member.troopnick
|
||||
@ -401,6 +398,6 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
@ -1,7 +1,470 @@
|
||||
package kritor.service
|
||||
|
||||
import io.kritor.message.MessageServiceGrpcKt
|
||||
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.common.*
|
||||
import io.kritor.message.*
|
||||
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.*
|
||||
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.*
|
||||
import qq.service.msg.ForwardMessageHelper
|
||||
import qq.service.msg.MessageHelper
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextUInt
|
||||
|
||||
internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImplBase() {
|
||||
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.newBuilder().apply {
|
||||
this.messageId = MessageHelper.sendMessage(
|
||||
contact,
|
||||
NtMsgConvertor.convertToNtMsgs(contact, uniseq, request.elementsList),
|
||||
request.retryCount,
|
||||
uniseq
|
||||
).onFailure {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
|
||||
}.getOrThrow().toString()
|
||||
}.build()
|
||||
}
|
||||
|
||||
@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.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "SetMessageReaded")
|
||||
override suspend fun setMessageReaded(request: SetMessageReadRequest): SetMessageReadResponse {
|
||||
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 SetMessageReadResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@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.toLong())) { code, msg ->
|
||||
if (code != 0) {
|
||||
LogCenter.log("消息撤回失败: $code:$msg", Level.WARN)
|
||||
}
|
||||
}
|
||||
|
||||
return RecallMessageResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@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.toLong())) { 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.newBuilder().apply {
|
||||
this.message = PushMessageBody.newBuilder().apply {
|
||||
this.messageId = msg.msgId.toString()
|
||||
this.contact = request.contact
|
||||
this.sender = Sender.newBuilder().apply {
|
||||
this.uid = msg.senderUid ?: ""
|
||||
this.uin = msg.senderUin
|
||||
this.nick = msg.sendNickName ?: ""
|
||||
}.build()
|
||||
this.messageSeq = msg.msgSeq
|
||||
this.addAllElements(msg.elements.toKritorReqMessages(contact))
|
||||
}.build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
@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.newBuilder().apply {
|
||||
this.message = PushMessageBody.newBuilder().apply {
|
||||
this.messageId = msg.msgId.toString()
|
||||
this.contact = request.contact
|
||||
this.sender = Sender.newBuilder().apply {
|
||||
this.uin = msg.senderUin
|
||||
this.nick = msg.sendNickName ?: ""
|
||||
this.uid = msg.senderUid ?: ""
|
||||
}.build()
|
||||
this.messageSeq = msg.msgSeq
|
||||
this.addAllElements(msg.elements.toKritorReqMessages(contact))
|
||||
}.build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
@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.toLong(), 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.newBuilder().apply {
|
||||
msgs.forEach {
|
||||
addMessages(PushMessageBody.newBuilder().apply {
|
||||
this.messageId = it.msgId.toString()
|
||||
this.contact = request.contact
|
||||
this.sender = Sender.newBuilder().apply {
|
||||
this.uin = it.senderUin
|
||||
this.nick = it.sendNickName ?: ""
|
||||
this.uid = it.senderUid ?: ""
|
||||
}.build()
|
||||
this.messageSeq = it.msgSeq
|
||||
this.addAllElements(it.elements.toKritorReqMessages(contact))
|
||||
})
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "UploadForwardMessage")
|
||||
override suspend fun uploadForwardMessage(request: UploadForwardMessageRequest): UploadForwardMessageResponse {
|
||||
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,
|
||||
request.messagesList
|
||||
).onFailure {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
|
||||
}.getOrThrow()
|
||||
|
||||
return UploadForwardMessageResponse.newBuilder().apply {
|
||||
this.resId = forwardMessage.resId
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "DownloadForwardMessage")
|
||||
override suspend fun downloadForwardMessage(request: DownloadForwardMessageRequest): DownloadForwardMessageResponse {
|
||||
return DownloadForwardMessageResponse.newBuilder().apply {
|
||||
this.addAllMessages(
|
||||
MessageHelper.getForwardMsg(request.resId).onFailure {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
|
||||
}.getOrThrow().map { detail ->
|
||||
PushMessageBody.newBuilder().apply {
|
||||
this.time = detail.time
|
||||
this.messageId = detail.qqMsgId.toString()
|
||||
this.messageSeq = detail.msgSeq
|
||||
this.contact = io.kritor.common.Contact.newBuilder().apply {
|
||||
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.peer = detail.peerId.toString()
|
||||
}.build()
|
||||
this.sender = Sender.newBuilder().apply {
|
||||
this.uin = detail.sender.userId
|
||||
this.nick = detail.sender.nickName
|
||||
this.uid = detail.sender.uid
|
||||
}.build()
|
||||
detail.message?.elements?.toKritorResponseMessages(
|
||||
com.tencent.qqnt.kernel.nativeinterface.Contact(
|
||||
detail.msgType,
|
||||
detail.peerId.toString(),
|
||||
null
|
||||
)
|
||||
)?.let {
|
||||
this.addAllElements(it)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "DeleteEssenceMessage")
|
||||
override suspend fun deleteEssenceMessage(request: DeleteEssenceMessageRequest): DeleteEssenceMessageResponse {
|
||||
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.toLong())) { 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 DeleteEssenceMessageResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "GetEssenceMessageList")
|
||||
override suspend fun getEssenceMessageList(request: GetEssenceMessageListRequest): GetEssenceMessageListResponse {
|
||||
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString())
|
||||
return GetEssenceMessageListResponse.newBuilder().apply {
|
||||
MessageHelper.getEssenceMessageList(request.groupId, request.page, request.pageSize).onFailure {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
|
||||
}.getOrThrow().forEach {
|
||||
addMessages(EssenceMessageBody.newBuilder().apply {
|
||||
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.toString()
|
||||
}
|
||||
this.messageSeq = it.messageSeq
|
||||
this.messageTime = 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()
|
||||
})
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@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.toLong())) { 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.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "ReactMessageWithEmoji")
|
||||
override suspend fun reactMessageWithEmoji(request: ReactMessageWithEmojiRequest): ReactMessageWithEmojiResponse {
|
||||
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.toLong())) { 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.isSet
|
||||
)
|
||||
return ReactMessageWithEmojiResponse.newBuilder().build()
|
||||
}
|
||||
}
|
33
xposed/src/main/java/kritor/service/QsignService.kt
Normal file
33
xposed/src/main/java/kritor/service/QsignService.kt
Normal file
@ -0,0 +1,33 @@
|
||||
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.*
|
||||
|
||||
|
||||
internal object QsignService: QsignServiceGrpcKt.QsignServiceCoroutineImplBase() {
|
||||
@Grpc("QsignService", "Sign")
|
||||
override suspend fun sign(request: SignRequest): SignResponse {
|
||||
return SignResponse.newBuilder().apply {
|
||||
val result = FEKit.getInstance().getSign(request.command, request.buffer.toByteArray(), request.seq, request.uin)
|
||||
this.secSig = ByteString.copyFrom(result.sign)
|
||||
this.secDeviceToken = ByteString.copyFrom(result.token)
|
||||
this.secExtra = ByteString.copyFrom(result.extra)
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("QsignService", "Energy")
|
||||
override suspend fun energy(request: EnergyRequest): EnergyResponse {
|
||||
return EnergyResponse.newBuilder().apply {
|
||||
this.result = ByteString.copyFrom(Dandelion.getInstance().fly(request.data, request.salt.toByteArray()))
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("QsignService", "GetCmdWhitelist")
|
||||
override suspend fun getCmdWhitelist(request: GetCmdWhitelistRequest): GetCmdWhitelistResponse {
|
||||
return GetCmdWhitelistResponse.newBuilder().apply {
|
||||
addAllCommands(FEKit.getInstance().cmdWhiteList)
|
||||
}.build()
|
||||
}
|
||||
}
|
58
xposed/src/main/java/kritor/service/WebService.kt
Normal file
58
xposed/src/main/java/kritor/service/WebService.kt
Normal file
@ -0,0 +1,58 @@
|
||||
package kritor.service
|
||||
|
||||
import io.grpc.Status
|
||||
import io.grpc.StatusRuntimeException
|
||||
import io.kritor.web.*
|
||||
import qq.service.ticket.TicketHelper
|
||||
|
||||
internal object WebService: WebServiceGrpcKt.WebServiceCoroutineImplBase() {
|
||||
@Grpc("WebService", "GetCookies")
|
||||
override suspend fun getCookies(request: GetCookiesRequest): GetCookiesResponse {
|
||||
return GetCookiesResponse.newBuilder().apply {
|
||||
if (request.domain.isNullOrEmpty()) {
|
||||
this.cookie = TicketHelper.getCookie()
|
||||
} else {
|
||||
this.cookie = TicketHelper.getCookie(request.domain)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("WebService", "GetCredentials")
|
||||
override suspend fun getCredentials(request: GetCredentialsRequest): GetCredentialsResponse {
|
||||
return GetCredentialsResponse.newBuilder().apply {
|
||||
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)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("WebService", "GetCSRFToken")
|
||||
override suspend fun getCSRFToken(request: GetCSRFTokenRequest): GetCSRFTokenResponse {
|
||||
return GetCSRFTokenResponse.newBuilder().apply {
|
||||
if (request.domain.isNullOrEmpty()) {
|
||||
this.bkn = TicketHelper.getCSRF()
|
||||
} else {
|
||||
this.bkn = TicketHelper.getCSRF(TicketHelper.getUin(), request.domain)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("WebService", "GetHttpCookies")
|
||||
override suspend fun getHttpCookies(request: GetHttpCookiesRequest): GetHttpCookiesResponse {
|
||||
return GetHttpCookiesResponse.newBuilder().apply {
|
||||
this.cookie = TicketHelper.getHttpCookies(request.appid, request.daid, request.jumpUrl)
|
||||
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get http cookies"))
|
||||
}.build()
|
||||
}
|
||||
}
|
@ -2,5 +2,5 @@ package moe.fuqiuluo.shamrock.config
|
||||
|
||||
object EnableOldBDH: ConfigKey<Boolean>() {
|
||||
override fun name() = "enable_old_bdh"
|
||||
override fun default() = false
|
||||
override fun default() = true
|
||||
}
|
@ -14,7 +14,10 @@ import moe.fuqiuluo.shamrock.tools.toast
|
||||
import moe.fuqiuluo.shamrock.xposed.helper.AppTalker
|
||||
import mqq.app.MobileQQ
|
||||
import java.io.File
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
|
||||
internal enum class Level(
|
||||
val id: Byte
|
||||
@ -31,7 +34,29 @@ internal object LogCenter {
|
||||
// 格式化时间
|
||||
SimpleDateFormat("yyyy-MM-dd").format(Date())
|
||||
}_"
|
||||
private val LogFile = MobileQQ.getContext().getExternalFilesDir(null)!!
|
||||
private var LogFile = generateLogFile()
|
||||
|
||||
private val format = SimpleDateFormat("[HH:mm:ss] ")
|
||||
private val timer = Timer()
|
||||
|
||||
init {
|
||||
val now = Calendar.getInstance()
|
||||
val tomorrowMidnight = Calendar.getInstance().apply {
|
||||
add(Calendar.DAY_OF_YEAR, 1)
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
val delay = tomorrowMidnight.timeInMillis - now.timeInMillis
|
||||
timer.scheduleAtFixedRate(object : TimerTask() {
|
||||
override fun run() {
|
||||
LogFile = generateLogFile()
|
||||
}
|
||||
}, delay, 24 * 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
private fun generateLogFile() = MobileQQ.getContext().getExternalFilesDir(null)!!
|
||||
.parentFile!!.resolve("Tencent/Shamrock/log").also {
|
||||
if (it.exists()) it.delete()
|
||||
it.mkdirs()
|
||||
@ -49,8 +74,6 @@ internal object LogCenter {
|
||||
return@let result
|
||||
}
|
||||
|
||||
private val format = SimpleDateFormat("[HH:mm:ss] ")
|
||||
|
||||
fun log(string: String, level: Level = Level.INFO, toast: Boolean = false) {
|
||||
if (!ShamrockConfig[DebugMode] && level == Level.DEBUG) {
|
||||
return
|
||||
|
@ -4,84 +4,59 @@ 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 io.kritor.event.*
|
||||
import io.kritor.common.PushMessageBody
|
||||
import io.kritor.common.Contact
|
||||
import io.kritor.common.Scene
|
||||
import io.kritor.common.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.toKritorMessages
|
||||
import qq.service.contact.ContactHelper
|
||||
import qq.service.msg.toKritorEventMessages
|
||||
|
||||
internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
private val messageEventFlow by lazy {
|
||||
MutableSharedFlow<Pair<MsgRecord, MessageEvent>>()
|
||||
internal object GlobalEventTransmitter : QQInterfaces() {
|
||||
private val MessageEventFlow by lazy {
|
||||
MutableSharedFlow<Pair<MsgRecord, PushMessageBody>>()
|
||||
}
|
||||
private val noticeEventFlow by lazy {
|
||||
MutableSharedFlow<NoticeEvent>()
|
||||
}
|
||||
private val requestEventFlow by lazy {
|
||||
MutableSharedFlow<RequestsEvent>()
|
||||
MutableSharedFlow<RequestEvent>()
|
||||
}
|
||||
|
||||
private suspend fun pushNotice(noticeEvent: NoticeEvent) = noticeEventFlow.emit(noticeEvent)
|
||||
|
||||
private suspend fun pushRequest(requestEvent: RequestsEvent) = requestEventFlow.emit(requestEvent)
|
||||
private suspend fun pushRequest(requestEvent: RequestEvent) = requestEventFlow.emit(requestEvent)
|
||||
|
||||
private suspend fun transMessageEvent(record: MsgRecord, message: MessageEvent) = messageEventFlow.emit(record to message)
|
||||
private suspend fun transMessageEvent(record: MsgRecord, message: PushMessageBody) =
|
||||
MessageEventFlow.emit(record to message)
|
||||
|
||||
object MessageTransmitter {
|
||||
suspend fun transGroupMessage(
|
||||
record: MsgRecord,
|
||||
elements: ArrayList<MsgElement>,
|
||||
): Boolean {
|
||||
transMessageEvent(record, messageEvent {
|
||||
transMessageEvent(record, PushMessageBody.newBuilder().apply {
|
||||
this.time = record.msgTime.toInt()
|
||||
this.scene = Scene.GROUP
|
||||
this.messageId = record.msgId
|
||||
this.messageId = record.msgId.toString()
|
||||
this.messageSeq = record.msgSeq
|
||||
this.contact = contact {
|
||||
this.scene = scene
|
||||
this.contact = Contact.newBuilder().apply {
|
||||
this.scene = Scene.GROUP
|
||||
this.peer = record.peerUin.toString()
|
||||
this.subPeer = record.peerUid
|
||||
}
|
||||
this.sender = sender {
|
||||
}.build()
|
||||
this.sender = Sender.newBuilder().apply {
|
||||
this.uin = record.senderUin
|
||||
this.uid = record.senderUid
|
||||
this.nick = record.sendNickName
|
||||
}
|
||||
this.elements.addAll(elements.toKritorMessages(record))
|
||||
})
|
||||
}.build()
|
||||
this.addAllElements(elements.toKritorEventMessages(record))
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
@ -89,23 +64,22 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
record: MsgRecord,
|
||||
elements: ArrayList<MsgElement>,
|
||||
): Boolean {
|
||||
transMessageEvent(record, messageEvent {
|
||||
transMessageEvent(record, PushMessageBody.newBuilder().apply {
|
||||
this.time = record.msgTime.toInt()
|
||||
this.scene = Scene.FRIEND
|
||||
this.messageId = record.msgId
|
||||
this.messageId = record.msgId.toString()
|
||||
this.messageSeq = record.msgSeq
|
||||
this.contact = contact {
|
||||
this.scene = scene
|
||||
this.peer = record.senderUin.toString()
|
||||
this.contact = Contact.newBuilder().apply {
|
||||
this.scene = Scene.FRIEND
|
||||
this.peer = record.senderUid
|
||||
this.subPeer = record.senderUid
|
||||
}
|
||||
this.sender = sender {
|
||||
}.build()
|
||||
this.sender = Sender.newBuilder().apply {
|
||||
this.uin = record.senderUin
|
||||
this.uid = record.senderUid
|
||||
this.nick = record.sendNickName
|
||||
}
|
||||
this.elements.addAll(elements.toKritorMessages(record))
|
||||
})
|
||||
}.build()
|
||||
this.addAllElements(elements.toKritorEventMessages(record))
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
@ -115,23 +89,22 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
groupCode: Long,
|
||||
fromNick: String,
|
||||
): Boolean {
|
||||
transMessageEvent(record, messageEvent {
|
||||
transMessageEvent(record, PushMessageBody.newBuilder().apply {
|
||||
this.time = record.msgTime.toInt()
|
||||
this.scene = Scene.FRIEND
|
||||
this.messageId = record.msgId
|
||||
this.messageId = record.msgId.toString()
|
||||
this.messageSeq = record.msgSeq
|
||||
this.contact = contact {
|
||||
this.scene = scene
|
||||
this.peer = record.senderUin.toString()
|
||||
this.contact = Contact.newBuilder().apply {
|
||||
this.scene = if (groupCode > 0) Scene.STRANGER_FROM_GROUP else Scene.STRANGER
|
||||
this.peer = record.senderUid
|
||||
this.subPeer = groupCode.toString()
|
||||
}
|
||||
this.sender = sender {
|
||||
}.build()
|
||||
this.sender = Sender.newBuilder().apply {
|
||||
this.uin = record.senderUin
|
||||
this.uid = record.senderUid
|
||||
this.nick = record.sendNickName
|
||||
}
|
||||
this.elements.addAll(elements.toKritorMessages(record))
|
||||
})
|
||||
}.build()
|
||||
this.addAllElements(elements.toKritorEventMessages(record))
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
@ -139,23 +112,22 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
record: MsgRecord,
|
||||
elements: ArrayList<MsgElement>,
|
||||
): Boolean {
|
||||
transMessageEvent(record, messageEvent {
|
||||
transMessageEvent(record, PushMessageBody.newBuilder().apply {
|
||||
this.time = record.msgTime.toInt()
|
||||
this.scene = Scene.GUILD
|
||||
this.messageId = record.msgId
|
||||
this.messageId = record.msgId.toString()
|
||||
this.messageSeq = record.msgSeq
|
||||
this.contact = contact {
|
||||
this.scene = scene
|
||||
this.peer = record.channelId.toString()
|
||||
this.subPeer = record.guildId
|
||||
}
|
||||
this.sender = sender {
|
||||
this.contact = Contact.newBuilder().apply {
|
||||
this.scene = Scene.GUILD
|
||||
this.peer = record.guildId ?: ""
|
||||
this.subPeer = record.channelId ?: ""
|
||||
}.build()
|
||||
this.sender = Sender.newBuilder().apply {
|
||||
this.uin = record.senderUin
|
||||
this.uid = record.senderUid
|
||||
this.nick = record.sendNickName
|
||||
}
|
||||
this.elements.addAll(elements.toKritorMessages(record))
|
||||
})
|
||||
}.build()
|
||||
this.addAllElements(elements.toKritorEventMessages(record))
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -169,7 +141,8 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
*/
|
||||
suspend fun transPrivateFileEvent(
|
||||
msgTime: Long,
|
||||
userId: Long,
|
||||
senderUid: String,
|
||||
senderUin: Long,
|
||||
fileId: String,
|
||||
fileSubId: String,
|
||||
fileName: String,
|
||||
@ -177,19 +150,20 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
expireTime: Long,
|
||||
url: String
|
||||
): Boolean {
|
||||
pushNotice(noticeEvent {
|
||||
this.type = NoticeType.FRIEND_FILE_COME
|
||||
pushNotice(NoticeEvent.newBuilder().apply {
|
||||
this.type = NoticeEvent.NoticeType.PRIVATE_FILE_UPLOADED
|
||||
this.time = msgTime.toInt()
|
||||
this.friendFileCome = friendFileComeNotice {
|
||||
this.privateFileUploaded = PrivateFileUploadedNotice.newBuilder().apply {
|
||||
this.fileId = fileId
|
||||
this.fileName = fileName
|
||||
this.operator = userId
|
||||
this.operatorUid = senderUid
|
||||
this.operatorUin = senderUin
|
||||
this.fileSize = fileSize
|
||||
this.expireTime = expireTime.toInt()
|
||||
this.fileSubId = fileSubId
|
||||
this.url = url
|
||||
}
|
||||
})
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
@ -198,7 +172,8 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
*/
|
||||
suspend fun transGroupFileEvent(
|
||||
msgTime: Long,
|
||||
userId: Long,
|
||||
senderUid: String,
|
||||
senderUin: Long,
|
||||
groupId: Long,
|
||||
uuid: String,
|
||||
fileName: String,
|
||||
@ -206,19 +181,20 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
bizId: Int,
|
||||
url: String
|
||||
): Boolean {
|
||||
pushNotice(noticeEvent {
|
||||
this.type = NoticeType.GROUP_FILE_COME
|
||||
pushNotice(NoticeEvent.newBuilder().apply {
|
||||
this.type = NoticeEvent.NoticeType.GROUP_FILE_UPLOADED
|
||||
this.time = msgTime.toInt()
|
||||
this.groupFileCome = groupFileComeNotice {
|
||||
this.groupFileUploaded = GroupFileUploadedNotice.newBuilder().apply {
|
||||
this.groupId = groupId
|
||||
this.operator = userId
|
||||
this.operatorUid = senderUid
|
||||
this.operatorUin = senderUin
|
||||
this.fileId = uuid
|
||||
this.fileName = fileName
|
||||
this.fileSize = fileSize
|
||||
this.biz = bizId
|
||||
this.url = url
|
||||
}
|
||||
})
|
||||
this.busId = bizId
|
||||
this.fileUrl = url
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -227,33 +203,50 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
* 群聊通知 通知器
|
||||
*/
|
||||
object GroupNoticeTransmitter {
|
||||
suspend fun transGroupSign(time: Long, target: Long, action: String?, rankImg: String?, groupCode: Long): Boolean {
|
||||
pushNotice(noticeEvent {
|
||||
this.type = NoticeType.GROUP_SIGN
|
||||
suspend fun transGroupSign(
|
||||
time: Long,
|
||||
target: Long,
|
||||
action: String,
|
||||
rankImg: String,
|
||||
groupCode: Long
|
||||
): Boolean {
|
||||
pushNotice(NoticeEvent.newBuilder().apply {
|
||||
this.type = NoticeEvent.NoticeType.GROUP_SIGN_IN
|
||||
this.time = time.toInt()
|
||||
this.groupSign = groupSignNotice {
|
||||
this.groupSignIn = GroupSignInNotice.newBuilder().apply {
|
||||
this.groupId = groupCode
|
||||
this.targetUid = ContactHelper.getUidByUinAsync(target)
|
||||
this.targetUin = target
|
||||
this.action = action ?: ""
|
||||
this.suffix = ""
|
||||
this.rankImage = rankImg ?: ""
|
||||
}
|
||||
})
|
||||
this.action = action
|
||||
this.rankImage = rankImg
|
||||
}.build()
|
||||
}.build())
|
||||
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
|
||||
suspend fun transGroupPoke(
|
||||
time: Long,
|
||||
operator: Long,
|
||||
target: Long,
|
||||
action: String,
|
||||
suffix: String,
|
||||
actionImg: String,
|
||||
groupCode: Long
|
||||
): Boolean {
|
||||
pushNotice(NoticeEvent.newBuilder().apply {
|
||||
this.type = NoticeEvent.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 ?: ""
|
||||
}
|
||||
})
|
||||
this.groupPoke = GroupPokeNotice.newBuilder().apply {
|
||||
this.groupId = groupCode
|
||||
this.action = action
|
||||
this.targetUid = ContactHelper.getUidByUinAsync(target)
|
||||
this.targetUin = target
|
||||
this.operatorUid = ContactHelper.getUidByUinAsync(operator)
|
||||
this.operatorUin = operator
|
||||
this.suffix = suffix
|
||||
this.actionImage = actionImg
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
@ -264,20 +257,20 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
groupCode: Long,
|
||||
operator: Long,
|
||||
operatorUid: String,
|
||||
type: GroupMemberIncreasedType
|
||||
type: GroupMemberIncreasedNotice.GroupMemberIncreasedType
|
||||
): Boolean {
|
||||
pushNotice(noticeEvent {
|
||||
this.type = NoticeType.GROUP_MEMBER_INCREASE
|
||||
pushNotice(NoticeEvent.newBuilder().apply {
|
||||
this.type = NoticeEvent.NoticeType.GROUP_MEMBER_INCREASE
|
||||
this.time = time.toInt()
|
||||
this.groupMemberIncrease = groupMemberIncreasedNotice {
|
||||
this.groupMemberIncrease = GroupMemberIncreasedNotice.newBuilder().apply {
|
||||
this.groupId = groupCode
|
||||
this.operatorUid = operatorUid
|
||||
this.operatorUin = operator
|
||||
this.targetUid = targetUid
|
||||
this.targetUin = target
|
||||
this.type = type
|
||||
}
|
||||
})
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
@ -288,20 +281,20 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
groupCode: Long,
|
||||
operator: Long,
|
||||
operatorUid: String,
|
||||
type: GroupMemberDecreasedType
|
||||
type: GroupMemberDecreasedNotice.GroupMemberDecreasedType
|
||||
): Boolean {
|
||||
pushNotice(noticeEvent {
|
||||
this.type = NoticeType.GROUP_MEMBER_INCREASE
|
||||
pushNotice(NoticeEvent.newBuilder().apply {
|
||||
this.type = NoticeEvent.NoticeType.GROUP_MEMBER_DECREASE
|
||||
this.time = time.toInt()
|
||||
this.groupMemberDecrease = groupMemberDecreasedNotice {
|
||||
this.groupMemberDecrease = GroupMemberDecreasedNotice.newBuilder().apply {
|
||||
this.groupId = groupCode
|
||||
this.operatorUid = operatorUid
|
||||
this.operatorUin = operator
|
||||
this.targetUid = targetUid
|
||||
this.targetUin = target
|
||||
this.type = type
|
||||
}
|
||||
})
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
@ -312,34 +305,36 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
groupCode: Long,
|
||||
setAdmin: Boolean
|
||||
): Boolean {
|
||||
pushNotice(noticeEvent {
|
||||
this.type = NoticeType.GROUP_ADMIN_CHANGED
|
||||
pushNotice(NoticeEvent.newBuilder().apply {
|
||||
this.type = NoticeEvent.NoticeType.GROUP_ADMIN_CHANGED
|
||||
this.time = msgTime.toInt()
|
||||
this.groupAdminChanged = groupAdminChangedNotice {
|
||||
this.groupAdminChanged = GroupAdminChangedNotice.newBuilder().apply {
|
||||
this.groupId = groupCode
|
||||
this.targetUid = targetUid
|
||||
this.targetUin = target
|
||||
this.isAdmin = setAdmin
|
||||
}
|
||||
})
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun transGroupWholeBan(
|
||||
msgTime: Long,
|
||||
operator: Long,
|
||||
groupCode: Long,
|
||||
operatorUid: String,
|
||||
operator: Long,
|
||||
isOpen: Boolean
|
||||
): Boolean {
|
||||
pushNotice(noticeEvent {
|
||||
this.type = NoticeType.GROUP_WHOLE_BAN
|
||||
pushNotice(NoticeEvent.newBuilder().apply {
|
||||
this.type = NoticeEvent.NoticeType.GROUP_WHOLE_BAN
|
||||
this.time = msgTime.toInt()
|
||||
this.groupWholeBan = groupWholeBanNotice {
|
||||
this.groupWholeBan = GroupWholeBanNotice.newBuilder().apply {
|
||||
this.groupId = groupCode
|
||||
this.isWholeBan = isOpen
|
||||
this.operator = operator
|
||||
}
|
||||
})
|
||||
this.isBan = isOpen
|
||||
this.operatorUid = operatorUid
|
||||
this.operatorUin = operator
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
@ -352,20 +347,20 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
groupCode: Long,
|
||||
duration: Int
|
||||
): Boolean {
|
||||
pushNotice(noticeEvent {
|
||||
this.type = NoticeType.GROUP_MEMBER_BANNED
|
||||
pushNotice(NoticeEvent.newBuilder().apply {
|
||||
this.type = NoticeEvent.NoticeType.GROUP_MEMBER_BAN
|
||||
this.time = msgTime.toInt()
|
||||
this.groupMemberBanned = groupMemberBannedNotice {
|
||||
this.groupMemberBan = GroupMemberBanNotice.newBuilder().apply {
|
||||
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
|
||||
}
|
||||
})
|
||||
this.type = if (duration > 0) GroupMemberBanNotice.GroupMemberBanType.BAN
|
||||
else GroupMemberBanNotice.GroupMemberBanType.LIFT_BAN
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
@ -379,48 +374,56 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
msgId: Long,
|
||||
tipText: String
|
||||
): Boolean {
|
||||
pushNotice(noticeEvent {
|
||||
this.type = NoticeType.GROUP_RECALL
|
||||
pushNotice(NoticeEvent.newBuilder().apply {
|
||||
this.type = NoticeEvent.NoticeType.GROUP_RECALL
|
||||
this.time = time.toInt()
|
||||
this.groupRecall = groupRecallNotice {
|
||||
this.groupRecall = GroupRecallNotice.newBuilder().apply {
|
||||
this.groupId = groupCode
|
||||
this.operatorUid = operatorUid
|
||||
this.operatorUin = operator
|
||||
this.targetUid = targetUid
|
||||
this.targetUin = target
|
||||
this.messageId = msgId
|
||||
this.messageId = msgId.toString()
|
||||
this.tipText = tipText
|
||||
}
|
||||
})
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun transCardChange(
|
||||
time: Long,
|
||||
targetId: Long,
|
||||
oldCard: String,
|
||||
newCard: String,
|
||||
groupId: Long
|
||||
): Boolean {
|
||||
|
||||
pushNotice(NoticeEvent.newBuilder().apply {
|
||||
this.type = NoticeEvent.NoticeType.GROUP_CARD_CHANGED
|
||||
this.time = time.toInt()
|
||||
this.groupCardChanged = GroupCardChangedNotice.newBuilder().apply {
|
||||
this.groupId = groupId
|
||||
this.targetUin = targetId
|
||||
this.newCard = newCard
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun transTitleChange(
|
||||
time: Long,
|
||||
targetId: Long,
|
||||
targetUin: Long,
|
||||
title: String,
|
||||
groupId: Long
|
||||
): Boolean {
|
||||
pushNotice(noticeEvent {
|
||||
this.type = NoticeType.GROUP_MEMBER_UNIQUE_TITLE_CHANGED
|
||||
pushNotice(NoticeEvent.newBuilder().apply {
|
||||
this.type = NoticeEvent.NoticeType.GROUP_MEMBER_UNIQUE_TITLE_CHANGED
|
||||
this.time = time.toInt()
|
||||
this.groupMemberUniqueTitleChanged = groupUniqueTitleChangedNotice {
|
||||
this.groupMemberUniqueTitleChanged = GroupUniqueTitleChangedNotice.newBuilder().apply {
|
||||
this.groupId = groupId
|
||||
this.target = targetId
|
||||
this.targetUid = ContactHelper.getUidByUinAsync(targetUin)
|
||||
this.targetUin = targetUin
|
||||
this.title = title
|
||||
}
|
||||
})
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
@ -432,17 +435,19 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
groupId: Long,
|
||||
subType: UInt
|
||||
): Boolean {
|
||||
pushNotice(noticeEvent {
|
||||
this.type = NoticeType.GROUP_ESSENCE_CHANGED
|
||||
pushNotice(NoticeEvent.newBuilder().apply {
|
||||
this.type = NoticeEvent.NoticeType.GROUP_ESSENCE_CHANGED
|
||||
this.time = time.toInt()
|
||||
this.groupEssenceChanged = essenceMessageNotice {
|
||||
this.groupEssenceChanged = GroupEssenceMessageNotice.newBuilder().apply {
|
||||
this.groupId = groupId
|
||||
this.messageId = msgId
|
||||
this.sender = senderUin
|
||||
this.operator = operatorUin
|
||||
this.subType = subType.toInt()
|
||||
}
|
||||
})
|
||||
this.messageId = msgId.toString()
|
||||
this.targetUid = ContactHelper.getUidByUinAsync(targetUin)
|
||||
this.targetUin = senderUin
|
||||
this.operatorUid = ContactHelper.getUidByUinAsync(operatorUin)
|
||||
this.operatorUin = operatorUin
|
||||
this.isSet = subType.toInt() == 1
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -451,31 +456,37 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
* 私聊通知 通知器
|
||||
*/
|
||||
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
|
||||
suspend fun transPrivatePoke(
|
||||
msgTime: Long,
|
||||
operator: Long,
|
||||
action: String?,
|
||||
suffix: String?,
|
||||
actionImg: String?
|
||||
): Boolean {
|
||||
pushNotice(NoticeEvent.newBuilder().apply {
|
||||
this.type = NoticeEvent.NoticeType.PRIVATE_POKE
|
||||
this.time = msgTime.toInt()
|
||||
this.friendPoke = friendPokeNotice {
|
||||
this.privatePoke = PrivatePokeNotice.newBuilder().apply {
|
||||
this.action = action ?: ""
|
||||
this.target = target
|
||||
this.operator = operator
|
||||
this.operatorUid = ContactHelper.getUidByUinAsync(operator)
|
||||
this.operatorUin = operator
|
||||
this.suffix = suffix ?: ""
|
||||
this.actionImage = actionImg ?: ""
|
||||
}
|
||||
})
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun transPrivateRecall(time: Long, operator: Long, msgId: Long, tipText: String): Boolean {
|
||||
pushNotice(noticeEvent {
|
||||
this.type = NoticeType.FRIEND_RECALL
|
||||
pushNotice(NoticeEvent.newBuilder().apply {
|
||||
this.type = NoticeEvent.NoticeType.PRIVATE_RECALL
|
||||
this.time = time.toInt()
|
||||
this.friendRecall = friendRecallNotice {
|
||||
this.operator = operator
|
||||
this.messageId = msgId
|
||||
this.privateRecall = PrivateRecallNotice.newBuilder().apply {
|
||||
this.operatorUin = operator
|
||||
this.messageId = msgId.toString()
|
||||
this.tipText = tipText
|
||||
}
|
||||
})
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
@ -485,46 +496,65 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
* 请求 通知器
|
||||
*/
|
||||
object RequestTransmitter {
|
||||
suspend fun transFriendApp(time: Long, operator: Long, tipText: String, flag: String): Boolean {
|
||||
pushRequest(requestsEvent {
|
||||
this.type = RequestType.FRIEND_APPLY
|
||||
suspend fun transFriendApp(time: Long, applierUid: String, operator: Long, tipText: String, flag: String): Boolean {
|
||||
pushRequest(RequestEvent.newBuilder().apply {
|
||||
this.type = RequestEvent.RequestType.FRIEND_APPLY
|
||||
this.time = time.toInt()
|
||||
this.friendApply = friendApplyRequest {
|
||||
this.requestId = flag
|
||||
this.friendApply = FriendApplyRequest.newBuilder().apply {
|
||||
this.applierUid = applierUid
|
||||
this.applierUin = operator
|
||||
this.message = tipText
|
||||
this.flag = flag
|
||||
}
|
||||
})
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun transGroupApply(
|
||||
time: Long,
|
||||
applier: Long,
|
||||
applierUin: Long,
|
||||
applierUid: String,
|
||||
reason: String,
|
||||
groupCode: Long,
|
||||
flag: String,
|
||||
type: GroupApplyType
|
||||
flag: String
|
||||
): Boolean {
|
||||
pushRequest(requestsEvent {
|
||||
this.type = RequestType.GROUP_APPLY
|
||||
pushRequest(RequestEvent.newBuilder().apply {
|
||||
this.type = RequestEvent.RequestType.GROUP_APPLY
|
||||
this.time = time.toInt()
|
||||
this.groupApply = groupApplyRequest {
|
||||
this.requestId = flag
|
||||
this.groupApply = GroupApplyRequest.newBuilder().apply {
|
||||
this.applierUid = applierUid
|
||||
this.applierUin = applier
|
||||
this.applierUin = applierUin
|
||||
this.groupId = groupCode
|
||||
this.reason = reason
|
||||
this.flag = flag
|
||||
this.type = type
|
||||
}
|
||||
})
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun transGroupInvite(
|
||||
time: Long,
|
||||
inviterUid: String,
|
||||
inviterUin: Long,
|
||||
groupCode: Long,
|
||||
flag: String
|
||||
): Boolean {
|
||||
pushRequest(RequestEvent.newBuilder().apply {
|
||||
this.type = RequestEvent.RequestType.GROUP_APPLY
|
||||
this.time = time.toInt()
|
||||
this.requestId = flag
|
||||
this.invitedGroup = InvitedJoinGroupRequest.newBuilder().apply {
|
||||
this.inviterUid = inviterUid
|
||||
this.inviterUin = inviterUin
|
||||
this.groupId = groupCode
|
||||
}.build()
|
||||
}.build())
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun onMessageEvent(collector: FlowCollector<Pair<MsgRecord, MessageEvent>>) {
|
||||
messageEventFlow.collect {
|
||||
suspend inline fun onMessageEvent(collector: FlowCollector<Pair<MsgRecord, PushMessageBody>>) {
|
||||
MessageEventFlow.collect {
|
||||
GlobalScope.launch {
|
||||
collector.emit(it)
|
||||
}
|
||||
@ -539,7 +569,7 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun onRequestEvent(collector: FlowCollector<RequestsEvent>) {
|
||||
suspend inline fun onRequestEvent(collector: FlowCollector<RequestEvent>) {
|
||||
requestEventFlow.collect {
|
||||
GlobalScope.launch {
|
||||
collector.emit(it)
|
||||
|
@ -39,7 +39,7 @@ class AntiDetection: IAction {
|
||||
if (ShamrockConfig[AntiJvmTrace])
|
||||
antiTrace()
|
||||
antiMemoryWalking()
|
||||
antiO3Report()
|
||||
//antiO3Report()
|
||||
}
|
||||
|
||||
private fun antiO3Report() {
|
||||
|
@ -6,8 +6,11 @@ import android.content.Context
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kritor.client.KritorClient
|
||||
import kritor.server.KritorServer
|
||||
import moe.fuqiuluo.shamrock.config.ActiveRPC
|
||||
import moe.fuqiuluo.shamrock.config.PassiveRPC
|
||||
import moe.fuqiuluo.shamrock.config.RPCAddress
|
||||
import moe.fuqiuluo.shamrock.config.RPCPort
|
||||
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||
import moe.fuqiuluo.shamrock.config.get
|
||||
@ -17,6 +20,7 @@ import moe.fuqiuluo.symbols.Process
|
||||
import moe.fuqiuluo.symbols.XposedHook
|
||||
|
||||
private lateinit var server: KritorServer
|
||||
private lateinit var client: KritorClient
|
||||
|
||||
@XposedHook(Process.MAIN, priority = 10)
|
||||
internal class InitRemoteService : IAction {
|
||||
@ -32,6 +36,21 @@ internal class InitRemoteService : IAction {
|
||||
LogCenter.log("ActiveRPC is disabled, KritorServer will not be started.")
|
||||
}
|
||||
|
||||
if (PassiveRPC.get()) {
|
||||
if (!::client.isInitialized) {
|
||||
val hostAndPort = RPCAddress.get().split(":").let {
|
||||
it.first() to it.last().toInt()
|
||||
}
|
||||
LogCenter.log("Connect RPC to ${hostAndPort.first}:${hostAndPort.second}")
|
||||
client = KritorClient(hostAndPort.first, hostAndPort.second)
|
||||
client.start()
|
||||
client.listen()
|
||||
|
||||
}
|
||||
} else {
|
||||
LogCenter.log("PassiveRPC is disabled, KritorServer will not be started.")
|
||||
}
|
||||
|
||||
|
||||
}.onFailure {
|
||||
LogCenter.log("Start RPC failed: ${it.message}", Level.ERROR)
|
||||
|
@ -9,11 +9,13 @@ 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)
|
||||
if (app.isLogin) {
|
||||
AppTalker.talk("switch_status") {
|
||||
put("account", app.currentAccountUin)
|
||||
put("nickname", if (app is QQAppInterface) (app.currentNickname ?: "unknown") else "unknown")
|
||||
put("voice", NativeLoader.isVoiceLoaded)
|
||||
put("core_version", ShamrockVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -83,6 +83,17 @@ abstract class QQInterfaces {
|
||||
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,
|
||||
|
@ -65,7 +65,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
internal object NtV2RichMediaSvc: QQInterfaces() {
|
||||
private val requestIdSeq = atomic(1L)
|
||||
|
||||
private fun fetchGroupResUploadTo(): String {
|
||||
fun fetchGroupResUploadTo(): String {
|
||||
return ShamrockConfig[ResourceGroup].ifNullOrEmpty { "100000000" }!!
|
||||
}
|
||||
|
||||
@ -370,13 +370,14 @@ internal object NtV2RichMediaSvc: QQInterfaces() {
|
||||
width: UInt,
|
||||
height: UInt,
|
||||
retryCnt: Int,
|
||||
chatType: Int,
|
||||
sceneBuilder: suspend SceneInfo.() -> Unit
|
||||
): Result<UploadRsp> {
|
||||
return runCatching {
|
||||
requestUploadNtPic(file, md5, sha, name, width, height, sceneBuilder).getOrThrow()
|
||||
requestUploadNtPic(file, md5, sha, name, width, height, chatType, sceneBuilder).getOrThrow()
|
||||
}.onFailure {
|
||||
if (retryCnt > 0) {
|
||||
return requestUploadNtPic(file, md5, sha, name, width, height, retryCnt - 1, sceneBuilder)
|
||||
return requestUploadNtPic(file, md5, sha, name, width, height, retryCnt - 1, chatType, sceneBuilder)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -388,6 +389,7 @@ internal object NtV2RichMediaSvc: QQInterfaces() {
|
||||
name: String,
|
||||
width: UInt,
|
||||
height: UInt,
|
||||
chatType: Int,
|
||||
sceneBuilder: suspend SceneInfo.() -> Unit
|
||||
): Result<UploadRsp> {
|
||||
val req = NtV2RichMediaReq(
|
||||
@ -427,12 +429,20 @@ internal object NtV2RichMediaSvc: QQInterfaces() {
|
||||
tryFastUploadCompleted = true,
|
||||
srvSendMsg = false,
|
||||
clientRandomId = Random.nextULong(),
|
||||
compatQMsgSceneType = 1u,
|
||||
compatQMsgSceneType = 2u,
|
||||
clientSeq = Random.nextUInt(),
|
||||
noNeedCompatMsg = false
|
||||
noNeedCompatMsg = true
|
||||
)
|
||||
).toByteArray()
|
||||
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x11c5_100", 4549, 100, req, true, timeout = 3.seconds)
|
||||
val fromServiceMsg = when (chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> {
|
||||
sendOidbAW("OidbSvcTrpcTcp.0x11c4_100", 4548, 100, req, true, timeout = 3.seconds)
|
||||
}
|
||||
MsgConstant.KCHATTYPEC2C -> {
|
||||
sendOidbAW("OidbSvcTrpcTcp.0x11c5_100", 4549, 100, req, true, timeout = 3.seconds)
|
||||
}
|
||||
else -> return Result.failure(Exception("unknown chat type: $chatType"))
|
||||
}
|
||||
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
||||
return Result.failure(Exception("unable to request upload nt pic"))
|
||||
}
|
||||
|
58
xposed/src/main/java/qq/service/bdh/ResourceData.kt
Normal file
58
xposed/src/main/java/qq/service/bdh/ResourceData.kt
Normal file
@ -0,0 +1,58 @@
|
||||
package qq.service.bdh
|
||||
|
||||
import com.tencent.mobileqq.data.MessageRecord
|
||||
import java.io.File
|
||||
|
||||
internal enum class ContactType {
|
||||
TROOP,
|
||||
PRIVATE,
|
||||
}
|
||||
|
||||
internal interface TransTarget {
|
||||
val id: String
|
||||
val type: ContactType
|
||||
|
||||
val mRec: MessageRecord?
|
||||
}
|
||||
|
||||
internal class Troop(
|
||||
override val id: String,
|
||||
override val mRec: MessageRecord? = null
|
||||
): TransTarget {
|
||||
override val type: ContactType = ContactType.TROOP
|
||||
}
|
||||
|
||||
internal class Private(
|
||||
override val id: String,
|
||||
override val mRec: MessageRecord? = null
|
||||
): TransTarget {
|
||||
override val type: ContactType = ContactType.PRIVATE
|
||||
}
|
||||
|
||||
internal enum class ResourceType {
|
||||
Picture,
|
||||
Video,
|
||||
Voice
|
||||
}
|
||||
|
||||
internal interface Resource {
|
||||
val type: ResourceType
|
||||
}
|
||||
|
||||
internal data class PictureResource(
|
||||
val src: File
|
||||
): Resource {
|
||||
override val type = ResourceType.Picture
|
||||
}
|
||||
|
||||
internal data class VideoResource(
|
||||
val src: File, val thumb: File
|
||||
): Resource {
|
||||
override val type = ResourceType.Video
|
||||
}
|
||||
|
||||
internal data class VoiceResource(
|
||||
val src: File
|
||||
): Resource {
|
||||
override val type = ResourceType.Voice
|
||||
}
|
135
xposed/src/main/java/qq/service/bdh/Transfer.kt
Normal file
135
xposed/src/main/java/qq/service/bdh/Transfer.kt
Normal file
@ -0,0 +1,135 @@
|
||||
package qq.service.bdh
|
||||
|
||||
import com.tencent.mobileqq.data.MessageForShortVideo
|
||||
import com.tencent.mobileqq.data.MessageRecord
|
||||
import com.tencent.mobileqq.transfile.FileMsg
|
||||
import com.tencent.mobileqq.transfile.TransferRequest
|
||||
import moe.fuqiuluo.shamrock.utils.MD5
|
||||
import qq.service.bdh.ResourceType.*
|
||||
import java.io.File
|
||||
|
||||
internal object Transfer: FileTransfer() {
|
||||
private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>(
|
||||
ContactType.TROOP to mapOf(
|
||||
Picture to { uploadGroupPic(id, (it as PictureResource).src, mRec) },
|
||||
Voice to { uploadGroupVoice(id, (it as VoiceResource).src) },
|
||||
Video to { uploadGroupVideo(id, (it as VideoResource).src, it.thumb) },
|
||||
|
||||
),
|
||||
ContactType.PRIVATE to mapOf(
|
||||
Picture to { uploadC2CPic(id, (it as PictureResource).src, mRec) },
|
||||
Voice to { uploadC2CVoice(id, (it as VoiceResource).src) },
|
||||
Video to { uploadC2CVideo(id, (it as VideoResource).src, it.thumb) },
|
||||
)
|
||||
)
|
||||
|
||||
suspend fun uploadC2CVideo(
|
||||
userId: String,
|
||||
file: File,
|
||||
thumb: File,
|
||||
wait: Boolean = true
|
||||
): Boolean {
|
||||
return transC2CResource(userId, file, FileMsg.TRANSFILE_TYPE_SHORT_VIDEO_C2C, BUSI_TYPE_SHORT_VIDEO, wait) {
|
||||
it.mSourceVideoCodecFormat = VIDEO_FORMAT_MP4
|
||||
it.mRec = MessageForShortVideo().also {
|
||||
it.busiType = BUSI_TYPE_SHORT_VIDEO
|
||||
}
|
||||
it.mThumbPath = thumb.absolutePath
|
||||
it.mThumbMd5 = MD5.genFileMd5Hex(thumb.absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun uploadGroupVideo(
|
||||
groupId: String,
|
||||
file: File,
|
||||
thumb: File,
|
||||
wait: Boolean = true
|
||||
): Boolean {
|
||||
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_SHORT_VIDEO_TROOP, BUSI_TYPE_SHORT_VIDEO, wait) {
|
||||
it.mSourceVideoCodecFormat = VIDEO_FORMAT_MP4
|
||||
it.mRec = MessageForShortVideo().also {
|
||||
it.busiType = BUSI_TYPE_SHORT_VIDEO
|
||||
}
|
||||
it.mThumbPath = thumb.absolutePath
|
||||
it.mThumbMd5 = MD5.genFileMd5Hex(thumb.absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun uploadC2CVoice(
|
||||
userId: String,
|
||||
file: File,
|
||||
wait: Boolean = true
|
||||
): Boolean {
|
||||
return transC2CResource(userId, file, FileMsg.TRANSFILE_TYPE_PTT, 1002, wait) {
|
||||
it.mPttUploadPanel = 3
|
||||
it.mPttCompressFinish = true
|
||||
it.mIsPttPreSend = true
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun uploadGroupVoice(
|
||||
groupId: String,
|
||||
file: File,
|
||||
wait: Boolean = true
|
||||
): Boolean {
|
||||
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PTT, 1002, wait) {
|
||||
it.mPttUploadPanel = 3
|
||||
it.mPttCompressFinish = true
|
||||
it.mIsPttPreSend = true
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun uploadC2CPic(
|
||||
peerId: String,
|
||||
file: File,
|
||||
record: MessageRecord? = null,
|
||||
wait: Boolean = true
|
||||
): Boolean {
|
||||
return transC2CResource(peerId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
|
||||
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
|
||||
picUpExtraInfo.mIsRaw = false
|
||||
picUpExtraInfo.mUinType = FileMsg.UIN_BUDDY
|
||||
it.mPicSendSource = 8
|
||||
it.mExtraObj = picUpExtraInfo
|
||||
it.mIsPresend = true
|
||||
it.delayShowProgressTimeInMs = 2000
|
||||
it.mRec = record
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun uploadGroupPic(
|
||||
groupId: String,
|
||||
file: File,
|
||||
record: MessageRecord? = null,
|
||||
wait: Boolean = true
|
||||
): Boolean {
|
||||
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
|
||||
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
|
||||
picUpExtraInfo.mIsRaw = false
|
||||
picUpExtraInfo.mUinType = FileMsg.UIN_TROOP
|
||||
it.mPicSendSource = 8
|
||||
it.delayShowProgressTimeInMs = 2000
|
||||
it.mExtraObj = picUpExtraInfo
|
||||
it.mRec = record
|
||||
}
|
||||
}
|
||||
|
||||
operator fun get(contactType: ContactType, resourceType: ResourceType): suspend TransTarget.(Resource) -> Boolean {
|
||||
return (ROUTE[contactType] ?: error("unsupported contact type: $contactType"))[resourceType]
|
||||
?: error("Unsupported resource type: $resourceType")
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend infix fun TransferTaskBuilder.trans(res: Resource): Boolean {
|
||||
return Transfer[contact.type, res.type](contact, res)
|
||||
}
|
||||
|
||||
internal class TransferTaskBuilder {
|
||||
lateinit var contact: TransTarget
|
||||
}
|
||||
|
||||
internal infix fun Transfer.with(contact: TransTarget): TransferTaskBuilder {
|
||||
return TransferTaskBuilder().also {
|
||||
it.contact = contact
|
||||
}
|
||||
}
|
21
xposed/src/main/java/qq/service/contact/ContactExt.kt
Normal file
21
xposed/src/main/java/qq/service/contact/ContactExt.kt
Normal file
@ -0,0 +1,21 @@
|
||||
package qq.service.contact
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import io.kritor.common.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.common.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
|
||||
}
|
||||
}
|
@ -7,11 +7,15 @@ 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() {
|
||||
@ -177,4 +181,31 @@ internal object ContactHelper: QQInterfaces() {
|
||||
}
|
||||
}[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()
|
||||
}
|
||||
}
|
@ -5,15 +5,7 @@ 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 io.kritor.file.*
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
|
||||
@ -73,15 +65,15 @@ internal object GroupFileHelper: QQInterfaces() {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response x2"))
|
||||
}
|
||||
|
||||
return getFileSystemInfoResponse {
|
||||
return GetFileSystemInfoResponse.newBuilder().apply {
|
||||
this.fileCount = fileCnt
|
||||
this.totalCount = limitCnt
|
||||
this.totalSpace = totalSpace.toInt()
|
||||
this.usedSpace = usedSpace.toInt()
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
suspend fun getGroupFiles(groupId: Long, folderId: String = "/"): GetFilesResponse {
|
||||
suspend fun getGroupFiles(groupId: Long, folderId: String = "/"): GetFileListResponse {
|
||||
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 {
|
||||
@ -108,7 +100,7 @@ internal object GroupFileHelper: QQInterfaces() {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||
}
|
||||
val files = arrayListOf<File>()
|
||||
val dirs = arrayListOf<Folder>()
|
||||
val folders = 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
|
||||
@ -119,13 +111,13 @@ internal object GroupFileHelper: QQInterfaces() {
|
||||
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 {
|
||||
files.add(File.newBuilder().apply {
|
||||
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.expireTime = 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()
|
||||
@ -133,18 +125,18 @@ internal object GroupFileHelper: QQInterfaces() {
|
||||
this.sha = fileInfo.bytes_sha.get().toByteArray().toHexString()
|
||||
this.sha3 = fileInfo.bytes_sha3.get().toByteArray().toHexString()
|
||||
this.md5 = fileInfo.bytes_md5.get().toByteArray().toHexString()
|
||||
})
|
||||
}.build())
|
||||
}
|
||||
else if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FOLDER) {
|
||||
val folderInfo = file.folder_info
|
||||
dirs.add(folder {
|
||||
folders.add(Folder.newBuilder().apply {
|
||||
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()
|
||||
})
|
||||
}.build())
|
||||
} else {
|
||||
LogCenter.log("未知文件类型: ${file.uint32_type.get()}", Level.WARN)
|
||||
}
|
||||
@ -154,9 +146,9 @@ internal object GroupFileHelper: QQInterfaces() {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response"))
|
||||
}
|
||||
|
||||
return getFilesResponse {
|
||||
this.files.addAll(files)
|
||||
this.folders.addAll(folders)
|
||||
}
|
||||
return GetFileListResponse.newBuilder().apply {
|
||||
this.addAllFiles(files)
|
||||
this.addAllFolders(folders)
|
||||
}.build()
|
||||
}
|
||||
}
|
@ -42,6 +42,7 @@ 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() }
|
||||
@ -99,6 +100,61 @@ internal object GroupHelper: QQInterfaces() {
|
||||
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 {
|
||||
@ -318,12 +374,12 @@ internal object GroupHelper: QQInterfaces() {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setGroupUniqueTitle(groupId: Long, userId: Long, title: String) {
|
||||
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)
|
||||
req.uint64_group_code.set(groupId.toLong())
|
||||
val memberInfo = Oidb_0x8fc.MemberInfo()
|
||||
memberInfo.uint64_uin.set(userId)
|
||||
memberInfo.uint64_uin.set(userId.toLong())
|
||||
memberInfo.bytes_uin_name.set(ByteStringMicro.copyFromUtf8(localMemberInfo.troopnick.ifEmpty {
|
||||
localMemberInfo.troopremark.ifNullOrEmpty { "" }
|
||||
}))
|
||||
@ -468,13 +524,13 @@ internal object GroupHelper: QQInterfaces() {
|
||||
}
|
||||
|
||||
suspend fun getTroopMemberInfoByUin(
|
||||
groupId: Long,
|
||||
uin: Long,
|
||||
groupId: String,
|
||||
uin: String,
|
||||
refresh: Boolean = false
|
||||
): Result<TroopMemberInfo> {
|
||||
val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all")
|
||||
var info = service.getTroopMember(groupId.toString(), uin.toString())
|
||||
if (refresh || !service.isMemberInCache(groupId.toString(), uin.toString()) || info == null || info.troopnick == null) {
|
||||
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) {
|
||||
@ -488,8 +544,8 @@ internal object GroupHelper: QQInterfaces() {
|
||||
try {
|
||||
if (info != null && (info.alias == null || info.alias.isBlank())) {
|
||||
val req = group_member_info.ReqBody()
|
||||
req.uint64_group_code.set(groupId)
|
||||
req.uint64_uin.set(uin)
|
||||
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)
|
||||
@ -523,8 +579,8 @@ internal object GroupHelper: QQInterfaces() {
|
||||
}
|
||||
|
||||
suspend fun getTroopMemberInfoByUinViaNt(
|
||||
groupId: Long,
|
||||
qq: Long,
|
||||
groupId: String,
|
||||
qq: String,
|
||||
timeout: Long = 5000L
|
||||
): Result<MemberInfo> {
|
||||
return runCatching {
|
||||
@ -533,13 +589,13 @@ internal object GroupHelper: QQInterfaces() {
|
||||
val groupService = sessionService.groupService
|
||||
val info = withTimeoutOrNull(timeout) {
|
||||
suspendCancellableCoroutine {
|
||||
groupService.getTransferableMemberInfo(groupId) { code, _, data ->
|
||||
groupService.getTransferableMemberInfo(groupId.toLong()) { code, _, data ->
|
||||
if (code != 0) {
|
||||
it.resume(null)
|
||||
return@getTransferableMemberInfo
|
||||
}
|
||||
data.forEach { (_, info) ->
|
||||
if (info.uin == qq) {
|
||||
if (info.uin == qq.toLong()) {
|
||||
it.resume(info)
|
||||
return@forEach
|
||||
}
|
||||
@ -556,21 +612,18 @@ internal object GroupHelper: QQInterfaces() {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: Long, memberUin: Long, timeout: Long = 10_000): Result<TroopMemberInfo> {
|
||||
private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: String, memberUin: String, timeout: Long = 10_000): Result<TroopMemberInfo> {
|
||||
val info = RefreshTroopMemberInfoLock.withLock {
|
||||
val groupIdStr = groupId.toString()
|
||||
val memberUinStr = memberUin.toString()
|
||||
|
||||
service.deleteTroopMember(groupIdStr, memberUinStr)
|
||||
service.deleteTroopMember(groupId, memberUin)
|
||||
|
||||
requestMemberInfoV2(groupId, memberUin)
|
||||
requestMemberInfo(groupId, memberUin)
|
||||
|
||||
withTimeoutOrNull(timeout) {
|
||||
while (!service.isMemberInCache(groupIdStr, memberUinStr)) {
|
||||
while (!service.isMemberInCache(groupId, memberUin)) {
|
||||
delay(200)
|
||||
}
|
||||
return@withTimeoutOrNull service.getTroopMember(groupIdStr, memberUinStr)
|
||||
return@withTimeoutOrNull service.getTroopMember(groupId, memberUin)
|
||||
}
|
||||
}
|
||||
return if (info != null) {
|
||||
@ -580,7 +633,7 @@ internal object GroupHelper: QQInterfaces() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestMemberInfo(groupId: Long, memberUin: Long) {
|
||||
private fun requestMemberInfo(groupId: String, memberUin: String) {
|
||||
val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER)
|
||||
|
||||
if (!::METHOD_REQ_MEMBER_INFO.isInitialized) {
|
||||
@ -592,10 +645,10 @@ internal object GroupHelper: QQInterfaces() {
|
||||
}
|
||||
}
|
||||
|
||||
METHOD_REQ_MEMBER_INFO.invoke(businessHandler, groupId, memberUin)
|
||||
METHOD_REQ_MEMBER_INFO.invoke(businessHandler, groupId.toLong(), memberUin.toLong())
|
||||
}
|
||||
|
||||
private fun requestMemberInfoV2(groupId: Long, memberUin: Long) {
|
||||
private fun requestMemberInfoV2(groupId: String, memberUin: String) {
|
||||
val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER)
|
||||
|
||||
if (!::METHOD_REQ_MEMBER_INFO_V2.isInitialized) {
|
||||
@ -607,7 +660,8 @@ internal object GroupHelper: QQInterfaces() {
|
||||
}
|
||||
}
|
||||
|
||||
METHOD_REQ_MEMBER_INFO_V2.invoke(businessHandler, groupId.toString(), groupUin2GroupCode(groupId).toString(), arrayListOf(memberUin.toString()))
|
||||
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>> {
|
||||
|
@ -14,7 +14,7 @@ import qq.service.bdh.RichProtoSvc
|
||||
import qq.service.kernel.SimpleKernelMsgListener
|
||||
import qq.service.msg.MessageHelper
|
||||
|
||||
object AioListener: SimpleKernelMsgListener() {
|
||||
object AioListener : SimpleKernelMsgListener() {
|
||||
override fun onRecvMsg(records: ArrayList<MsgRecord>) {
|
||||
records.forEach {
|
||||
GlobalScope.launch {
|
||||
@ -60,7 +60,12 @@ object AioListener: SimpleKernelMsgListener() {
|
||||
|
||||
LogCenter.log("私聊临时消息(private = ${record.senderUin}, groupId=$groupCode)")
|
||||
|
||||
if (!GlobalEventTransmitter.MessageTransmitter.transTempMessage(record, record.elements, groupCode, fromNick)
|
||||
if (!GlobalEventTransmitter.MessageTransmitter.transTempMessage(
|
||||
record,
|
||||
record.elements,
|
||||
groupCode,
|
||||
fromNick
|
||||
)
|
||||
) {
|
||||
LogCenter.log("私聊临时消息推送失败 -> MessageTransmitter", Level.WARN)
|
||||
}
|
||||
@ -92,7 +97,6 @@ object AioListener: SimpleKernelMsgListener() {
|
||||
}
|
||||
|
||||
private suspend fun onC2CFileMsg(record: MsgRecord) {
|
||||
val userId = record.senderUin
|
||||
val fileMsg = record.elements.firstOrNull {
|
||||
it.elementType == MsgConstant.KELEMTYPEFILE
|
||||
}?.fileElement ?: kotlin.run {
|
||||
@ -108,7 +112,7 @@ object AioListener: SimpleKernelMsgListener() {
|
||||
val url = RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
|
||||
|
||||
if (!GlobalEventTransmitter.FileNoticeTransmitter
|
||||
.transPrivateFileEvent(record.msgTime, userId, fileId, fileSubId, fileName, fileSize, expireTime, url)
|
||||
.transPrivateFileEvent(record.msgTime, record.senderUid, record.senderUin, fileId, fileSubId, fileName, fileSize, expireTime, url)
|
||||
) {
|
||||
LogCenter.log("私聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN)
|
||||
}
|
||||
@ -116,7 +120,6 @@ object AioListener: SimpleKernelMsgListener() {
|
||||
|
||||
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 {
|
||||
@ -132,9 +135,15 @@ object AioListener: SimpleKernelMsgListener() {
|
||||
val url = RichProtoSvc.getGroupFileDownUrl(record.peerUin, uuid, bizId)
|
||||
|
||||
if (!GlobalEventTransmitter.FileNoticeTransmitter
|
||||
.transGroupFileEvent(record.msgTime, userId, groupId, uuid, fileName, fileSize, bizId, url)
|
||||
.transGroupFileEvent(record.msgTime, record.senderUid, record.senderUin, groupId, uuid, fileName, fileSize, bizId, url)
|
||||
) {
|
||||
LogCenter.log("群聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
override fun onRecvSysMsg(arrayList: ArrayList<Byte>?) {
|
||||
LogCenter.log("onRecvSysMsg")
|
||||
LogCenter.log(arrayList?.toByteArray()?.toHexString() ?: "")
|
||||
}
|
||||
}
|
14
xposed/src/main/java/qq/service/internals/LineDevListener.kt
Normal file
14
xposed/src/main/java/qq/service/internals/LineDevListener.kt
Normal file
@ -0,0 +1,14 @@
|
||||
package qq.service.internals
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.DevInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.KickedInfo
|
||||
import qq.service.kernel.SimpleKernelMsgListener
|
||||
import java.util.ArrayList
|
||||
|
||||
object LineDevListener: SimpleKernelMsgListener() {
|
||||
override fun onKickedOffLine(kickedInfo: KickedInfo) {
|
||||
}
|
||||
|
||||
override fun onLineDev(devs: ArrayList<DevInfo>) {
|
||||
}
|
||||
}
|
@ -58,6 +58,7 @@ internal object NTServiceFetcher {
|
||||
try {
|
||||
LogCenter.log("Register MSG listener successfully.")
|
||||
msgService.addMsgListener(AioListener)
|
||||
msgService.addMsgListener(LineDevListener)
|
||||
|
||||
// 接口缺失 暂不使用
|
||||
//groupService.addKernelGroupListener(GroupEventListener)
|
||||
|
@ -5,9 +5,8 @@ import com.tencent.mobileqq.qroute.QRoute
|
||||
import com.tencent.qphone.base.remote.FromServiceMsg
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import com.tencent.qqnt.msg.api.IMsgService
|
||||
import io.kritor.event.GroupApplyType
|
||||
import io.kritor.event.GroupMemberDecreasedType
|
||||
import io.kritor.event.GroupMemberIncreasedType
|
||||
import io.kritor.event.GroupMemberDecreasedNotice.GroupMemberDecreasedType
|
||||
import io.kritor.event.GroupMemberIncreasedNotice.GroupMemberIncreasedType
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
@ -127,7 +126,7 @@ internal object PrimitiveListener {
|
||||
LogCenter.log("私聊戳一戳: $operation $action $target $suffix")
|
||||
|
||||
if (!GlobalEventTransmitter.PrivateNoticeTransmitter
|
||||
.transPrivatePoke(msgTime, operation.toLong(), target.toLong(), action, suffix, actionImg)
|
||||
.transPrivatePoke(msgTime, operation.toLong(), action, suffix, actionImg)
|
||||
) {
|
||||
LogCenter.log("私聊戳一戳推送失败!", Level.WARN)
|
||||
}
|
||||
@ -162,7 +161,7 @@ internal object PrimitiveListener {
|
||||
}
|
||||
LogCenter.log("来自$applier 的好友申请:$msg ($source)")
|
||||
if (!GlobalEventTransmitter.RequestTransmitter
|
||||
.transFriendApp(msgTime, applier, msg, flag)
|
||||
.transFriendApp(msgTime, applierUid, applier, msg, flag)
|
||||
) {
|
||||
LogCenter.log("好友申请推送失败!", Level.WARN)
|
||||
}
|
||||
@ -321,8 +320,8 @@ internal object PrimitiveListener {
|
||||
it.key to it.value
|
||||
}
|
||||
|
||||
val target = params["uin_str2"] ?: params["mqq_uin"] ?: return
|
||||
val operation = params["uin_str1"] ?: return
|
||||
val target = params["uin_str2"] ?: params["mqq_uin"] ?: ""
|
||||
val operator = params["uin_str1"] ?: ""
|
||||
val suffix = params["suffix_str"] ?: ""
|
||||
val actionImg = params["action_img_url"] ?: ""
|
||||
val action = params["alt_str1"]
|
||||
@ -333,9 +332,9 @@ internal object PrimitiveListener {
|
||||
|
||||
when (detail.type) {
|
||||
1061u -> {
|
||||
LogCenter.log("群戳一戳($groupId): $operation $action $target $suffix")
|
||||
LogCenter.log("群戳一戳($groupId): $operator $action $target $suffix")
|
||||
if (!GlobalEventTransmitter.GroupNoticeTransmitter
|
||||
.transGroupPoke(time, operation.toLong(), target.toLong(), action, suffix, actionImg, groupId)
|
||||
.transGroupPoke(time, operator.toLong(), target.toLong(), action, suffix, actionImg, groupId)
|
||||
) {
|
||||
LogCenter.log("群戳一戳推送失败!", Level.WARN)
|
||||
}
|
||||
@ -507,7 +506,7 @@ internal object PrimitiveListener {
|
||||
if (wholeBan) {
|
||||
LogCenter.log("群全员禁言($groupCode): $operator -> ${if (rawDuration != 0) "开启" else "关闭"}")
|
||||
if (!GlobalEventTransmitter.GroupNoticeTransmitter
|
||||
.transGroupWholeBan(msgTime, groupCode, operator, rawDuration != 0)
|
||||
.transGroupWholeBan(msgTime, groupCode, operatorUid, operator, rawDuration != 0)
|
||||
) {
|
||||
LogCenter.log("群禁言推送失败!", Level.WARN)
|
||||
}
|
||||
@ -595,7 +594,7 @@ internal object PrimitiveListener {
|
||||
}
|
||||
LogCenter.log("入群申请($groupCode) $applier: \"$reason\", seq: $msgSeq")
|
||||
if (!GlobalEventTransmitter.RequestTransmitter
|
||||
.transGroupApply(time, applier, applierUid, reason, groupCode, flag, GroupApplyType.GROUP_APPLY_ADD)
|
||||
.transGroupApply(time, applier, applierUid, reason, groupCode, flag)
|
||||
) {
|
||||
LogCenter.log("入群申请推送失败!", Level.WARN)
|
||||
}
|
||||
@ -630,7 +629,7 @@ internal object PrimitiveListener {
|
||||
}
|
||||
LogCenter.log("邀请入群申请($groupCode): $applier")
|
||||
if (!GlobalEventTransmitter.RequestTransmitter
|
||||
.transGroupApply(time, applier, applierUid, "", groupCode, flag, GroupApplyType.GROUP_APPLY_ADD)
|
||||
.transGroupApply(time, applier, applierUid, "", groupCode, flag)
|
||||
) {
|
||||
LogCenter.log("邀请入群申请推送失败!", Level.WARN)
|
||||
}
|
||||
@ -658,7 +657,7 @@ internal object PrimitiveListener {
|
||||
"$time;$groupCode;$uin"
|
||||
}
|
||||
if (!GlobalEventTransmitter.RequestTransmitter
|
||||
.transGroupApply(time, invitor, invitorUid, "", groupCode, flag, GroupApplyType.GROUP_APPLY_INVITE)
|
||||
.transGroupInvite(time, invitorUid, invitor, groupCode, flag)
|
||||
) {
|
||||
LogCenter.log("邀请入群推送失败!", Level.WARN)
|
||||
}
|
||||
|
@ -155,11 +155,11 @@ abstract class SimpleKernelMsgListener: IKernelMsgListener {
|
||||
|
||||
}
|
||||
|
||||
override fun onKickedOffLine(kickedInfo: KickedInfo?) {
|
||||
override fun onKickedOffLine(kickedInfo: KickedInfo) {
|
||||
|
||||
}
|
||||
|
||||
override fun onLineDev(arrayList: ArrayList<DevInfo>?) {
|
||||
override fun onLineDev(devs: ArrayList<DevInfo>) {
|
||||
|
||||
}
|
||||
|
||||
|
40
xposed/src/main/java/qq/service/lightapp/ArkAppInfo.kt
Normal file
40
xposed/src/main/java/qq/service/lightapp/ArkAppInfo.kt
Normal file
@ -0,0 +1,40 @@
|
||||
package qq.service.lightapp
|
||||
|
||||
sealed class ArkAppInfo(
|
||||
val appId: Long,
|
||||
val version: String,
|
||||
val packageName: String,
|
||||
val signature: String,
|
||||
val miniAppId: Long = 0,
|
||||
val appName: String = ""
|
||||
) {
|
||||
data object QQMusic: ArkAppInfo(
|
||||
appId = 100497308,
|
||||
version = "0.0.0",
|
||||
packageName = "com.tencent.qqmusic",
|
||||
signature = "cbd27cd7c861227d013a25b2d10f0799"
|
||||
)
|
||||
data object NetEaseMusic: ArkAppInfo(
|
||||
appId = 100495085,
|
||||
version = "0.0.0",
|
||||
packageName = "com.netease.cloudmusic",
|
||||
signature = "da6b069da1e2982db3e386233f68d76d"
|
||||
)
|
||||
|
||||
data object DanMaKu: ArkAppInfo(
|
||||
appId = 100951776,
|
||||
version = "0.0.0",
|
||||
packageName = "tv.danmaku.bili",
|
||||
signature = "7194d531cbe7960a22007b9f6bdaa38b",
|
||||
miniAppId = 1109937557,
|
||||
appName = "哔哩哔哩"
|
||||
)
|
||||
|
||||
data object Docs: ArkAppInfo(
|
||||
appId = 0,
|
||||
version = "0.0.0",
|
||||
packageName = "",
|
||||
signature = "f3da3147654d9a21f3237b88f20dce9c",
|
||||
miniAppId = 1108338344
|
||||
)
|
||||
}
|
48
xposed/src/main/java/qq/service/lightapp/ArkMsgHelper.kt
Normal file
48
xposed/src/main/java/qq/service/lightapp/ArkMsgHelper.kt
Normal file
@ -0,0 +1,48 @@
|
||||
package qq.service.lightapp
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import qq.service.QQInterfaces
|
||||
import qq.service.contact.longPeer
|
||||
import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77
|
||||
|
||||
internal object ArkMsgHelper: QQInterfaces() {
|
||||
suspend fun tryShareMusic(
|
||||
contact: Contact,
|
||||
msgId: Long,
|
||||
arkAppInfo: ArkAppInfo,
|
||||
title: String,
|
||||
singer: String,
|
||||
jumpUrl: String,
|
||||
previewUrl: String,
|
||||
musicUrl: String,
|
||||
) {
|
||||
val req = oidb_cmd0xb77.ReqBody()
|
||||
req.appid.set(arkAppInfo.appId)
|
||||
req.app_type.set(1)
|
||||
req.msg_style.set(4)
|
||||
req.client_info.set(oidb_cmd0xb77.ClientInfo().also {
|
||||
it.platform.set(1)
|
||||
it.sdk_version.set(arkAppInfo.version)
|
||||
it.android_package_name.set(arkAppInfo.packageName)
|
||||
it.android_signature.set(arkAppInfo.signature)
|
||||
})
|
||||
req.ext_info.set(oidb_cmd0xb77.ExtInfo().also {
|
||||
it.msg_seq.set(msgId)
|
||||
})
|
||||
req.recv_uin.set(contact.longPeer())
|
||||
req.rich_msg_body.set(oidb_cmd0xb77.RichMsgBody().also {
|
||||
it.title.set(title)
|
||||
it.summary.set(singer)
|
||||
it.url.set(jumpUrl)
|
||||
it.picture_url.set(previewUrl)
|
||||
it.music_url.set(musicUrl)
|
||||
})
|
||||
when (contact.chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> req.send_type.set(1)
|
||||
MsgConstant.KCHATTYPEC2C -> req.send_type.set(0)
|
||||
else -> error("不支持该聊天类型发送音乐分享: chatType: ${contact.chatType}")
|
||||
}
|
||||
sendOidb("OidbSvc.0xb77_9", 0xb77, 9, req.toByteArray())
|
||||
}
|
||||
}
|
58
xposed/src/main/java/qq/service/lightapp/LbsHelper.kt
Normal file
58
xposed/src/main/java/qq/service/lightapp/LbsHelper.kt
Normal file
@ -0,0 +1,58 @@
|
||||
package qq.service.lightapp
|
||||
|
||||
import com.tencent.biz.map.trpcprotocol.LbsSendInfo
|
||||
import com.tencent.proto.lbsshare.LBSShare
|
||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import moe.fuqiuluo.shamrock.helper.IllegalParamsException
|
||||
import moe.fuqiuluo.shamrock.tools.slice
|
||||
import qq.service.QQInterfaces
|
||||
import qq.service.contact.longPeer
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
internal object LbsHelper: QQInterfaces() {
|
||||
suspend fun tryShareLocation(contact: Contact, lat: Double, lon: Double): Result<Unit> {
|
||||
val req = LbsSendInfo.SendMessageReq()
|
||||
req.uint64_peer_account.set(contact.longPeer())
|
||||
when (contact.chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> req.enum_relation_type.set(1)
|
||||
MsgConstant.KCHATTYPEC2C -> req.enum_relation_type.set(0)
|
||||
else -> error("Not supported chat type: $contact")
|
||||
}
|
||||
req.str_name.set("位置分享")
|
||||
req.str_address.set(getAddressWithLonLat(lat, lon).onFailure {
|
||||
return Result.failure(it)
|
||||
}.getOrNull())
|
||||
req.str_lat.set(lat.toString())
|
||||
req.str_lng.set(lon.toString())
|
||||
sendBuffer("trpc.qq_lbs.qq_lbs_ark.LocationArk.SsoSendMessage", true, req.toByteArray())
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
suspend fun getAddressWithLonLat(lat: Double, lon: Double): Result<String> {
|
||||
if (lat > 90 || lat < 0) {
|
||||
return Result.failure(IllegalParamsException("纬度大小错误"))
|
||||
}
|
||||
if (lon > 180 || lon < 0) {
|
||||
return Result.failure(IllegalParamsException("经度大小错误"))
|
||||
}
|
||||
val latO = (lat * 1000000).roundToInt()
|
||||
val lngO = (lon * 1000000).roundToInt()
|
||||
val req = LBSShare.LocationReq()
|
||||
req.lat.set(latO)
|
||||
req.lng.set(lngO)
|
||||
req.coordinate.set(1)
|
||||
req.keyword.set("")
|
||||
req.category.set("")
|
||||
req.page.set(0)
|
||||
req.count.set(20)
|
||||
req.requireMyLbs.set(1)
|
||||
req.imei.set("")
|
||||
val fromServiceMsg = sendBufferAW("LbsShareSvr.location", true, req.toByteArray())
|
||||
?: return Result.failure(Exception("获取位置失败"))
|
||||
val resp = LBSShare.LocationResp()
|
||||
resp.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||
val location = resp.mylbs
|
||||
return Result.success(location.addr.get())
|
||||
}
|
||||
}
|
96
xposed/src/main/java/qq/service/lightapp/MusicHelper.kt
Normal file
96
xposed/src/main/java/qq/service/lightapp/MusicHelper.kt
Normal file
@ -0,0 +1,96 @@
|
||||
package qq.service.lightapp
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import kotlinx.serialization.json.Json
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.tools.GlobalClient
|
||||
import moe.fuqiuluo.shamrock.tools.asInt
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonArray
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonArrayOrNull
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||||
import moe.fuqiuluo.shamrock.tools.asString
|
||||
import moe.fuqiuluo.shamrock.tools.asStringOrNull
|
||||
import moe.fuqiuluo.shamrock.utils.MD5
|
||||
|
||||
internal object MusicHelper {
|
||||
suspend fun tryShare163MusicById(contact: Contact, msgId: Long, id: String): Boolean {
|
||||
try {
|
||||
val respond = GlobalClient.get("https://music.163.com/api/song/detail/?id=$id&ids=[$id]")
|
||||
val songInfo = Json.parseToJsonElement(respond.bodyAsText()).asJsonObject["songs"].asJsonArray.first().asJsonObject
|
||||
val name = songInfo["name"].asString
|
||||
val title = songInfo["name"].asString
|
||||
val singerName = songInfo["artists"].asJsonArray.first().asJsonObject["name"].asString
|
||||
val previewUrl = songInfo["album"].asJsonObject["picUrl"].asString
|
||||
val playUrl = "https://music.163.com/song/media/outer/url?id=$id.mp3"
|
||||
val jumpUrl = "https://music.163.com/#/song?id=$id"
|
||||
ArkMsgHelper.tryShareMusic(
|
||||
contact,
|
||||
msgId,
|
||||
ArkAppInfo.NetEaseMusic,
|
||||
title.ifBlank { name },
|
||||
singerName,
|
||||
jumpUrl,
|
||||
previewUrl,
|
||||
playUrl
|
||||
)
|
||||
return true
|
||||
} catch (e: Throwable) {
|
||||
LogCenter.log(e.stackTraceToString(), Level.ERROR)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun tryShareQQMusicById(contact: Contact, msgId: Long, id: String): Boolean {
|
||||
try {
|
||||
val respond = GlobalClient.get("https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={%22comm%22:{%22ct%22:24,%22cv%22:0},%22songinfo%22:{%22method%22:%22get_song_detail_yqq%22,%22param%22:{%22song_type%22:0,%22song_mid%22:%22%22,%22song_id%22:$id},%22module%22:%22music.pf_song_detail_svr%22}}")
|
||||
val songInfo = Json.parseToJsonElement(respond.bodyAsText()).asJsonObject["songinfo"].asJsonObject
|
||||
if (songInfo["code"].asInt != 0) {
|
||||
LogCenter.log("获取QQ音乐($id)的歌曲信息失败。")
|
||||
return false
|
||||
} else {
|
||||
val data = songInfo["data"].asJsonObject
|
||||
val trackInfo = data["track_info"].asJsonObject
|
||||
val mid = trackInfo["mid"].asString
|
||||
val previewMid = trackInfo["album"].asJsonObject["mid"].asString
|
||||
val singerMid = trackInfo["singer"].asJsonArrayOrNull?.let {
|
||||
it[0].asJsonObject["mid"].asStringOrNull
|
||||
} ?: ""
|
||||
val name = trackInfo["name"].asString
|
||||
val title = trackInfo["title"].asString
|
||||
val singerName = trackInfo["singer"].asJsonArray.first().asJsonObject["name"].asString
|
||||
val vs = trackInfo["vs"].asJsonArrayOrNull?.let {
|
||||
it[0].asStringOrNull
|
||||
} ?: ""
|
||||
val code = MD5.getMd5Hex("${mid}q;z(&l~sdf2!nK".toByteArray()).substring(0 .. 4).uppercase()
|
||||
val playUrl = "http://c6.y.qq.com/rsc/fcgi-bin/fcg_pyq_play.fcg?songid=&songmid=$mid&songtype=1&fromtag=50&uin=&code=$code"
|
||||
val previewUrl = if (vs.isNotEmpty()) {
|
||||
"http://y.gtimg.cn/music/photo_new/T062R150x150M000$vs}.jpg"
|
||||
} else if (previewMid.isNotEmpty()) {
|
||||
"http://y.gtimg.cn/music/photo_new/T002R150x150M000$previewMid.jpg"
|
||||
} else if (singerMid.isNotEmpty()){
|
||||
"http://y.gtimg.cn/music/photo_new/T001R150x150M000$singerMid.jpg"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val jumpUrl = "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=${mid}&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare"
|
||||
ArkMsgHelper.tryShareMusic(
|
||||
contact,
|
||||
msgId,
|
||||
ArkAppInfo.QQMusic,
|
||||
title.ifBlank { name },
|
||||
singerName,
|
||||
jumpUrl,
|
||||
previewUrl,
|
||||
playUrl
|
||||
)
|
||||
return true
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
LogCenter.log(e.stackTraceToString(), Level.ERROR)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
10
xposed/src/main/java/qq/service/lightapp/Region.kt
Normal file
10
xposed/src/main/java/qq/service/lightapp/Region.kt
Normal file
@ -0,0 +1,10 @@
|
||||
package qq.service.lightapp
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data class Region(
|
||||
val adcode: Int,
|
||||
val province: String?,
|
||||
val city: String?
|
||||
)
|
78
xposed/src/main/java/qq/service/lightapp/WeatherHelper.kt
Normal file
78
xposed/src/main/java/qq/service/lightapp/WeatherHelper.kt
Normal file
@ -0,0 +1,78 @@
|
||||
package qq.service.lightapp
|
||||
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.encodeURLQueryComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.tools.GlobalClient
|
||||
import moe.fuqiuluo.shamrock.tools.GlobalJson
|
||||
import moe.fuqiuluo.shamrock.tools.asInt
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonArray
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||||
import moe.fuqiuluo.shamrock.tools.asStringOrNull
|
||||
import qq.service.QQInterfaces
|
||||
import qq.service.ticket.TicketHelper
|
||||
|
||||
internal object WeatherHelper: QQInterfaces() {
|
||||
suspend fun fetchWeatherCard(code: Int): Result<JsonObject> {
|
||||
val cookie = TicketHelper.getCookie("mp.qq.com")
|
||||
val resp = GlobalClient.get("https://weather.mp.qq.com/page/poster?_wv=2&&_wwv=4&adcode=$code") {
|
||||
header("Cookie", cookie)
|
||||
}
|
||||
|
||||
if (resp.status != HttpStatusCode.OK) {
|
||||
LogCenter.log("fetchWeatherCard: error: ${resp.status}, cookie: $cookie", Level.ERROR)
|
||||
return Result.failure(Exception("search city failed"))
|
||||
}
|
||||
|
||||
val textJson = resp.bodyAsText()
|
||||
.replace("\n", "")
|
||||
.split("window.__INITIAL_STATE__ =")[1]
|
||||
.split("};")[0].trim() + "}"
|
||||
|
||||
//LogCenter.log(textJson)
|
||||
|
||||
return Result.success(Json.parseToJsonElement(textJson).asJsonObject)
|
||||
}
|
||||
|
||||
suspend fun searchCity(query: String): Result<List<Region>> {
|
||||
val pskey = TicketHelper.getPSKey(app.currentAccountUin, "mp.qq.com") ?: ""
|
||||
val cookie = TicketHelper.getCookie("mp.qq.com")
|
||||
val gtk = TicketHelper.getCSRF(pskey)
|
||||
val resp = GlobalClient.get {
|
||||
url("https://weather.mp.qq.com/trpc/weather/SearchRegions?g_tk=$gtk&key=${query.encodeURLQueryComponent()}&offset=0&count=25")
|
||||
header("Cookie", cookie)
|
||||
}
|
||||
|
||||
if (resp.status != HttpStatusCode.OK) {
|
||||
LogCenter.log("GetWeatherCityCode: error: ${resp.status}, cookie: $cookie, bkn: $gtk", Level.ERROR)
|
||||
return Result.failure(Exception("search city failed"))
|
||||
}
|
||||
|
||||
val json = GlobalJson.parseToJsonElement(resp.bodyAsText()).asJsonObject
|
||||
|
||||
|
||||
val cnt = json["totalCount"].asInt
|
||||
if (cnt == 0) {
|
||||
return Result.success(emptyList())
|
||||
}
|
||||
|
||||
val regions = json["regions"].asJsonArray.map {
|
||||
val region = it.asJsonObject
|
||||
Region(
|
||||
region["adcode"].asInt,
|
||||
region["province"].asStringOrNull,
|
||||
region["city"].asStringOrNull
|
||||
)
|
||||
}
|
||||
|
||||
return Result.success(regions)
|
||||
}
|
||||
|
||||
}
|
229
xposed/src/main/java/qq/service/msg/ForwardMessageHelper.kt
Normal file
229
xposed/src/main/java/qq/service/msg/ForwardMessageHelper.kt
Normal file
@ -0,0 +1,229 @@
|
||||
package qq.service.msg
|
||||
|
||||
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.common.ForwardElement
|
||||
import io.kritor.common.ForwardMessageBody
|
||||
import io.kritor.common.Scene
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.tools.slice
|
||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
||||
import protobuf.auto.toByteArray
|
||||
import protobuf.message.*
|
||||
import protobuf.message.longmsg.*
|
||||
import qq.service.QQInterfaces
|
||||
import qq.service.contact.ContactHelper
|
||||
import qq.service.msg.MessageHelper.getMultiMsg
|
||||
import qq.service.ticket.TicketHelper
|
||||
import java.util.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
internal object ForwardMessageHelper : QQInterfaces() {
|
||||
suspend fun uploadMultiMsg(
|
||||
contact: Contact,
|
||||
messages: List<ForwardMessageBody>,
|
||||
): Result<ForwardElement> {
|
||||
var i = -1
|
||||
val desc = MutableList(messages.size) { "" }
|
||||
val forwardMsg = mutableMapOf<String, String>()
|
||||
|
||||
val msgs = messages.mapNotNull { msg ->
|
||||
kotlin.runCatching {
|
||||
when (msg.forwardMessageCase) {
|
||||
ForwardMessageBody.ForwardMessageCase.MESSAGE_ID -> {
|
||||
val record: MsgRecord = withTimeoutOrNull(5000) {
|
||||
val service = QRoute.api(IMsgService::class.java)
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
service.getMsgsByMsgId(
|
||||
contact,
|
||||
arrayListOf(msg.messageId.toLong())
|
||||
) { code, _, msgRecords ->
|
||||
if (code == 0 && msgRecords.isNotEmpty()) {
|
||||
continuation.resume(msgRecords.first())
|
||||
} else {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
continuation.invokeOnCancellation {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
} ?: error("合并转发消息节点消息(id = ${msg.messageId})获取失败")
|
||||
PushMsgBody(
|
||||
msgHead = ResponseHead(
|
||||
peerUid = record.senderUid,
|
||||
receiverUid = record.peerUid,
|
||||
forward = ResponseForward(
|
||||
friendName = record.sendNickName
|
||||
),
|
||||
responseGrp = if (record.chatType == MsgConstant.KCHATTYPEGROUP) ResponseGrp(
|
||||
groupCode = record.peerUin.toULong(),
|
||||
memberCard = record.sendMemberName,
|
||||
u1 = 2
|
||||
) else null
|
||||
),
|
||||
contentHead = ContentHead(
|
||||
msgType = when (record.chatType) {
|
||||
MsgConstant.KCHATTYPEC2C -> 9
|
||||
MsgConstant.KCHATTYPEGROUP -> 82
|
||||
else -> throw UnsupportedOperationException("Unsupported chatType: ${contact.chatType}")
|
||||
},
|
||||
msgSubType = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
|
||||
divSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
|
||||
msgViaRandom = record.msgId,
|
||||
sequence = record.msgSeq, // idk what this is(i++)
|
||||
msgTime = record.msgTime,
|
||||
u2 = 1,
|
||||
u6 = 0,
|
||||
u7 = 0,
|
||||
msgSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) record.msgSeq else null, // seq for dm
|
||||
forwardHead = ForwardHead(
|
||||
u1 = 0,
|
||||
u2 = 0,
|
||||
u3 = 0,
|
||||
ub641 = "",
|
||||
avatar = ""
|
||||
)
|
||||
),
|
||||
body = MsgBody(
|
||||
richText = record.elements.toKritorReqMessages(contact).toRichText(contact).onFailure {
|
||||
error("消息合成失败: ${it.stackTraceToString()}")
|
||||
}.onSuccess {
|
||||
desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": " + it.first
|
||||
}.getOrThrow().second
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
ForwardMessageBody.ForwardMessageCase.MESSAGE -> {
|
||||
val _msg = msg.message
|
||||
PushMsgBody(
|
||||
msgHead = if (_msg.hasSender()) ResponseHead(
|
||||
peer = if (_msg.sender.hasUin()) _msg.sender.uin else TicketHelper.getUin().toLong(),
|
||||
peerUid = _msg.sender.uid,
|
||||
receiverUid = TicketHelper.getUid(),
|
||||
forward = ResponseForward(
|
||||
friendName = if (_msg.sender.hasNick()) _msg.sender.nick else TicketHelper.getNickname()
|
||||
)
|
||||
) else ResponseHead(
|
||||
peer = TicketHelper.getUin().toLong(),
|
||||
peerUid = TicketHelper.getUid(),
|
||||
receiverUid = TicketHelper.getUid(),
|
||||
forward = ResponseForward(
|
||||
friendName = TicketHelper.getNickname()
|
||||
)
|
||||
),
|
||||
contentHead = ContentHead(
|
||||
msgType = 9,
|
||||
msgSubType = 175,
|
||||
divSeq = 175,
|
||||
msgViaRandom = Random.nextLong(),
|
||||
sequence = _msg.messageSeq,
|
||||
msgTime = _msg.time.toLong(),
|
||||
u2 = 1,
|
||||
u6 = 0,
|
||||
u7 = 0,
|
||||
msgSeq = _msg.messageSeq,
|
||||
forwardHead = ForwardHead(
|
||||
u1 = 0,
|
||||
u2 = 0,
|
||||
u3 = 2,
|
||||
ub641 = "",
|
||||
avatar = ""
|
||||
)
|
||||
),
|
||||
body = MsgBody(
|
||||
richText = _msg.elementsList.toRichText(contact).onSuccess {
|
||||
desc[++i] =
|
||||
(if (_msg.hasSender() && _msg.sender.hasNick()) _msg.sender.nick else TicketHelper.getNickname()) + ": " + it.first
|
||||
}.onFailure {
|
||||
error("消息合成失败: ${it.stackTraceToString()}")
|
||||
}.getOrThrow().second
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}.onFailure {
|
||||
LogCenter.log("消息节点解析失败:${it.stackTraceToString()}", Level.WARN)
|
||||
}.getOrNull()
|
||||
}.ifEmpty {
|
||||
return Result.failure(Exception("消息节点为空"))
|
||||
}
|
||||
|
||||
val payload = LongMsgPayload(
|
||||
action = mutableListOf(
|
||||
LongMsgAction(
|
||||
command = "MultiMsg",
|
||||
data = LongMsgContent(
|
||||
body = msgs
|
||||
)
|
||||
)
|
||||
).apply {
|
||||
forwardMsg.map { msg ->
|
||||
addAll(getMultiMsg(msg.value).getOrElse { return Result.failure(Exception("无法获取嵌套转发消息: $it")) }
|
||||
.map { action ->
|
||||
if (action.command == "MultiMsg") LongMsgAction(
|
||||
command = msg.key,
|
||||
data = action.data
|
||||
) else action
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val req = LongMsgReq(
|
||||
sendInfo = when (contact.chatType) {
|
||||
MsgConstant.KCHATTYPEC2C -> SendLongMsgInfo(
|
||||
type = 1,
|
||||
uid = LongMsgUid(contact.peerUid),
|
||||
payload = DeflateTools.gzip(payload.toByteArray())
|
||||
)
|
||||
|
||||
MsgConstant.KCHATTYPEGROUP -> SendLongMsgInfo(
|
||||
type = 3,
|
||||
uid = LongMsgUid(contact.peerUid),
|
||||
groupUin = contact.peerUid.toULong(),
|
||||
payload = DeflateTools.gzip(payload.toByteArray())
|
||||
)
|
||||
|
||||
else -> throw UnsupportedOperationException("Unsupported chatType: ${contact.chatType}")
|
||||
},
|
||||
setting = LongMsgSettings(
|
||||
field1 = 4,
|
||||
field2 = 2,
|
||||
field3 = 9,
|
||||
field4 = 0
|
||||
)
|
||||
).toByteArray()
|
||||
|
||||
val fromServiceMsg =
|
||||
sendBufferAW("trpc.group.long_msg_interface.MsgService.SsoSendLongMsg", true, req, timeout = 60.seconds)
|
||||
?: return Result.failure(Exception("unable to upload multi message, response timeout"))
|
||||
val rsp = runCatching {
|
||||
fromServiceMsg.wupBuffer.slice(4).decodeProtobuf<LongMsgRsp>()
|
||||
}.getOrElse {
|
||||
fromServiceMsg.wupBuffer.decodeProtobuf<LongMsgRsp>()
|
||||
}
|
||||
val resId = rsp.sendResult?.resId ?: return Result.failure(Exception("unable to upload multi message"))
|
||||
|
||||
return Result.success(ForwardElement.newBuilder().apply {
|
||||
this.resId = resId
|
||||
this.summary = summary
|
||||
this.uniseq = UUID.randomUUID().toString()
|
||||
this.description = desc.slice(0..if (i < 3) i else 3).joinToString("\n")
|
||||
}.build())
|
||||
}
|
||||
}
|
69
xposed/src/main/java/qq/service/msg/MessageData.kt
Normal file
69
xposed/src/main/java/qq/service/msg/MessageData.kt
Normal file
@ -0,0 +1,69 @@
|
||||
package qq.service.msg
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import protobuf.message.RichText
|
||||
|
||||
@Serializable
|
||||
internal data class MessageResult(
|
||||
@SerialName("message_id") val msgId: Int,
|
||||
@SerialName("time") val time: Long
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class UploadForwardMessageResult(
|
||||
@SerialName("res_id") val resId: String,
|
||||
@SerialName("filename") val filename: String,
|
||||
@SerialName("summary") val summary: String,
|
||||
@SerialName("desc") val desc: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class SendForwardMessageResult(
|
||||
@SerialName("message_id") val msgId: Int,
|
||||
@SerialName("res_id") val resId: String,
|
||||
@SerialName("forward_id") val forwardId: String = resId
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class MessageDetail(
|
||||
@SerialName("time") val time: Int,
|
||||
@SerialName("message_type") val msgType: Int,
|
||||
@SerialName("message_id") val msgId: Int,
|
||||
@SerialName("message_id_qq") val qqMsgId: Long,
|
||||
@SerialName("message_seq") val msgSeq: Long,
|
||||
@SerialName("real_id") val realId: Long,
|
||||
@SerialName("sender") val sender: MessageSender,
|
||||
@SerialName("message") val message: RichText?,
|
||||
@SerialName("group_id") val groupId: Long = 0,
|
||||
@SerialName("peer_id") val peerId: Long,
|
||||
@SerialName("target_id") val targetId: Long = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class GetForwardMsgResult(
|
||||
@SerialName("messages") val msgs: List<MessageDetail>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class MessageSender(
|
||||
@SerialName("user_id") val userId: Long,
|
||||
@SerialName("nickname") val nickName: String,
|
||||
@SerialName("sex") val sex: String,
|
||||
@SerialName("age") val age: Int,
|
||||
@SerialName("uid") val uid: String,
|
||||
@SerialName("tiny_id") val tinyId: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class EssenceMessage(
|
||||
@SerialName("sender_id") val senderId: Long,
|
||||
@SerialName("sender_nick") val senderNick: String,
|
||||
@SerialName("sender_time") val senderTime: Long,
|
||||
@SerialName("operator_id") val operatorId: Long,
|
||||
@SerialName("operator_nick") val operatorNick: String,
|
||||
@SerialName("operator_time") val operatorTime: Long,
|
||||
@SerialName("message_seq") val messageSeq: Long,
|
||||
@SerialName("message_content") val messageContent: JsonElement,
|
||||
)
|
@ -1,20 +1,210 @@
|
||||
package qq.service.msg
|
||||
|
||||
import com.tencent.mobileqq.qroute.QRoute
|
||||
import com.tencent.mobileqq.troop.api.ITroopMemberNameService
|
||||
import com.tencent.qqnt.kernel.api.IKernelService
|
||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
||||
import com.tencent.qqnt.kernel.nativeinterface.TempChatGameSession
|
||||
import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo
|
||||
import com.tencent.qqnt.msg.api.IMsgService
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
|
||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonArray
|
||||
import moe.fuqiuluo.shamrock.tools.GlobalClient
|
||||
import moe.fuqiuluo.shamrock.tools.asInt
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonArrayOrNull
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||||
import moe.fuqiuluo.shamrock.tools.asLong
|
||||
import moe.fuqiuluo.shamrock.tools.asString
|
||||
import moe.fuqiuluo.shamrock.tools.asStringOrNull
|
||||
import moe.fuqiuluo.shamrock.tools.slice
|
||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
||||
import protobuf.auto.toByteArray
|
||||
import protobuf.message.longmsg.LongMsgAction
|
||||
import protobuf.message.longmsg.LongMsgPayload
|
||||
import protobuf.message.longmsg.LongMsgReq
|
||||
import protobuf.message.longmsg.LongMsgRsp
|
||||
import protobuf.message.longmsg.LongMsgSettings
|
||||
import protobuf.message.longmsg.LongMsgUid
|
||||
import protobuf.message.longmsg.RecvLongMsgInfo
|
||||
import protobuf.oidb.cmd0x9082.Oidb0x9082
|
||||
import qq.service.QQInterfaces
|
||||
import qq.service.contact.ContactHelper
|
||||
import qq.service.internals.msgService
|
||||
import qq.service.ticket.TicketHelper
|
||||
import tencent.im.oidb.cmd0xeac.oidb_0xeac
|
||||
import tencent.im.oidb.oidb_sso
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
typealias MessageId = Long
|
||||
|
||||
internal object MessageHelper: QQInterfaces() {
|
||||
suspend fun getEssenceMessageList(groupId: Long, page: Int = 0, pageSize: Int = 20): Result<List<EssenceMessage>>{
|
||||
val cookie = TicketHelper.getCookie("qun.qq.com")
|
||||
val bkn = TicketHelper.getBkn(TicketHelper.getRealSkey(TicketHelper.getUin()))
|
||||
val url = "https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=${bkn}&group_code=${groupId}&page_start=${page}&page_limit=${pageSize}"
|
||||
val response = GlobalClient.get(url) {
|
||||
header("Cookie", cookie)
|
||||
}
|
||||
val body = Json.decodeFromStream<JsonElement>(response.body())
|
||||
if (body.jsonObject["retcode"].asInt == 0) {
|
||||
val data = body.jsonObject["data"].asJsonObject
|
||||
val list = data["msg_list"].asJsonArrayOrNull
|
||||
?: // is_end
|
||||
return Result.success(ArrayList())
|
||||
return Result.success(list.map {
|
||||
val obj = it.jsonObject
|
||||
val msgSeq = obj["msg_seq"].asLong
|
||||
EssenceMessage(
|
||||
senderId = obj["sender_uin"].asString.toLong(),
|
||||
senderNick = obj["sender_nick"].asString,
|
||||
senderTime = obj["sender_time"].asLong,
|
||||
operatorId = obj["add_digest_uin"].asString.toLong(),
|
||||
operatorNick = obj["add_digest_nick"].asString,
|
||||
operatorTime = obj["add_digest_time"].asLong,
|
||||
messageSeq = msgSeq,
|
||||
messageContent = obj["msg_content"] ?: EmptyJsonArray
|
||||
)
|
||||
})
|
||||
} else {
|
||||
return Result.failure(Exception(body.jsonObject["retmsg"].asStringOrNull))
|
||||
}
|
||||
}
|
||||
|
||||
fun setGroupMessageCommentFace(peer: Long, msgSeq: ULong, faceIndex: String, isSet: Boolean) {
|
||||
val serviceId = if (isSet) 1 else 2
|
||||
sendOidb("OidbSvcTrpcTcp.0x9082_$serviceId", 36994, serviceId, Oidb0x9082(
|
||||
peer = peer.toULong(),
|
||||
msgSeq = msgSeq,
|
||||
faceIndex = faceIndex,
|
||||
flag = 1u,
|
||||
u1 = 0u,
|
||||
u2 = 0u
|
||||
).toByteArray())
|
||||
}
|
||||
|
||||
suspend fun setEssenceMessage(groupId: Long, seq: Long, rand: Long): String? {
|
||||
val fromServiceMsg = sendOidbAW("OidbSvc.0xeac_1", 3756, 1, oidb_0xeac.ReqBody().apply {
|
||||
group_code.set(groupId)
|
||||
msg_seq.set(seq.toInt())
|
||||
msg_random.set(rand.toInt())
|
||||
}.toByteArray())
|
||||
if (fromServiceMsg?.wupBuffer == null) {
|
||||
return "no response"
|
||||
}
|
||||
val body = oidb_sso.OIDBSSOPkg()
|
||||
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||
val result = oidb_0xeac.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
|
||||
return if (result.wording.has()) {
|
||||
LogCenter.log("设置群精华失败: ${result.wording.get()}", Level.WARN)
|
||||
"设置群精华失败: ${result.wording.get()}"
|
||||
} else {
|
||||
LogCenter.log("设置群精华 -> $groupId: $seq")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteEssenceMessage(groupId: Long, seq: Long, rand: Long): String? {
|
||||
val fromServiceMsg = sendOidbAW("OidbSvc.0xeac_2", 3756, 2, oidb_0xeac.ReqBody().apply {
|
||||
group_code.set(groupId)
|
||||
msg_seq.set(seq.toInt())
|
||||
msg_random.set(rand.toInt())
|
||||
}.toByteArray())
|
||||
if (fromServiceMsg?.wupBuffer == null) {
|
||||
return "no response"
|
||||
}
|
||||
val body = oidb_sso.OIDBSSOPkg()
|
||||
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||
val result = oidb_0xeac.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
|
||||
return if (result.wording.has()) {
|
||||
LogCenter.log("移除群精华失败: ${result.wording.get()}", Level.WARN)
|
||||
"移除群精华失败: ${result.wording.get()}"
|
||||
} else {
|
||||
LogCenter.log("移除群精华 -> $groupId: $seq")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prepareTempChatFromGroup(
|
||||
groupId: String,
|
||||
peerId: String
|
||||
): Result<Unit> {
|
||||
LogCenter.log("主动临时消息,创建临时会话。", Level.INFO)
|
||||
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
|
||||
?: return Result.failure(Exception("获取消息服务失败"))
|
||||
msgService.prepareTempChat(
|
||||
TempChatPrepareInfo(
|
||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP,
|
||||
ContactHelper.getUidByUinAsync(peerId = peerId.toLong()),
|
||||
app.getRuntimeService(ITroopMemberNameService::class.java, "all")
|
||||
.getTroopMemberNameRemarkFirst(groupId, peerId),
|
||||
groupId,
|
||||
EMPTY_BYTE_ARRAY,
|
||||
app.currentUid,
|
||||
"",
|
||||
TempChatGameSession()
|
||||
)
|
||||
) { code, reason ->
|
||||
if (code != 0) {
|
||||
LogCenter.log("临时会话创建失败: $code, $reason", Level.ERROR)
|
||||
}
|
||||
}
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
suspend fun sendMessage(contact: Contact, msgs: ArrayList<MsgElement>, retry: Int, uniseq: Long): Result<MessageId> {
|
||||
if (contact.chatType == MsgConstant.KCHATTYPETEMPC2CFROMGROUP) {
|
||||
prepareTempChatFromGroup(contact.guildId, contact.peerUid).getOrThrow()
|
||||
}
|
||||
return withTimeoutOrNull(5000) {
|
||||
suspendCancellableCoroutine {
|
||||
QRoute.api(IMsgService::class.java).sendMsg(contact, uniseq, msgs) { code: Int, msg: String ->
|
||||
if (code == 0) {
|
||||
it.resume(uniseq)
|
||||
} else {
|
||||
LogCenter.log("消息发送失败: $code:$msg", Level.WARN)
|
||||
it.resume(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}?.let { Result.success(it) } ?: resendMsg(contact, uniseq, retry)
|
||||
}
|
||||
|
||||
private suspend fun resendMsg(contact: Contact, msgId: MessageId, retry: Int): Result<MessageId> {
|
||||
if (retry > 0) {
|
||||
return withTimeoutOrNull(5000) {
|
||||
suspendCancellableCoroutine {
|
||||
QRoute.api(IMsgService::class.java).resendMsg(contact, msgId) { code, msg ->
|
||||
if (code == 0) {
|
||||
it.resume(msgId)
|
||||
} else {
|
||||
LogCenter.log("消息重发失败: $code:$msg", Level.WARN)
|
||||
it.resume(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}?.let { Result.success(it) } ?: resendMsg(contact, msgId, retry - 1)
|
||||
} else {
|
||||
return Result.failure(Exception("消息发送失败:重试已达上限"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTempChatInfo(chatType: Int, uid: String): Result<TempChatInfo> {
|
||||
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
|
||||
?: return Result.failure(Exception("获取消息服务失败"))
|
||||
@ -64,6 +254,70 @@ internal object MessageHelper: QQInterfaces() {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMultiMsg(resId: String): Result<List<LongMsgAction>> {
|
||||
val req = LongMsgReq(
|
||||
recvInfo = RecvLongMsgInfo(
|
||||
uid = LongMsgUid(app.currentUid),
|
||||
resId = resId,
|
||||
u1 = 3
|
||||
),
|
||||
setting = LongMsgSettings(
|
||||
field1 = 2,
|
||||
field2 = 2,
|
||||
field3 = 9,
|
||||
field4 = 0
|
||||
)
|
||||
)
|
||||
val fromServiceMsg = sendBufferAW(
|
||||
"trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg",
|
||||
true,
|
||||
req.toByteArray()
|
||||
) ?: return Result.failure(Exception("unable to get multi message"))
|
||||
val rsp = fromServiceMsg.wupBuffer.slice(4).decodeProtobuf<LongMsgRsp>()
|
||||
val zippedPayload = DeflateTools.ungzip(
|
||||
rsp.recvResult?.payload ?: return Result.failure(Exception("payload is empty"))
|
||||
)
|
||||
LogCenter.log(zippedPayload.toHexString(), Level.DEBUG)
|
||||
return Result.success(
|
||||
zippedPayload.decodeProtobuf<LongMsgPayload>().action
|
||||
?: return Result.failure(Exception("action is empty"))
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getForwardMsg(resId: String): Result<List<MessageDetail>> {
|
||||
val result = getMultiMsg(resId).getOrElse { return Result.failure(it) }
|
||||
result.forEach {
|
||||
if (it.command == "MultiMsg") {
|
||||
return Result.success(it.data?.body?.map { msg ->
|
||||
val chatType = if (msg.contentHead!!.msgType == 82) MsgConstant.KCHATTYPEGROUP else MsgConstant.KCHATTYPEC2C
|
||||
MessageDetail(
|
||||
time = msg.contentHead?.msgTime?.toInt() ?: 0,
|
||||
msgType = chatType,
|
||||
msgId = 0, // msgViaRandom为空 tx不给
|
||||
qqMsgId = 0,
|
||||
msgSeq = msg.contentHead!!.msgSeq ?: 0,
|
||||
realId = msg.contentHead!!.msgSeq ?: 0,
|
||||
sender = MessageSender(
|
||||
msg.msgHead?.peer ?: 0,
|
||||
msg.msgHead?.responseGrp?.memberCard ?: msg.msgHead?.forward?.friendName ?: "",
|
||||
"unknown",
|
||||
0,
|
||||
msg.msgHead?.peerUid ?: "",
|
||||
msg.msgHead?.peerUid ?: ""
|
||||
),
|
||||
message = msg.body?.richText,
|
||||
peerId = msg.msgHead?.peer ?: 0,
|
||||
groupId = if (chatType == MsgConstant.KCHATTYPEGROUP) msg.msgHead?.responseGrp?.groupCode?.toLong()
|
||||
?: 0 else 0,
|
||||
targetId = if (chatType != MsgConstant.KCHATTYPEGROUP) msg.msgHead?.peer ?: 0 else 0
|
||||
)
|
||||
} ?: return Result.failure(Exception("Msg is empty")))
|
||||
}
|
||||
}
|
||||
return Result.failure(Exception("Can't find msg"))
|
||||
}
|
||||
|
||||
|
||||
fun generateMsgId(chatType: Int): Long {
|
||||
return createMessageUniseq(chatType, System.currentTimeMillis())
|
||||
}
|
||||
|
@ -1,33 +1,13 @@
|
||||
package qq.service.msg
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import com.tencent.mobileqq.qroute.QRoute
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
||||
import com.tencent.qqnt.msg.api.IMsgService
|
||||
import io.kritor.event.AtElement
|
||||
import io.kritor.event.Element
|
||||
import io.kritor.event.ElementKt
|
||||
import io.kritor.event.ImageType
|
||||
import io.kritor.event.Scene
|
||||
import io.kritor.event.atElement
|
||||
import io.kritor.event.basketballElement
|
||||
import io.kritor.event.buttonAction
|
||||
import io.kritor.event.buttonActionPermission
|
||||
import io.kritor.event.buttonRender
|
||||
import io.kritor.event.contactElement
|
||||
import io.kritor.event.diceElement
|
||||
import io.kritor.event.faceElement
|
||||
import io.kritor.event.forwardElement
|
||||
import io.kritor.event.imageElement
|
||||
import io.kritor.event.jsonElement
|
||||
import io.kritor.event.locationElement
|
||||
import io.kritor.event.pokeElement
|
||||
import io.kritor.event.replyElement
|
||||
import io.kritor.event.rpsElement
|
||||
import io.kritor.event.textElement
|
||||
import io.kritor.event.videoElement
|
||||
import io.kritor.event.voiceElement
|
||||
import io.kritor.common.*
|
||||
import io.kritor.common.Element.ElementType
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import moe.fuqiuluo.shamrock.helper.ActionMsgException
|
||||
@ -47,23 +27,13 @@ import qq.service.bdh.RichProtoSvc
|
||||
import qq.service.contact.ContactHelper
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* 将NT消息(com.tencent.qqnt.*)转换为事件消息(io.kritor.event.*)推送
|
||||
*/
|
||||
|
||||
typealias NtMessages = ArrayList<MsgElement>
|
||||
typealias Convertor = suspend (MsgRecord, MsgElement) -> Result<Element>
|
||||
|
||||
suspend fun NtMessages.toKritorMessages(record: MsgRecord): ArrayList<Element> {
|
||||
val result = arrayListOf<Element>()
|
||||
forEach {
|
||||
MsgConvertor[it.elementType]?.invoke(record, it)?.onSuccess {
|
||||
result.add(it)
|
||||
}?.onFailure {
|
||||
if (it !is ActionMsgException) {
|
||||
LogCenter.log("消息转换异常: " + it.stackTraceToString(), Level.WARN)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private object MsgConvertor {
|
||||
private val convertorMap = hashMapOf(
|
||||
MsgConstant.KELEMTYPETEXT to ::convertText,
|
||||
@ -87,12 +57,14 @@ private object MsgConvertor {
|
||||
val text = element.textElement
|
||||
val elem = Element.newBuilder()
|
||||
if (text.atType != MsgConstant.ATTYPEUNKNOWN) {
|
||||
elem.setAt(atElement {
|
||||
elem.type = ElementType.AT
|
||||
elem.setAt(AtElement.newBuilder().apply {
|
||||
this.uid = text.atNtUid
|
||||
this.uin = ContactHelper.getUinByUidAsync(text.atNtUid).toLong()
|
||||
})
|
||||
} else {
|
||||
elem.setText(textElement {
|
||||
elem.type = ElementType.TEXT
|
||||
elem.setText(TextElement.newBuilder().apply {
|
||||
this.text = text.content
|
||||
})
|
||||
}
|
||||
@ -103,31 +75,51 @@ private object MsgConvertor {
|
||||
val face = element.faceElement
|
||||
val elem = Element.newBuilder()
|
||||
if (face.faceType == 5) {
|
||||
elem.setPoke(pokeElement {
|
||||
elem.type = ElementType.POKE
|
||||
elem.setPoke(PokeElement.newBuilder().apply {
|
||||
this.id = face.vaspokeId
|
||||
this.type = face.pokeType
|
||||
this.strength = face.pokeStrength
|
||||
})
|
||||
} else {
|
||||
when(face.faceIndex) {
|
||||
114 -> elem.setBasketball(basketballElement {
|
||||
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
|
||||
})
|
||||
358 -> elem.setDice(diceElement {
|
||||
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
|
||||
})
|
||||
359 -> elem.setRps(rpsElement {
|
||||
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
|
||||
})
|
||||
394 -> elem.setFace(faceElement {
|
||||
this.id = face.faceIndex
|
||||
this.isBig = face.faceType == 3
|
||||
this.result = face.resultId.ifNullOrEmpty { "1" }?.toInt() ?: 1
|
||||
})
|
||||
else -> elem.setFace(faceElement {
|
||||
this.id = face.faceIndex
|
||||
this.isBig = face.faceType == 3
|
||||
})
|
||||
when (face.faceIndex) {
|
||||
114 -> {
|
||||
elem.type = ElementType.BASKETBALL
|
||||
elem.setBasketball(BasketballElement.newBuilder().apply {
|
||||
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
|
||||
})
|
||||
}
|
||||
|
||||
358 -> {
|
||||
elem.type = ElementType.DICE
|
||||
elem.setDice(DiceElement.newBuilder().apply {
|
||||
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
|
||||
})
|
||||
}
|
||||
|
||||
359 -> {
|
||||
elem.type = ElementType.RPS
|
||||
elem.setRps(RpsElement.newBuilder().apply {
|
||||
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
|
||||
})
|
||||
}
|
||||
|
||||
394 -> {
|
||||
elem.type = ElementType.FACE
|
||||
elem.setFace(FaceElement.newBuilder().apply {
|
||||
this.id = face.faceIndex
|
||||
this.isBig = face.faceType == 3
|
||||
this.result = face.resultId.ifNullOrEmpty { "1" }?.toInt() ?: 1
|
||||
})
|
||||
}
|
||||
|
||||
else -> {
|
||||
elem.type = ElementType.FACE
|
||||
elem.setFace(FaceElement.newBuilder().apply {
|
||||
this.id = face.faceIndex
|
||||
this.isBig = face.faceType == 3
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result.success(elem.build())
|
||||
@ -162,9 +154,10 @@ private object MsgConvertor {
|
||||
LogCenter.log({ "receive image: $image" }, Level.DEBUG)
|
||||
|
||||
val elem = Element.newBuilder()
|
||||
elem.setImage(imageElement {
|
||||
this.file = md5
|
||||
this.url = when (record.chatType) {
|
||||
elem.type = ElementType.IMAGE
|
||||
elem.setImage(ImageElement.newBuilder().apply {
|
||||
this.file = ByteString.copyFromUtf8(md5)
|
||||
this.fileUrl = when (record.chatType) {
|
||||
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
|
||||
originalUrl = originalUrl,
|
||||
md5 = md5,
|
||||
@ -202,7 +195,8 @@ private object MsgConvertor {
|
||||
|
||||
else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}")
|
||||
}
|
||||
this.type = if (image.isFlashPic == true) ImageType.FLASH else if (image.original) ImageType.ORIGIN else ImageType.COMMON
|
||||
this.type =
|
||||
if (image.isFlashPic == true) ImageElement.ImageType.FLASH else if (image.original) ImageElement.ImageType.ORIGIN else ImageElement.ImageType.COMMON
|
||||
this.subType = image.picSubType
|
||||
})
|
||||
|
||||
@ -217,14 +211,19 @@ private object MsgConvertor {
|
||||
ptt.fileName.substring(5)
|
||||
else ptt.md5HexStr
|
||||
|
||||
elem.setVoice(voiceElement {
|
||||
this.url = when (record.chatType) {
|
||||
elem.type = ElementType.VOICE
|
||||
elem.setVoice(VoiceElement.newBuilder().apply {
|
||||
this.fileUrl = when (record.chatType) {
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", ptt.fileUuid)
|
||||
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl("0", md5.hex2ByteArray(), ptt.fileUuid)
|
||||
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
|
||||
"0",
|
||||
md5.hex2ByteArray(),
|
||||
ptt.fileUuid
|
||||
)
|
||||
|
||||
else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}")
|
||||
}
|
||||
this.file = md5
|
||||
this.file = ByteString.copyFromUtf8(md5)
|
||||
this.magic = ptt.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE
|
||||
})
|
||||
|
||||
@ -241,9 +240,10 @@ private object MsgConvertor {
|
||||
it[it.size - 2].hex2ByteArray()
|
||||
}
|
||||
} else video.fileName.split(".")[0].hex2ByteArray()
|
||||
elem.setVideo(videoElement {
|
||||
this.file = md5.toHexString()
|
||||
this.url = when (record.chatType) {
|
||||
elem.type = ElementType.VIDEO
|
||||
elem.setVideo(VideoElement.newBuilder().apply {
|
||||
this.file = ByteString.copyFromUtf8(md5.toHexString())
|
||||
this.fileUrl = when (record.chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
||||
@ -256,7 +256,8 @@ private object MsgConvertor {
|
||||
suspend fun convertMarketFace(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
val marketFace = element.marketFaceElement
|
||||
val elem = Element.newBuilder()
|
||||
elem.setMarketFace(io.kritor.event.marketFaceElement {
|
||||
elem.type = ElementType.MARKET_FACE
|
||||
elem.setMarketFace(MarketFaceElement.newBuilder().apply {
|
||||
this.id = marketFace.emojiId.lowercase()
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
@ -268,8 +269,9 @@ private object MsgConvertor {
|
||||
when (data["app"].asString) {
|
||||
"com.tencent.multimsg" -> {
|
||||
val info = data["meta"].asJsonObject["detail"].asJsonObject
|
||||
elem.setForward(forwardElement {
|
||||
this.id = info["resid"].asString
|
||||
elem.type = ElementType.FORWARD
|
||||
elem.setForward(ForwardElement.newBuilder().apply {
|
||||
this.resId = info["resid"].asString
|
||||
this.uniseq = info["uniseq"].asString
|
||||
this.summary = info["summary"].asString
|
||||
this.description = info["news"].asJsonArray.joinToString("\n") {
|
||||
@ -280,7 +282,8 @@ private object MsgConvertor {
|
||||
|
||||
"com.tencent.troopsharecard" -> {
|
||||
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
||||
elem.setContact(contactElement {
|
||||
elem.type = ElementType.CONTACT
|
||||
elem.setContact(ContactElement.newBuilder().apply {
|
||||
this.scene = Scene.GROUP
|
||||
this.peer = info["jumpUrl"].asString.split("group_code=")[1]
|
||||
})
|
||||
@ -288,7 +291,8 @@ private object MsgConvertor {
|
||||
|
||||
"com.tencent.contact.lua" -> {
|
||||
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
||||
elem.setContact(contactElement {
|
||||
elem.type = ElementType.CONTACT
|
||||
elem.setContact(ContactElement.newBuilder().apply {
|
||||
this.scene = Scene.FRIEND
|
||||
this.peer = info["jumpUrl"].asString.split("uin=")[1]
|
||||
})
|
||||
@ -296,7 +300,8 @@ private object MsgConvertor {
|
||||
|
||||
"com.tencent.map" -> {
|
||||
val info = data["meta"].asJsonObject["Location.Search"].asJsonObject
|
||||
elem.setLocation(locationElement {
|
||||
elem.type = ElementType.LOCATION
|
||||
elem.setLocation(LocationElement.newBuilder().apply {
|
||||
this.lat = info["lat"].asString.toFloat()
|
||||
this.lon = info["lng"].asString.toFloat()
|
||||
this.address = info["address"].asString
|
||||
@ -304,9 +309,12 @@ private object MsgConvertor {
|
||||
})
|
||||
}
|
||||
|
||||
else -> elem.setJson(jsonElement {
|
||||
this.json = data.toString()
|
||||
})
|
||||
else -> {
|
||||
elem.type = ElementType.JSON
|
||||
elem.setJson(JsonElement.newBuilder().apply {
|
||||
this.json = data.toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
@ -314,21 +322,23 @@ private object MsgConvertor {
|
||||
suspend fun convertReply(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
val reply = element.replyElement
|
||||
val elem = Element.newBuilder()
|
||||
elem.setReply(replyElement {
|
||||
elem.type = ElementType.REPLY
|
||||
elem.setReply(ReplyElement.newBuilder().apply {
|
||||
val msgSeq = reply.replayMsgSeq
|
||||
val contact = MessageHelper.generateContact(record)
|
||||
val sourceRecords = withTimeoutOrNull(3000) {
|
||||
suspendCancellableCoroutine {
|
||||
QRoute.api(IMsgService::class.java).getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records ->
|
||||
it.resume(records)
|
||||
}
|
||||
QRoute.api(IMsgService::class.java)
|
||||
.getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records ->
|
||||
it.resume(records)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sourceRecords.isNullOrEmpty()) {
|
||||
LogCenter.log("无法查询到回复的消息ID: seq = $msgSeq", Level.WARN)
|
||||
this.messageId = reply.replayMsgId
|
||||
this.messageId = reply.replayMsgId.toString()
|
||||
} else {
|
||||
this.messageId = sourceRecords.first().msgId
|
||||
this.messageId = sourceRecords.first().msgId.toString()
|
||||
}
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
@ -344,11 +354,18 @@ private object MsgConvertor {
|
||||
val fileSubId = fileMsg.fileSubId ?: ""
|
||||
val url = when (record.chatType) {
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(record.guildId, record.channelId, fileId, bizId)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(
|
||||
record.guildId,
|
||||
record.channelId,
|
||||
fileId,
|
||||
bizId
|
||||
)
|
||||
|
||||
else -> RichProtoSvc.getGroupFileDownUrl(record.peerUin, fileId, bizId)
|
||||
}
|
||||
val elem = Element.newBuilder()
|
||||
elem.setFile(io.kritor.event.fileElement {
|
||||
elem.type = ElementType.FILE
|
||||
elem.setFile(FileElement.newBuilder().apply {
|
||||
this.name = fileName
|
||||
this.size = fileSize
|
||||
this.url = url
|
||||
@ -363,7 +380,8 @@ private object MsgConvertor {
|
||||
suspend fun convertMarkdown(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
val markdown = element.markdownElement
|
||||
val elem = Element.newBuilder()
|
||||
elem.setMarkdown(io.kritor.event.markdownElement {
|
||||
elem.type = ElementType.MARKDOWN
|
||||
elem.setMarkdown(MarkdownElement.newBuilder().apply {
|
||||
this.markdown = markdown.content
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
@ -372,7 +390,8 @@ private object MsgConvertor {
|
||||
suspend fun convertBubbleFace(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
val bubbleFace = element.faceBubbleElement
|
||||
val elem = Element.newBuilder()
|
||||
elem.setBubbleFace(io.kritor.event.bubbleFaceElement {
|
||||
elem.type = ElementType.BUBBLE_FACE
|
||||
elem.setBubbleFace(BubbleFaceElement.newBuilder().apply {
|
||||
this.id = bubbleFace.yellowFaceInfo.index
|
||||
this.count = bubbleFace.faceCount ?: 1
|
||||
})
|
||||
@ -382,34 +401,35 @@ private object MsgConvertor {
|
||||
suspend fun convertInlineKeyboard(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
val inlineKeyboard = element.inlineKeyboardElement
|
||||
val elem = Element.newBuilder()
|
||||
elem.setButton(io.kritor.event.buttonElement {
|
||||
elem.type = ElementType.KEYBOARD
|
||||
elem.setKeyboard(KeyboardElement.newBuilder().apply {
|
||||
inlineKeyboard.rows.forEach { row ->
|
||||
this.rows.add(io.kritor.event.row {
|
||||
row.buttons.forEach buttonsLoop@ { button ->
|
||||
this.addRows(KeyboardRow.newBuilder().apply {
|
||||
row.buttons.forEach buttonsLoop@{ button ->
|
||||
if (button == null) return@buttonsLoop
|
||||
this.buttons.add(io.kritor.event.button {
|
||||
this.addButtons(Button.newBuilder().apply {
|
||||
this.id = button.id
|
||||
this.action = buttonAction {
|
||||
this.action = ButtonAction.newBuilder().apply {
|
||||
this.type = button.type
|
||||
this.permission = buttonActionPermission {
|
||||
this.permission = ButtonActionPermission.newBuilder().apply {
|
||||
this.type = button.permissionType
|
||||
button.specifyRoleIds?.let {
|
||||
this.roleIds.addAll(it)
|
||||
this.addAllRoleIds(it)
|
||||
}
|
||||
button.specifyTinyids?.let {
|
||||
this.userIds.addAll(it)
|
||||
this.addAllUserIds(it)
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
this.unsupportedTips = button.unsupportTips ?: ""
|
||||
this.data = button.data ?: ""
|
||||
this.reply = button.isReply
|
||||
this.enter = button.enter
|
||||
}
|
||||
this.renderData = buttonRender {
|
||||
}.build()
|
||||
this.renderData = ButtonRender.newBuilder().apply {
|
||||
this.label = button.label ?: ""
|
||||
this.visitedLabel = button.visitedLabel ?: ""
|
||||
this.style = button.style
|
||||
}
|
||||
}.build()
|
||||
})
|
||||
}
|
||||
})
|
||||
@ -423,3 +443,16 @@ private object MsgConvertor {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun NtMessages.toKritorEventMessages(record: MsgRecord): ArrayList<Element> {
|
||||
val result = arrayListOf<Element>()
|
||||
forEach {
|
||||
MsgConvertor[it.elementType]?.invoke(record, it)?.onSuccess {
|
||||
result.add(it)
|
||||
}?.onFailure {
|
||||
if (it !is ActionMsgException) {
|
||||
LogCenter.log("消息转换异常: " + it.stackTraceToString(), Level.WARN)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
269
xposed/src/main/java/qq/service/msg/MultiConvertor.kt
Normal file
269
xposed/src/main/java/qq/service/msg/MultiConvertor.kt
Normal file
@ -0,0 +1,269 @@
|
||||
@file:OptIn(ExperimentalUnsignedTypes::class)
|
||||
|
||||
package qq.service.msg
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import io.kritor.common.*
|
||||
import io.kritor.common.Element.ElementType
|
||||
import io.kritor.common.ImageElement.ImageType
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readUInt
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonArray
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||||
import moe.fuqiuluo.shamrock.tools.asString
|
||||
import moe.fuqiuluo.shamrock.tools.slice
|
||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
||||
import protobuf.message.Elem
|
||||
import protobuf.message.element.commelem.ButtonExtra
|
||||
import protobuf.message.element.commelem.MarkdownExtra
|
||||
import protobuf.message.element.commelem.QFaceExtra
|
||||
import qq.service.bdh.RichProtoSvc
|
||||
|
||||
/**
|
||||
* 将合并转发PB(protobuf.message.*)转请求消息(io.kritor.message.*)发送
|
||||
*/
|
||||
|
||||
suspend fun List<Elem>.toKritorResponseMessages(contact: Contact): ArrayList<Element> {
|
||||
val kritorMessages = ArrayList<Element>()
|
||||
forEach { element ->
|
||||
if (element.text != null) {
|
||||
val text = element.text!!
|
||||
if (text.attr6Buf != null) {
|
||||
val at = ByteReadPacket(text.attr6Buf!!)
|
||||
at.discardExact(7)
|
||||
val uin = at.readUInt()
|
||||
kritorMessages.add(Element.newBuilder().apply {
|
||||
this.type = ElementType.AT
|
||||
this.at = AtElement.newBuilder().apply {
|
||||
this.uin = uin.toLong()
|
||||
}.build()
|
||||
}.build())
|
||||
} else {
|
||||
kritorMessages.add(Element.newBuilder().apply {
|
||||
this.type = ElementType.TEXT
|
||||
this.text = TextElement.newBuilder().apply {
|
||||
this.text = text.str ?: ""
|
||||
}.build()
|
||||
}.build())
|
||||
}
|
||||
} else if (element.face != null) {
|
||||
kritorMessages.add(Element.newBuilder().apply {
|
||||
this.type = ElementType.FACE
|
||||
this.face = FaceElement.newBuilder().apply {
|
||||
this.id = element.face!!.index ?: 0
|
||||
}.build()
|
||||
}.build())
|
||||
} else if (element.customFace != null) {
|
||||
val customFace = element.customFace!!
|
||||
val md5 = customFace.md5.toHexString()
|
||||
val origUrl = customFace.origUrl!!
|
||||
kritorMessages.add(Element.newBuilder().apply {
|
||||
this.type = ElementType.IMAGE
|
||||
this.image = ImageElement.newBuilder().apply {
|
||||
this.fileMd5 = md5
|
||||
this.type = if (customFace.origin == true) ImageType.ORIGIN else ImageType.COMMON
|
||||
this.fileUrl = when (contact.chatType) {
|
||||
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
|
||||
origUrl,
|
||||
md5
|
||||
)
|
||||
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5)
|
||||
else -> throw UnsupportedOperationException("Not supported chat type: $contact")
|
||||
}
|
||||
}.build()
|
||||
}.build())
|
||||
} else if (element.notOnlineImage != null) {
|
||||
val md5 = element.notOnlineImage!!.picMd5.toHexString()
|
||||
val origUrl = element.notOnlineImage!!.origUrl!!
|
||||
kritorMessages.add(Element.newBuilder().apply {
|
||||
this.type = ElementType.IMAGE
|
||||
this.image = ImageElement.newBuilder().apply {
|
||||
this.fileMd5 = md5
|
||||
this.type = if (element.notOnlineImage?.original == true) ImageType.ORIGIN else ImageType.COMMON
|
||||
this.fileUrl = when (contact.chatType) {
|
||||
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
|
||||
origUrl,
|
||||
md5
|
||||
)
|
||||
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5)
|
||||
else -> throw UnsupportedOperationException("Not supported chat type: $contact")
|
||||
}
|
||||
}.build()
|
||||
}.build())
|
||||
} else if (element.generalFlags != null) {
|
||||
// val generalFlags = element.generalFlags!!
|
||||
// if (generalFlags.longTextFlag == 1u) {
|
||||
// kritorMessages.add(Element.newBuilder().apply {
|
||||
// this.type = ElementType.FORWARD
|
||||
// this.forward = forwardElement {
|
||||
// this.id = generalFlags.longTextResid ?: ""
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
} else if (element.srcMsg != null) {
|
||||
val srcMsg = element.srcMsg!!
|
||||
val msgId = srcMsg.pbReserve?.msgRand?.toLong() ?: 0
|
||||
kritorMessages.add(Element.newBuilder().apply {
|
||||
this.type = ElementType.REPLY
|
||||
this.reply = ReplyElement.newBuilder().apply {
|
||||
this.messageId = msgId.toString()
|
||||
}.build()
|
||||
}.build())
|
||||
} else if (element.lightApp != null) {
|
||||
val data = element.lightApp!!.data!!
|
||||
val jsonStr =
|
||||
(if (data[0].toInt() == 1) DeflateTools.uncompress(data.slice(1)) else data.slice(1)).decodeToString()
|
||||
val json = jsonStr.asJsonObject
|
||||
when (json["app"].asString) {
|
||||
"com.tencent.multimsg" -> {
|
||||
val info = json["meta"].asJsonObject["detail"].asJsonObject
|
||||
kritorMessages.add(Element.newBuilder().apply {
|
||||
this.type = ElementType.FORWARD
|
||||
this.forward = ForwardElement.newBuilder().apply {
|
||||
this.resId = info["resid"].asString
|
||||
this.uniseq = info["uniseq"].asString
|
||||
this.summary = info["summary"].asString
|
||||
this.description = info["news"].asJsonArray.joinToString("\n") {
|
||||
it.asJsonObject["text"].asString
|
||||
}
|
||||
}.build()
|
||||
}.build())
|
||||
}
|
||||
|
||||
"com.tencent.troopsharecard" -> {
|
||||
val info = json["meta"].asJsonObject["contact"].asJsonObject
|
||||
kritorMessages.add(Element.newBuilder().apply {
|
||||
this.type = ElementType.CONTACT
|
||||
this.contact = ContactElement.newBuilder().apply {
|
||||
this.scene = Scene.GROUP
|
||||
this.peer = info["jumpUrl"].asString.split("group_code=")[1]
|
||||
}.build()
|
||||
}.build())
|
||||
|
||||
}
|
||||
|
||||
"com.tencent.contact.lua" -> {
|
||||
val info = json["meta"].asJsonObject["contact"].asJsonObject
|
||||
kritorMessages.add(Element.newBuilder().apply {
|
||||
this.type = ElementType.CONTACT
|
||||
this.contact = ContactElement.newBuilder().apply {
|
||||
this.scene = Scene.FRIEND
|
||||
this.peer = info["jumpUrl"].asString.split("uin=")[1]
|
||||
}.build()
|
||||
}.build())
|
||||
}
|
||||
|
||||
"com.tencent.map" -> {
|
||||
val info = json["meta"].asJsonObject["Location.Search"].asJsonObject
|
||||
kritorMessages.add(Element.newBuilder().apply {
|
||||
this.type = ElementType.LOCATION
|
||||
this.location = LocationElement.newBuilder().apply {
|
||||
this.lat = info["lat"].asString.toFloat()
|
||||
this.lon = info["lng"].asString.toFloat()
|
||||
this.address = info["address"].asString
|
||||
this.title = info["name"].asString
|
||||
}.build()
|
||||
}.build())
|
||||
}
|
||||
|
||||
else -> {
|
||||
kritorMessages.add(Element.newBuilder().apply {
|
||||
this.type = ElementType.JSON
|
||||
this.json = JsonElement.newBuilder().apply {
|
||||
this.json = jsonStr
|
||||
}.build()
|
||||
}.build())
|
||||
}
|
||||
}
|
||||
} else if (element.commonElem != null) {
|
||||
val commonElem = element.commonElem!!
|
||||
when (commonElem.serviceType) {
|
||||
37 -> {
|
||||
val qFaceExtra = commonElem.elem!!.decodeProtobuf<QFaceExtra>()
|
||||
when (qFaceExtra.faceId) {
|
||||
358 -> kritorMessages.add(Element.newBuilder().apply {
|
||||
this.type = ElementType.DICE
|
||||
this.dice = DiceElement.newBuilder().apply {
|
||||
this.id = qFaceExtra.result!!.toInt()
|
||||
}.build()
|
||||
}.build())
|
||||
|
||||
359 -> kritorMessages.add(Element.newBuilder().apply {
|
||||
this.type = ElementType.RPS
|
||||
this.rps = RpsElement.newBuilder().apply {
|
||||
this.id = qFaceExtra.result!!.toInt()
|
||||
}.build()
|
||||
}.build())
|
||||
|
||||
else -> kritorMessages.add(Element.newBuilder().apply {
|
||||
this.type = ElementType.FACE
|
||||
this.face = FaceElement.newBuilder().apply {
|
||||
this.id = qFaceExtra.faceId ?: 0
|
||||
this.isBig = false
|
||||
this.result = qFaceExtra.result?.toInt() ?: 0
|
||||
}.build()
|
||||
}.build())
|
||||
}
|
||||
}
|
||||
|
||||
45 -> {
|
||||
val markdownExtra = commonElem.elem!!.decodeProtobuf<MarkdownExtra>()
|
||||
kritorMessages.add(Element.newBuilder().apply {
|
||||
this.type = ElementType.MARKDOWN
|
||||
this.markdown = MarkdownElement.newBuilder().apply {
|
||||
this.markdown = markdownExtra.content!!
|
||||
}.build()
|
||||
}.build())
|
||||
}
|
||||
|
||||
46 -> {
|
||||
val buttonExtra = commonElem.elem!!.decodeProtobuf<ButtonExtra>()
|
||||
kritorMessages.add(
|
||||
Element.newBuilder().setKeyboard(KeyboardElement.newBuilder().apply {
|
||||
this.addAllRows(buttonExtra.field1!!.rows!!.map { row ->
|
||||
KeyboardRow.newBuilder().apply {
|
||||
this.addAllButtons(row.buttons!!.map { button ->
|
||||
Button.newBuilder().apply {
|
||||
this.id = button.id
|
||||
this.renderData = ButtonRender.newBuilder().apply {
|
||||
this.label = button.renderData?.label ?: ""
|
||||
this.visitedLabel = button.renderData?.visitedLabel ?: ""
|
||||
this.style = button.renderData?.style ?: 0
|
||||
}.build()
|
||||
this.action = ButtonAction.newBuilder().apply {
|
||||
this.type = button.action?.type?:0
|
||||
this.permission = ButtonActionPermission.newBuilder().apply {
|
||||
this.type = button.action?.permission?.type?:0
|
||||
button.action?.permission?.specifyRoleIds?.let {
|
||||
this.addAllRoleIds(it)
|
||||
}
|
||||
button.action?.permission?.specifyUserIds?.let {
|
||||
this.addAllUserIds(it)
|
||||
}
|
||||
}.build()
|
||||
this.unsupportedTips = button.action?.unsupportTips ?: ""
|
||||
this.data = button.action?.data ?: ""
|
||||
this.reply = button.action?.reply ?: false
|
||||
this.enter = button.action?.enter ?: false
|
||||
}.build()
|
||||
}.build()
|
||||
})
|
||||
}.build()
|
||||
})
|
||||
this.botAppid = buttonExtra.field1?.appid?.toLong() ?: 0L
|
||||
}.build()).build()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return kritorMessages
|
||||
}
|
916
xposed/src/main/java/qq/service/msg/NtMsgConvertor.kt
Normal file
916
xposed/src/main/java/qq/service/msg/NtMsgConvertor.kt
Normal file
@ -0,0 +1,916 @@
|
||||
package qq.service.msg
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import com.tencent.mobileqq.emoticon.QQSysFaceUtil
|
||||
import com.tencent.mobileqq.pb.ByteStringMicro
|
||||
import com.tencent.mobileqq.qroute.QRoute
|
||||
import com.tencent.qphone.base.remote.ToServiceMsg
|
||||
import com.tencent.qqnt.aio.adapter.api.IAIOPttApi
|
||||
import com.tencent.qqnt.kernel.nativeinterface.*
|
||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||
import com.tencent.qqnt.kernel.nativeinterface.FaceElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MarkdownElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MarketFaceElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.ReplyElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.TextElement
|
||||
import com.tencent.qqnt.msg.api.IMsgService
|
||||
import io.kritor.common.*
|
||||
import io.kritor.common.Element.ElementType
|
||||
import io.kritor.common.ImageElement.ImageType
|
||||
import io.kritor.common.MusicElement.MusicPlatform
|
||||
import io.kritor.common.VideoElement
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import moe.fuqiuluo.shamrock.config.EnableOldBDH
|
||||
import moe.fuqiuluo.shamrock.config.get
|
||||
import moe.fuqiuluo.shamrock.helper.ActionMsgException
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.helper.LogicException
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||||
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
||||
import moe.fuqiuluo.shamrock.tools.json
|
||||
import moe.fuqiuluo.shamrock.utils.AudioUtils
|
||||
import moe.fuqiuluo.shamrock.utils.DownloadUtils
|
||||
import moe.fuqiuluo.shamrock.utils.FileUtils
|
||||
import moe.fuqiuluo.shamrock.utils.MediaType
|
||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||
import mqq.app.MobileQQ
|
||||
import qq.service.QQInterfaces.Companion.app
|
||||
import qq.service.bdh.FileTransfer
|
||||
import qq.service.bdh.PictureResource
|
||||
import qq.service.bdh.Private
|
||||
import qq.service.bdh.Transfer
|
||||
import qq.service.bdh.Troop
|
||||
import qq.service.bdh.VideoResource
|
||||
import qq.service.bdh.VoiceResource
|
||||
import qq.service.bdh.trans
|
||||
import qq.service.bdh.with
|
||||
import qq.service.contact.ContactHelper
|
||||
import qq.service.contact.longPeer
|
||||
import qq.service.group.GroupHelper
|
||||
import qq.service.internals.NTServiceFetcher
|
||||
import qq.service.internals.msgService
|
||||
import qq.service.lightapp.ArkAppInfo
|
||||
import qq.service.lightapp.ArkMsgHelper
|
||||
import qq.service.lightapp.LbsHelper
|
||||
import qq.service.lightapp.MusicHelper
|
||||
import qq.service.lightapp.WeatherHelper
|
||||
import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77
|
||||
import tencent.im.oidb.cmd0xdc2.oidb_cmd0xdc2
|
||||
import tencent.im.oidb.oidb_sso
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
|
||||
/**
|
||||
* 将请求消息(io.kritor.message)转成NT消息(com.tencent.qqnt.*)发送
|
||||
*/
|
||||
|
||||
|
||||
typealias Messages = Collection<Element>
|
||||
private typealias NtConvertor = suspend (Contact, Long, Element) -> Result<MsgElement>
|
||||
|
||||
object NtMsgConvertor {
|
||||
private val ntConvertors = mapOf<ElementType, NtConvertor>(
|
||||
ElementType.TEXT to ::textConvertor,
|
||||
ElementType.AT to ::atConvertor,
|
||||
ElementType.FACE to ::faceConvertor,
|
||||
ElementType.BUBBLE_FACE to ::bubbleFaceConvertor,
|
||||
ElementType.REPLY to ::replyConvertor,
|
||||
ElementType.IMAGE to ::imageConvertor,
|
||||
ElementType.VOICE to ::voiceConvertor,
|
||||
ElementType.VIDEO to ::videoConvertor,
|
||||
ElementType.BASKETBALL to ::basketballConvertor,
|
||||
ElementType.DICE to ::diceConvertor,
|
||||
ElementType.RPS to ::rpsConvertor,
|
||||
ElementType.POKE to ::pokeConvertor,
|
||||
ElementType.MUSIC to ::musicConvertor,
|
||||
ElementType.WEATHER to ::weatherConvertor,
|
||||
ElementType.LOCATION to ::locationConvertor,
|
||||
ElementType.SHARE to ::shareConvertor,
|
||||
ElementType.CONTACT to ::contactConvertor,
|
||||
ElementType.JSON to ::jsonConvertor,
|
||||
ElementType.FORWARD to ::forwardConvertor,
|
||||
ElementType.MARKDOWN to ::markdownConvertor,
|
||||
ElementType.KEYBOARD to ::buttonConvertor,
|
||||
)
|
||||
|
||||
suspend fun convertToNtMsgs(contact: Contact, msgId: Long, msgs: Messages): ArrayList<MsgElement> {
|
||||
val ntMsgs = ArrayList<MsgElement>()
|
||||
msgs.forEach {
|
||||
val convertor = ntConvertors[it.type]
|
||||
if (convertor == null) {
|
||||
LogCenter.log("未知的消息类型: ${it.type}", Level.WARN)
|
||||
} else {
|
||||
try {
|
||||
ntMsgs.add(convertor(contact, msgId, it).getOrThrow())
|
||||
} catch (e: Throwable) {
|
||||
if (e !is ActionMsgException) {
|
||||
LogCenter.log("消息转换失败: ${it.type}", Level.WARN)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ntMsgs
|
||||
}
|
||||
|
||||
private suspend fun textConvertor(contact: Contact, msgId: Long, text: Element): Result<MsgElement> {
|
||||
val elem = MsgElement()
|
||||
elem.elementType = MsgConstant.KELEMTYPETEXT
|
||||
elem.textElement = TextElement()
|
||||
elem.textElement.content = text.text.text
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun atConvertor(contact: Contact, msgId: Long, sourceAt: Element): Result<MsgElement> {
|
||||
if (contact.chatType != MsgConstant.KCHATTYPEGROUP) {
|
||||
LogCenter.log("暂不支持非群聊的@元素", Level.WARN)
|
||||
return Result.failure(ActionMsgException)
|
||||
}
|
||||
|
||||
val elem = MsgElement()
|
||||
val at = TextElement()
|
||||
val uid = sourceAt.at.uid
|
||||
if (uid == "all" || uid == "0") {
|
||||
at.content = "@全体成员"
|
||||
at.atType = MsgConstant.ATTYPEALL
|
||||
at.atNtUid = "0"
|
||||
} else {
|
||||
val uin = ContactHelper.getUinByUidAsync(uid)
|
||||
val info = GroupHelper.getTroopMemberInfoByUinV2(contact.peerUid, uin, true).onFailure {
|
||||
LogCenter.log("无法获取群成员信息: contact=$contact, id=${uin}", Level.WARN)
|
||||
}.getOrNull()
|
||||
at.content = "@${
|
||||
info?.troopnick.ifNullOrEmpty { info?.friendnick }
|
||||
?: uin
|
||||
}"
|
||||
at.atType = MsgConstant.ATTYPEONE
|
||||
at.atNtUid = uid
|
||||
}
|
||||
elem.textElement = at
|
||||
elem.elementType = MsgConstant.KELEMTYPETEXT
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun faceConvertor(contact: Contact, msgId: Long, sourceFace: Element): Result<MsgElement> {
|
||||
val serverId = sourceFace.face.id
|
||||
val big = sourceFace.face.isBig || serverId == 394
|
||||
|
||||
val elem = MsgElement()
|
||||
elem.elementType = MsgConstant.KELEMTYPEFACE
|
||||
val face = FaceElement()
|
||||
|
||||
// 1 old face
|
||||
// 2 normal face
|
||||
// 3 super face
|
||||
// 4 is market face
|
||||
// 5 is vas poke
|
||||
face.faceType = if (big) 3 else 2
|
||||
face.faceIndex = serverId
|
||||
face.faceText = QQSysFaceUtil.getFaceDescription(QQSysFaceUtil.convertToLocal(serverId))
|
||||
if (serverId == 394) {
|
||||
face.stickerId = "40"
|
||||
face.packId = "1"
|
||||
face.sourceType = 1
|
||||
face.stickerType = 3
|
||||
face.randomType = 1
|
||||
face.resultId = Random.nextInt(1..5).toString()
|
||||
} else if (big) {
|
||||
face.imageType = 0
|
||||
face.stickerId = "30"
|
||||
face.packId = "1"
|
||||
face.sourceType = 1
|
||||
face.stickerType = 1
|
||||
face.randomType = 1
|
||||
} else {
|
||||
face.imageType = 0
|
||||
face.packId = "0"
|
||||
}
|
||||
elem.faceElement = face
|
||||
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun bubbleFaceConvertor(
|
||||
contact: Contact,
|
||||
msgId: Long,
|
||||
sourceBubbleFace: Element
|
||||
): Result<MsgElement> {
|
||||
val faceId = sourceBubbleFace.bubbleFace.id
|
||||
val local = QQSysFaceUtil.convertToLocal(faceId)
|
||||
val name = QQSysFaceUtil.getFaceDescription(local)
|
||||
val count = sourceBubbleFace.bubbleFace.count
|
||||
val elem = MsgElement()
|
||||
elem.elementType = MsgConstant.KELEMTYPEFACEBUBBLE
|
||||
val face = FaceBubbleElement()
|
||||
face.faceType = 13
|
||||
face.faceCount = count
|
||||
face.faceSummary = QQSysFaceUtil.getPrueFaceDescription(name)
|
||||
val smallYellowFaceInfo = SmallYellowFaceInfo()
|
||||
smallYellowFaceInfo.index = faceId
|
||||
smallYellowFaceInfo.compatibleText = face.faceSummary
|
||||
smallYellowFaceInfo.text = face.faceSummary
|
||||
face.yellowFaceInfo = smallYellowFaceInfo
|
||||
face.faceFlag = 0
|
||||
face.content = "[${face.faceSummary}]x$count"
|
||||
elem.faceBubbleElement = face
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun replyConvertor(contact: Contact, msgId: Long, sourceReply: Element): Result<MsgElement> {
|
||||
val element = MsgElement()
|
||||
element.elementType = MsgConstant.KELEMTYPEREPLY
|
||||
val reply = ReplyElement()
|
||||
|
||||
reply.replayMsgId = sourceReply.reply.messageId.toLong()
|
||||
reply.sourceMsgIdInRecords = reply.replayMsgId
|
||||
|
||||
if (reply.replayMsgId == 0L) {
|
||||
LogCenter.log("无法获取被回复消息", Level.ERROR)
|
||||
}
|
||||
|
||||
withTimeoutOrNull(3000) {
|
||||
suspendCancellableCoroutine {
|
||||
QRoute.api(IMsgService::class.java)
|
||||
.getMsgsByMsgId(contact, arrayListOf(reply.replayMsgId)) { _, _, records ->
|
||||
it.resume(records)
|
||||
}
|
||||
}
|
||||
}?.firstOrNull()?.let {
|
||||
reply.replayMsgSeq = it.msgSeq
|
||||
//reply.sourceMsgText = it.elements.firstOrNull { it.elementType == MsgConstant.KELEMTYPETEXT }?.textElement?.content
|
||||
reply.replyMsgTime = it.msgTime
|
||||
reply.senderUidStr = it.senderUid
|
||||
reply.senderUid = it.senderUin
|
||||
}
|
||||
|
||||
element.replyElement = reply
|
||||
return Result.success(element)
|
||||
}
|
||||
|
||||
private suspend fun imageConvertor(contact: Contact, msgId: Long, sourceImage: Element): Result<MsgElement> {
|
||||
val isOriginal = sourceImage.image.type == ImageType.ORIGIN
|
||||
val isFlash = sourceImage.image.type == ImageType.FLASH
|
||||
val file = when (sourceImage.image.dataCase!!) {
|
||||
ImageElement.DataCase.FILE_NAME -> {
|
||||
val fileMd5 = sourceImage.image.fileName.replace(regex = "[{}\\-]".toRegex(), replacement = "")
|
||||
.split(".")[0].lowercase()
|
||||
FileUtils.getFileByMd5(fileMd5)
|
||||
}
|
||||
|
||||
ImageElement.DataCase.FILE_PATH -> {
|
||||
val filePath = sourceImage.image.filePath
|
||||
File(filePath).inputStream().use {
|
||||
FileUtils.saveFileToCache(it)
|
||||
}
|
||||
}
|
||||
|
||||
ImageElement.DataCase.FILE -> {
|
||||
FileUtils.saveFileToCache(
|
||||
ByteArrayInputStream(
|
||||
sourceImage.image.file.toByteArray()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
ImageElement.DataCase.FILE_URL -> {
|
||||
val tmp = FileUtils.getTmpFile()
|
||||
if (DownloadUtils.download(sourceImage.image.fileUrl, tmp)) {
|
||||
tmp.inputStream().use {
|
||||
FileUtils.saveFileToCache(it)
|
||||
}.also {
|
||||
tmp.delete()
|
||||
}
|
||||
} else {
|
||||
tmp.delete()
|
||||
return Result.failure(LogicException("图片资源下载失败: ${sourceImage.image.fileUrl}"))
|
||||
}
|
||||
}
|
||||
|
||||
ImageElement.DataCase.DATA_NOT_SET -> return Result.failure(IllegalArgumentException("ImageElement data is not set"))
|
||||
}
|
||||
|
||||
if (EnableOldBDH.get()) {
|
||||
Transfer with when (contact.chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> Troop(contact.peerUid)
|
||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP, MsgConstant.KCHATTYPEC2C -> Private(
|
||||
contact.longPeer().toString()
|
||||
)
|
||||
|
||||
MsgConstant.KCHATTYPEGUILD -> Troop(contact.peerUid)
|
||||
else -> return Result.failure(Exception("Not supported chatType(${contact.chatType}) for PictureMsg"))
|
||||
} trans PictureResource(file)
|
||||
}
|
||||
|
||||
val elem = MsgElement()
|
||||
elem.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 = isOriginal
|
||||
pic.picType = FileUtils.getPicType(file)
|
||||
pic.picSubType = 0
|
||||
pic.isFlashPic = isFlash
|
||||
|
||||
elem.picElement = pic
|
||||
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun voiceConvertor(contact: Contact, msgId: Long, sourceVoice: Element): Result<MsgElement> {
|
||||
var file = when (sourceVoice.voice.dataCase!!) {
|
||||
VoiceElement.DataCase.FILE_NAME -> {
|
||||
val fileMd5 = sourceVoice.voice.fileName.replace(regex = "[{}\\-]".toRegex(), replacement = "")
|
||||
.split(".")[0].lowercase()
|
||||
FileUtils.getFileByMd5(fileMd5)
|
||||
}
|
||||
|
||||
VoiceElement.DataCase.FILE_PATH -> {
|
||||
val filePath = sourceVoice.voice.filePath
|
||||
File(filePath).inputStream().use {
|
||||
FileUtils.saveFileToCache(it)
|
||||
}
|
||||
}
|
||||
|
||||
VoiceElement.DataCase.FILE -> {
|
||||
FileUtils.saveFileToCache(
|
||||
sourceVoice.voice.file.toByteArray().inputStream()
|
||||
)
|
||||
}
|
||||
|
||||
VoiceElement.DataCase.FILE_URL -> {
|
||||
val tmp = FileUtils.getTmpFile()
|
||||
if (DownloadUtils.download(sourceVoice.voice.fileUrl, tmp)) {
|
||||
tmp.inputStream().use {
|
||||
FileUtils.saveFileToCache(it)
|
||||
}.also {
|
||||
tmp.delete()
|
||||
}
|
||||
} else {
|
||||
tmp.delete()
|
||||
return Result.failure(LogicException("音频资源下载失败: ${sourceVoice.voice.fileUrl}"))
|
||||
}
|
||||
}
|
||||
|
||||
VoiceElement.DataCase.DATA_NOT_SET -> return Result.failure(IllegalArgumentException("VoiceElement data is not set"))
|
||||
}
|
||||
|
||||
val isMagic = sourceVoice.voice.magic
|
||||
|
||||
val ptt = PttElement()
|
||||
|
||||
when (AudioUtils.getMediaType(file)) {
|
||||
MediaType.Silk -> {
|
||||
LogCenter.log({ "Silk: $file" }, Level.DEBUG)
|
||||
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
|
||||
ptt.duration = QRoute.api(IAIOPttApi::class.java)
|
||||
.getPttFileDuration(file.absolutePath)
|
||||
}
|
||||
|
||||
MediaType.Amr -> {
|
||||
LogCenter.log({ "Amr: $file" }, Level.DEBUG)
|
||||
ptt.duration = AudioUtils.getDurationSec(file)
|
||||
ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR
|
||||
}
|
||||
|
||||
MediaType.Pcm -> {
|
||||
LogCenter.log({ "Pcm To Silk: $file" }, Level.DEBUG)
|
||||
val result = AudioUtils.pcmToSilk(file)
|
||||
ptt.duration = (result.second * 0.001).roundToInt()
|
||||
file = result.first
|
||||
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
|
||||
}
|
||||
|
||||
else -> {
|
||||
LogCenter.log({ "Audio To SILK: $file" }, Level.DEBUG)
|
||||
val result = AudioUtils.audioToSilk(file)
|
||||
ptt.duration = runCatching {
|
||||
QRoute.api(IAIOPttApi::class.java)
|
||||
.getPttFileDuration(result.second.absolutePath)
|
||||
}.getOrElse {
|
||||
result.first
|
||||
}
|
||||
file = result.second
|
||||
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
|
||||
}
|
||||
}
|
||||
|
||||
val elem = MsgElement()
|
||||
elem.elementType = MsgConstant.KELEMTYPEPTT
|
||||
ptt.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath)
|
||||
|
||||
if (EnableOldBDH.get()) {
|
||||
if (!(Transfer with when (contact.chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> Troop(contact.peerUid)
|
||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP, MsgConstant.KCHATTYPEC2C -> Private(
|
||||
contact.longPeer().toString()
|
||||
)
|
||||
|
||||
MsgConstant.KCHATTYPEGUILD -> Troop(contact.peerUid)
|
||||
else -> return Result.failure(Exception("Not supported chatType(${contact.chatType}) for VoiceMsg"))
|
||||
} trans VoiceResource(file))
|
||||
) {
|
||||
return Result.failure(RuntimeException("上传语音失败: $file"))
|
||||
}
|
||||
ptt.filePath = file.absolutePath
|
||||
} else {
|
||||
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) != file.length()) {
|
||||
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
|
||||
}
|
||||
if (originalPath != null) {
|
||||
ptt.filePath = originalPath
|
||||
} else {
|
||||
ptt.filePath = file.absolutePath
|
||||
}
|
||||
}
|
||||
|
||||
ptt.canConvert2Text = true
|
||||
ptt.fileId = 0
|
||||
ptt.fileUuid = ""
|
||||
ptt.text = ""
|
||||
|
||||
if (!isMagic) {
|
||||
ptt.voiceType = MsgConstant.KPTTVOICETYPESOUNDRECORD
|
||||
ptt.voiceChangeType = MsgConstant.KPTTVOICECHANGETYPENONE
|
||||
} else {
|
||||
ptt.voiceType = MsgConstant.KPTTVOICETYPEVOICECHANGE
|
||||
ptt.voiceChangeType = MsgConstant.KPTTVOICECHANGETYPEECHO
|
||||
}
|
||||
|
||||
elem.pttElement = ptt
|
||||
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun videoConvertor(contact: Contact, msgId: Long, sourceVideo: Element): Result<MsgElement> {
|
||||
val elem = MsgElement()
|
||||
val video = com.tencent.qqnt.kernel.nativeinterface.VideoElement()
|
||||
|
||||
val file = when (sourceVideo.video.dataCase!!) {
|
||||
VideoElement.DataCase.FILE -> {
|
||||
FileUtils.saveFileToCache(
|
||||
sourceVideo.video.file.toByteArray().inputStream()
|
||||
)
|
||||
}
|
||||
|
||||
VideoElement.DataCase.FILE_NAME -> {
|
||||
val fileMd5 = sourceVideo.video.fileName.replace(regex = "[{}\\-]".toRegex(), replacement = "")
|
||||
.split(".")[0].lowercase()
|
||||
FileUtils.getFileByMd5(fileMd5)
|
||||
}
|
||||
|
||||
VideoElement.DataCase.FILE_PATH -> {
|
||||
val filePath = sourceVideo.video.filePath
|
||||
File(filePath).inputStream().use {
|
||||
FileUtils.saveFileToCache(it)
|
||||
}
|
||||
}
|
||||
|
||||
VideoElement.DataCase.FILE_URL -> {
|
||||
val tmp = FileUtils.getTmpFile()
|
||||
if (DownloadUtils.download(sourceVideo.video.fileUrl, tmp)) {
|
||||
tmp.inputStream().use {
|
||||
FileUtils.saveFileToCache(it)
|
||||
}.also {
|
||||
tmp.delete()
|
||||
}
|
||||
} else {
|
||||
tmp.delete()
|
||||
return Result.failure(LogicException("视频资源下载失败: ${sourceVideo.video.fileUrl}"))
|
||||
}
|
||||
}
|
||||
|
||||
VideoElement.DataCase.DATA_NOT_SET -> return Result.failure(IllegalArgumentException("VideoElement data is not set"))
|
||||
}
|
||||
|
||||
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!!)
|
||||
}
|
||||
|
||||
if (EnableOldBDH.get()) {
|
||||
Transfer with when (contact.chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> Troop(contact.peerUid)
|
||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP, MsgConstant.KCHATTYPEC2C -> Private(
|
||||
contact.longPeer().toString()
|
||||
)
|
||||
|
||||
MsgConstant.KCHATTYPEGUILD -> Troop(contact.peerUid)
|
||||
else -> return Result.failure(Exception("Not supported chatType(${contact.chatType}) for VideoMsg"))
|
||||
} trans VideoResource(file, File(thumbPath.toString()))
|
||||
}
|
||||
|
||||
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
|
||||
elem.elementType = MsgConstant.KELEMTYPEVIDEO
|
||||
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun basketballConvertor(
|
||||
contact: Contact,
|
||||
msgId: Long,
|
||||
sourceBasketball: Element
|
||||
): Result<MsgElement> {
|
||||
val elem = MsgElement()
|
||||
elem.elementType = MsgConstant.KELEMTYPEFACE
|
||||
val face = FaceElement()
|
||||
face.faceIndex = 114
|
||||
face.faceText = "/篮球"
|
||||
face.faceType = 3
|
||||
face.packId = "1"
|
||||
face.stickerId = "13"
|
||||
face.sourceType = 1
|
||||
face.stickerType = 2
|
||||
face.resultId = Random.nextInt(1..5).toString()
|
||||
face.surpriseId = ""
|
||||
face.randomType = 1
|
||||
elem.faceElement = face
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun diceConvertor(contact: Contact, msgId: Long, sourceDice: Element): Result<MsgElement> {
|
||||
val elem = MsgElement()
|
||||
elem.elementType = MsgConstant.KELEMTYPEMARKETFACE
|
||||
val market = MarketFaceElement(
|
||||
6, 1, 11464, 3, 0, 200, 200,
|
||||
"[骰子]", "4823d3adb15df08014ce5d6796b76ee1", "409e2a69b16918f9",
|
||||
null, null, 0, 0, 0, 1, 0,
|
||||
null, null, null, // jumpurl
|
||||
"", null, null,
|
||||
null, null, arrayListOf(MarketFaceSupportSize(200, 200)), null
|
||||
)
|
||||
elem.marketFaceElement = market
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun rpsConvertor(contact: Contact, msgId: Long, sourceRps: Element): Result<MsgElement> {
|
||||
val elem = MsgElement()
|
||||
elem.elementType = MsgConstant.KELEMTYPEMARKETFACE
|
||||
val market = MarketFaceElement(
|
||||
6, 1, 11415, 3, 0, 200, 200,
|
||||
"[猜拳]", "83C8A293AE65CA140F348120A77448EE", "7de39febcf45e6db",
|
||||
null, null, 0, 0, 0, 1, 0,
|
||||
null, null, null,
|
||||
"", null, null,
|
||||
null, null, arrayListOf(MarketFaceSupportSize(200, 200)), null
|
||||
)
|
||||
elem.marketFaceElement = market
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun pokeConvertor(contact: Contact, msgId: Long, sourcePoke: Element): Result<MsgElement> {
|
||||
val elem = MsgElement()
|
||||
val face = FaceElement()
|
||||
face.faceIndex = 0
|
||||
face.faceText = ""
|
||||
face.faceType = 5
|
||||
face.packId = null
|
||||
face.pokeType = sourcePoke.poke.type
|
||||
face.spokeSummary = ""
|
||||
face.doubleHit = 0
|
||||
face.vaspokeId = sourcePoke.poke.id
|
||||
face.vaspokeName = ""
|
||||
face.vaspokeMinver = ""
|
||||
face.pokeStrength = sourcePoke.poke.strength
|
||||
face.msgType = 0
|
||||
face.faceBubbleCount = 0
|
||||
face.oldVersionStr = "[截一戳]请使用最新版手机QQ体验新功能。"
|
||||
face.pokeFlag = 0
|
||||
elem.elementType = MsgConstant.KELEMTYPEFACE
|
||||
elem.faceElement = face
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun musicConvertor(contact: Contact, msgId: Long, sourceMusic: Element): Result<MsgElement> {
|
||||
when (val type = sourceMusic.music.platform) {
|
||||
MusicPlatform.QQ -> {
|
||||
val id = sourceMusic.music.id
|
||||
if (!MusicHelper.tryShareQQMusicById(contact, msgId, id)) {
|
||||
LogCenter.log("无法发送QQ音乐分享", Level.ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
MusicPlatform.NETEASE -> {
|
||||
val id = sourceMusic.music.id
|
||||
if (!MusicHelper.tryShare163MusicById(contact, msgId, id)) {
|
||||
LogCenter.log("无法发送网易云音乐分享", Level.ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
MusicPlatform.CUSTOM -> {
|
||||
val data = sourceMusic.music.custom
|
||||
ArkMsgHelper.tryShareMusic(
|
||||
contact,
|
||||
msgId,
|
||||
ArkAppInfo.QQMusic,
|
||||
data.title,
|
||||
data.author,
|
||||
data.url,
|
||||
data.pic,
|
||||
data.audio
|
||||
)
|
||||
}
|
||||
|
||||
else -> LogCenter.log("不支持的音乐分享类型: $type", Level.ERROR)
|
||||
}
|
||||
|
||||
return Result.failure(ActionMsgException)
|
||||
}
|
||||
|
||||
private suspend fun weatherConvertor(contact: Contact, msgId: Long, sourceWeather: Element): Result<MsgElement> {
|
||||
val code = if (sourceWeather.weather.code.isNullOrEmpty()) {
|
||||
val city = sourceWeather.weather.city
|
||||
WeatherHelper.searchCity(city).onFailure {
|
||||
LogCenter.log("无法获取城市天气: $city", Level.ERROR)
|
||||
}.getOrThrow().first().adcode
|
||||
} else sourceWeather.weather.code.toInt()
|
||||
WeatherHelper.fetchWeatherCard(code).onSuccess {
|
||||
val element = MsgElement()
|
||||
element.elementType = MsgConstant.KELEMTYPEARKSTRUCT
|
||||
val share = it["weekStore"]
|
||||
.asJsonObject["share"]
|
||||
.asJsonObject["data"].toString()
|
||||
element.arkElement =
|
||||
ArkElement(share, null, MsgConstant.ARKSTRUCTELEMENTSUBTYPEUNKNOWN)
|
||||
return Result.success(element)
|
||||
}.onFailure {
|
||||
return Result.failure(it)
|
||||
}
|
||||
return Result.failure(ActionMsgException)
|
||||
}
|
||||
|
||||
private suspend fun locationConvertor(contact: Contact, msgId: Long, sourceLocation: Element): Result<MsgElement> {
|
||||
LbsHelper.tryShareLocation(
|
||||
contact,
|
||||
sourceLocation.location.lat.toDouble(),
|
||||
sourceLocation.location.lon.toDouble()
|
||||
).onFailure {
|
||||
LogCenter.log("无法发送位置分享", Level.ERROR)
|
||||
}
|
||||
return Result.failure(ActionMsgException)
|
||||
}
|
||||
|
||||
private suspend fun shareConvertor(contact: Contact, msgId: Long, sourceShare: Element): Result<MsgElement> {
|
||||
val url = sourceShare.share.url
|
||||
val image = sourceShare.share.image.ifNullOrEmpty {
|
||||
val startWithPrefix = url.startsWith("http://") || url.startsWith("https://")
|
||||
val endWithPrefix = url.startsWith("/")
|
||||
"http://" + url.split("/")[if (startWithPrefix) 2 else 0] + if (!endWithPrefix) {
|
||||
"/favicon.ico"
|
||||
} else {
|
||||
"favicon.ico"
|
||||
}
|
||||
}!!
|
||||
val title = sourceShare.share.title
|
||||
val content = sourceShare.share.content
|
||||
|
||||
val reqBody = oidb_cmd0xdc2.ReqBody()
|
||||
val info = oidb_cmd0xb77.ReqBody()
|
||||
info.appid.set(100446242L)
|
||||
info.app_type.set(1)
|
||||
info.msg_style.set(0)
|
||||
info.recv_uin.set(contact.longPeer())
|
||||
val clientInfo = oidb_cmd0xb77.ClientInfo()
|
||||
clientInfo.platform.set(1)
|
||||
info.client_info.set(clientInfo)
|
||||
val richMsgBody = oidb_cmd0xb77.RichMsgBody()
|
||||
richMsgBody.using_ark.set(true)
|
||||
richMsgBody.title.set(title)
|
||||
richMsgBody.summary.set(content ?: url)
|
||||
richMsgBody.brief.set("[分享] $title")
|
||||
richMsgBody.url.set(url)
|
||||
richMsgBody.picture_url.set(image)
|
||||
info.ext_info.set(oidb_cmd0xb77.ExtInfo().also {
|
||||
it.msg_seq.set(msgId)
|
||||
})
|
||||
info.rich_msg_body.set(richMsgBody)
|
||||
reqBody.msg_body.set(info)
|
||||
val sendTo = oidb_cmd0xdc2.BatchSendReq()
|
||||
when (contact.chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> sendTo.send_type.set(1)
|
||||
MsgConstant.KCHATTYPEC2C -> sendTo.send_type.set(0)
|
||||
else -> return Result.failure(Exception("Not supported chatType(${contact.chatType}) for ShareMsg"))
|
||||
}
|
||||
sendTo.recv_uin.set(contact.peerUid.toLong())
|
||||
reqBody.batch_send_req.add(sendTo)
|
||||
val to = ToServiceMsg("mobileqq.service", app.currentAccountUin, "OidbSvc.0xdc2_34")
|
||||
val oidb = oidb_sso.OIDBSSOPkg()
|
||||
oidb.uint32_command.set(0xdc2)
|
||||
oidb.uint32_service_type.set(34)
|
||||
oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(reqBody.toByteArray()))
|
||||
oidb.str_client_version.set(PlatformUtils.getClientVersion(MobileQQ.getContext()))
|
||||
to.putWupBuffer(oidb.toByteArray())
|
||||
to.addAttribute("req_pb_protocol_flag", true)
|
||||
app.sendToService(to)
|
||||
return Result.failure(ActionMsgException)
|
||||
}
|
||||
|
||||
private suspend fun contactConvertor(contact: Contact, msgId: Long, sourceContact: Element): Result<MsgElement> {
|
||||
val elem = MsgElement()
|
||||
|
||||
when (val scene = sourceContact.contact.scene) {
|
||||
Scene.FRIEND -> {
|
||||
val ark = ArkElement(ContactHelper.getSharePrivateArkMsg(contact.longPeer()), null, null)
|
||||
elem.arkElement = ark
|
||||
}
|
||||
|
||||
Scene.GROUP -> {
|
||||
val ark = ArkElement(ContactHelper.getShareTroopArkMsg(contact.longPeer()), null, null)
|
||||
elem.arkElement = ark
|
||||
}
|
||||
|
||||
else -> return Result.failure(LogicException("不支持的联系人分享类型: $scene"))
|
||||
}
|
||||
|
||||
elem.elementType = MsgConstant.KELEMTYPEARKSTRUCT
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun jsonConvertor(contact: Contact, msgId: Long, sourceJson: Element): Result<MsgElement> {
|
||||
val elem = MsgElement()
|
||||
elem.elementType = MsgConstant.KELEMTYPEARKSTRUCT
|
||||
val ark = ArkElement(sourceJson.json.json, null, null)
|
||||
elem.arkElement = ark
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun markdownConvertor(contact: Contact, msgId: Long, sourceMarkdown: Element): Result<MsgElement> {
|
||||
val elem = MsgElement()
|
||||
elem.elementType = MsgConstant.KELEMTYPEMARKDOWN
|
||||
val markdownElement = MarkdownElement(sourceMarkdown.markdown.markdown)
|
||||
elem.markdownElement = markdownElement
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun buttonConvertor(contact: Contact, msgId: Long, sourceButton: Element): Result<MsgElement> {
|
||||
fun tryNewKeyboardButton(button: Button): InlineKeyboardButton {
|
||||
val renderData = button.renderData
|
||||
val action = button.action
|
||||
val permission = action.permission
|
||||
return runCatching {
|
||||
InlineKeyboardButton(
|
||||
button.id, renderData.label, renderData.visitedLabel, renderData.style,
|
||||
action.type, 0,
|
||||
action.unsupportedTips,
|
||||
action.data, false,
|
||||
permission.type,
|
||||
ArrayList(permission.roleIdsList),
|
||||
ArrayList(permission.userIdsList),
|
||||
false, 0, false, arrayListOf()
|
||||
)
|
||||
}.getOrElse {
|
||||
InlineKeyboardButton(
|
||||
button.id, renderData.label, renderData.visitedLabel, renderData.style,
|
||||
action.type, 0,
|
||||
action.unsupportedTips,
|
||||
action.data, false,
|
||||
permission.type,
|
||||
ArrayList(permission.roleIdsList),
|
||||
ArrayList(permission.userIdsList)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val elem = MsgElement()
|
||||
elem.elementType = MsgConstant.KELEMTYPEINLINEKEYBOARD
|
||||
val rows = arrayListOf<InlineKeyboardRow>()
|
||||
|
||||
val keyboard = sourceButton.keyboard
|
||||
keyboard.rowsList.forEach { row ->
|
||||
val buttons = arrayListOf<InlineKeyboardButton>()
|
||||
row.buttonsList.forEach { button ->
|
||||
buttons.add(tryNewKeyboardButton(button))
|
||||
}
|
||||
rows.add(InlineKeyboardRow(buttons))
|
||||
}
|
||||
elem.inlineKeyboardElement = InlineKeyboardElement(rows, 0)
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun forwardConvertor(contact: Contact, msgId: Long, sourceForward: Element): Result<MsgElement> {
|
||||
val resId = sourceForward.forward.resId
|
||||
val filename = sourceForward.forward.uniseq
|
||||
var summary = sourceForward.forward.summary
|
||||
val descriptions = sourceForward.forward.description
|
||||
var news = descriptions?.split("\n")?.map { "text" to it }
|
||||
|
||||
if (news == null || summary == null) {
|
||||
val forwardMsg = MessageHelper.getForwardMsg(resId).getOrElse { return Result.failure(it) }
|
||||
if (news == null) {
|
||||
news = forwardMsg.map {
|
||||
"text" to it.sender.nickName + ": " + descriptions
|
||||
}
|
||||
}
|
||||
if (summary == null) {
|
||||
summary = "查看${forwardMsg.size}条转发消息"
|
||||
}
|
||||
}
|
||||
|
||||
val json = mapOf(
|
||||
"app" to "com.tencent.multimsg",
|
||||
"config" to mapOf(
|
||||
"autosize" to 1,
|
||||
"forward" to 1,
|
||||
"round" to 1,
|
||||
"type" to "normal",
|
||||
"width" to 300
|
||||
),
|
||||
"desc" to "[聊天记录]",
|
||||
"extra" to mapOf(
|
||||
"filename" to filename,
|
||||
"tsum" to 2
|
||||
).json.toString(),
|
||||
"meta" to mapOf(
|
||||
"detail" to mapOf(
|
||||
"news" to news,
|
||||
"resid" to resId,
|
||||
"source" to "群聊的聊天记录",
|
||||
"summary" to summary,
|
||||
"uniseq" to filename
|
||||
)
|
||||
),
|
||||
"prompt" to "[聊天记录]",
|
||||
"ver" to "0.0.0.5",
|
||||
"view" to "contact"
|
||||
)
|
||||
|
||||
val elem = MsgElement()
|
||||
elem.elementType = MsgConstant.KELEMTYPEARKSTRUCT
|
||||
val ark = ArkElement(json.json.toString(), null, null)
|
||||
elem.arkElement = ark
|
||||
return Result.success(elem)
|
||||
}
|
||||
}
|
422
xposed/src/main/java/qq/service/msg/ReqMessageConvertor.kt
Normal file
422
xposed/src/main/java/qq/service/msg/ReqMessageConvertor.kt
Normal file
@ -0,0 +1,422 @@
|
||||
package qq.service.msg
|
||||
|
||||
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.MsgElement
|
||||
import com.tencent.qqnt.msg.api.IMsgService
|
||||
import io.kritor.common.*
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import moe.fuqiuluo.shamrock.helper.ActionMsgException
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.helper.db.ImageDB
|
||||
import moe.fuqiuluo.shamrock.helper.db.ImageMapping
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonArray
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||||
import moe.fuqiuluo.shamrock.tools.asString
|
||||
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
|
||||
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils.QQ_9_0_8_VER
|
||||
import qq.service.bdh.RichProtoSvc
|
||||
import qq.service.contact.ContactHelper
|
||||
import qq.service.contact.longPeer
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* 将NT消息(com.tencent.qqnt.*)转换为请求消息(io.kritor.message.*)推送
|
||||
*/
|
||||
|
||||
private typealias ReqConvertor = suspend (Contact, MsgElement) -> Result<Element>
|
||||
|
||||
private object ReqMsgConvertor {
|
||||
private val convertorMap = hashMapOf(
|
||||
MsgConstant.KELEMTYPETEXT to ::convertText,
|
||||
MsgConstant.KELEMTYPEFACE to ::convertFace,
|
||||
MsgConstant.KELEMTYPEPIC to ::convertImage,
|
||||
MsgConstant.KELEMTYPEPTT to ::convertVoice,
|
||||
MsgConstant.KELEMTYPEVIDEO to ::convertVideo,
|
||||
MsgConstant.KELEMTYPEMARKETFACE to ::convertMarketFace,
|
||||
MsgConstant.KELEMTYPEARKSTRUCT to ::convertStructJson,
|
||||
MsgConstant.KELEMTYPEREPLY to ::convertReply,
|
||||
//MsgConstant.KELEMTYPEGRAYTIP to ::convertGrayTips,
|
||||
MsgConstant.KELEMTYPEFILE to ::convertFile,
|
||||
MsgConstant.KELEMTYPEMARKDOWN to ::convertMarkdown,
|
||||
//MsgConstant.KELEMTYPEMULTIFORWARD to MsgElementConverter::convertXmlMultiMsgElem,
|
||||
//MsgConstant.KELEMTYPESTRUCTLONGMSG to MsgElementConverter::convertXmlLongMsgElem,
|
||||
MsgConstant.KELEMTYPEFACEBUBBLE to ::convertBubbleFace,
|
||||
MsgConstant.KELEMTYPEINLINEKEYBOARD to ::convertInlineKeyboard
|
||||
)
|
||||
|
||||
suspend fun convertText(contact: Contact, element: MsgElement): Result<Element> {
|
||||
val text = element.textElement
|
||||
val elem = Element.newBuilder()
|
||||
if (text.atType != MsgConstant.ATTYPEUNKNOWN) {
|
||||
elem.setAt(AtElement.newBuilder().apply {
|
||||
this.uid = text.atNtUid
|
||||
this.uin = ContactHelper.getUinByUidAsync(text.atNtUid).toLong()
|
||||
})
|
||||
} else {
|
||||
elem.setText(TextElement.newBuilder().apply {
|
||||
this.text = text.content
|
||||
})
|
||||
}
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertFace(contact: Contact, element: MsgElement): Result<Element> {
|
||||
val face = element.faceElement
|
||||
val elem = Element.newBuilder()
|
||||
if (face.faceType == 5) {
|
||||
elem.setPoke(PokeElement.newBuilder().apply {
|
||||
this.id = face.vaspokeId
|
||||
this.type = face.pokeType
|
||||
this.strength = face.pokeStrength
|
||||
})
|
||||
} else {
|
||||
when (face.faceIndex) {
|
||||
114 -> elem.setBasketball(BasketballElement.newBuilder().apply {
|
||||
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
|
||||
})
|
||||
|
||||
358 -> elem.setDice(DiceElement.newBuilder().apply {
|
||||
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
|
||||
})
|
||||
|
||||
359 -> elem.setRps(RpsElement.newBuilder().apply {
|
||||
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
|
||||
})
|
||||
|
||||
394 -> elem.setFace(FaceElement.newBuilder().apply {
|
||||
this.id = face.faceIndex
|
||||
this.isBig = face.faceType == 3
|
||||
this.result = face.resultId.ifNullOrEmpty { "1" }?.toInt() ?: 1
|
||||
})
|
||||
|
||||
else -> elem.setFace(FaceElement.newBuilder().apply {
|
||||
this.id = face.faceIndex
|
||||
this.isBig = face.faceType == 3
|
||||
})
|
||||
}
|
||||
}
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertImage(contact: Contact, element: MsgElement): Result<Element> {
|
||||
val image = element.picElement
|
||||
val md5 = (image.md5HexStr ?: image.fileName
|
||||
.replace("{", "")
|
||||
.replace("}", "")
|
||||
.replace("-", "").split(".")[0])
|
||||
.uppercase()
|
||||
|
||||
var storeId = 0
|
||||
if (PlatformUtils.getQQVersionCode() > QQ_9_0_8_VER) {
|
||||
storeId = image.storeID
|
||||
}
|
||||
|
||||
ImageDB.getInstance().imageMappingDao().insert(
|
||||
ImageMapping(
|
||||
fileName = md5,
|
||||
md5 = md5,
|
||||
chatType = contact.chatType,
|
||||
size = image.fileSize,
|
||||
sha = "",
|
||||
fileId = image.fileUuid,
|
||||
storeId = storeId,
|
||||
)
|
||||
)
|
||||
|
||||
val originalUrl = image.originImageUrl ?: ""
|
||||
LogCenter.log({ "receive image: $image" }, Level.DEBUG)
|
||||
|
||||
val elem = Element.newBuilder()
|
||||
elem.setImage(ImageElement.newBuilder().apply {
|
||||
this.fileMd5 = md5
|
||||
this.fileUrl = when (contact.chatType) {
|
||||
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
|
||||
originalUrl = originalUrl,
|
||||
md5 = md5,
|
||||
fileId = image.fileUuid,
|
||||
width = image.picWidth.toUInt(),
|
||||
height = image.picHeight.toUInt(),
|
||||
sha = "",
|
||||
fileSize = image.fileSize.toULong(),
|
||||
peer = contact.longPeer().toString()
|
||||
)
|
||||
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(
|
||||
originalUrl = originalUrl,
|
||||
md5 = md5,
|
||||
fileId = image.fileUuid,
|
||||
width = image.picWidth.toUInt(),
|
||||
height = image.picHeight.toUInt(),
|
||||
sha = "",
|
||||
fileSize = image.fileSize.toULong(),
|
||||
peer = contact.longPeer().toString(),
|
||||
storeId = storeId
|
||||
)
|
||||
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(
|
||||
originalUrl = originalUrl,
|
||||
md5 = md5,
|
||||
fileId = image.fileUuid,
|
||||
width = image.picWidth.toUInt(),
|
||||
height = image.picHeight.toUInt(),
|
||||
sha = "",
|
||||
fileSize = image.fileSize.toULong(),
|
||||
peer = contact.longPeer().toString(),
|
||||
subPeer = "0"
|
||||
)
|
||||
|
||||
else -> throw UnsupportedOperationException("Not supported chat type: ${contact.chatType}")
|
||||
}
|
||||
this.type =
|
||||
if (image.isFlashPic == true) ImageElement.ImageType.FLASH else if (image.original) ImageElement.ImageType.ORIGIN else ImageElement.ImageType.COMMON
|
||||
this.subType = image.picSubType
|
||||
})
|
||||
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertVoice(contact: Contact, element: MsgElement): Result<Element> {
|
||||
val ptt = element.pttElement
|
||||
val elem = Element.newBuilder()
|
||||
|
||||
val md5 = if (ptt.fileName.startsWith("silk"))
|
||||
ptt.fileName.substring(5)
|
||||
else ptt.md5HexStr
|
||||
|
||||
elem.setVoice(VoiceElement.newBuilder().apply {
|
||||
this.fileUrl = when (contact.chatType) {
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", ptt.fileUuid)
|
||||
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
|
||||
"0",
|
||||
md5.hex2ByteArray(),
|
||||
ptt.fileUuid
|
||||
)
|
||||
|
||||
else -> throw UnsupportedOperationException("Not supported chat type: ${contact.chatType}")
|
||||
}
|
||||
this.fileMd5 = md5
|
||||
this.magic = ptt.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE
|
||||
})
|
||||
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertVideo(contact: Contact, element: MsgElement): Result<Element> {
|
||||
val video = element.videoElement
|
||||
val elem = Element.newBuilder()
|
||||
val md5 = if (video.fileName.contains("/")) {
|
||||
video.videoMd5.takeIf {
|
||||
!it.isNullOrEmpty()
|
||||
}?.hex2ByteArray() ?: video.fileName.split("/").let {
|
||||
it[it.size - 2].hex2ByteArray()
|
||||
}
|
||||
} else video.fileName.split(".")[0].hex2ByteArray()
|
||||
elem.setVideo(VideoElement.newBuilder().apply {
|
||||
this.fileMd5 = md5.toHexString()
|
||||
this.fileUrl = when (contact.chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
||||
else -> throw UnsupportedOperationException("Not supported chat type: ${contact.chatType}")
|
||||
}
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertMarketFace(contact: Contact, element: MsgElement): Result<Element> {
|
||||
val marketFace = element.marketFaceElement
|
||||
val elem = Element.newBuilder()
|
||||
elem.setMarketFace(MarketFaceElement.newBuilder().apply {
|
||||
this.id = marketFace.emojiId
|
||||
})
|
||||
// TODO
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertStructJson(contact: Contact, element: MsgElement): Result<Element> {
|
||||
val data = element.arkElement.bytesData.asJsonObject
|
||||
val elem = Element.newBuilder()
|
||||
when (data["app"].asString) {
|
||||
"com.tencent.multimsg" -> {
|
||||
val info = data["meta"].asJsonObject["detail"].asJsonObject
|
||||
elem.setForward(ForwardElement.newBuilder().apply {
|
||||
this.resId = info["resid"].asString
|
||||
this.uniseq = info["uniseq"].asString
|
||||
this.summary = info["summary"].asString
|
||||
this.description = info["news"].asJsonArray.joinToString("\n") {
|
||||
it.asJsonObject["text"].asString
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
"com.tencent.troopsharecard" -> {
|
||||
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
||||
elem.setContact(ContactElement.newBuilder().apply {
|
||||
this.scene = Scene.GROUP
|
||||
this.peer = info["jumpUrl"].asString.split("group_code=")[1]
|
||||
})
|
||||
}
|
||||
|
||||
"com.tencent.contact.lua" -> {
|
||||
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
||||
elem.setContact(ContactElement.newBuilder().apply {
|
||||
this.scene = Scene.FRIEND
|
||||
this.peer = info["jumpUrl"].asString.split("uin=")[1]
|
||||
})
|
||||
}
|
||||
|
||||
"com.tencent.map" -> {
|
||||
val info = data["meta"].asJsonObject["Location.Search"].asJsonObject
|
||||
elem.setLocation(LocationElement.newBuilder().apply {
|
||||
this.lat = info["lat"].asString.toFloat()
|
||||
this.lon = info["lng"].asString.toFloat()
|
||||
this.address = info["address"].asString
|
||||
this.title = info["name"].asString
|
||||
})
|
||||
}
|
||||
|
||||
else -> elem.setJson(JsonElement.newBuilder().apply {
|
||||
this.json = data.toString()
|
||||
})
|
||||
}
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertReply(contact: Contact, element: MsgElement): Result<Element> {
|
||||
val reply = element.replyElement
|
||||
val elem = Element.newBuilder()
|
||||
elem.setReply(ReplyElement.newBuilder().apply {
|
||||
val msgSeq = reply.replayMsgSeq
|
||||
val sourceRecords = withTimeoutOrNull(3000) {
|
||||
suspendCancellableCoroutine {
|
||||
QRoute.api(IMsgService::class.java)
|
||||
.getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records ->
|
||||
it.resume(records)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sourceRecords.isNullOrEmpty()) {
|
||||
LogCenter.log("无法查询到回复的消息ID: seq = $msgSeq", Level.WARN)
|
||||
this.messageId = reply.replayMsgId.toString()
|
||||
} else {
|
||||
this.messageId = sourceRecords.first().msgId.toString()
|
||||
}
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertFile(contact: Contact, element: MsgElement): Result<Element> {
|
||||
val fileMsg = element.fileElement
|
||||
val fileName = fileMsg.fileName
|
||||
val fileSize = fileMsg.fileSize
|
||||
val expireTime = fileMsg.expireTime ?: 0
|
||||
val fileId = fileMsg.fileUuid
|
||||
val bizId = fileMsg.fileBizId ?: 0
|
||||
val fileSubId = fileMsg.fileSubId ?: ""
|
||||
val url = when (contact.chatType) {
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(
|
||||
contact.guildId,
|
||||
contact.longPeer().toString(),
|
||||
fileId,
|
||||
bizId
|
||||
)
|
||||
|
||||
else -> RichProtoSvc.getGroupFileDownUrl(contact.longPeer(), fileId, bizId)
|
||||
}
|
||||
val elem = Element.newBuilder()
|
||||
elem.setFile(FileElement.newBuilder().apply {
|
||||
this.name = fileName
|
||||
this.size = fileSize
|
||||
this.url = url
|
||||
this.expireTime = expireTime
|
||||
this.id = fileId
|
||||
this.subId = fileSubId
|
||||
this.biz = bizId
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertMarkdown(contact: Contact, element: MsgElement): Result<Element> {
|
||||
val markdown = element.markdownElement
|
||||
val elem = Element.newBuilder()
|
||||
elem.setMarkdown(MarkdownElement.newBuilder().apply {
|
||||
this.markdown = markdown.content
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertBubbleFace(contact: Contact, element: MsgElement): Result<Element> {
|
||||
val bubbleFace = element.faceBubbleElement
|
||||
val elem = Element.newBuilder()
|
||||
elem.setBubbleFace(BubbleFaceElement.newBuilder().apply {
|
||||
this.id = bubbleFace.yellowFaceInfo.index
|
||||
this.count = bubbleFace.faceCount ?: 1
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertInlineKeyboard(contact: Contact, element: MsgElement): Result<Element> {
|
||||
val inlineKeyboard = element.inlineKeyboardElement
|
||||
val elem = Element.newBuilder()
|
||||
elem.setKeyboard(KeyboardElement.newBuilder().apply {
|
||||
this.addAllRows(inlineKeyboard.rows.map { row ->
|
||||
KeyboardRow.newBuilder().apply {
|
||||
this.addAllButtons(row.buttons.map { button ->
|
||||
Button.newBuilder().apply {
|
||||
this.id = button.id
|
||||
this.renderData = ButtonRender.newBuilder().apply {
|
||||
this.label = button.label ?: ""
|
||||
this.visitedLabel = button.visitedLabel ?: ""
|
||||
this.style = button.style
|
||||
}.build()
|
||||
this.action = ButtonAction.newBuilder().apply {
|
||||
this.type = button.type
|
||||
this.permission = ButtonActionPermission.newBuilder().apply {
|
||||
this.type = button.permissionType
|
||||
button.specifyRoleIds?.let {
|
||||
this.addAllRoleIds(it)
|
||||
}
|
||||
button.specifyTinyids?.let {
|
||||
this.addAllUserIds(it)
|
||||
}
|
||||
}.build()
|
||||
this.unsupportedTips = button.unsupportTips ?: ""
|
||||
this.data = button.data ?: ""
|
||||
this.reply = button.isReply
|
||||
this.enter = button.enter
|
||||
}.build()
|
||||
}.build()
|
||||
})
|
||||
}.build()
|
||||
})
|
||||
this.botAppid = inlineKeyboard.botAppid
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
operator fun get(case: Int): ReqConvertor? {
|
||||
return convertorMap[case]
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun NtMessages.toKritorReqMessages(contact: Contact): ArrayList<Element> {
|
||||
val result = arrayListOf<Element>()
|
||||
forEach {
|
||||
ReqMsgConvertor[it.elementType]?.invoke(contact, it)?.onSuccess {
|
||||
result.add(it)
|
||||
}?.onFailure {
|
||||
if (it !is ActionMsgException) {
|
||||
LogCenter.log("消息转换异常: " + it.stackTraceToString(), Level.WARN)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
566
xposed/src/main/java/qq/service/msg/ReqMultiConvertor.kt
Normal file
566
xposed/src/main/java/qq/service/msg/ReqMultiConvertor.kt
Normal file
@ -0,0 +1,566 @@
|
||||
package qq.service.msg
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Base64
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import com.tencent.mobileqq.qroute.QRoute
|
||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import com.tencent.qqnt.msg.api.IMsgService
|
||||
import io.kritor.common.Element
|
||||
import io.kritor.common.ImageElement
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.helper.LogicException
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||||
import moe.fuqiuluo.shamrock.tools.asString
|
||||
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
|
||||
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
||||
import moe.fuqiuluo.shamrock.tools.json
|
||||
import moe.fuqiuluo.shamrock.tools.putBuf32Long
|
||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||
import moe.fuqiuluo.shamrock.utils.DownloadUtils
|
||||
import moe.fuqiuluo.shamrock.utils.FileUtils
|
||||
import protobuf.auto.toByteArray
|
||||
import protobuf.message.Elem
|
||||
import protobuf.message.RichText
|
||||
import protobuf.message.element.*
|
||||
import protobuf.message.element.commelem.Action
|
||||
import protobuf.message.element.commelem.Button
|
||||
import protobuf.message.element.commelem.ButtonExtra
|
||||
import protobuf.message.element.commelem.MarkdownExtra
|
||||
import protobuf.message.element.commelem.Object1
|
||||
import protobuf.message.element.commelem.Permission
|
||||
import protobuf.message.element.commelem.PokeExtra
|
||||
import protobuf.message.element.commelem.QFaceExtra
|
||||
import protobuf.message.element.commelem.RenderData
|
||||
import protobuf.message.element.commelem.Row
|
||||
import protobuf.oidb.cmd0x11c5.C2CUserInfo
|
||||
import protobuf.oidb.cmd0x11c5.GroupUserInfo
|
||||
import qq.service.QQInterfaces
|
||||
import qq.service.bdh.NtV2RichMediaSvc
|
||||
import qq.service.bdh.NtV2RichMediaSvc.fetchGroupResUploadTo
|
||||
import qq.service.contact.ContactHelper
|
||||
import qq.service.contact.longPeer
|
||||
import qq.service.group.GroupHelper
|
||||
import qq.service.lightapp.WeatherHelper
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.UUID
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextULong
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* 请求消息(io.kritor.message.*)转换合并转发消息
|
||||
*/
|
||||
|
||||
suspend fun List<Element>.toRichText(contact: Contact): Result<Pair<String, RichText>> {
|
||||
val summary = StringBuilder()
|
||||
val elems = ArrayList<Elem>()
|
||||
forEach {
|
||||
try {
|
||||
when(it.type!!) {
|
||||
Element.ElementType.TEXT -> {
|
||||
val text = it.text.text
|
||||
val elem = Elem(
|
||||
text = TextMsg(text)
|
||||
)
|
||||
elems.add(elem)
|
||||
summary.append(text)
|
||||
}
|
||||
Element.ElementType.AT -> {
|
||||
when (contact.chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> {
|
||||
val qq = ContactHelper.getUinByUidAsync(it.at.uid)
|
||||
|
||||
val type: Int
|
||||
val nick = if (it.at.uid == "all" || it.at.uin == 0L) {
|
||||
type = 1
|
||||
"@全体成员"
|
||||
} else {
|
||||
type = 0
|
||||
"@" + (GroupHelper.getTroopMemberInfoByUinV2(contact.longPeer().toString(), qq, true).let {
|
||||
val info = it.getOrNull()
|
||||
if (info == null)
|
||||
LogCenter.log("无法获取群成员信息: $qq", Level.ERROR)
|
||||
else info.troopnick
|
||||
.ifNullOrEmpty { info.friendnick }
|
||||
.ifNullOrEmpty { qq }
|
||||
})
|
||||
}
|
||||
val attr6 = ByteBuffer.allocate(6)
|
||||
attr6.put(byteArrayOf(0, 1, 0, 0, 0))
|
||||
attr6.put(nick.length.toByte())
|
||||
attr6.putChar(type.toChar())
|
||||
attr6.putBuf32Long(qq.toLong())
|
||||
attr6.put(byteArrayOf(0, 0))
|
||||
val elem = Elem(
|
||||
text = TextMsg(str = nick, attr6Buf = attr6.array())
|
||||
)
|
||||
elems.add(elem)
|
||||
summary.append(nick)
|
||||
}
|
||||
|
||||
MsgConstant.KCHATTYPEC2C -> {
|
||||
val qq = ContactHelper.getUinByUidAsync(it.at.uid)
|
||||
val display = "@" + (ContactHelper.getProfileCard(qq.toLong()).onSuccess {
|
||||
it.strNick.ifNullOrEmpty { qq }
|
||||
}.onFailure {
|
||||
LogCenter.log("无法获取QQ信息: $qq", Level.WARN)
|
||||
})
|
||||
val elem = Elem(
|
||||
text = TextMsg(str = display)
|
||||
)
|
||||
elems.add(elem)
|
||||
summary.append(display)
|
||||
}
|
||||
else -> throw UnsupportedOperationException("Unsupported chatType($contact) for AtMsg")
|
||||
}
|
||||
}
|
||||
Element.ElementType.FACE -> {
|
||||
val faceId = it.face.id
|
||||
val elem = if (it.face.isBig) {
|
||||
Elem(
|
||||
commonElem = CommonElem(
|
||||
serviceType = 37,
|
||||
elem = QFaceExtra(
|
||||
packId = "1",
|
||||
stickerId = "1",
|
||||
faceId = faceId,
|
||||
field4 = 1,
|
||||
field5 = 1,
|
||||
result = "",
|
||||
faceText = "", //todo 表情名字
|
||||
field9 = 1
|
||||
).toByteArray(),
|
||||
businessType = 1
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Elem(
|
||||
face = FaceMsg(
|
||||
index = faceId
|
||||
)
|
||||
)
|
||||
}
|
||||
elems.add(elem)
|
||||
summary.append("[表情]")
|
||||
}
|
||||
Element.ElementType.BUBBLE_FACE -> throw UnsupportedOperationException("Unsupported Element.ElementType.BUBBLE_FACE")
|
||||
Element.ElementType.REPLY -> {
|
||||
val msgId = it.reply.messageId
|
||||
withTimeoutOrNull(3000) {
|
||||
suspendCancellableCoroutine {
|
||||
QRoute.api(IMsgService::class.java).getMsgsByMsgId(contact, arrayListOf(msgId.toLong())) { _, _, records ->
|
||||
it.resume(records)
|
||||
}
|
||||
}
|
||||
}?.firstOrNull()?.let {
|
||||
val sourceContact = MessageHelper.generateContact(it)
|
||||
elems.add(Elem(
|
||||
srcMsg = SourceMsg(
|
||||
origSeqs = listOf(it.msgSeq.toInt()),
|
||||
senderUin = it.senderUin.toULong(),
|
||||
time = it.msgTime.toULong(),
|
||||
flag = 1u,
|
||||
elems = it.elements
|
||||
.toKritorReqMessages(sourceContact)
|
||||
.toRichText(contact).getOrThrow().second.elements,
|
||||
type = 0u,
|
||||
pbReserve = SourceMsg.Companion.PbReserve(
|
||||
msgRand = Random.nextULong(),
|
||||
senderUid = it.senderUid,
|
||||
receiverUid = QQInterfaces.app.currentUid,
|
||||
field8 = Random.nextInt(0, 10000)
|
||||
),
|
||||
)
|
||||
))
|
||||
}
|
||||
summary.append("[回复消息]")
|
||||
}
|
||||
Element.ElementType.IMAGE -> {
|
||||
val type = it.image.type
|
||||
val isOriginal = type == ImageElement.ImageType.ORIGIN
|
||||
val file = when(it.image.dataCase!!) {
|
||||
ImageElement.DataCase.FILE_NAME -> {
|
||||
val fileMd5 = it.image.fileName.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase()
|
||||
FileUtils.getFileByMd5(fileMd5)
|
||||
}
|
||||
ImageElement.DataCase.FILE_PATH -> {
|
||||
val filePath = it.image.filePath
|
||||
File(filePath).inputStream().use {
|
||||
FileUtils.saveFileToCache(it)
|
||||
}
|
||||
}
|
||||
ImageElement.DataCase.FILE -> {
|
||||
FileUtils.saveFileToCache(
|
||||
ByteArrayInputStream(
|
||||
it.image.file.toByteArray()
|
||||
)
|
||||
)
|
||||
}
|
||||
ImageElement.DataCase.FILE_URL -> {
|
||||
val tmp = FileUtils.getTmpFile()
|
||||
if(DownloadUtils.download(it.image.fileUrl, tmp)) {
|
||||
tmp.inputStream().use {
|
||||
FileUtils.saveFileToCache(it)
|
||||
}.also {
|
||||
tmp.delete()
|
||||
}
|
||||
} else {
|
||||
tmp.delete()
|
||||
throw LogicException("图片资源下载失败: ${it.image.fileUrl}")
|
||||
}
|
||||
}
|
||||
ImageElement.DataCase.DATA_NOT_SET -> throw IllegalArgumentException("ImageElement data is not set")
|
||||
}
|
||||
|
||||
val options = BitmapFactory.Options()
|
||||
options.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeFile(file.absolutePath, options)
|
||||
val exifInterface = ExifInterface(file.absolutePath)
|
||||
val orientation = exifInterface.getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.ORIENTATION_UNDEFINED
|
||||
)
|
||||
val picWidth: Int
|
||||
val picHeight: Int
|
||||
if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) {
|
||||
picWidth = options.outWidth
|
||||
picHeight = options.outHeight
|
||||
} else {
|
||||
picWidth = options.outHeight
|
||||
picHeight = options.outWidth
|
||||
}
|
||||
|
||||
val fileInfo = NtV2RichMediaSvc.tryUploadResourceByNt(
|
||||
chatType = contact.chatType,
|
||||
elementType = MsgConstant.KELEMTYPEPIC,
|
||||
resources = arrayListOf(file),
|
||||
timeout = 30.seconds
|
||||
).getOrThrow().first()
|
||||
|
||||
runCatching {
|
||||
fileInfo.uuid.toUInt()
|
||||
}.onFailure {
|
||||
NtV2RichMediaSvc.requestUploadNtPic(file, fileInfo.md5, fileInfo.sha, fileInfo.fileName, picWidth.toUInt(), picHeight.toUInt(), 5, contact.chatType) {
|
||||
when(contact.chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> {
|
||||
sceneType = 2u
|
||||
grp = GroupUserInfo(fetchGroupResUploadTo().toULong())
|
||||
}
|
||||
MsgConstant.KCHATTYPEC2C -> {
|
||||
sceneType = 1u
|
||||
c2c = C2CUserInfo(
|
||||
accountType = 2u,
|
||||
uid = contact.peerUid
|
||||
)
|
||||
}
|
||||
else -> error("不支持的合并转发图片类型")
|
||||
}
|
||||
}.onFailure {
|
||||
LogCenter.log("获取MultiMedia图片信息失败: $it", Level.ERROR)
|
||||
}.onSuccess {
|
||||
//LogCenter.log({ "获取MultiMedia图片信息成功: ${it.hashCode()}" }, Level.INFO)
|
||||
elems.add(Elem(
|
||||
commonElem = CommonElem(
|
||||
serviceType = 48,
|
||||
businessType = 10,
|
||||
elem = it.msgInfo!!.toByteArray()
|
||||
)
|
||||
))
|
||||
}
|
||||
}.onSuccess { uuid ->
|
||||
elems.add(when (contact.chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> Elem(
|
||||
customFace = CustomFace(
|
||||
filePath = fileInfo.fileName,
|
||||
fileId = uuid,
|
||||
serverIp = 0u,
|
||||
serverPort = 0u,
|
||||
fileType = FileUtils.getPicType(file).toUInt(),
|
||||
useful = 1u,
|
||||
md5 = fileInfo.md5.hex2ByteArray(),
|
||||
bizType = 0u,
|
||||
imageType = FileUtils.getPicType(file).toUInt(),
|
||||
width = picWidth.toUInt(),
|
||||
height = picHeight.toUInt(),
|
||||
size = fileInfo.fileSize.toUInt(),
|
||||
origin = isOriginal,
|
||||
thumbWidth = 0u,
|
||||
thumbHeight = 0u,
|
||||
pbReserve = CustomFace.Companion.PbReserve(
|
||||
field1 = 0,
|
||||
field3 = 0,
|
||||
field4 = 0,
|
||||
field10 = 0,
|
||||
field21 = CustomFace.Companion.Object1(
|
||||
field1 = 0,
|
||||
field2 = "",
|
||||
field3 = 0,
|
||||
field4 = 0,
|
||||
field5 = 0,
|
||||
md5Str = fileInfo.md5
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
MsgConstant.KCHATTYPEC2C -> Elem(
|
||||
notOnlineImage = NotOnlineImage(
|
||||
filePath = fileInfo.fileName,
|
||||
fileLen = fileInfo.fileSize.toUInt(),
|
||||
downloadPath = fileInfo.uuid,
|
||||
imgType = FileUtils.getPicType(file).toUInt(),
|
||||
picMd5 = fileInfo.md5.hex2ByteArray(),
|
||||
picHeight = picWidth.toUInt(),
|
||||
picWidth = picHeight.toUInt(),
|
||||
resId = fileInfo.uuid,
|
||||
original = isOriginal, // true
|
||||
pbReserve = NotOnlineImage.Companion.PbReserve(
|
||||
field1 = 0,
|
||||
field3 = 0,
|
||||
field4 = 0,
|
||||
field10 = 0,
|
||||
field20 = NotOnlineImage.Companion.Object1(
|
||||
field1 = 0,
|
||||
field2 = "",
|
||||
field3 = 0,
|
||||
field4 = 0,
|
||||
field5 = 0,
|
||||
field7 = "",
|
||||
),
|
||||
md5Str = fileInfo.md5
|
||||
)
|
||||
)
|
||||
)
|
||||
else -> throw LogicException("Not supported chatType($contact) for PictureMsg")
|
||||
})
|
||||
}
|
||||
|
||||
summary.append("[图片]")
|
||||
}
|
||||
Element.ElementType.VOICE -> throw UnsupportedOperationException("Unsupported Element.ElementType.VOICE")
|
||||
Element.ElementType.VIDEO -> throw UnsupportedOperationException("Unsupported Element.ElementType.VIDEO")
|
||||
Element.ElementType.BASKETBALL -> throw UnsupportedOperationException("Unsupported Element.ElementType.BASKETBALL")
|
||||
Element.ElementType.DICE -> {
|
||||
val elem = Elem(
|
||||
commonElem = CommonElem(
|
||||
serviceType = 37,
|
||||
elem = QFaceExtra(
|
||||
packId = "1",
|
||||
stickerId = "33",
|
||||
faceId = 358,
|
||||
field4 = 1,
|
||||
field5 = 2,
|
||||
result = "",
|
||||
faceText = "/骰子",
|
||||
field9 = 1
|
||||
).toByteArray(),
|
||||
businessType = 2
|
||||
)
|
||||
)
|
||||
elems.add(elem)
|
||||
summary .append( "[骰子]" )
|
||||
}
|
||||
Element.ElementType.RPS -> {
|
||||
val elem = Elem(
|
||||
commonElem = CommonElem(
|
||||
serviceType = 37,
|
||||
elem = QFaceExtra(
|
||||
packId = "1",
|
||||
stickerId = "34",
|
||||
faceId = 359,
|
||||
field4 = 1,
|
||||
field5 = 2,
|
||||
result = "",
|
||||
faceText = "/包剪锤",
|
||||
field9 = 1
|
||||
).toByteArray(),
|
||||
businessType = 1
|
||||
)
|
||||
)
|
||||
elems.add(elem)
|
||||
summary .append( "[包剪锤]" )
|
||||
}
|
||||
Element.ElementType.POKE -> {
|
||||
val elem = Elem(
|
||||
commonElem = CommonElem(
|
||||
serviceType = 2,
|
||||
elem = PokeExtra(
|
||||
type = it.poke.type,
|
||||
field7 = 0,
|
||||
field8 = 0
|
||||
).toByteArray(),
|
||||
businessType = it.poke.id
|
||||
)
|
||||
)
|
||||
elems.add(elem)
|
||||
summary .append( "[戳一戳]" )
|
||||
}
|
||||
Element.ElementType.MUSIC -> throw UnsupportedOperationException("Unsupported Element.ElementType.MUSIC")
|
||||
Element.ElementType.WEATHER -> {
|
||||
var code = it.weather.code.toIntOrNull()
|
||||
if (code == null) {
|
||||
val city = it.weather.city
|
||||
WeatherHelper.searchCity(city).onFailure {
|
||||
LogCenter.log("无法获取城市天气: $city", Level.ERROR)
|
||||
}.getOrNull()?.firstOrNull()?.let {
|
||||
code = it.adcode
|
||||
}
|
||||
}
|
||||
|
||||
if (code != null) {
|
||||
val weatherCard = WeatherHelper.fetchWeatherCard(code!!).getOrThrow()
|
||||
val elem = Elem(
|
||||
lightApp = LightAppElem(
|
||||
data = byteArrayOf(1) + DeflateTools.compress(
|
||||
weatherCard["weekStore"]
|
||||
.asJsonObject["share"].asString.toByteArray()
|
||||
)
|
||||
)
|
||||
)
|
||||
elems.add(elem)
|
||||
summary .append( "[天气卡片]" )
|
||||
} else {
|
||||
throw LogicException("无法获取城市天气")
|
||||
}
|
||||
}
|
||||
Element.ElementType.LOCATION -> throw UnsupportedOperationException("Unsupported Element.ElementType.LOCATION")
|
||||
Element.ElementType.SHARE -> throw UnsupportedOperationException("Unsupported Element.ElementType.SHARE")
|
||||
Element.ElementType.GIFT -> throw UnsupportedOperationException("Unsupported Element.ElementType.GIFT")
|
||||
Element.ElementType.MARKET_FACE -> throw UnsupportedOperationException("Unsupported Element.ElementType.MARKET_FACE")
|
||||
Element.ElementType.FORWARD -> {
|
||||
val resId = it.forward.resId
|
||||
val filename = UUID.randomUUID().toString().uppercase()
|
||||
var content = it.forward.summary
|
||||
val descriptions = it.forward.description
|
||||
var news = descriptions?.split("\n")?.map { "text" to it }
|
||||
|
||||
if (news == null || content == null) {
|
||||
val forwardMsg = MessageHelper.getForwardMsg(resId).getOrThrow()
|
||||
if (news == null) {
|
||||
news = forwardMsg.map {
|
||||
"text" to it.sender.nickName + ": " + descriptions
|
||||
}
|
||||
}
|
||||
if (content == null) {
|
||||
content = "查看${forwardMsg.size}条转发消息"
|
||||
}
|
||||
}
|
||||
|
||||
val json = mapOf(
|
||||
"app" to "com.tencent.multimsg",
|
||||
"config" to mapOf(
|
||||
"autosize" to 1,
|
||||
"forward" to 1,
|
||||
"round" to 1,
|
||||
"type" to "normal",
|
||||
"width" to 300
|
||||
),
|
||||
"desc" to "[聊天记录]",
|
||||
"extra" to mapOf(
|
||||
"filename" to filename,
|
||||
"tsum" to 2
|
||||
).json.toString(),
|
||||
"meta" to mapOf(
|
||||
"detail" to mapOf(
|
||||
"news" to news,
|
||||
"resid" to resId,
|
||||
"source" to "群聊的聊天记录",
|
||||
"summary" to content,
|
||||
"uniseq" to filename
|
||||
)
|
||||
),
|
||||
"prompt" to "[聊天记录]",
|
||||
"ver" to "0.0.0.5",
|
||||
"view" to "contact"
|
||||
)
|
||||
val elem = Elem(
|
||||
lightApp = LightAppElem(
|
||||
data = byteArrayOf(1) + DeflateTools.compress(json.json.toString().toByteArray())
|
||||
)
|
||||
)
|
||||
elems.add(elem)
|
||||
summary.append( "[聊天记录]" )
|
||||
}
|
||||
Element.ElementType.CONTACT -> throw UnsupportedOperationException("Unsupported Element.ElementType.CONTACT")
|
||||
Element.ElementType.JSON -> {
|
||||
val elem = Elem(
|
||||
lightApp = LightAppElem(
|
||||
data = byteArrayOf(1) + DeflateTools.compress(it.json.json.toByteArray())
|
||||
)
|
||||
)
|
||||
elems.add(elem)
|
||||
summary .append( "[Json消息]" )
|
||||
}
|
||||
Element.ElementType.XML -> throw UnsupportedOperationException("Unsupported Element.ElementType.XML")
|
||||
Element.ElementType.FILE -> throw UnsupportedOperationException("Unsupported Element.ElementType.FILE")
|
||||
Element.ElementType.MARKDOWN -> {
|
||||
val elem = Elem(
|
||||
commonElem = CommonElem(
|
||||
serviceType = 45,
|
||||
elem = MarkdownExtra(it.markdown.markdown).toByteArray(),
|
||||
businessType = 1
|
||||
)
|
||||
)
|
||||
elems.add(elem)
|
||||
summary.append("[Markdown消息]")
|
||||
}
|
||||
Element.ElementType.KEYBOARD -> {
|
||||
val elem = Elem(
|
||||
commonElem = CommonElem(
|
||||
serviceType = 46,
|
||||
elem = ButtonExtra(
|
||||
field1 = Object1(
|
||||
rows = it.keyboard.rowsList.map { row ->
|
||||
Row(buttons = row.buttonsList.map { button ->
|
||||
val renderData = button.renderData
|
||||
val action = button.action
|
||||
val permission = action.permission
|
||||
Button(
|
||||
id = button.id,
|
||||
renderData = RenderData(
|
||||
label = renderData.label,
|
||||
visitedLabel = renderData.visitedLabel,
|
||||
style = renderData.style
|
||||
),
|
||||
action = Action(
|
||||
type = action.type,
|
||||
permission = Permission(
|
||||
type = permission.type,
|
||||
specifyRoleIds = permission.roleIdsList,
|
||||
specifyUserIds = permission.userIdsList
|
||||
),
|
||||
unsupportTips = action.unsupportedTips,
|
||||
data = action.data,
|
||||
reply = action.reply,
|
||||
enter = action.enter
|
||||
)
|
||||
)
|
||||
})
|
||||
},
|
||||
appid = it.keyboard.botAppid.toULong()
|
||||
)
|
||||
).toByteArray(),
|
||||
businessType = 1
|
||||
)
|
||||
)
|
||||
elems.add(elem)
|
||||
summary.append("[Button消息]")
|
||||
}
|
||||
Element.ElementType.UNRECOGNIZED -> throw UnsupportedOperationException("Unsupported Element.ElementType.UNRECOGNIZED")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
LogCenter.log("转换消息失败(Multi): ${e.stackTraceToString()}", Level.ERROR)
|
||||
}
|
||||
}
|
||||
return Result.success(summary.toString() to RichText(
|
||||
elements = elems
|
||||
))
|
||||
}
|
||||
|
195
xposed/src/main/java/qq/service/ticket/TicketHelper.kt
Normal file
195
xposed/src/main/java/qq/service/ticket/TicketHelper.kt
Normal file
@ -0,0 +1,195 @@
|
||||
package qq.service.ticket
|
||||
|
||||
import com.tencent.guild.api.transfile.IGuildTransFileApi
|
||||
import com.tencent.mobileqq.app.QQAppInterface
|
||||
import com.tencent.mobileqq.pskey.oidb.cmd0x102a.oidb_cmd0x102a
|
||||
import com.tencent.mobileqq.qroute.QRoute
|
||||
import com.tencent.qqnt.kernel.nativeinterface.BigDataTicket
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import moe.fuqiuluo.shamrock.tools.GlobalClient
|
||||
import moe.fuqiuluo.shamrock.tools.slice
|
||||
import mqq.app.MobileQQ
|
||||
import mqq.manager.TicketManager
|
||||
import oicq.wlogin_sdk.request.Ticket
|
||||
import qq.service.QQInterfaces
|
||||
import tencent.im.oidb.oidb_sso
|
||||
|
||||
internal object TicketHelper: QQInterfaces() {
|
||||
object SigType {
|
||||
const val WLOGIN_A5 = 2
|
||||
const val WLOGIN_RESERVED = 16
|
||||
const val WLOGIN_STWEB = 32 // TLV 103
|
||||
const val WLOGIN_A2 = 64
|
||||
const val WLOGIN_ST = 128
|
||||
const val WLOGIN_AQSIG = 2097152
|
||||
const val WLOGIN_D2 = 262144
|
||||
const val WLOGIN_DA2 = 33554432
|
||||
const val WLOGIN_LHSIG = 4194304
|
||||
const val WLOGIN_LSKEY = 512
|
||||
const val WLOGIN_OPENKEY = 16384
|
||||
const val WLOGIN_PAYTOKEN = 8388608
|
||||
const val WLOGIN_PF = 16777216
|
||||
const val WLOGIN_PSKEY = 1048576
|
||||
const val WLOGIN_PT4Token = 134217728
|
||||
const val WLOGIN_QRPUSH = 67108864
|
||||
const val WLOGIN_SID = 524288
|
||||
const val WLOGIN_SIG64 = 8192
|
||||
const val WLOGIN_SKEY = 4096
|
||||
const val WLOGIN_TOKEN = 32768
|
||||
const val WLOGIN_VKEY = 131072
|
||||
|
||||
val ALL_TICKET = arrayOf(
|
||||
WLOGIN_A5, WLOGIN_RESERVED, WLOGIN_STWEB, WLOGIN_A2, WLOGIN_ST, WLOGIN_AQSIG, WLOGIN_D2, WLOGIN_DA2,
|
||||
WLOGIN_LHSIG, WLOGIN_LSKEY, WLOGIN_OPENKEY, WLOGIN_PAYTOKEN, WLOGIN_PF, WLOGIN_PSKEY, WLOGIN_PT4Token,
|
||||
WLOGIN_QRPUSH, WLOGIN_SID, WLOGIN_SIG64, WLOGIN_SKEY, WLOGIN_TOKEN, WLOGIN_VKEY
|
||||
)
|
||||
}
|
||||
|
||||
inline fun getUin(): String {
|
||||
return app.currentUin.ifBlank { "0" }
|
||||
}
|
||||
|
||||
fun getUid(): String {
|
||||
return app.currentUid.ifBlank { "u_" }
|
||||
}
|
||||
|
||||
inline fun getNickname(): String {
|
||||
return app.currentNickname
|
||||
}
|
||||
|
||||
fun getCookie(): String {
|
||||
val uin = getUin()
|
||||
val skey = getRealSkey(uin)
|
||||
val pskey = getPSKey(uin)
|
||||
return "uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey"
|
||||
}
|
||||
|
||||
suspend fun getCookie(domain: String): String {
|
||||
val uin = getUin()
|
||||
val skey = getRealSkey(uin)
|
||||
val pskey = getPSKey(uin, domain) ?: ""
|
||||
val pt4token = getPt4Token(uin, domain) ?: ""
|
||||
return "uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey; pt4_token=$pt4token"
|
||||
}
|
||||
|
||||
fun getBigdataTicket(): BigDataTicket? {
|
||||
return runCatching {
|
||||
QRoute.api(IGuildTransFileApi::class.java).bigDataTicket?.let {
|
||||
BigDataTicket(it.getSessionKey(), it.getSessionSig())
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun getCSRF(pskey: String = getPSKey(getUin())): String {
|
||||
if (pskey.isEmpty()) {
|
||||
return "0"
|
||||
}
|
||||
var v = 5381
|
||||
for (element in pskey) {
|
||||
v += ((v shl 5) + element.code.toLong()).toInt()
|
||||
}
|
||||
return (v and Int.MAX_VALUE).toString()
|
||||
}
|
||||
|
||||
suspend fun getCSRF(uin: String, domain: String): String {
|
||||
// 是不是要用Skey?
|
||||
return getBkn(getPSKey(uin, domain) ?: getSKey(uin))
|
||||
}
|
||||
|
||||
fun getBkn(arg: String): String {
|
||||
var v: Long = 5381
|
||||
for (element in arg) {
|
||||
v += (v shl 5 and 2147483647L) + element.code.toLong()
|
||||
}
|
||||
return (v and 2147483647L).toString()
|
||||
}
|
||||
|
||||
fun getTicket(uin: String, id: Int): Ticket? {
|
||||
require(app is QQAppInterface)
|
||||
return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getLocalTicket(uin, id)
|
||||
}
|
||||
|
||||
fun getStWeb(uin: String): String {
|
||||
require(app is QQAppInterface)
|
||||
return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getStweb(uin)
|
||||
}
|
||||
|
||||
fun getSKey(uin: String): String {
|
||||
require(app is QQAppInterface)
|
||||
return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getRealSkey(uin)
|
||||
}
|
||||
|
||||
fun getRealSkey(uin: String): String {
|
||||
require(app is QQAppInterface)
|
||||
return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getRealSkey(uin)
|
||||
}
|
||||
|
||||
fun getPSKey(uin: String): String {
|
||||
require(app is QQAppInterface)
|
||||
val manager = (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager)
|
||||
manager.reloadCache(MobileQQ.getContext())
|
||||
return manager.getSuperkey(uin) ?: ""
|
||||
}
|
||||
|
||||
suspend fun getLessPSKey(vararg domain: String): Result<List<oidb_cmd0x102a.PSKey>> {
|
||||
val req = oidb_cmd0x102a.GetPSkeyRequest()
|
||||
req.domains.set(domain.toList())
|
||||
val fromServiceMsg = sendOidbAW("OidbSvcTcp.0x102a", 4138, 0, req.toByteArray())
|
||||
?: return Result.failure(Exception("getLessPSKey failed"))
|
||||
if (fromServiceMsg.wupBuffer == null) return Result.failure(Exception("getLessPSKey failed: no response"))
|
||||
val body = oidb_sso.OIDBSSOPkg()
|
||||
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||
val rsp = oidb_cmd0x102a.GetPSkeyResponse().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
|
||||
return Result.success(rsp.private_keys.get())
|
||||
}
|
||||
|
||||
suspend fun getPSKey(uin: String, domain: String): String? {
|
||||
require(app is QQAppInterface)
|
||||
return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getPskey(uin, domain).let {
|
||||
if (it.isNullOrBlank())
|
||||
getLessPSKey(domain).getOrNull()?.firstOrNull()?.key?.get()
|
||||
else it
|
||||
}
|
||||
}
|
||||
|
||||
fun getPt4Token(uin: String, domain: String): String? {
|
||||
require(app is QQAppInterface)
|
||||
return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getPt4Token(uin, domain)
|
||||
}
|
||||
|
||||
suspend fun getHttpCookies(appid: String, daid: String, jumpurl: String): String? {
|
||||
val client = HttpClient {
|
||||
followRedirects = false
|
||||
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = 15000
|
||||
connectTimeoutMillis = 15000
|
||||
socketTimeoutMillis = 15000
|
||||
}
|
||||
}
|
||||
val uin = getUin()
|
||||
val clientkey = getStWeb(uin)
|
||||
var url = "https://ui.ptlogin2.qq.com/cgi-bin/login?pt_hide_ad=1&style=9&appid=$appid&pt_no_auth=1&pt_wxtest=1&daid=$daid&s_url=$jumpurl"
|
||||
var cookie = client.get(url).headers.getAll("Set-Cookie")?.joinToString(";")
|
||||
url = "https://ssl.ptlogin2.qq.com/jump?u1=$jumpurl&pt_report=1&daid=$daid&style=9&keyindex=19&clientuin=$uin&clientkey=$clientkey"
|
||||
client.get(url) {
|
||||
header("Cookie", cookie)
|
||||
}.let {
|
||||
cookie = it.headers.getAll("Set-Cookie")?.joinToString(";")
|
||||
url = it.headers["Location"].toString()
|
||||
}
|
||||
cookie = client.get(url).headers.getAll("Set-Cookie")?.joinToString(";")
|
||||
val extractedCookie = StringBuilder()
|
||||
val cookies = cookie?.split(";")
|
||||
cookies?.filter { cookie ->
|
||||
val cookiePair = cookie.trim().split("=")
|
||||
cookiePair.size == 2 && cookiePair[1].isNotBlank() && cookiePair[0].trim() in listOf("uin", "skey", "p_uin", "p_skey", "pt4_token")
|
||||
}?.forEach {
|
||||
extractedCookie.append("$it; ")
|
||||
}
|
||||
return extractedCookie.toString().trim()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user