51 Commits

Author SHA1 Message Date
59d762eecf fix https://github.com/KarinJS/kritor/issues/10 2024-04-11 12:37:54 +08:00
e891bc8512 fix build 2024-04-11 01:17:13 +08:00
494c70f2f8 update kritor 2024-04-11 00:58:36 +08:00
7baf459b2a Shamrock: fix scene and group code 2024-04-10 21:09:10 +08:00
36a09ca088 update kritor 2024-04-08 20:00:44 +08:00
926c4659f6 Shamrock: add kritor metadata 2024-04-07 16:51:21 +08:00
cb7c68f36c fix: build error 2024-04-07 16:27:35 +08:00
72af39208c update kritor 2024-04-07 16:08:33 +08:00
042f4bd330 fix build err 2024-04-04 19:44:56 +08:00
9aef71b09f fix build err 2024-04-04 18:56:48 +08:00
9cbe755520 fix missing elem-type for kritor 2024-04-04 18:51:58 +08:00
df02f9f872 fix #316 2024-03-28 19:37:10 +08:00
5cbb695a66 fix crash 2024-03-28 19:27:11 +08:00
c014e85faa update kritor 2024-03-27 16:21:49 +08:00
4a396b0935 Update kritor 2024-03-25 01:12:59 +08:00
d59fcf9f6a update kritor 2024-03-25 01:10:33 +08:00
cdc664f44a fix build error 2024-03-24 05:33:57 +08:00
ad313f384c fix kritor 2024-03-24 05:19:50 +08:00
b6a510ce05 Update .gitmodules 2024-03-21 19:35:16 +08:00
bed5947909 update kritor 2024-03-21 19:15:58 +08:00
fb6578d243 chore: try to fix ci 2024-03-21 17:50:39 +08:00
d33cace7aa Shamrock: forward messages resources upload 2024-03-21 17:39:51 +08:00
659d4e5da4 commit Readme.md 2024-03-21 16:21:54 +08:00
ac2aee8c0e 関連プロジェクトのヒントを追加する 2024-03-21 16:17:25 +08:00
0faada7b5a Merge pull request #310 from whitechi73/kritor
kritorをmasterブランチに設定する
2024-03-21 16:15:12 +08:00
680317da13 kritorをmasterブランチに設定する
kritorをmasterブランチに設定する
2024-03-21 16:13:45 +08:00
7782feb6ac Merge pull request #303 from tobycroft/master
GetFile的type新增gzip,会将数据流压缩后再b64降低带宽占用
2024-03-18 22:43:18 +08:00
d66358a1f3 Shamrock: 提供开发者服务支持
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-18 21:02:05 +08:00
824f280b3a 修改error 提示 2024-03-18 15:40:14 +08:00
6936262d62 GetFile的type新增gzip,会将数据流压缩后再b64降低带宽占用
- 使用gzip压缩
2024-03-18 15:24:24 +08:00
0955267ee5 Merge branch 'whitechi73:master' into master 2024-03-18 13:56:26 +08:00
f3da62fa74 GetFile的type新增gzip,会将数据流压缩后再b64降低带宽占用
- 使用gzip压缩
2024-03-18 13:49:03 +08:00
abbac6315c Merge pull request #301 from tobycroft/master
新增get_file方法(算是补全下功能)
2024-03-18 13:34:51 +08:00
0cf10eabd6 fix: set field file_type not required 2024-03-18 13:34:13 +08:00
8c33267887 fileType加入空匹配,可支持空传 2024-03-18 13:30:18 +08:00
f030104ff2 get_record的ws加入单独的md5字段,方便后续get_file拿文件 2024-03-18 13:23:20 +08:00
ee5fcc3403 Shamrock: 精华消息支持
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-18 11:49:38 +08:00
5e819179b4 get_record的ws加入单独的md5字段,方便后续get_file拿文件 2024-03-18 04:08:07 +08:00
ea206faf4f get_record的ws加入单独的md5字段,方便后续get_file拿文件 2024-03-18 04:07:11 +08:00
5adfc544a2 修正file_type参数不正确问题 2024-03-18 03:46:51 +08:00
bdb75841cf AGP更新 2024-03-18 03:23:11 +08:00
a3dc0d06b2 新增get_file方法,主要解决使用反向websocket的时候获取文件麻烦的问题,目前仅支持base64的type返回,未来将支持更多模式,测试后将发布至文档 2024-03-18 03:17:36 +08:00
3664352f23 新增get_file方法,主要解决使用反向websocket的时候获取文件麻烦的问题,目前仅支持base64的type返回,未来将支持更多模式,测试后将发布至文档 2024-03-18 03:05:28 +08:00
2770979fee 新增get_file方法,主要解决使用反向websocket的时候获取文件麻烦的问题,目前仅支持base64的type返回,未来将支持更多模式,测试后将发布至文档 2024-03-18 02:57:44 +08:00
6c9b282c6a Shamrock: 实现消息服务
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-16 19:39:45 +08:00
be58c368e9 Merge pull request #295 from tobycroft/master
notice类消息,新增source字段
2024-03-15 00:34:42 +08:00
1d035fa378 Shamrock: fix 群聊和私聊转发分别处理 2024-03-14 18:57:45 +08:00
7d0b60271e Shamrock: fix 群聊转发图片 2024-03-14 18:25:01 +08:00
d38777d06a notice类消息,新增source,解决poke等特殊消息没有办法直接判断消息来源的问题。修改后通过notice类消息的source字段,则可判断需要使用那种struct来接收 2024-03-14 15:05:01 +08:00
93c49953cf Merge pull request #289 from Linwenxuan05/patch-1
添加相关项目
2024-03-11 11:45:56 +08:00
883e949cc1 添加相关项目 2024-03-11 11:41:46 +08:00
70 changed files with 5708 additions and 1006 deletions

View File

@ -19,7 +19,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 submodules: recursive
- name: Setup JDK 17 - name: Setup JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v4

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "kritor"] [submodule "kritor"]
path = kritor path = kritor/kritor
url = https://github.com/KarinJS/kritor url = https://github.com/KarinJS/kritor

View File

@ -16,20 +16,35 @@
## 简介 ## 简介
☘ 基于 Lsposed(**Non**-Riru) 实现 OneBot 标准的 QQ 机器人框架,原作者[**fuqiuluo**](https://github.com/fuqiuluo)已脱离开发接下来由白池接手哦本项目为OpenShamrock不会有任何收费行为欢迎大家的加入 ☘ 基于 Lsposed(**Non**-Riru) 实现 Kritor 标准的 QQ 机器人框架!
> 本项目仅提供学习与交流用途请在24小时内删除。 > 本项目仅提供学习与交流用途请在24小时内删除。
> 本项目目的是研究 Xposed 和 LSPosed 框架的使用。 Epic 框架开发相关知识。 > 本项目目的是研究 Xposed 和 LSPosed 框架的使用。 Epic 框架开发相关知识。
> Riru可能导致封禁请减少使用。 > Riru可能导致封禁请减少使用。
> 如有违反法律,请联系删除。 > 如有违反法律,请联系删除。
> 请勿在任何平台宣传,宣扬,转发本项目,请勿恶意修改企业安装包造成相关企业产生损失,如有违背,必将追责到底。 > 请勿在任何平台宣传,宣扬,转发本项目,请勿恶意修改企业安装包造成相关企业产生损失,如有违背,必将追责到底。
> 官方论坛,[点我直达](https://forum.libfekit.so/)
## 兼容|迁移|替代 说明 ## 兼容|迁移|替代 说明
- 一键移植:本项目基于 go-cqhttp 的文档进行开发实现。 - 一键移植:本项目基于 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>
## 权限声明 ## 权限声明

View File

@ -1,7 +1,9 @@
package kritor.service package kritor.service
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FUNCTION) @Target(AnnotationTarget.FUNCTION)
annotation class Grpc( annotation class Grpc(
val serviceName: String, val serviceName: String,
val funcName: String val funcName: String,
)
)

View File

@ -1,8 +0,0 @@
package moe.fuqiuluo.symbols
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS)
annotation class OneBotHandler(
val actionName: String,
val alias: Array<String> = []
)

View File

@ -118,9 +118,7 @@ private fun APIInfoCard(
text = rpcAddress, text = rpcAddress,
hint = "请输入回调地址", hint = "请输入回调地址",
error = "输入的地址不合法", error = "输入的地址不合法",
checker = { checker = { true },
it.isEmpty() || it.contains(":")
},
confirm = { confirm = {
ShamrockConfig[ctx, RPCAddress] = rpcAddress.value ShamrockConfig[ctx, RPCAddress] = rpcAddress.value
AppRuntime.log("设置回调RPC地址为[${rpcAddress.value}]。") AppRuntime.log("设置回调RPC地址为[${rpcAddress.value}]。")

View File

@ -1,5 +1,5 @@
plugins { plugins {
kotlin("jvm") version "1.9.21" kotlin("jvm") version "1.9.22"
} }
repositories { repositories {

View File

@ -15,8 +15,9 @@ fun ktor(target: String, name: String): String {
return "io.ktor:ktor-$target-$name:${Versions.ktorVersion}" return "io.ktor:ktor-$target-$name:${Versions.ktorVersion}"
} }
fun grpc(name: String, version: String) = "io.grpc:grpc-$name:$version"
object Versions { object Versions {
const val roomVersion = "2.5.0" const val roomVersion = "2.5.0"
const val ktorVersion = "2.3.3" const val ktorVersion = "2.3.3"
} }

1
kritor

Submodule kritor deleted from e4aac653e1

42
kritor/.gitignore vendored Normal file
View 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
View 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"
}

View File

1
kritor/kritor Submodule

Submodule kritor/kritor added at 27669a8f57

21
kritor/proguard-rules.pro vendored Normal file
View 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

View File

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

View File

@ -0,0 +1,17 @@
package moe.fuqiuluo.ksp.providers
import com.google.auto.service.AutoService
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import moe.fuqiuluo.ksp.impl.GrpcProcessor
@AutoService(SymbolProcessorProvider::class)
class GrpcProvider: SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return GrpcProcessor(
environment.codeGenerator,
environment.logger
)
}
}

View File

@ -37,7 +37,6 @@ android {
} }
dependencies { dependencies {
//implementation(DEPENDENCY_PROTOBUF)
implementation(kotlinx("serialization-protobuf", "1.6.2")) implementation(kotlinx("serialization-protobuf", "1.6.2"))
implementation(kotlinx("serialization-json", "1.6.2")) implementation(kotlinx("serialization-json", "1.6.2"))

View File

@ -13,7 +13,7 @@ data class ButtonExtra(
@Serializable @Serializable
data class Object1( data class Object1(
@ProtoNumber(1) val rows: List<Row>? = null, @ProtoNumber(1) val rows: List<Row>? = null,
@ProtoNumber(2) val appid: Int? = null, @ProtoNumber(2) val appid: ULong? = null,
) )
@Serializable @Serializable

View File

@ -26,7 +26,7 @@ buildscript {
} }
} }
dependencies { 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( include(
":app", ":app",
":xposed", ":xposed",
":qqinterface" ":qqinterface",
) ":protobuf",
include(":protobuf") ":processor",
include(":processor") ":annotations",
include(":annotations") ":kritor"
include(":kritor") )
project(":kritor").projectDir = file("kritor/protos")

View File

@ -5,7 +5,6 @@ plugins {
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("kotlin-kapt") id("kotlin-kapt")
id("com.google.devtools.ksp") version "1.9.22-1.0.17" id("com.google.devtools.ksp") version "1.9.22-1.0.17"
id("com.google.protobuf") version "0.9.4"
kotlin("plugin.serialization") version "1.9.22" kotlin("plugin.serialization") version "1.9.22"
} }
@ -61,11 +60,10 @@ kotlin {
} }
dependencies { dependencies {
compileOnly ("de.robv.android.xposed:api:82") compileOnly("de.robv.android.xposed:api:82")
compileOnly (project(":qqinterface")) compileOnly(project(":qqinterface"))
protobuf(project(":kritor"))
implementation(project(":kritor"))
implementation(project(":protobuf")) implementation(project(":protobuf"))
implementation(project(":annotations")) implementation(project(":annotations"))
ksp(project(":processor")) ksp(project(":processor"))
@ -75,24 +73,20 @@ dependencies {
DEPENDENCY_ANDROIDX.forEach { DEPENDENCY_ANDROIDX.forEach {
implementation(it) implementation(it)
} }
//implementation(DEPENDENCY_PROTOBUF)
implementation(room("runtime")) implementation(room("runtime"))
kapt(room("compiler")) kapt(room("compiler"))
implementation(room("ktx")) implementation(room("ktx"))
implementation(kotlinx("io-jvm", "0.1.16")) implementation(kotlinx("io-jvm", "0.1.16"))
implementation(kotlinx("serialization-protobuf", "1.6.2"))
implementation(ktor("client", "core")) implementation(ktor("client", "core"))
implementation(ktor("client", "okhttp")) implementation(ktor("client", "okhttp"))
implementation(ktor("serialization", "kotlinx-json")) implementation(ktor("serialization", "kotlinx-json"))
implementation("io.grpc:grpc-stub:1.62.2") implementation(grpc("protobuf", "1.62.2"))
implementation("io.grpc:grpc-protobuf-lite:1.62.2") implementation(grpc("kotlin-stub", "1.4.1"))
implementation("com.google.protobuf:protobuf-kotlin-lite:3.25.3") implementation(grpc("okhttp", "1.62.2"))
implementation("io.grpc:grpc-kotlin-stub:1.4.1")
implementation("io.grpc:grpc-okhttp:1.62.2")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")
@ -106,40 +100,3 @@ tasks.withType<KotlinCompile>().all {
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn") freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn")
} }
} }
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.25.3"
}
plugins {
create("java") {
artifact = "io.grpc:protoc-gen-grpc-java:1.62.2"
}
create("grpc") {
artifact = "io.grpc:protoc-gen-grpc-java:1.62.2"
}
create("grpckt") {
artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar"
}
}
generateProtoTasks {
all().forEach {
it.plugins {
create("java") {
option("lite")
}
create("grpc") {
option("lite")
}
create("grpckt") {
option("lite")
}
}
it.builtins {
create("kotlin") {
option("lite")
}
}
}
}
}

View File

@ -34,7 +34,7 @@ active_ticket=
enable_self_message=false enable_self_message=false
# 旧BDH兼容开关 # 旧BDH兼容开关
enable_old_bdh=false enable_old_bdh=true
# 反JVM调用栈跟踪 # 反JVM调用栈跟踪
anti_jvm_trace=true anti_jvm_trace=true

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

View File

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

View File

@ -2,7 +2,12 @@
package kritor.server package kritor.server
import io.grpc.Grpc import io.grpc.Grpc
import io.grpc.Metadata
import io.grpc.InsecureServerCredentials 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -10,22 +15,45 @@ import kotlinx.coroutines.asExecutor
import kritor.auth.AuthInterceptor import kritor.auth.AuthInterceptor
import kritor.service.* import kritor.service.*
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
import qq.service.ticket.TicketHelper
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class KritorServer( class KritorServer(
private val port: Int private val port: Int
): CoroutineScope { ): 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()) private val server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create())
.executor(Dispatchers.IO.asExecutor()) .executor(Dispatchers.IO.asExecutor())
.intercept(AuthInterceptor) .intercept(AuthInterceptor)
.addService(Authentication) .intercept(serverInterceptor)
.addService(ContactService) .addService(AuthenticationService)
.addService(KritorService) .addService(CoreService)
.addService(FriendService) .addService(FriendService)
.addService(GroupService) .addService(GroupService)
.addService(GroupFileService) .addService(GroupFileService)
.addService(MessageService) .addService(MessageService)
.addService(EventService) .addService(EventService)
.addService(WebService)
.addService(DeveloperService)
.addService(QsignService)
.build()!! .build()!!
fun start(block: Boolean = false) { fun start(block: Boolean = false) {

View File

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

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

View File

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

View File

@ -4,60 +4,35 @@ import android.util.Base64
import com.tencent.mobileqq.app.QQAppInterface import com.tencent.mobileqq.app.QQAppInterface
import io.grpc.Status import io.grpc.Status
import io.grpc.StatusRuntimeException import io.grpc.StatusRuntimeException
import io.kritor.core.ClearCacheRequest import io.kritor.core.*
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 moe.fuqiuluo.shamrock.tools.ShamrockVersion import moe.fuqiuluo.shamrock.tools.ShamrockVersion
import moe.fuqiuluo.shamrock.utils.DownloadUtils import moe.fuqiuluo.shamrock.utils.DownloadUtils
import moe.fuqiuluo.shamrock.utils.FileUtils import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MD5 import moe.fuqiuluo.shamrock.utils.MD5
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
import mqq.app.MobileQQ import mqq.app.MobileQQ
import qq.service.QQInterfaces
import qq.service.QQInterfaces.Companion.app import qq.service.QQInterfaces.Companion.app
import qq.service.contact.ContactHelper import qq.service.contact.ContactHelper
import java.io.File import java.io.File
object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() { internal object CoreService : CoreServiceGrpcKt.CoreServiceCoroutineImplBase() {
@Grpc("KritorService", "GetVersion") @Grpc("CoreService", "GetVersion")
override suspend fun getVersion(request: GetVersionRequest): GetVersionResponse { override suspend fun getVersion(request: GetVersionRequest): GetVersionResponse {
return getVersionResponse { return GetVersionResponse.newBuilder().apply {
this.version = ShamrockVersion this.version = ShamrockVersion
this.appName = "Shamrock" this.appName = "Shamrock"
} }.build()
} }
@Grpc("KritorService", "ClearCache") @Grpc("CoreService", "GetCurrentAccount")
override suspend fun clearCache(request: ClearCacheRequest): ClearCacheResponse {
FileUtils.clearCache()
MMKVFetcher.mmkvWithId("audio2silk")
.clear()
return clearCacheResponse {}
}
@Grpc("KritorService", "GetCurrentAccount")
override suspend fun getCurrentAccount(request: GetCurrentAccountRequest): GetCurrentAccountResponse { override suspend fun getCurrentAccount(request: GetCurrentAccountRequest): GetCurrentAccountResponse {
return getCurrentAccountResponse { return GetCurrentAccountResponse.newBuilder().apply {
this.accountName = if (app is QQAppInterface) app.currentNickname else "unknown" this.accountName = if (app is QQAppInterface) app.currentNickname else "unknown"
this.accountUid = app.currentUid ?: "" this.accountUid = app.currentUid ?: ""
this.accountUin = (app.currentUin ?: "0").toLong() this.accountUin = (app.currentUin ?: "0").toLong()
} }.build()
} }
@Grpc("KritorService", "DownloadFile") @Grpc("CoreService", "DownloadFile")
override suspend fun downloadFile(request: DownloadFileRequest): DownloadFileResponse { override suspend fun downloadFile(request: DownloadFileRequest): DownloadFileResponse {
val headerMap = mutableMapOf( val headerMap = mutableMapOf(
"User-Agent" to "Shamrock" "User-Agent" to "Shamrock"
@ -76,13 +51,14 @@ object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() {
if (request.hasBase64()) { if (request.hasBase64()) {
val bytes = Base64.decode(request.base64, Base64.DEFAULT) val bytes = Base64.decode(request.base64, Base64.DEFAULT)
tmp.writeBytes(bytes) tmp.writeBytes(bytes)
} else if(request.hasUrl()) { } else if (request.hasUrl()) {
if(!DownloadUtils.download( if (!DownloadUtils.download(
urlAdr = request.url, urlAdr = request.url,
dest = tmp, dest = tmp,
headers = headerMap, headers = headerMap,
threadCount = if (request.hasThreadCnt()) request.threadCnt else 3 threadCount = if (request.hasThreadCnt()) request.threadCnt else 3
)) { )
) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("download failed")) 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.fileMd5 = MD5.genFileMd5Hex(tmp.absolutePath)
this.fileAbsolutePath = tmp.absolutePath this.fileAbsolutePath = tmp.absolutePath
} }.build()
} }
@Grpc("KritorService", "SwitchAccount") @Grpc("CoreService", "SwitchAccount")
override suspend fun switchAccount(request: SwitchAccountRequest): SwitchAccountResponse { 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_UID -> ContactHelper.getUinByUidAsync(request.accountUid)
SwitchAccountRequest.AccountCase.ACCOUNT_UIN -> request.accountUin.toString() 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 } val account = MobileQQ.getMobileQQ().allAccounts.firstOrNull { it.uin == uin }
?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("account not found")) ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("account not found"))
@ -116,6 +96,6 @@ object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() {
}.onFailure { }.onFailure {
throw StatusRuntimeException(Status.INTERNAL.withCause(it).withDescription("failed to switch account")) throw StatusRuntimeException(Status.INTERNAL.withCause(it).withDescription("failed to switch account"))
} }
return switchAccountResponse { } return SwitchAccountResponse.newBuilder().build()
} }
} }

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

View File

@ -2,39 +2,40 @@ package kritor.service
import io.grpc.Status import io.grpc.Status
import io.grpc.StatusRuntimeException import io.grpc.StatusRuntimeException
import io.kritor.event.EventRequest
import io.kritor.event.EventServiceGrpcKt import io.kritor.event.EventServiceGrpcKt
import io.kritor.event.EventStructure import io.kritor.event.EventStructure
import io.kritor.event.EventType import io.kritor.event.EventType
import io.kritor.event.RequestPushEvent import io.kritor.event.RequestPushEvent
import io.kritor.event.eventStructure
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter
object EventService: EventServiceGrpcKt.EventServiceCoroutineImplBase() { internal object EventService : EventServiceGrpcKt.EventServiceCoroutineImplBase() {
override fun registerActiveListener(request: RequestPushEvent): Flow<EventStructure> { override fun registerActiveListener(request: RequestPushEvent): Flow<EventStructure> {
return channelFlow { return channelFlow {
when(request.type!!) { when (request.type!!) {
EventType.EVENT_TYPE_CORE_EVENT -> {} EventType.EVENT_TYPE_CORE_EVENT -> {}
EventType.EVENT_TYPE_MESSAGE -> GlobalEventTransmitter.onMessageEvent { EventType.EVENT_TYPE_MESSAGE -> GlobalEventTransmitter.onMessageEvent {
send(eventStructure { send(EventStructure.newBuilder().apply {
this.type = EventType.EVENT_TYPE_MESSAGE this.type = EventType.EVENT_TYPE_MESSAGE
this.message = it.second this.message = it.second
}) }.build())
} }
EventType.EVENT_TYPE_NOTICE -> GlobalEventTransmitter.onRequestEvent { EventType.EVENT_TYPE_NOTICE -> GlobalEventTransmitter.onRequestEvent {
send(eventStructure { send(EventStructure.newBuilder().apply {
this.type = EventType.EVENT_TYPE_NOTICE this.type = EventType.EVENT_TYPE_NOTICE
this.request = it this.request = it
}) }.build())
} }
EventType.EVENT_TYPE_REQUEST -> GlobalEventTransmitter.onNoticeEvent { EventType.EVENT_TYPE_REQUEST -> GlobalEventTransmitter.onNoticeEvent {
send(eventStructure { send(EventStructure.newBuilder().apply {
this.type = EventType.EVENT_TYPE_NOTICE this.type = EventType.EVENT_TYPE_NOTICE
this.notice = it this.notice = it
}) }.build())
} }
EventType.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT) EventType.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT)
} }
} }

View File

@ -1,28 +1,33 @@
package kritor.service 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.Status
import io.grpc.StatusRuntimeException import io.grpc.StatusRuntimeException
import io.kritor.friend.FriendServiceGrpcKt import io.kritor.friend.*
import io.kritor.friend.GetFriendListRequest import kotlinx.coroutines.suspendCancellableCoroutine
import io.kritor.friend.GetFriendListResponse import kotlinx.coroutines.withTimeoutOrNull
import io.kritor.friend.friendData import qq.service.QQInterfaces
import io.kritor.friend.friendExt
import io.kritor.friend.getFriendListResponse
import qq.service.contact.ContactHelper import qq.service.contact.ContactHelper
import qq.service.friend.FriendHelper import qq.service.friend.FriendHelper
import kotlin.coroutines.resume
object FriendService: FriendServiceGrpcKt.FriendServiceCoroutineImplBase() { internal object FriendService : FriendServiceGrpcKt.FriendServiceCoroutineImplBase() {
@Grpc("FriendService", "GetFriendList") @Grpc("FriendService", "GetFriendList")
override suspend fun getFriendList(request: GetFriendListRequest): GetFriendListResponse { override suspend fun getFriendList(request: GetFriendListRequest): GetFriendListResponse {
val friendList = FriendHelper.getFriendList(if(request.hasRefresh()) request.refresh else false).onFailure { val friendList = FriendHelper.getFriendList(if (request.hasRefresh()) request.refresh else false).onFailure {
throw StatusRuntimeException(Status.INTERNAL throw StatusRuntimeException(
.withDescription(it.stackTraceToString()) Status.INTERNAL
.withDescription(it.stackTraceToString())
) )
}.getOrThrow() }.getOrThrow()
return getFriendListResponse { return GetFriendListResponse.newBuilder().apply {
friendList.forEach { friendList.forEach {
this.friendList.add(friendData { this.addFriendsInfo(FriendInfo.newBuilder().apply {
uin = it.uin.toLong() uin = it.uin.toLong()
uid = ContactHelper.getUidByUinAsync(uin) uid = ContactHelper.getUidByUinAsync(uin)
qid = "" qid = ""
@ -32,10 +37,208 @@ object FriendService: FriendServiceGrpcKt.FriendServiceCoroutineImplBase() {
level = 0 level = 0
gender = it.gender.toInt() gender = it.gender.toInt()
groupId = it.groupid 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()
}
} }

View File

@ -15,10 +15,9 @@ import qq.service.QQInterfaces
import qq.service.file.GroupFileHelper import qq.service.file.GroupFileHelper
import qq.service.file.GroupFileHelper.getGroupFileSystemInfo import qq.service.file.GroupFileHelper.getGroupFileSystemInfo
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6 import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
import tencent.im.oidb.cmd0x6d8.oidb_0x6d8
import tencent.im.oidb.oidb_sso import tencent.im.oidb.oidb_sso
internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCoroutineImplBase() { internal object GroupFileService : GroupFileServiceGrpcKt.GroupFileServiceCoroutineImplBase() {
@Grpc("GroupFileService", "CreateFolder") @Grpc("GroupFileService", "CreateFolder")
override suspend fun createFolder(request: CreateFolderRequest): CreateFolderResponse { override suspend fun createFolder(request: CreateFolderRequest): CreateFolderResponse {
val data = Oidb0x6d7ReqBody( val data = Oidb0x6d7ReqBody(
@ -42,21 +41,23 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
if (rsp.createFolder?.retCode != 0) { if (rsp.createFolder?.retCode != 0) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to create folder: ${rsp.createFolder?.retCode}")) 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.id = rsp.createFolder?.folderInfo?.folderId ?: ""
this.usedSpace = 0 this.usedSpace = 0
} }.build()
} }
@Grpc("GroupFileService", "DeleteFolder") @Grpc("GroupFileService", "DeleteFolder")
override suspend fun deleteFolder(request: DeleteFolderRequest): DeleteFolderResponse { override suspend fun deleteFolder(request: DeleteFolderRequest): DeleteFolderResponse {
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_1", 1751, 1, Oidb0x6d7ReqBody( val fromServiceMsg = QQInterfaces.sendOidbAW(
deleteFolder = DeleteFolderReq( "OidbSvc.0x6d7_1", 1751, 1, Oidb0x6d7ReqBody(
groupCode = request.groupId.toULong(), deleteFolder = DeleteFolderReq(
appId = 3u, groupCode = request.groupId.toULong(),
folderId = request.folderId appId = 3u,
) folderId = request.folderId
).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) )
).toByteArray()
) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
if (fromServiceMsg.wupBuffer == null) { if (fromServiceMsg.wupBuffer == null) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
} }
@ -66,7 +67,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
if (rsp.deleteFolder?.retCode != 0) { if (rsp.deleteFolder?.retCode != 0) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to delete folder: ${rsp.deleteFolder?.retCode}")) throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to delete folder: ${rsp.deleteFolder?.retCode}"))
} }
return deleteFolderResponse { } return DeleteFolderResponse.newBuilder().build()
} }
@Grpc("GroupFileService", "DeleteFile") @Grpc("GroupFileService", "DeleteFile")
@ -93,19 +94,21 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
if (rsp.delete_file_rsp.int32_ret_code.get() != 0) { 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()}")) 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") @Grpc("GroupFileService", "RenameFolder")
override suspend fun renameFolder(request: RenameFolderRequest): RenameFolderResponse { override suspend fun renameFolder(request: RenameFolderRequest): RenameFolderResponse {
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_3", 1751, 3, Oidb0x6d7ReqBody( val fromServiceMsg = QQInterfaces.sendOidbAW(
renameFolder = RenameFolderReq( "OidbSvc.0x6d7_3", 1751, 3, Oidb0x6d7ReqBody(
groupCode = request.groupId.toULong(), renameFolder = RenameFolderReq(
appId = 3u, groupCode = request.groupId.toULong(),
folderId = request.folderId, appId = 3u,
folderName = request.name folderId = request.folderId,
) folderName = request.name
).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) )
).toByteArray()
) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
if (fromServiceMsg.wupBuffer == null) { if (fromServiceMsg.wupBuffer == null) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
} }
@ -115,24 +118,19 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
if (rsp.renameFolder?.retCode != 0) { if (rsp.renameFolder?.retCode != 0) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to rename folder: ${rsp.renameFolder?.retCode}")) 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 { override suspend fun getFileSystemInfo(request: GetFileSystemInfoRequest): GetFileSystemInfoResponse {
return getGroupFileSystemInfo(request.groupId) return getGroupFileSystemInfo(request.groupId)
} }
@Grpc("GroupFileService", "GetRootFiles") @Grpc("GroupFileService", "GetFileList")
override suspend fun getRootFiles(request: GetRootFilesRequest): GetRootFilesResponse { override suspend fun getFileList(request: GetFileListRequest): GetFileListResponse {
return getRootFilesResponse { return if (request.hasFolderId())
val response = GroupFileHelper.getGroupFiles(request.groupId) GroupFileHelper.getGroupFiles(request.groupId, request.folderId)
this.files.addAll(response.filesList) else
this.folders.addAll(response.foldersList) GroupFileHelper.getGroupFiles(request.groupId)
}
}
@Grpc("GroupFileService", "GetFiles")
override suspend fun getFiles(request: GetFilesRequest): GetFilesResponse {
return GroupFileHelper.getGroupFiles(request.groupId, request.folderId)
} }
} }

View File

@ -2,213 +2,187 @@ package kritor.service
import io.grpc.Status import io.grpc.Status
import io.grpc.StatusRuntimeException import io.grpc.StatusRuntimeException
import io.kritor.group.BanMemberRequest import io.kritor.group.*
import io.kritor.group.BanMemberResponse import moe.fuqiuluo.shamrock.helper.LogCenter
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 moe.fuqiuluo.shamrock.helper.TroopHonorHelper.decodeHonor import moe.fuqiuluo.shamrock.helper.TroopHonorHelper.decodeHonor
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
import qq.service.contact.ContactHelper import qq.service.contact.ContactHelper
import qq.service.group.GroupHelper import qq.service.group.GroupHelper
internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase() { internal object GroupService : GroupServiceGrpcKt.GroupServiceCoroutineImplBase() {
@Grpc("GroupService", "BanMember") @Grpc("GroupService", "BanMember")
override suspend fun banMember(request: BanMemberRequest): BanMemberResponse { override suspend fun banMember(request: BanMemberRequest): BanMemberResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) { if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(Status.PERMISSION_DENIED throw StatusRuntimeException(
.withDescription("You are not admin of this group") Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
) )
} }
GroupHelper.banMember(request.groupId, when(request.targetCase!!) { GroupHelper.banMember(
BanMemberRequest.TargetCase.TARGET_UIN -> request.targetUin request.groupId, when (request.targetCase!!) {
BanMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() BanMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT BanMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
.withDescription("target not set") else -> throw StatusRuntimeException(
) Status.INVALID_ARGUMENT
}, request.duration) .withDescription("target not set")
)
}, request.duration
)
return banMemberResponse { return BanMemberResponse.newBuilder().apply {
groupId = request.groupId groupId = request.groupId
} }.build()
} }
@Grpc("GroupService", "PokeMember") @Grpc("GroupService", "PokeMember")
override suspend fun pokeMember(request: PokeMemberRequest): PokeMemberResponse { override suspend fun pokeMember(request: PokeMemberRequest): PokeMemberResponse {
GroupHelper.pokeMember(request.groupId, when(request.targetCase!!) { GroupHelper.pokeMember(
PokeMemberRequest.TargetCase.TARGET_UIN -> request.targetUin request.groupId, when (request.targetCase!!) {
PokeMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() PokeMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT PokeMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
.withDescription("target not set") else -> throw StatusRuntimeException(
) Status.INVALID_ARGUMENT
}) .withDescription("target not set")
return pokeMemberResponse { } )
}
)
return PokeMemberResponse.newBuilder().build()
} }
@Grpc("GroupService", "KickMember") @Grpc("GroupService", "KickMember")
override suspend fun kickMember(request: KickMemberRequest): KickMemberResponse { override suspend fun kickMember(request: KickMemberRequest): KickMemberResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) { if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(Status.PERMISSION_DENIED throw StatusRuntimeException(
.withDescription("You are not admin of this group") 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!!) { GroupHelper.kickMember(
KickMemberRequest.TargetCase.TARGET_UIN -> request.targetUin request.groupId,
KickMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() request.rejectAddRequest,
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT if (request.hasKickReason()) request.kickReason else "",
.withDescription("target not set") when (request.targetCase!!) {
) KickMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
}) KickMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
return kickMemberResponse { } else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}
)
return KickMemberResponse.newBuilder().build()
} }
@Grpc("GroupService", "LeaveGroup") @Grpc("GroupService", "LeaveGroup")
override suspend fun leaveGroup(request: LeaveGroupRequest): LeaveGroupResponse { override suspend fun leaveGroup(request: LeaveGroupRequest): LeaveGroupResponse {
GroupHelper.resignTroop(request.groupId.toString()) GroupHelper.resignTroop(request.groupId.toString())
return leaveGroupResponse { } return LeaveGroupResponse.newBuilder().build()
} }
@Grpc("GroupService", "ModifyMemberCard") @Grpc("GroupService", "ModifyMemberCard")
override suspend fun modifyMemberCard(request: ModifyMemberCardRequest): ModifyMemberCardResponse { override suspend fun modifyMemberCard(request: ModifyMemberCardRequest): ModifyMemberCardResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) { if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(Status.PERMISSION_DENIED throw StatusRuntimeException(
.withDescription("You are not admin of this group") Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
) )
} }
GroupHelper.modifyGroupMemberCard(request.groupId, when(request.targetCase!!) { GroupHelper.modifyGroupMemberCard(
ModifyMemberCardRequest.TargetCase.TARGET_UIN -> request.targetUin request.groupId, when (request.targetCase!!) {
ModifyMemberCardRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() ModifyMemberCardRequest.TargetCase.TARGET_UIN -> request.targetUin
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT ModifyMemberCardRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid)
.withDescription("target not set") .toLong()
)
}, request.card) else -> throw StatusRuntimeException(
return modifyMemberCardResponse { } Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}, request.card
)
return ModifyMemberCardResponse.newBuilder().build()
} }
@Grpc("GroupService", "ModifyGroupName") @Grpc("GroupService", "ModifyGroupName")
override suspend fun modifyGroupName(request: ModifyGroupNameRequest): ModifyGroupNameResponse { override suspend fun modifyGroupName(request: ModifyGroupNameRequest): ModifyGroupNameResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) { if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(Status.PERMISSION_DENIED throw StatusRuntimeException(
.withDescription("You are not admin of this group") Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
) )
} }
GroupHelper.modifyTroopName(request.groupId.toString(), request.groupName) GroupHelper.modifyTroopName(request.groupId.toString(), request.groupName)
return modifyGroupNameResponse { } return ModifyGroupNameResponse.newBuilder().build()
} }
@Grpc("GroupService", "ModifyGroupRemark") @Grpc("GroupService", "ModifyGroupRemark")
override suspend fun modifyGroupRemark(request: ModifyGroupRemarkRequest): ModifyGroupRemarkResponse { override suspend fun modifyGroupRemark(request: ModifyGroupRemarkRequest): ModifyGroupRemarkResponse {
GroupHelper.modifyGroupRemark(request.groupId, request.remark) GroupHelper.modifyGroupRemark(request.groupId, request.remark)
return modifyGroupRemarkResponse { } return ModifyGroupRemarkResponse.newBuilder().build()
} }
@Grpc("GroupService", "SetGroupAdmin") @Grpc("GroupService", "SetGroupAdmin")
override suspend fun setGroupAdmin(request: SetGroupAdminRequest): SetGroupAdminResponse { override suspend fun setGroupAdmin(request: SetGroupAdminRequest): SetGroupAdminResponse {
if (!GroupHelper.isOwner(request.groupId.toString())) { if (!GroupHelper.isOwner(request.groupId.toString())) {
throw StatusRuntimeException(Status.PERMISSION_DENIED throw StatusRuntimeException(
.withDescription("You are not admin of this group") Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
) )
} }
GroupHelper.setGroupAdmin(request.groupId, when(request.targetCase!!) { GroupHelper.setGroupAdmin(
SetGroupAdminRequest.TargetCase.TARGET_UIN -> request.targetUin request.groupId, when (request.targetCase!!) {
SetGroupAdminRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() SetGroupAdminRequest.TargetCase.TARGET_UIN -> request.targetUin
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT SetGroupAdminRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
.withDescription("target not set") else -> throw StatusRuntimeException(
) Status.INVALID_ARGUMENT
}, request.isAdmin) .withDescription("target not set")
)
}, request.isAdmin
)
return setGroupAdminResponse { } return SetGroupAdminResponse.newBuilder().build()
} }
@Grpc("GroupService", "SetGroupUniqueTitle") @Grpc("GroupService", "SetGroupUniqueTitle")
override suspend fun setGroupUniqueTitle(request: SetGroupUniqueTitleRequest): SetGroupUniqueTitleResponse { override suspend fun setGroupUniqueTitle(request: SetGroupUniqueTitleRequest): SetGroupUniqueTitleResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) { if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(Status.PERMISSION_DENIED throw StatusRuntimeException(
.withDescription("You are not admin of this group") Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
) )
} }
GroupHelper.setGroupUniqueTitle(request.groupId, when(request.targetCase!!) { GroupHelper.setGroupUniqueTitle(
SetGroupUniqueTitleRequest.TargetCase.TARGET_UIN -> request.targetUin request.groupId.toString(), when (request.targetCase!!) {
SetGroupUniqueTitleRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() SetGroupUniqueTitleRequest.TargetCase.TARGET_UIN -> request.targetUin
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT SetGroupUniqueTitleRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid)
.withDescription("target not set") .toLong()
)
}, request.uniqueTitle)
return setGroupUniqueTitleResponse { } else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}.toString(), request.uniqueTitle
)
return SetGroupUniqueTitleResponse.newBuilder().build()
} }
@Grpc("GroupService", "SetGroupWholeBan") @Grpc("GroupService", "SetGroupWholeBan")
override suspend fun setGroupWholeBan(request: SetGroupWholeBanRequest): SetGroupWholeBanResponse { override suspend fun setGroupWholeBan(request: SetGroupWholeBanRequest): SetGroupWholeBanResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) { if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(Status.PERMISSION_DENIED throw StatusRuntimeException(
.withDescription("You are not admin of this group") Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
) )
} }
GroupHelper.setGroupWholeBan(request.groupId, request.isBan) GroupHelper.setGroupWholeBan(request.groupId, request.isBan)
return setGroupWholeBanResponse { } return SetGroupWholeBanResponse.newBuilder().build()
} }
@Grpc("GroupService", "GetGroupInfo") @Grpc("GroupService", "GetGroupInfo")
@ -216,18 +190,20 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
val groupInfo = GroupHelper.getGroupInfo(request.groupId.toString(), true).onFailure { val groupInfo = GroupHelper.getGroupInfo(request.groupId.toString(), true).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group info").withCause(it)) throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group info").withCause(it))
}.getOrThrow() }.getOrThrow()
return getGroupInfoResponse { return GetGroupInfoResponse.newBuilder().apply {
this.groupInfo = io.kritor.group.groupInfo { this.groupInfo = GroupInfo.newBuilder().apply {
groupId = groupInfo.troopcode.toLong() 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 ?: "" groupRemark = groupInfo.troopRemark ?: ""
owner = groupInfo.troopowneruin?.toLong() ?: 0 owner = groupInfo.troopowneruin?.toLong() ?: 0
admins.addAll(GroupHelper.getAdminList(groupId)) addAllAdmins(GroupHelper.getAdminList(groupId))
maxMemberCount = groupInfo.wMemberMax maxMemberCount = groupInfo.wMemberMax
memberCount = groupInfo.wMemberNum memberCount = groupInfo.wMemberNum
groupUin = groupInfo.troopuin?.toLong() ?: 0 groupUin = groupInfo.troopuin?.toLong() ?: 0
} }.build()
} }.build()
} }
@Grpc("GroupService", "GetGroupList") @Grpc("GroupService", "GetGroupList")
@ -235,36 +211,46 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
val groupList = GroupHelper.getGroupList(if (request.hasRefresh()) request.refresh else false).onFailure { val groupList = GroupHelper.getGroupList(if (request.hasRefresh()) request.refresh else false).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group list").withCause(it)) throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group list").withCause(it))
}.getOrThrow() }.getOrThrow()
return getGroupListResponse { return GetGroupListResponse.newBuilder().apply {
groupList.forEach { groupInfo -> groupList.forEach { groupInfo ->
this.groupInfo.add(io.kritor.group.groupInfo { this.addGroupsInfo(GroupInfo.newBuilder().apply {
groupId = groupInfo.troopcode.toLong() groupId = groupInfo.troopcode.ifNullOrEmpty { groupInfo.uin }.ifNullOrEmpty { groupInfo.troopuin }?.toLong() ?: 0
groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName } ?: "" groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }
.ifNullOrEmpty { groupInfo.newTroopName }
?: ""
groupRemark = groupInfo.troopRemark ?: "" groupRemark = groupInfo.troopRemark ?: ""
owner = groupInfo.troopowneruin?.toLong() ?: 0 owner = groupInfo.troopowneruin?.toLong() ?: 0
admins.addAll(GroupHelper.getAdminList(groupId)) addAllAdmins(GroupHelper.getAdminList(groupId))
maxMemberCount = groupInfo.wMemberMax maxMemberCount = groupInfo.wMemberMax
memberCount = groupInfo.wMemberNum memberCount = groupInfo.wMemberNum
groupUin = groupInfo.troopuin?.toLong() ?: 0 groupUin = groupInfo.troopuin?.toLong() ?: 0
}) })
} }
} }.build()
} }
@Grpc("GroupService", "GetGroupMemberInfo") @Grpc("GroupService", "GetGroupMemberInfo")
override suspend fun getGroupMemberInfo(request: GetGroupMemberInfoRequest): GetGroupMemberInfoResponse { override suspend fun getGroupMemberInfo(request: GetGroupMemberInfoRequest): GetGroupMemberInfoResponse {
val memberInfo = GroupHelper.getTroopMemberInfoByUin(request.groupId, when(request.targetCase!!) { val memberInfo = GroupHelper.getTroopMemberInfoByUin(
GetGroupMemberInfoRequest.TargetCase.UIN -> request.uin request.groupId.toString(), when (request.targetCase!!) {
GetGroupMemberInfoRequest.TargetCase.UID -> ContactHelper.getUinByUidAsync(request.uid).toLong() GetGroupMemberInfoRequest.TargetCase.TARGET_UID -> request.targetUin
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT GetGroupMemberInfoRequest.TargetCase.TARGET_UIN -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
.withDescription("target not set") 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() }.getOrThrow()
return getGroupMemberInfoResponse { return GetGroupMemberInfoResponse.newBuilder().apply {
groupMemberInfo = groupMemberInfo { groupMemberInfo = GroupMemberInfo.newBuilder().apply {
uid = if (request.targetCase == GetGroupMemberInfoRequest.TargetCase.UID) request.uid else ContactHelper.getUidByUinAsync(request.uin) uid =
if (request.targetCase == GetGroupMemberInfoRequest.TargetCase.TARGET_UID) request.targetUid else ContactHelper.getUidByUinAsync(
request.targetUin
)
uin = memberInfo.memberuin?.toLong() ?: 0 uin = memberInfo.memberuin?.toLong() ?: 0
nick = memberInfo.troopnick nick = memberInfo.troopnick
.ifNullOrEmpty { memberInfo.hwName } .ifNullOrEmpty { memberInfo.hwName }
@ -280,24 +266,29 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
shutUpTimestamp = memberInfo.gagTimeStamp shutUpTimestamp = memberInfo.gagTimeStamp
distance = memberInfo.distance distance = memberInfo.distance
honor.addAll((memberInfo.honorList ?: "") addAllHonors((memberInfo.honorList ?: "")
.split("|") .split("|")
.filter { it.isNotBlank() } .filter { it.isNotBlank() }
.map { it.toInt() }) .map { it.toInt() })
unfriendly = false unfriendly = false
cardChangeable = GroupHelper.isAdmin(request.groupId.toString()) cardChangeable = GroupHelper.isAdmin(request.groupId.toString())
} }.build()
} }.build()
} }
@Grpc("GroupService", "GetGroupMemberList") @Grpc("GroupService", "GetGroupMemberList")
override suspend fun getGroupMemberList(request: GetGroupMemberListRequest): GetGroupMemberListResponse { override suspend fun getGroupMemberList(request: GetGroupMemberListRequest): GetGroupMemberListResponse {
val memberList = GroupHelper.getGroupMemberList(request.groupId.toString(), if (request.hasRefresh()) request.refresh else false).onFailure { val memberList = GroupHelper.getGroupMemberList(
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member list").withCause(it)) 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() }.getOrThrow()
return getGroupMemberListResponse { return GetGroupMemberListResponse.newBuilder().apply {
memberList.forEach { memberInfo -> memberList.forEach { memberInfo ->
this.groupMemberInfo.add(groupMemberInfo { this.addGroupMembersInfo(GroupMemberInfo.newBuilder().apply {
uid = ContactHelper.getUidByUinAsync(memberInfo.memberuin?.toLong() ?: 0) uid = ContactHelper.getUidByUinAsync(memberInfo.memberuin?.toLong() ?: 0)
uin = memberInfo.memberuin?.toLong() ?: 0 uin = memberInfo.memberuin?.toLong() ?: 0
nick = memberInfo.troopnick nick = memberInfo.troopnick
@ -314,7 +305,7 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
shutUpTimestamp = memberInfo.gagTimeStamp shutUpTimestamp = memberInfo.gagTimeStamp
distance = memberInfo.distance distance = memberInfo.distance
honor.addAll((memberInfo.honorList ?: "") addAllHonors((memberInfo.honorList ?: "")
.split("|") .split("|")
.filter { it.isNotBlank() } .filter { it.isNotBlank() }
.map { it.toInt() }) .map { it.toInt() })
@ -322,23 +313,25 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
cardChangeable = GroupHelper.isAdmin(request.groupId.toString()) cardChangeable = GroupHelper.isAdmin(request.groupId.toString())
}) })
} }
} }.build()
} }
@Grpc("GroupService", "GetProhibitedUserList") @Grpc("GroupService", "GetProhibitedUserList")
override suspend fun getProhibitedUserList(request: GetProhibitedUserListRequest): GetProhibitedUserListResponse { override suspend fun getProhibitedUserList(request: GetProhibitedUserListRequest): GetProhibitedUserListResponse {
val prohibitedList = GroupHelper.getProhibitedMemberList(request.groupId).onFailure { 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() }.getOrThrow()
return getProhibitedUserListResponse { return GetProhibitedUserListResponse.newBuilder().apply {
prohibitedList.forEach { prohibitedList.forEach {
this.prohibitedUserInfo.add(prohibitedUserInfo { this.addProhibitedUsersInfo(ProhibitedUserInfo.newBuilder().apply {
uid = ContactHelper.getUidByUinAsync(it.memberUin) uid = ContactHelper.getUidByUinAsync(it.memberUin)
uin = it.memberUin uin = it.memberUin
prohibitedTime = it.shutuptimestap prohibitedTime = it.shutuptimestap
}) })
} }
} }.build()
} }
@Grpc("GroupService", "GetRemainCountAtAll") @Grpc("GroupService", "GetRemainCountAtAll")
@ -346,20 +339,22 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
val remainAtAllRsp = GroupHelper.getGroupRemainAtAllRemain(request.groupId).onFailure { val remainAtAllRsp = GroupHelper.getGroupRemainAtAllRemain(request.groupId).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get remain count").withCause(it)) throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get remain count").withCause(it))
}.getOrThrow() }.getOrThrow()
return getRemainCountAtAllResponse { return GetRemainCountAtAllResponse.newBuilder().apply {
accessAtAll = remainAtAllRsp.bool_can_at_all.get() accessAtAll = remainAtAllRsp.bool_can_at_all.get()
remainCountForGroup = remainAtAllRsp.uint32_remain_at_all_count_for_group.get() remainCountForGroup = remainAtAllRsp.uint32_remain_at_all_count_for_group.get()
remainCountForSelf = remainAtAllRsp.uint32_remain_at_all_count_for_uin.get() remainCountForSelf = remainAtAllRsp.uint32_remain_at_all_count_for_uin.get()
} }.build()
} }
@Grpc("GroupService", "GetNotJoinedGroupInfo") @Grpc("GroupService", "GetNotJoinedGroupInfo")
override suspend fun getNotJoinedGroupInfo(request: GetNotJoinedGroupInfoRequest): GetNotJoinedGroupInfoResponse { override suspend fun getNotJoinedGroupInfo(request: GetNotJoinedGroupInfoRequest): GetNotJoinedGroupInfoResponse {
val groupInfo = GroupHelper.getNotJoinedGroupInfo(request.groupId).onFailure { 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() }.getOrThrow()
return getNotJoinedGroupInfoResponse { return GetNotJoinedGroupInfoResponse.newBuilder().apply {
this.groupInfo = notJoinedGroupInfo { this.groupInfo = NotJoinedGroupInfo.newBuilder().apply {
groupId = groupInfo.groupId groupId = groupInfo.groupId
groupName = groupInfo.groupName groupName = groupInfo.groupName
owner = groupInfo.owner owner = groupInfo.owner
@ -369,15 +364,17 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
createTime = groupInfo.createTime.toInt() createTime = groupInfo.createTime.toInt()
groupFlag = groupInfo.groupFlag groupFlag = groupInfo.groupFlag
groupFlagExt = groupInfo.groupFlagExt groupFlagExt = groupInfo.groupFlagExt
} }.build()
} }.build()
} }
@Grpc("GroupService", "GetGroupHonor") @Grpc("GroupService", "GetGroupHonor")
override suspend fun getGroupHonor(request: GetGroupHonorRequest): GetGroupHonorResponse { override suspend fun getGroupHonor(request: GetGroupHonorRequest): GetGroupHonorResponse {
return getGroupHonorResponse { return GetGroupHonorResponse.newBuilder().apply {
GroupHelper.getGroupMemberList(request.groupId.toString(), true).onFailure { 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 -> }.onSuccess { memberList ->
memberList.forEach { member -> memberList.forEach { member ->
(member.honorList ?: "").split("|") (member.honorList ?: "").split("|")
@ -385,7 +382,7 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
.map { it.toInt() }.forEach { .map { it.toInt() }.forEach {
val honor = decodeHonor(member.memberuin.toLong(), it, member.mHonorRichFlag) val honor = decodeHonor(member.memberuin.toLong(), it, member.mHonorRichFlag)
if (honor != null) { if (honor != null) {
groupHonorInfo.add(groupHonorInfo { addGroupHonorsInfo(GroupHonorInfo.newBuilder().apply {
uid = ContactHelper.getUidByUinAsync(member.memberuin.toLong()) uid = ContactHelper.getUidByUinAsync(member.memberuin.toLong())
uin = member.memberuin.toLong() uin = member.memberuin.toLong()
nick = member.troopnick nick = member.troopnick
@ -401,6 +398,6 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
} }
} }
} }
} }.build()
} }
} }

View File

@ -1,7 +1,470 @@
package kritor.service 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()
}
} }

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

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

View File

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

View File

@ -14,7 +14,10 @@ import moe.fuqiuluo.shamrock.tools.toast
import moe.fuqiuluo.shamrock.xposed.helper.AppTalker import moe.fuqiuluo.shamrock.xposed.helper.AppTalker
import mqq.app.MobileQQ import mqq.app.MobileQQ
import java.io.File import java.io.File
import java.util.Calendar
import java.util.Date import java.util.Date
import java.util.Timer
import java.util.TimerTask
internal enum class Level( internal enum class Level(
val id: Byte val id: Byte
@ -31,7 +34,29 @@ internal object LogCenter {
// 格式化时间 // 格式化时间
SimpleDateFormat("yyyy-MM-dd").format(Date()) 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 { .parentFile!!.resolve("Tencent/Shamrock/log").also {
if (it.exists()) it.delete() if (it.exists()) it.delete()
it.mkdirs() it.mkdirs()
@ -49,8 +74,6 @@ internal object LogCenter {
return@let result return@let result
} }
private val format = SimpleDateFormat("[HH:mm:ss] ")
fun log(string: String, level: Level = Level.INFO, toast: Boolean = false) { fun log(string: String, level: Level = Level.INFO, toast: Boolean = false) {
if (!ShamrockConfig[DebugMode] && level == Level.DEBUG) { if (!ShamrockConfig[DebugMode] && level == Level.DEBUG) {
return return

View File

@ -4,84 +4,59 @@ package moe.fuqiuluo.shamrock.internals
import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import io.kritor.event.GroupApplyType import io.kritor.event.*
import io.kritor.event.GroupMemberBanType import io.kritor.common.PushMessageBody
import io.kritor.event.GroupMemberDecreasedType import io.kritor.common.Contact
import io.kritor.event.GroupMemberIncreasedType import io.kritor.common.Scene
import io.kritor.event.MessageEvent import io.kritor.common.Sender
import io.kritor.event.NoticeEvent
import io.kritor.event.NoticeType
import io.kritor.event.RequestType
import io.kritor.event.RequestsEvent
import io.kritor.event.Scene
import io.kritor.event.contact
import io.kritor.event.essenceMessageNotice
import io.kritor.event.friendApplyRequest
import io.kritor.event.friendFileComeNotice
import io.kritor.event.friendPokeNotice
import io.kritor.event.friendRecallNotice
import io.kritor.event.groupAdminChangedNotice
import io.kritor.event.groupApplyRequest
import io.kritor.event.groupFileComeNotice
import io.kritor.event.groupMemberBannedNotice
import io.kritor.event.groupMemberDecreasedNotice
import io.kritor.event.groupMemberIncreasedNotice
import io.kritor.event.groupPokeNotice
import io.kritor.event.groupRecallNotice
import io.kritor.event.groupSignNotice
import io.kritor.event.groupUniqueTitleChangedNotice
import io.kritor.event.groupWholeBanNotice
import io.kritor.event.messageEvent
import io.kritor.event.noticeEvent
import io.kritor.event.requestsEvent
import io.kritor.event.sender
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import qq.service.QQInterfaces import qq.service.QQInterfaces
import qq.service.msg.toKritorMessages import qq.service.contact.ContactHelper
import qq.service.msg.toKritorEventMessages
internal object GlobalEventTransmitter: QQInterfaces() { internal object GlobalEventTransmitter : QQInterfaces() {
private val messageEventFlow by lazy { private val MessageEventFlow by lazy {
MutableSharedFlow<Pair<MsgRecord, MessageEvent>>() MutableSharedFlow<Pair<MsgRecord, PushMessageBody>>()
} }
private val noticeEventFlow by lazy { private val noticeEventFlow by lazy {
MutableSharedFlow<NoticeEvent>() MutableSharedFlow<NoticeEvent>()
} }
private val requestEventFlow by lazy { private val requestEventFlow by lazy {
MutableSharedFlow<RequestsEvent>() MutableSharedFlow<RequestEvent>()
} }
private suspend fun pushNotice(noticeEvent: NoticeEvent) = noticeEventFlow.emit(noticeEvent) 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 { object MessageTransmitter {
suspend fun transGroupMessage( suspend fun transGroupMessage(
record: MsgRecord, record: MsgRecord,
elements: ArrayList<MsgElement>, elements: ArrayList<MsgElement>,
): Boolean { ): Boolean {
transMessageEvent(record, messageEvent { transMessageEvent(record, PushMessageBody.newBuilder().apply {
this.time = record.msgTime.toInt() this.time = record.msgTime.toInt()
this.scene = Scene.GROUP this.messageId = record.msgId.toString()
this.messageId = record.msgId
this.messageSeq = record.msgSeq this.messageSeq = record.msgSeq
this.contact = contact { this.contact = Contact.newBuilder().apply {
this.scene = scene this.scene = Scene.GROUP
this.peer = record.peerUin.toString() this.peer = record.peerUin.toString()
this.subPeer = record.peerUid this.subPeer = record.peerUid
} }.build()
this.sender = sender { this.sender = Sender.newBuilder().apply {
this.uin = record.senderUin this.uin = record.senderUin
this.uid = record.senderUid this.uid = record.senderUid
this.nick = record.sendNickName this.nick = record.sendNickName
} }.build()
this.elements.addAll(elements.toKritorMessages(record)) this.addAllElements(elements.toKritorEventMessages(record))
}) }.build())
return true return true
} }
@ -89,23 +64,22 @@ internal object GlobalEventTransmitter: QQInterfaces() {
record: MsgRecord, record: MsgRecord,
elements: ArrayList<MsgElement>, elements: ArrayList<MsgElement>,
): Boolean { ): Boolean {
transMessageEvent(record, messageEvent { transMessageEvent(record, PushMessageBody.newBuilder().apply {
this.time = record.msgTime.toInt() this.time = record.msgTime.toInt()
this.scene = Scene.FRIEND this.messageId = record.msgId.toString()
this.messageId = record.msgId
this.messageSeq = record.msgSeq this.messageSeq = record.msgSeq
this.contact = contact { this.contact = Contact.newBuilder().apply {
this.scene = scene this.scene = Scene.FRIEND
this.peer = record.senderUin.toString() this.peer = record.senderUid
this.subPeer = record.senderUid this.subPeer = record.senderUid
} }.build()
this.sender = sender { this.sender = Sender.newBuilder().apply {
this.uin = record.senderUin this.uin = record.senderUin
this.uid = record.senderUid this.uid = record.senderUid
this.nick = record.sendNickName this.nick = record.sendNickName
} }.build()
this.elements.addAll(elements.toKritorMessages(record)) this.addAllElements(elements.toKritorEventMessages(record))
}) }.build())
return true return true
} }
@ -115,23 +89,22 @@ internal object GlobalEventTransmitter: QQInterfaces() {
groupCode: Long, groupCode: Long,
fromNick: String, fromNick: String,
): Boolean { ): Boolean {
transMessageEvent(record, messageEvent { transMessageEvent(record, PushMessageBody.newBuilder().apply {
this.time = record.msgTime.toInt() this.time = record.msgTime.toInt()
this.scene = Scene.FRIEND this.messageId = record.msgId.toString()
this.messageId = record.msgId
this.messageSeq = record.msgSeq this.messageSeq = record.msgSeq
this.contact = contact { this.contact = Contact.newBuilder().apply {
this.scene = scene this.scene = if (groupCode > 0) Scene.STRANGER_FROM_GROUP else Scene.STRANGER
this.peer = record.senderUin.toString() this.peer = record.senderUid
this.subPeer = groupCode.toString() this.subPeer = groupCode.toString()
} }.build()
this.sender = sender { this.sender = Sender.newBuilder().apply {
this.uin = record.senderUin this.uin = record.senderUin
this.uid = record.senderUid this.uid = record.senderUid
this.nick = record.sendNickName this.nick = record.sendNickName
} }.build()
this.elements.addAll(elements.toKritorMessages(record)) this.addAllElements(elements.toKritorEventMessages(record))
}) }.build())
return true return true
} }
@ -139,23 +112,22 @@ internal object GlobalEventTransmitter: QQInterfaces() {
record: MsgRecord, record: MsgRecord,
elements: ArrayList<MsgElement>, elements: ArrayList<MsgElement>,
): Boolean { ): Boolean {
transMessageEvent(record, messageEvent { transMessageEvent(record, PushMessageBody.newBuilder().apply {
this.time = record.msgTime.toInt() this.time = record.msgTime.toInt()
this.scene = Scene.GUILD this.messageId = record.msgId.toString()
this.messageId = record.msgId
this.messageSeq = record.msgSeq this.messageSeq = record.msgSeq
this.contact = contact { this.contact = Contact.newBuilder().apply {
this.scene = scene this.scene = Scene.GUILD
this.peer = record.channelId.toString() this.peer = record.guildId ?: ""
this.subPeer = record.guildId this.subPeer = record.channelId ?: ""
} }.build()
this.sender = sender { this.sender = Sender.newBuilder().apply {
this.uin = record.senderUin this.uin = record.senderUin
this.uid = record.senderUid this.uid = record.senderUid
this.nick = record.sendNickName this.nick = record.sendNickName
} }.build()
this.elements.addAll(elements.toKritorMessages(record)) this.addAllElements(elements.toKritorEventMessages(record))
}) }.build())
return true return true
} }
} }
@ -169,7 +141,8 @@ internal object GlobalEventTransmitter: QQInterfaces() {
*/ */
suspend fun transPrivateFileEvent( suspend fun transPrivateFileEvent(
msgTime: Long, msgTime: Long,
userId: Long, senderUid: String,
senderUin: Long,
fileId: String, fileId: String,
fileSubId: String, fileSubId: String,
fileName: String, fileName: String,
@ -177,19 +150,20 @@ internal object GlobalEventTransmitter: QQInterfaces() {
expireTime: Long, expireTime: Long,
url: String url: String
): Boolean { ): Boolean {
pushNotice(noticeEvent { pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeType.FRIEND_FILE_COME this.type = NoticeEvent.NoticeType.PRIVATE_FILE_UPLOADED
this.time = msgTime.toInt() this.time = msgTime.toInt()
this.friendFileCome = friendFileComeNotice { this.privateFileUploaded = PrivateFileUploadedNotice.newBuilder().apply {
this.fileId = fileId this.fileId = fileId
this.fileName = fileName this.fileName = fileName
this.operator = userId this.operatorUid = senderUid
this.operatorUin = senderUin
this.fileSize = fileSize this.fileSize = fileSize
this.expireTime = expireTime.toInt() this.expireTime = expireTime.toInt()
this.fileSubId = fileSubId this.fileSubId = fileSubId
this.url = url this.url = url
} }.build()
}) }.build())
return true return true
} }
@ -198,7 +172,8 @@ internal object GlobalEventTransmitter: QQInterfaces() {
*/ */
suspend fun transGroupFileEvent( suspend fun transGroupFileEvent(
msgTime: Long, msgTime: Long,
userId: Long, senderUid: String,
senderUin: Long,
groupId: Long, groupId: Long,
uuid: String, uuid: String,
fileName: String, fileName: String,
@ -206,19 +181,20 @@ internal object GlobalEventTransmitter: QQInterfaces() {
bizId: Int, bizId: Int,
url: String url: String
): Boolean { ): Boolean {
pushNotice(noticeEvent { pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeType.GROUP_FILE_COME this.type = NoticeEvent.NoticeType.GROUP_FILE_UPLOADED
this.time = msgTime.toInt() this.time = msgTime.toInt()
this.groupFileCome = groupFileComeNotice { this.groupFileUploaded = GroupFileUploadedNotice.newBuilder().apply {
this.groupId = groupId this.groupId = groupId
this.operator = userId this.operatorUid = senderUid
this.operatorUin = senderUin
this.fileId = uuid this.fileId = uuid
this.fileName = fileName this.fileName = fileName
this.fileSize = fileSize this.fileSize = fileSize
this.biz = bizId this.busId = bizId
this.url = url this.fileUrl = url
} }.build()
}) }.build())
return true return true
} }
} }
@ -227,33 +203,50 @@ internal object GlobalEventTransmitter: QQInterfaces() {
* 群聊通知 通知器 * 群聊通知 通知器
*/ */
object GroupNoticeTransmitter { object GroupNoticeTransmitter {
suspend fun transGroupSign(time: Long, target: Long, action: String?, rankImg: String?, groupCode: Long): Boolean { suspend fun transGroupSign(
pushNotice(noticeEvent { time: Long,
this.type = NoticeType.GROUP_SIGN 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.time = time.toInt()
this.groupSign = groupSignNotice { this.groupSignIn = GroupSignInNotice.newBuilder().apply {
this.groupId = groupCode this.groupId = groupCode
this.targetUid = ContactHelper.getUidByUinAsync(target)
this.targetUin = target this.targetUin = target
this.action = action ?: "" this.action = action
this.suffix = "" this.rankImage = rankImg
this.rankImage = rankImg ?: "" }.build()
} }.build())
})
return true return true
} }
suspend fun transGroupPoke(time: Long, operator: Long, target: Long, action: String?, suffix: String?, actionImg: String?, groupCode: Long): Boolean { suspend fun transGroupPoke(
pushNotice(noticeEvent { time: Long,
this.type = NoticeType.GROUP_POKE 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.time = time.toInt()
this.groupPoke = groupPokeNotice { this.groupPoke = GroupPokeNotice.newBuilder().apply {
this.action = action ?: "" this.groupId = groupCode
this.target = target this.action = action
this.operator = operator this.targetUid = ContactHelper.getUidByUinAsync(target)
this.suffix = suffix ?: "" this.targetUin = target
this.actionImage = actionImg ?: "" this.operatorUid = ContactHelper.getUidByUinAsync(operator)
} this.operatorUin = operator
}) this.suffix = suffix
this.actionImage = actionImg
}.build()
}.build())
return true return true
} }
@ -264,20 +257,20 @@ internal object GlobalEventTransmitter: QQInterfaces() {
groupCode: Long, groupCode: Long,
operator: Long, operator: Long,
operatorUid: String, operatorUid: String,
type: GroupMemberIncreasedType type: GroupMemberIncreasedNotice.GroupMemberIncreasedType
): Boolean { ): Boolean {
pushNotice(noticeEvent { pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeType.GROUP_MEMBER_INCREASE this.type = NoticeEvent.NoticeType.GROUP_MEMBER_INCREASE
this.time = time.toInt() this.time = time.toInt()
this.groupMemberIncrease = groupMemberIncreasedNotice { this.groupMemberIncrease = GroupMemberIncreasedNotice.newBuilder().apply {
this.groupId = groupCode this.groupId = groupCode
this.operatorUid = operatorUid this.operatorUid = operatorUid
this.operatorUin = operator this.operatorUin = operator
this.targetUid = targetUid this.targetUid = targetUid
this.targetUin = target this.targetUin = target
this.type = type this.type = type
} }.build()
}) }.build())
return true return true
} }
@ -288,20 +281,20 @@ internal object GlobalEventTransmitter: QQInterfaces() {
groupCode: Long, groupCode: Long,
operator: Long, operator: Long,
operatorUid: String, operatorUid: String,
type: GroupMemberDecreasedType type: GroupMemberDecreasedNotice.GroupMemberDecreasedType
): Boolean { ): Boolean {
pushNotice(noticeEvent { pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeType.GROUP_MEMBER_INCREASE this.type = NoticeEvent.NoticeType.GROUP_MEMBER_DECREASE
this.time = time.toInt() this.time = time.toInt()
this.groupMemberDecrease = groupMemberDecreasedNotice { this.groupMemberDecrease = GroupMemberDecreasedNotice.newBuilder().apply {
this.groupId = groupCode this.groupId = groupCode
this.operatorUid = operatorUid this.operatorUid = operatorUid
this.operatorUin = operator this.operatorUin = operator
this.targetUid = targetUid this.targetUid = targetUid
this.targetUin = target this.targetUin = target
this.type = type this.type = type
} }.build()
}) }.build())
return true return true
} }
@ -312,34 +305,36 @@ internal object GlobalEventTransmitter: QQInterfaces() {
groupCode: Long, groupCode: Long,
setAdmin: Boolean setAdmin: Boolean
): Boolean { ): Boolean {
pushNotice(noticeEvent { pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeType.GROUP_ADMIN_CHANGED this.type = NoticeEvent.NoticeType.GROUP_ADMIN_CHANGED
this.time = msgTime.toInt() this.time = msgTime.toInt()
this.groupAdminChanged = groupAdminChangedNotice { this.groupAdminChanged = GroupAdminChangedNotice.newBuilder().apply {
this.groupId = groupCode this.groupId = groupCode
this.targetUid = targetUid this.targetUid = targetUid
this.targetUin = target this.targetUin = target
this.isAdmin = setAdmin this.isAdmin = setAdmin
} }.build()
}) }.build())
return true return true
} }
suspend fun transGroupWholeBan( suspend fun transGroupWholeBan(
msgTime: Long, msgTime: Long,
operator: Long,
groupCode: Long, groupCode: Long,
operatorUid: String,
operator: Long,
isOpen: Boolean isOpen: Boolean
): Boolean { ): Boolean {
pushNotice(noticeEvent { pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeType.GROUP_WHOLE_BAN this.type = NoticeEvent.NoticeType.GROUP_WHOLE_BAN
this.time = msgTime.toInt() this.time = msgTime.toInt()
this.groupWholeBan = groupWholeBanNotice { this.groupWholeBan = GroupWholeBanNotice.newBuilder().apply {
this.groupId = groupCode this.groupId = groupCode
this.isWholeBan = isOpen this.isBan = isOpen
this.operator = operator this.operatorUid = operatorUid
} this.operatorUin = operator
}) }.build()
}.build())
return true return true
} }
@ -352,20 +347,20 @@ internal object GlobalEventTransmitter: QQInterfaces() {
groupCode: Long, groupCode: Long,
duration: Int duration: Int
): Boolean { ): Boolean {
pushNotice(noticeEvent { pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeType.GROUP_MEMBER_BANNED this.type = NoticeEvent.NoticeType.GROUP_MEMBER_BAN
this.time = msgTime.toInt() this.time = msgTime.toInt()
this.groupMemberBanned = groupMemberBannedNotice { this.groupMemberBan = GroupMemberBanNotice.newBuilder().apply {
this.groupId = groupCode this.groupId = groupCode
this.operatorUid = operatorUid this.operatorUid = operatorUid
this.operatorUin = operator this.operatorUin = operator
this.targetUid = targetUid this.targetUid = targetUid
this.targetUin = target this.targetUin = target
this.duration = duration this.duration = duration
this.type = if (duration > 0) GroupMemberBanType.BAN this.type = if (duration > 0) GroupMemberBanNotice.GroupMemberBanType.BAN
else GroupMemberBanType.LIFT_BAN else GroupMemberBanNotice.GroupMemberBanType.LIFT_BAN
} }.build()
}) }.build())
return true return true
} }
@ -379,48 +374,56 @@ internal object GlobalEventTransmitter: QQInterfaces() {
msgId: Long, msgId: Long,
tipText: String tipText: String
): Boolean { ): Boolean {
pushNotice(noticeEvent { pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeType.GROUP_RECALL this.type = NoticeEvent.NoticeType.GROUP_RECALL
this.time = time.toInt() this.time = time.toInt()
this.groupRecall = groupRecallNotice { this.groupRecall = GroupRecallNotice.newBuilder().apply {
this.groupId = groupCode this.groupId = groupCode
this.operatorUid = operatorUid this.operatorUid = operatorUid
this.operatorUin = operator this.operatorUin = operator
this.targetUid = targetUid this.targetUid = targetUid
this.targetUin = target this.targetUin = target
this.messageId = msgId this.messageId = msgId.toString()
this.tipText = tipText this.tipText = tipText
} }.build()
}) }.build())
return true return true
} }
suspend fun transCardChange( suspend fun transCardChange(
time: Long, time: Long,
targetId: Long, targetId: Long,
oldCard: String,
newCard: String, newCard: String,
groupId: Long groupId: Long
): Boolean { ): 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 return true
} }
suspend fun transTitleChange( suspend fun transTitleChange(
time: Long, time: Long,
targetId: Long, targetUin: Long,
title: String, title: String,
groupId: Long groupId: Long
): Boolean { ): Boolean {
pushNotice(noticeEvent { pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeType.GROUP_MEMBER_UNIQUE_TITLE_CHANGED this.type = NoticeEvent.NoticeType.GROUP_MEMBER_UNIQUE_TITLE_CHANGED
this.time = time.toInt() this.time = time.toInt()
this.groupMemberUniqueTitleChanged = groupUniqueTitleChangedNotice { this.groupMemberUniqueTitleChanged = GroupUniqueTitleChangedNotice.newBuilder().apply {
this.groupId = groupId this.groupId = groupId
this.target = targetId this.targetUid = ContactHelper.getUidByUinAsync(targetUin)
this.targetUin = targetUin
this.title = title this.title = title
} }.build()
}) }.build())
return true return true
} }
@ -432,17 +435,19 @@ internal object GlobalEventTransmitter: QQInterfaces() {
groupId: Long, groupId: Long,
subType: UInt subType: UInt
): Boolean { ): Boolean {
pushNotice(noticeEvent { pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeType.GROUP_ESSENCE_CHANGED this.type = NoticeEvent.NoticeType.GROUP_ESSENCE_CHANGED
this.time = time.toInt() this.time = time.toInt()
this.groupEssenceChanged = essenceMessageNotice { this.groupEssenceChanged = GroupEssenceMessageNotice.newBuilder().apply {
this.groupId = groupId this.groupId = groupId
this.messageId = msgId this.messageId = msgId.toString()
this.sender = senderUin this.targetUid = ContactHelper.getUidByUinAsync(targetUin)
this.operator = operatorUin this.targetUin = senderUin
this.subType = subType.toInt() this.operatorUid = ContactHelper.getUidByUinAsync(operatorUin)
} this.operatorUin = operatorUin
}) this.isSet = subType.toInt() == 1
}.build()
}.build())
return true return true
} }
} }
@ -451,31 +456,37 @@ internal object GlobalEventTransmitter: QQInterfaces() {
* 私聊通知 通知器 * 私聊通知 通知器
*/ */
object PrivateNoticeTransmitter { object PrivateNoticeTransmitter {
suspend fun transPrivatePoke(msgTime: Long, operator: Long, target: Long, action: String?, suffix: String?, actionImg: String?): Boolean { suspend fun transPrivatePoke(
pushNotice(noticeEvent { msgTime: Long,
this.type = NoticeType.FRIEND_POKE operator: Long,
action: String?,
suffix: String?,
actionImg: String?
): Boolean {
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.PRIVATE_POKE
this.time = msgTime.toInt() this.time = msgTime.toInt()
this.friendPoke = friendPokeNotice { this.privatePoke = PrivatePokeNotice.newBuilder().apply {
this.action = action ?: "" this.action = action ?: ""
this.target = target this.operatorUid = ContactHelper.getUidByUinAsync(operator)
this.operator = operator this.operatorUin = operator
this.suffix = suffix ?: "" this.suffix = suffix ?: ""
this.actionImage = actionImg ?: "" this.actionImage = actionImg ?: ""
} }.build()
}) }.build())
return true return true
} }
suspend fun transPrivateRecall(time: Long, operator: Long, msgId: Long, tipText: String): Boolean { suspend fun transPrivateRecall(time: Long, operator: Long, msgId: Long, tipText: String): Boolean {
pushNotice(noticeEvent { pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeType.FRIEND_RECALL this.type = NoticeEvent.NoticeType.PRIVATE_RECALL
this.time = time.toInt() this.time = time.toInt()
this.friendRecall = friendRecallNotice { this.privateRecall = PrivateRecallNotice.newBuilder().apply {
this.operator = operator this.operatorUin = operator
this.messageId = msgId this.messageId = msgId.toString()
this.tipText = tipText this.tipText = tipText
} }.build()
}) }.build())
return true return true
} }
@ -485,46 +496,65 @@ internal object GlobalEventTransmitter: QQInterfaces() {
* 请求 通知器 * 请求 通知器
*/ */
object RequestTransmitter { object RequestTransmitter {
suspend fun transFriendApp(time: Long, operator: Long, tipText: String, flag: String): Boolean { suspend fun transFriendApp(time: Long, applierUid: String, operator: Long, tipText: String, flag: String): Boolean {
pushRequest(requestsEvent { pushRequest(RequestEvent.newBuilder().apply {
this.type = RequestType.FRIEND_APPLY this.type = RequestEvent.RequestType.FRIEND_APPLY
this.time = time.toInt() this.time = time.toInt()
this.friendApply = friendApplyRequest { this.requestId = flag
this.friendApply = FriendApplyRequest.newBuilder().apply {
this.applierUid = applierUid
this.applierUin = operator this.applierUin = operator
this.message = tipText this.message = tipText
this.flag = flag }.build()
} }.build())
})
return true return true
} }
suspend fun transGroupApply( suspend fun transGroupApply(
time: Long, time: Long,
applier: Long, applierUin: Long,
applierUid: String, applierUid: String,
reason: String, reason: String,
groupCode: Long, groupCode: Long,
flag: String, flag: String
type: GroupApplyType
): Boolean { ): Boolean {
pushRequest(requestsEvent { pushRequest(RequestEvent.newBuilder().apply {
this.type = RequestType.GROUP_APPLY this.type = RequestEvent.RequestType.GROUP_APPLY
this.time = time.toInt() this.time = time.toInt()
this.groupApply = groupApplyRequest { this.requestId = flag
this.groupApply = GroupApplyRequest.newBuilder().apply {
this.applierUid = applierUid this.applierUid = applierUid
this.applierUin = applier this.applierUin = applierUin
this.groupId = groupCode this.groupId = groupCode
this.reason = reason this.reason = reason
this.flag = flag }.build()
this.type = type }.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 return true
} }
} }
suspend inline fun onMessageEvent(collector: FlowCollector<Pair<MsgRecord, MessageEvent>>) { suspend inline fun onMessageEvent(collector: FlowCollector<Pair<MsgRecord, PushMessageBody>>) {
messageEventFlow.collect { MessageEventFlow.collect {
GlobalScope.launch { GlobalScope.launch {
collector.emit(it) 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 { requestEventFlow.collect {
GlobalScope.launch { GlobalScope.launch {
collector.emit(it) collector.emit(it)

View File

@ -39,7 +39,7 @@ class AntiDetection: IAction {
if (ShamrockConfig[AntiJvmTrace]) if (ShamrockConfig[AntiJvmTrace])
antiTrace() antiTrace()
antiMemoryWalking() antiMemoryWalking()
antiO3Report() //antiO3Report()
} }
private fun antiO3Report() { private fun antiO3Report() {

View File

@ -6,8 +6,11 @@ import android.content.Context
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kritor.client.KritorClient
import kritor.server.KritorServer import kritor.server.KritorServer
import moe.fuqiuluo.shamrock.config.ActiveRPC import moe.fuqiuluo.shamrock.config.ActiveRPC
import moe.fuqiuluo.shamrock.config.PassiveRPC
import moe.fuqiuluo.shamrock.config.RPCAddress
import moe.fuqiuluo.shamrock.config.RPCPort import moe.fuqiuluo.shamrock.config.RPCPort
import moe.fuqiuluo.shamrock.config.ShamrockConfig import moe.fuqiuluo.shamrock.config.ShamrockConfig
import moe.fuqiuluo.shamrock.config.get import moe.fuqiuluo.shamrock.config.get
@ -17,6 +20,7 @@ import moe.fuqiuluo.symbols.Process
import moe.fuqiuluo.symbols.XposedHook import moe.fuqiuluo.symbols.XposedHook
private lateinit var server: KritorServer private lateinit var server: KritorServer
private lateinit var client: KritorClient
@XposedHook(Process.MAIN, priority = 10) @XposedHook(Process.MAIN, priority = 10)
internal class InitRemoteService : IAction { internal class InitRemoteService : IAction {
@ -32,6 +36,21 @@ internal class InitRemoteService : IAction {
LogCenter.log("ActiveRPC is disabled, KritorServer will not be started.") LogCenter.log("ActiveRPC is disabled, KritorServer will not be started.")
} }
if (PassiveRPC.get()) {
if (!::client.isInitialized) {
val hostAndPort = RPCAddress.get().split(":").let {
it.first() to it.last().toInt()
}
LogCenter.log("Connect RPC to ${hostAndPort.first}:${hostAndPort.second}")
client = KritorClient(hostAndPort.first, hostAndPort.second)
client.start()
client.listen()
}
} else {
LogCenter.log("PassiveRPC is disabled, KritorServer will not be started.")
}
}.onFailure { }.onFailure {
LogCenter.log("Start RPC failed: ${it.message}", Level.ERROR) LogCenter.log("Start RPC failed: ${it.message}", Level.ERROR)

View File

@ -9,11 +9,13 @@ import qq.service.QQInterfaces
object SwitchStatus: IInteract, QQInterfaces() { object SwitchStatus: IInteract, QQInterfaces() {
override fun invoke(intent: Intent) { override fun invoke(intent: Intent) {
AppTalker.talk("switch_status") { if (app.isLogin) {
put("account", app.currentAccountUin) AppTalker.talk("switch_status") {
put("nickname", if (app is QQAppInterface) app.currentNickname else "unknown") put("account", app.currentAccountUin)
put("voice", NativeLoader.isVoiceLoaded) put("nickname", if (app is QQAppInterface) (app.currentNickname ?: "unknown") else "unknown")
put("core_version", ShamrockVersion) put("voice", NativeLoader.isVoiceLoaded)
put("core_version", ShamrockVersion)
}
} }
} }
} }

View File

@ -83,6 +83,17 @@ abstract class QQInterfaces {
app.sendToService(to) 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 @DelicateCoroutinesApi
suspend fun sendBufferAW( suspend fun sendBufferAW(
cmd: String, cmd: String,

View File

@ -65,7 +65,7 @@ import kotlin.time.Duration.Companion.seconds
internal object NtV2RichMediaSvc: QQInterfaces() { internal object NtV2RichMediaSvc: QQInterfaces() {
private val requestIdSeq = atomic(1L) private val requestIdSeq = atomic(1L)
private fun fetchGroupResUploadTo(): String { fun fetchGroupResUploadTo(): String {
return ShamrockConfig[ResourceGroup].ifNullOrEmpty { "100000000" }!! return ShamrockConfig[ResourceGroup].ifNullOrEmpty { "100000000" }!!
} }
@ -370,13 +370,14 @@ internal object NtV2RichMediaSvc: QQInterfaces() {
width: UInt, width: UInt,
height: UInt, height: UInt,
retryCnt: Int, retryCnt: Int,
chatType: Int,
sceneBuilder: suspend SceneInfo.() -> Unit sceneBuilder: suspend SceneInfo.() -> Unit
): Result<UploadRsp> { ): Result<UploadRsp> {
return runCatching { return runCatching {
requestUploadNtPic(file, md5, sha, name, width, height, sceneBuilder).getOrThrow() requestUploadNtPic(file, md5, sha, name, width, height, chatType, sceneBuilder).getOrThrow()
}.onFailure { }.onFailure {
if (retryCnt > 0) { 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, name: String,
width: UInt, width: UInt,
height: UInt, height: UInt,
chatType: Int,
sceneBuilder: suspend SceneInfo.() -> Unit sceneBuilder: suspend SceneInfo.() -> Unit
): Result<UploadRsp> { ): Result<UploadRsp> {
val req = NtV2RichMediaReq( val req = NtV2RichMediaReq(
@ -427,12 +429,20 @@ internal object NtV2RichMediaSvc: QQInterfaces() {
tryFastUploadCompleted = true, tryFastUploadCompleted = true,
srvSendMsg = false, srvSendMsg = false,
clientRandomId = Random.nextULong(), clientRandomId = Random.nextULong(),
compatQMsgSceneType = 1u, compatQMsgSceneType = 2u,
clientSeq = Random.nextUInt(), clientSeq = Random.nextUInt(),
noNeedCompatMsg = false noNeedCompatMsg = true
) )
).toByteArray() ).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) { if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
return Result.failure(Exception("unable to request upload nt pic")) return Result.failure(Exception("unable to request upload nt pic"))
} }

View File

@ -0,0 +1,58 @@
package qq.service.bdh
import com.tencent.mobileqq.data.MessageRecord
import java.io.File
internal enum class ContactType {
TROOP,
PRIVATE,
}
internal interface TransTarget {
val id: String
val type: ContactType
val mRec: MessageRecord?
}
internal class Troop(
override val id: String,
override val mRec: MessageRecord? = null
): TransTarget {
override val type: ContactType = ContactType.TROOP
}
internal class Private(
override val id: String,
override val mRec: MessageRecord? = null
): TransTarget {
override val type: ContactType = ContactType.PRIVATE
}
internal enum class ResourceType {
Picture,
Video,
Voice
}
internal interface Resource {
val type: ResourceType
}
internal data class PictureResource(
val src: File
): Resource {
override val type = ResourceType.Picture
}
internal data class VideoResource(
val src: File, val thumb: File
): Resource {
override val type = ResourceType.Video
}
internal data class VoiceResource(
val src: File
): Resource {
override val type = ResourceType.Voice
}

View File

@ -0,0 +1,135 @@
package qq.service.bdh
import com.tencent.mobileqq.data.MessageForShortVideo
import com.tencent.mobileqq.data.MessageRecord
import com.tencent.mobileqq.transfile.FileMsg
import com.tencent.mobileqq.transfile.TransferRequest
import moe.fuqiuluo.shamrock.utils.MD5
import qq.service.bdh.ResourceType.*
import java.io.File
internal object Transfer: FileTransfer() {
private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>(
ContactType.TROOP to mapOf(
Picture to { uploadGroupPic(id, (it as PictureResource).src, mRec) },
Voice to { uploadGroupVoice(id, (it as VoiceResource).src) },
Video to { uploadGroupVideo(id, (it as VideoResource).src, it.thumb) },
),
ContactType.PRIVATE to mapOf(
Picture to { uploadC2CPic(id, (it as PictureResource).src, mRec) },
Voice to { uploadC2CVoice(id, (it as VoiceResource).src) },
Video to { uploadC2CVideo(id, (it as VideoResource).src, it.thumb) },
)
)
suspend fun uploadC2CVideo(
userId: String,
file: File,
thumb: File,
wait: Boolean = true
): Boolean {
return transC2CResource(userId, file, FileMsg.TRANSFILE_TYPE_SHORT_VIDEO_C2C, BUSI_TYPE_SHORT_VIDEO, wait) {
it.mSourceVideoCodecFormat = VIDEO_FORMAT_MP4
it.mRec = MessageForShortVideo().also {
it.busiType = BUSI_TYPE_SHORT_VIDEO
}
it.mThumbPath = thumb.absolutePath
it.mThumbMd5 = MD5.genFileMd5Hex(thumb.absolutePath)
}
}
suspend fun uploadGroupVideo(
groupId: String,
file: File,
thumb: File,
wait: Boolean = true
): Boolean {
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_SHORT_VIDEO_TROOP, BUSI_TYPE_SHORT_VIDEO, wait) {
it.mSourceVideoCodecFormat = VIDEO_FORMAT_MP4
it.mRec = MessageForShortVideo().also {
it.busiType = BUSI_TYPE_SHORT_VIDEO
}
it.mThumbPath = thumb.absolutePath
it.mThumbMd5 = MD5.genFileMd5Hex(thumb.absolutePath)
}
}
suspend fun uploadC2CVoice(
userId: String,
file: File,
wait: Boolean = true
): Boolean {
return transC2CResource(userId, file, FileMsg.TRANSFILE_TYPE_PTT, 1002, wait) {
it.mPttUploadPanel = 3
it.mPttCompressFinish = true
it.mIsPttPreSend = true
}
}
suspend fun uploadGroupVoice(
groupId: String,
file: File,
wait: Boolean = true
): Boolean {
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PTT, 1002, wait) {
it.mPttUploadPanel = 3
it.mPttCompressFinish = true
it.mIsPttPreSend = true
}
}
suspend fun uploadC2CPic(
peerId: String,
file: File,
record: MessageRecord? = null,
wait: Boolean = true
): Boolean {
return transC2CResource(peerId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
picUpExtraInfo.mIsRaw = false
picUpExtraInfo.mUinType = FileMsg.UIN_BUDDY
it.mPicSendSource = 8
it.mExtraObj = picUpExtraInfo
it.mIsPresend = true
it.delayShowProgressTimeInMs = 2000
it.mRec = record
}
}
suspend fun uploadGroupPic(
groupId: String,
file: File,
record: MessageRecord? = null,
wait: Boolean = true
): Boolean {
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
picUpExtraInfo.mIsRaw = false
picUpExtraInfo.mUinType = FileMsg.UIN_TROOP
it.mPicSendSource = 8
it.delayShowProgressTimeInMs = 2000
it.mExtraObj = picUpExtraInfo
it.mRec = record
}
}
operator fun get(contactType: ContactType, resourceType: ResourceType): suspend TransTarget.(Resource) -> Boolean {
return (ROUTE[contactType] ?: error("unsupported contact type: $contactType"))[resourceType]
?: error("Unsupported resource type: $resourceType")
}
}
internal suspend infix fun TransferTaskBuilder.trans(res: Resource): Boolean {
return Transfer[contact.type, res.type](contact, res)
}
internal class TransferTaskBuilder {
lateinit var contact: TransTarget
}
internal infix fun Transfer.with(contact: TransTarget): TransferTaskBuilder {
return TransferTaskBuilder().also {
it.contact = contact
}
}

View File

@ -0,0 +1,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
}
}

View File

@ -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.IProfileProtocolConst.PARAM_TARGET_UIN
import com.tencent.mobileqq.profilecard.api.IProfileProtocolService import com.tencent.mobileqq.profilecard.api.IProfileProtocolService
import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver
import com.tencent.protofile.join_group_link.join_group_link
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import moe.fuqiuluo.shamrock.tools.slice
import qq.service.internals.NTServiceFetcher import qq.service.internals.NTServiceFetcher
import qq.service.QQInterfaces import qq.service.QQInterfaces
import tencent.im.oidb.cmd0x11b2.oidb_0x11b2
import tencent.im.oidb.oidb_sso
import kotlin.coroutines.resume import kotlin.coroutines.resume
internal object ContactHelper: QQInterfaces() { internal object ContactHelper: QQInterfaces() {
@ -177,4 +181,31 @@ internal object ContactHelper: QQInterfaces() {
} }
}[peerId]!! }[peerId]!!
} }
suspend fun getSharePrivateArkMsg(peerId: Long): String {
val reqBody = oidb_0x11b2.BusinessCardV3Req()
reqBody.uin.set(peerId)
reqBody.jump_url.set("mqqapi://card/show_pslcard?src_type=internal&source=sharecard&version=1&uin=$peerId")
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x11ca_0", 4790, 0, reqBody.toByteArray())
?: error("unable to fetch contact ark_json_text")
val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val rsp = oidb_0x11b2.BusinessCardV3Rsp()
rsp.mergeFrom(body.bytes_bodybuffer.get().toByteArray())
return rsp.signed_ark_msg.get()
}
suspend fun getShareTroopArkMsg(groupId: Long): String {
val reqBody = join_group_link.ReqBody()
reqBody.get_ark.set(true)
reqBody.type.set(1)
reqBody.group_code.set(groupId)
val fromServiceMsg = sendBufferAW("GroupSvc.JoinGroupLink", true, reqBody.toByteArray())
?: error("unable to fetch contact ark_json_text")
val body = join_group_link.RspBody()
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
return body.signed_ark.get().toStringUtf8()
}
} }

View File

@ -5,15 +5,7 @@ package qq.service.file
import com.tencent.mobileqq.pb.ByteStringMicro import com.tencent.mobileqq.pb.ByteStringMicro
import io.grpc.Status import io.grpc.Status
import io.grpc.StatusRuntimeException import io.grpc.StatusRuntimeException
import io.kritor.file.File import io.kritor.file.*
import io.kritor.file.Folder
import io.kritor.file.GetFileSystemInfoResponse
import io.kritor.file.GetFilesRequest
import io.kritor.file.GetFilesResponse
import io.kritor.file.folder
import io.kritor.file.getFileSystemInfoResponse
import io.kritor.file.getFilesRequest
import io.kritor.file.getFilesResponse
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
@ -73,15 +65,15 @@ internal object GroupFileHelper: QQInterfaces() {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response x2")) throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response x2"))
} }
return getFileSystemInfoResponse { return GetFileSystemInfoResponse.newBuilder().apply {
this.fileCount = fileCnt this.fileCount = fileCnt
this.totalCount = limitCnt this.totalCount = limitCnt
this.totalSpace = totalSpace.toInt() this.totalSpace = totalSpace.toInt()
this.usedSpace = usedSpace.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 fileSystemInfo = getGroupFileSystemInfo(groupId)
val fromServiceMsg = sendOidbAW("OidbSvc.0x6d8_1", 1752, 1, oidb_0x6d8.ReqBody().also { val fromServiceMsg = sendOidbAW("OidbSvc.0x6d8_1", 1752, 1, oidb_0x6d8.ReqBody().also {
it.file_list_info_req.set(oidb_0x6d8.GetFileListReqBody().apply { 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")) throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
} }
val files = arrayListOf<File>() val files = arrayListOf<File>()
val dirs = arrayListOf<Folder>() val folders = arrayListOf<Folder>()
if (fromServiceMsg.wupBuffer != null) { if (fromServiceMsg.wupBuffer != null) {
val oidb = oidb_sso.OIDBSSOPkg().mergeFrom(fromServiceMsg.wupBuffer.slice(4).let { val oidb = oidb_sso.OIDBSSOPkg().mergeFrom(fromServiceMsg.wupBuffer.slice(4).let {
if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it
@ -119,13 +111,13 @@ internal object GroupFileHelper: QQInterfaces() {
rpt_item_list.get().forEach { file -> rpt_item_list.get().forEach { file ->
if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FILE) { if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FILE) {
val fileInfo = file.file_info val fileInfo = file.file_info
files.add(io.kritor.file.file { files.add(File.newBuilder().apply {
this.fileId = fileInfo.str_file_id.get() this.fileId = fileInfo.str_file_id.get()
this.fileName = fileInfo.str_file_name.get() this.fileName = fileInfo.str_file_name.get()
this.fileSize = fileInfo.uint64_file_size.get() this.fileSize = fileInfo.uint64_file_size.get()
this.busId = fileInfo.uint32_bus_id.get() this.busId = fileInfo.uint32_bus_id.get()
this.uploadTime = fileInfo.uint32_upload_time.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.modifyTime = fileInfo.uint32_modify_time.get()
this.downloadTimes = fileInfo.uint32_download_times.get() this.downloadTimes = fileInfo.uint32_download_times.get()
this.uploader = fileInfo.uint64_uploader_uin.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.sha = fileInfo.bytes_sha.get().toByteArray().toHexString()
this.sha3 = fileInfo.bytes_sha3.get().toByteArray().toHexString() this.sha3 = fileInfo.bytes_sha3.get().toByteArray().toHexString()
this.md5 = fileInfo.bytes_md5.get().toByteArray().toHexString() this.md5 = fileInfo.bytes_md5.get().toByteArray().toHexString()
}) }.build())
} }
else if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FOLDER) { else if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FOLDER) {
val folderInfo = file.folder_info val folderInfo = file.folder_info
dirs.add(folder { folders.add(Folder.newBuilder().apply {
this.folderId = folderInfo.str_folder_id.get() this.folderId = folderInfo.str_folder_id.get()
this.folderName = folderInfo.str_folder_name.get() this.folderName = folderInfo.str_folder_name.get()
this.totalFileCount = folderInfo.uint32_total_file_count.get() this.totalFileCount = folderInfo.uint32_total_file_count.get()
this.createTime = folderInfo.uint32_create_time.get() this.createTime = folderInfo.uint32_create_time.get()
this.creator = folderInfo.uint64_create_uin.get() this.creator = folderInfo.uint64_create_uin.get()
this.creatorName = folderInfo.str_creator_name.get() this.creatorName = folderInfo.str_creator_name.get()
}) }.build())
} else { } else {
LogCenter.log("未知文件类型: ${file.uint32_type.get()}", Level.WARN) 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")) throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response"))
} }
return getFilesResponse { return GetFileListResponse.newBuilder().apply {
this.files.addAll(files) this.addAllFiles(files)
this.folders.addAll(folders) this.addAllFolders(folders)
} }.build()
} }
} }

View File

@ -42,6 +42,7 @@ import java.lang.reflect.Method
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.time.Duration.Companion.seconds
internal object GroupHelper: QQInterfaces() { internal object GroupHelper: QQInterfaces() {
private val RefreshTroopMemberInfoLock by lazy { Mutex() } private val RefreshTroopMemberInfoLock by lazy { Mutex() }
@ -99,6 +100,61 @@ internal object GroupHelper: QQInterfaces() {
return Result.success(troopList) 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( private suspend fun requestGroupInfo(
service: ITroopInfoService service: ITroopInfoService
): Boolean { ): 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 localMemberInfo = getTroopMemberInfoByUin(groupId, userId, true).getOrThrow()
val req = Oidb_0x8fc.ReqBody() val req = Oidb_0x8fc.ReqBody()
req.uint64_group_code.set(groupId) req.uint64_group_code.set(groupId.toLong())
val memberInfo = Oidb_0x8fc.MemberInfo() 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 { memberInfo.bytes_uin_name.set(ByteStringMicro.copyFromUtf8(localMemberInfo.troopnick.ifEmpty {
localMemberInfo.troopremark.ifNullOrEmpty { "" } localMemberInfo.troopremark.ifNullOrEmpty { "" }
})) }))
@ -468,13 +524,13 @@ internal object GroupHelper: QQInterfaces() {
} }
suspend fun getTroopMemberInfoByUin( suspend fun getTroopMemberInfoByUin(
groupId: Long, groupId: String,
uin: Long, uin: String,
refresh: Boolean = false refresh: Boolean = false
): Result<TroopMemberInfo> { ): Result<TroopMemberInfo> {
val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all") val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all")
var info = service.getTroopMember(groupId.toString(), uin.toString()) var info = service.getTroopMember(groupId, uin)
if (refresh || !service.isMemberInCache(groupId.toString(), uin.toString()) || info == null || info.troopnick == null) { if (refresh || !service.isMemberInCache(groupId, uin) || info == null || info.troopnick == null) {
info = requestTroopMemberInfo(service, groupId, uin).getOrNull() info = requestTroopMemberInfo(service, groupId, uin).getOrNull()
} }
if (info == null) { if (info == null) {
@ -488,8 +544,8 @@ internal object GroupHelper: QQInterfaces() {
try { try {
if (info != null && (info.alias == null || info.alias.isBlank())) { if (info != null && (info.alias == null || info.alias.isBlank())) {
val req = group_member_info.ReqBody() val req = group_member_info.ReqBody()
req.uint64_group_code.set(groupId) req.uint64_group_code.set(groupId.toLong())
req.uint64_uin.set(uin) req.uint64_uin.set(uin.toLong())
req.bool_new_client.set(true) req.bool_new_client.set(true)
req.uint32_client_type.set(1) req.uint32_client_type.set(1)
req.uint32_rich_card_name_ver.set(1) req.uint32_rich_card_name_ver.set(1)
@ -523,8 +579,8 @@ internal object GroupHelper: QQInterfaces() {
} }
suspend fun getTroopMemberInfoByUinViaNt( suspend fun getTroopMemberInfoByUinViaNt(
groupId: Long, groupId: String,
qq: Long, qq: String,
timeout: Long = 5000L timeout: Long = 5000L
): Result<MemberInfo> { ): Result<MemberInfo> {
return runCatching { return runCatching {
@ -533,13 +589,13 @@ internal object GroupHelper: QQInterfaces() {
val groupService = sessionService.groupService val groupService = sessionService.groupService
val info = withTimeoutOrNull(timeout) { val info = withTimeoutOrNull(timeout) {
suspendCancellableCoroutine { suspendCancellableCoroutine {
groupService.getTransferableMemberInfo(groupId) { code, _, data -> groupService.getTransferableMemberInfo(groupId.toLong()) { code, _, data ->
if (code != 0) { if (code != 0) {
it.resume(null) it.resume(null)
return@getTransferableMemberInfo return@getTransferableMemberInfo
} }
data.forEach { (_, info) -> data.forEach { (_, info) ->
if (info.uin == qq) { if (info.uin == qq.toLong()) {
it.resume(info) it.resume(info)
return@forEach 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 info = RefreshTroopMemberInfoLock.withLock {
val groupIdStr = groupId.toString() service.deleteTroopMember(groupId, memberUin)
val memberUinStr = memberUin.toString()
service.deleteTroopMember(groupIdStr, memberUinStr)
requestMemberInfoV2(groupId, memberUin) requestMemberInfoV2(groupId, memberUin)
requestMemberInfo(groupId, memberUin) requestMemberInfo(groupId, memberUin)
withTimeoutOrNull(timeout) { withTimeoutOrNull(timeout) {
while (!service.isMemberInCache(groupIdStr, memberUinStr)) { while (!service.isMemberInCache(groupId, memberUin)) {
delay(200) delay(200)
} }
return@withTimeoutOrNull service.getTroopMember(groupIdStr, memberUinStr) return@withTimeoutOrNull service.getTroopMember(groupId, memberUin)
} }
} }
return if (info != null) { 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) val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER)
if (!::METHOD_REQ_MEMBER_INFO.isInitialized) { 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) val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER)
if (!::METHOD_REQ_MEMBER_INFO_V2.isInitialized) { 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>> { private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: String): Result<List<TroopMemberInfo>> {

View File

@ -14,7 +14,7 @@ import qq.service.bdh.RichProtoSvc
import qq.service.kernel.SimpleKernelMsgListener import qq.service.kernel.SimpleKernelMsgListener
import qq.service.msg.MessageHelper import qq.service.msg.MessageHelper
object AioListener: SimpleKernelMsgListener() { object AioListener : SimpleKernelMsgListener() {
override fun onRecvMsg(records: ArrayList<MsgRecord>) { override fun onRecvMsg(records: ArrayList<MsgRecord>) {
records.forEach { records.forEach {
GlobalScope.launch { GlobalScope.launch {
@ -60,7 +60,12 @@ object AioListener: SimpleKernelMsgListener() {
LogCenter.log("私聊临时消息(private = ${record.senderUin}, groupId=$groupCode)") 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) LogCenter.log("私聊临时消息推送失败 -> MessageTransmitter", Level.WARN)
} }
@ -92,7 +97,6 @@ object AioListener: SimpleKernelMsgListener() {
} }
private suspend fun onC2CFileMsg(record: MsgRecord) { private suspend fun onC2CFileMsg(record: MsgRecord) {
val userId = record.senderUin
val fileMsg = record.elements.firstOrNull { val fileMsg = record.elements.firstOrNull {
it.elementType == MsgConstant.KELEMTYPEFILE it.elementType == MsgConstant.KELEMTYPEFILE
}?.fileElement ?: kotlin.run { }?.fileElement ?: kotlin.run {
@ -108,7 +112,7 @@ object AioListener: SimpleKernelMsgListener() {
val url = RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId) val url = RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
if (!GlobalEventTransmitter.FileNoticeTransmitter 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) LogCenter.log("私聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN)
} }
@ -116,7 +120,6 @@ object AioListener: SimpleKernelMsgListener() {
private suspend fun onGroupFileMsg(record: MsgRecord) { private suspend fun onGroupFileMsg(record: MsgRecord) {
val groupId = record.peerUin val groupId = record.peerUin
val userId = record.senderUin
val fileMsg = record.elements.firstOrNull { val fileMsg = record.elements.firstOrNull {
it.elementType == MsgConstant.KELEMTYPEFILE it.elementType == MsgConstant.KELEMTYPEFILE
}?.fileElement ?: kotlin.run { }?.fileElement ?: kotlin.run {
@ -132,9 +135,15 @@ object AioListener: SimpleKernelMsgListener() {
val url = RichProtoSvc.getGroupFileDownUrl(record.peerUin, uuid, bizId) val url = RichProtoSvc.getGroupFileDownUrl(record.peerUin, uuid, bizId)
if (!GlobalEventTransmitter.FileNoticeTransmitter 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) LogCenter.log("群聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN)
} }
} }
@OptIn(ExperimentalStdlibApi::class)
override fun onRecvSysMsg(arrayList: ArrayList<Byte>?) {
LogCenter.log("onRecvSysMsg")
LogCenter.log(arrayList?.toByteArray()?.toHexString() ?: "")
}
} }

View File

@ -0,0 +1,14 @@
package qq.service.internals
import com.tencent.qqnt.kernel.nativeinterface.DevInfo
import com.tencent.qqnt.kernel.nativeinterface.KickedInfo
import qq.service.kernel.SimpleKernelMsgListener
import java.util.ArrayList
object LineDevListener: SimpleKernelMsgListener() {
override fun onKickedOffLine(kickedInfo: KickedInfo) {
}
override fun onLineDev(devs: ArrayList<DevInfo>) {
}
}

View File

@ -58,6 +58,7 @@ internal object NTServiceFetcher {
try { try {
LogCenter.log("Register MSG listener successfully.") LogCenter.log("Register MSG listener successfully.")
msgService.addMsgListener(AioListener) msgService.addMsgListener(AioListener)
msgService.addMsgListener(LineDevListener)
// 接口缺失 暂不使用 // 接口缺失 暂不使用
//groupService.addKernelGroupListener(GroupEventListener) //groupService.addKernelGroupListener(GroupEventListener)

View File

@ -5,9 +5,8 @@ import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qphone.base.remote.FromServiceMsg import com.tencent.qphone.base.remote.FromServiceMsg
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.msg.api.IMsgService import com.tencent.qqnt.msg.api.IMsgService
import io.kritor.event.GroupApplyType import io.kritor.event.GroupMemberDecreasedNotice.GroupMemberDecreasedType
import io.kritor.event.GroupMemberDecreasedType import io.kritor.event.GroupMemberIncreasedNotice.GroupMemberIncreasedType
import io.kritor.event.GroupMemberIncreasedType
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -127,7 +126,7 @@ internal object PrimitiveListener {
LogCenter.log("私聊戳一戳: $operation $action $target $suffix") LogCenter.log("私聊戳一戳: $operation $action $target $suffix")
if (!GlobalEventTransmitter.PrivateNoticeTransmitter if (!GlobalEventTransmitter.PrivateNoticeTransmitter
.transPrivatePoke(msgTime, operation.toLong(), target.toLong(), action, suffix, actionImg) .transPrivatePoke(msgTime, operation.toLong(), action, suffix, actionImg)
) { ) {
LogCenter.log("私聊戳一戳推送失败!", Level.WARN) LogCenter.log("私聊戳一戳推送失败!", Level.WARN)
} }
@ -162,7 +161,7 @@ internal object PrimitiveListener {
} }
LogCenter.log("来自$applier 的好友申请:$msg ($source)") LogCenter.log("来自$applier 的好友申请:$msg ($source)")
if (!GlobalEventTransmitter.RequestTransmitter if (!GlobalEventTransmitter.RequestTransmitter
.transFriendApp(msgTime, applier, msg, flag) .transFriendApp(msgTime, applierUid, applier, msg, flag)
) { ) {
LogCenter.log("好友申请推送失败!", Level.WARN) LogCenter.log("好友申请推送失败!", Level.WARN)
} }
@ -321,8 +320,8 @@ internal object PrimitiveListener {
it.key to it.value it.key to it.value
} }
val target = params["uin_str2"] ?: params["mqq_uin"] ?: return val target = params["uin_str2"] ?: params["mqq_uin"] ?: ""
val operation = params["uin_str1"] ?: return val operator = params["uin_str1"] ?: ""
val suffix = params["suffix_str"] ?: "" val suffix = params["suffix_str"] ?: ""
val actionImg = params["action_img_url"] ?: "" val actionImg = params["action_img_url"] ?: ""
val action = params["alt_str1"] val action = params["alt_str1"]
@ -333,9 +332,9 @@ internal object PrimitiveListener {
when (detail.type) { when (detail.type) {
1061u -> { 1061u -> {
LogCenter.log("群戳一戳($groupId): $operation $action $target $suffix") LogCenter.log("群戳一戳($groupId): $operator $action $target $suffix")
if (!GlobalEventTransmitter.GroupNoticeTransmitter 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) LogCenter.log("群戳一戳推送失败!", Level.WARN)
} }
@ -507,7 +506,7 @@ internal object PrimitiveListener {
if (wholeBan) { if (wholeBan) {
LogCenter.log("群全员禁言($groupCode): $operator -> ${if (rawDuration != 0) "开启" else "关闭"}") LogCenter.log("群全员禁言($groupCode): $operator -> ${if (rawDuration != 0) "开启" else "关闭"}")
if (!GlobalEventTransmitter.GroupNoticeTransmitter if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupWholeBan(msgTime, groupCode, operator, rawDuration != 0) .transGroupWholeBan(msgTime, groupCode, operatorUid, operator, rawDuration != 0)
) { ) {
LogCenter.log("群禁言推送失败!", Level.WARN) LogCenter.log("群禁言推送失败!", Level.WARN)
} }
@ -595,7 +594,7 @@ internal object PrimitiveListener {
} }
LogCenter.log("入群申请($groupCode) $applier: \"$reason\", seq: $msgSeq") LogCenter.log("入群申请($groupCode) $applier: \"$reason\", seq: $msgSeq")
if (!GlobalEventTransmitter.RequestTransmitter 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) LogCenter.log("入群申请推送失败!", Level.WARN)
} }
@ -630,7 +629,7 @@ internal object PrimitiveListener {
} }
LogCenter.log("邀请入群申请($groupCode): $applier") LogCenter.log("邀请入群申请($groupCode): $applier")
if (!GlobalEventTransmitter.RequestTransmitter if (!GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, applier, applierUid, "", groupCode, flag, GroupApplyType.GROUP_APPLY_ADD) .transGroupApply(time, applier, applierUid, "", groupCode, flag)
) { ) {
LogCenter.log("邀请入群申请推送失败!", Level.WARN) LogCenter.log("邀请入群申请推送失败!", Level.WARN)
} }
@ -658,7 +657,7 @@ internal object PrimitiveListener {
"$time;$groupCode;$uin" "$time;$groupCode;$uin"
} }
if (!GlobalEventTransmitter.RequestTransmitter if (!GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, invitor, invitorUid, "", groupCode, flag, GroupApplyType.GROUP_APPLY_INVITE) .transGroupInvite(time, invitorUid, invitor, groupCode, flag)
) { ) {
LogCenter.log("邀请入群推送失败!", Level.WARN) LogCenter.log("邀请入群推送失败!", Level.WARN)
} }

View File

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

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

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

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

View 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&notice=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
}
}

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

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

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

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

View File

@ -1,20 +1,210 @@
package qq.service.msg 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.api.IKernelService
import com.tencent.qqnt.kernel.nativeinterface.Contact import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant 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.MsgRecord
import com.tencent.qqnt.kernel.nativeinterface.TempChatGameSession
import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo
import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo
import com.tencent.qqnt.msg.api.IMsgService
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.header
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.jsonObject
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
import moe.fuqiuluo.shamrock.tools.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.QQInterfaces
import qq.service.contact.ContactHelper import qq.service.contact.ContactHelper
import qq.service.internals.msgService import qq.service.internals.msgService
import qq.service.ticket.TicketHelper
import tencent.im.oidb.cmd0xeac.oidb_0xeac
import tencent.im.oidb.oidb_sso
import kotlin.coroutines.resume import kotlin.coroutines.resume
typealias MessageId = Long
internal object MessageHelper: QQInterfaces() { internal object MessageHelper: QQInterfaces() {
suspend fun getEssenceMessageList(groupId: Long, page: Int = 0, pageSize: Int = 20): Result<List<EssenceMessage>>{
val cookie = TicketHelper.getCookie("qun.qq.com")
val bkn = TicketHelper.getBkn(TicketHelper.getRealSkey(TicketHelper.getUin()))
val url = "https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=${bkn}&group_code=${groupId}&page_start=${page}&page_limit=${pageSize}"
val response = GlobalClient.get(url) {
header("Cookie", cookie)
}
val body = Json.decodeFromStream<JsonElement>(response.body())
if (body.jsonObject["retcode"].asInt == 0) {
val data = body.jsonObject["data"].asJsonObject
val list = data["msg_list"].asJsonArrayOrNull
?: // is_end
return Result.success(ArrayList())
return Result.success(list.map {
val obj = it.jsonObject
val msgSeq = obj["msg_seq"].asLong
EssenceMessage(
senderId = obj["sender_uin"].asString.toLong(),
senderNick = obj["sender_nick"].asString,
senderTime = obj["sender_time"].asLong,
operatorId = obj["add_digest_uin"].asString.toLong(),
operatorNick = obj["add_digest_nick"].asString,
operatorTime = obj["add_digest_time"].asLong,
messageSeq = msgSeq,
messageContent = obj["msg_content"] ?: EmptyJsonArray
)
})
} else {
return Result.failure(Exception(body.jsonObject["retmsg"].asStringOrNull))
}
}
fun setGroupMessageCommentFace(peer: Long, msgSeq: ULong, faceIndex: String, isSet: Boolean) {
val serviceId = if (isSet) 1 else 2
sendOidb("OidbSvcTrpcTcp.0x9082_$serviceId", 36994, serviceId, Oidb0x9082(
peer = peer.toULong(),
msgSeq = msgSeq,
faceIndex = faceIndex,
flag = 1u,
u1 = 0u,
u2 = 0u
).toByteArray())
}
suspend fun setEssenceMessage(groupId: Long, seq: Long, rand: Long): String? {
val fromServiceMsg = sendOidbAW("OidbSvc.0xeac_1", 3756, 1, oidb_0xeac.ReqBody().apply {
group_code.set(groupId)
msg_seq.set(seq.toInt())
msg_random.set(rand.toInt())
}.toByteArray())
if (fromServiceMsg?.wupBuffer == null) {
return "no response"
}
val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val result = oidb_0xeac.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
return if (result.wording.has()) {
LogCenter.log("设置群精华失败: ${result.wording.get()}", Level.WARN)
"设置群精华失败: ${result.wording.get()}"
} else {
LogCenter.log("设置群精华 -> $groupId: $seq")
null
}
}
suspend fun deleteEssenceMessage(groupId: Long, seq: Long, rand: Long): String? {
val fromServiceMsg = sendOidbAW("OidbSvc.0xeac_2", 3756, 2, oidb_0xeac.ReqBody().apply {
group_code.set(groupId)
msg_seq.set(seq.toInt())
msg_random.set(rand.toInt())
}.toByteArray())
if (fromServiceMsg?.wupBuffer == null) {
return "no response"
}
val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val result = oidb_0xeac.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
return if (result.wording.has()) {
LogCenter.log("移除群精华失败: ${result.wording.get()}", Level.WARN)
"移除群精华失败: ${result.wording.get()}"
} else {
LogCenter.log("移除群精华 -> $groupId: $seq")
null
}
}
private suspend fun prepareTempChatFromGroup(
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> { suspend fun getTempChatInfo(chatType: Int, uid: String): Result<TempChatInfo> {
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
?: return Result.failure(Exception("获取消息服务失败")) ?: 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 { fun generateMsgId(chatType: Int): Long {
return createMessageUniseq(chatType, System.currentTimeMillis()) return createMessageUniseq(chatType, System.currentTimeMillis())
} }

View File

@ -1,33 +1,13 @@
package qq.service.msg package qq.service.msg
import com.google.protobuf.ByteString
import com.tencent.mobileqq.qroute.QRoute import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import com.tencent.qqnt.msg.api.IMsgService import com.tencent.qqnt.msg.api.IMsgService
import io.kritor.event.AtElement import io.kritor.common.*
import io.kritor.event.Element import io.kritor.common.Element.ElementType
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 kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.shamrock.helper.ActionMsgException import moe.fuqiuluo.shamrock.helper.ActionMsgException
@ -47,23 +27,13 @@ import qq.service.bdh.RichProtoSvc
import qq.service.contact.ContactHelper import qq.service.contact.ContactHelper
import kotlin.coroutines.resume import kotlin.coroutines.resume
/**
* 将NT消息com.tencent.qqnt.*转换为事件消息io.kritor.event.*)推送
*/
typealias NtMessages = ArrayList<MsgElement> typealias NtMessages = ArrayList<MsgElement>
typealias Convertor = suspend (MsgRecord, MsgElement) -> Result<Element> 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 object MsgConvertor {
private val convertorMap = hashMapOf( private val convertorMap = hashMapOf(
MsgConstant.KELEMTYPETEXT to ::convertText, MsgConstant.KELEMTYPETEXT to ::convertText,
@ -87,12 +57,14 @@ private object MsgConvertor {
val text = element.textElement val text = element.textElement
val elem = Element.newBuilder() val elem = Element.newBuilder()
if (text.atType != MsgConstant.ATTYPEUNKNOWN) { if (text.atType != MsgConstant.ATTYPEUNKNOWN) {
elem.setAt(atElement { elem.type = ElementType.AT
elem.setAt(AtElement.newBuilder().apply {
this.uid = text.atNtUid this.uid = text.atNtUid
this.uin = ContactHelper.getUinByUidAsync(text.atNtUid).toLong() this.uin = ContactHelper.getUinByUidAsync(text.atNtUid).toLong()
}) })
} else { } else {
elem.setText(textElement { elem.type = ElementType.TEXT
elem.setText(TextElement.newBuilder().apply {
this.text = text.content this.text = text.content
}) })
} }
@ -103,31 +75,51 @@ private object MsgConvertor {
val face = element.faceElement val face = element.faceElement
val elem = Element.newBuilder() val elem = Element.newBuilder()
if (face.faceType == 5) { if (face.faceType == 5) {
elem.setPoke(pokeElement { elem.type = ElementType.POKE
elem.setPoke(PokeElement.newBuilder().apply {
this.id = face.vaspokeId this.id = face.vaspokeId
this.type = face.pokeType this.type = face.pokeType
this.strength = face.pokeStrength this.strength = face.pokeStrength
}) })
} else { } else {
when(face.faceIndex) { when (face.faceIndex) {
114 -> elem.setBasketball(basketballElement { 114 -> {
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0 elem.type = ElementType.BASKETBALL
}) elem.setBasketball(BasketballElement.newBuilder().apply {
358 -> elem.setDice(diceElement { this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0 })
}) }
359 -> elem.setRps(rpsElement {
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0 358 -> {
}) elem.type = ElementType.DICE
394 -> elem.setFace(faceElement { elem.setDice(DiceElement.newBuilder().apply {
this.id = face.faceIndex this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
this.isBig = face.faceType == 3 })
this.result = face.resultId.ifNullOrEmpty { "1" }?.toInt() ?: 1 }
})
else -> elem.setFace(faceElement { 359 -> {
this.id = face.faceIndex elem.type = ElementType.RPS
this.isBig = face.faceType == 3 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()) return Result.success(elem.build())
@ -162,9 +154,10 @@ private object MsgConvertor {
LogCenter.log({ "receive image: $image" }, Level.DEBUG) LogCenter.log({ "receive image: $image" }, Level.DEBUG)
val elem = Element.newBuilder() val elem = Element.newBuilder()
elem.setImage(imageElement { elem.type = ElementType.IMAGE
this.file = md5 elem.setImage(ImageElement.newBuilder().apply {
this.url = when (record.chatType) { this.file = ByteString.copyFromUtf8(md5)
this.fileUrl = when (record.chatType) {
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl( MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
originalUrl = originalUrl, originalUrl = originalUrl,
md5 = md5, md5 = md5,
@ -202,7 +195,8 @@ private object MsgConvertor {
else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}") 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 this.subType = image.picSubType
}) })
@ -217,14 +211,19 @@ private object MsgConvertor {
ptt.fileName.substring(5) ptt.fileName.substring(5)
else ptt.md5HexStr else ptt.md5HexStr
elem.setVoice(voiceElement { elem.type = ElementType.VOICE
this.url = when (record.chatType) { elem.setVoice(VoiceElement.newBuilder().apply {
this.fileUrl = when (record.chatType) {
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", ptt.fileUuid) 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}") else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}")
} }
this.file = md5 this.file = ByteString.copyFromUtf8(md5)
this.magic = ptt.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE this.magic = ptt.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE
}) })
@ -241,9 +240,10 @@ private object MsgConvertor {
it[it.size - 2].hex2ByteArray() it[it.size - 2].hex2ByteArray()
} }
} else video.fileName.split(".")[0].hex2ByteArray() } else video.fileName.split(".")[0].hex2ByteArray()
elem.setVideo(videoElement { elem.type = ElementType.VIDEO
this.file = md5.toHexString() elem.setVideo(VideoElement.newBuilder().apply {
this.url = when (record.chatType) { this.file = ByteString.copyFromUtf8(md5.toHexString())
this.fileUrl = when (record.chatType) {
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid) MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid) MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("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> { suspend fun convertMarketFace(record: MsgRecord, element: MsgElement): Result<Element> {
val marketFace = element.marketFaceElement val marketFace = element.marketFaceElement
val elem = Element.newBuilder() 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() this.id = marketFace.emojiId.lowercase()
}) })
return Result.success(elem.build()) return Result.success(elem.build())
@ -268,8 +269,9 @@ private object MsgConvertor {
when (data["app"].asString) { when (data["app"].asString) {
"com.tencent.multimsg" -> { "com.tencent.multimsg" -> {
val info = data["meta"].asJsonObject["detail"].asJsonObject val info = data["meta"].asJsonObject["detail"].asJsonObject
elem.setForward(forwardElement { elem.type = ElementType.FORWARD
this.id = info["resid"].asString elem.setForward(ForwardElement.newBuilder().apply {
this.resId = info["resid"].asString
this.uniseq = info["uniseq"].asString this.uniseq = info["uniseq"].asString
this.summary = info["summary"].asString this.summary = info["summary"].asString
this.description = info["news"].asJsonArray.joinToString("\n") { this.description = info["news"].asJsonArray.joinToString("\n") {
@ -280,7 +282,8 @@ private object MsgConvertor {
"com.tencent.troopsharecard" -> { "com.tencent.troopsharecard" -> {
val info = data["meta"].asJsonObject["contact"].asJsonObject val info = data["meta"].asJsonObject["contact"].asJsonObject
elem.setContact(contactElement { elem.type = ElementType.CONTACT
elem.setContact(ContactElement.newBuilder().apply {
this.scene = Scene.GROUP this.scene = Scene.GROUP
this.peer = info["jumpUrl"].asString.split("group_code=")[1] this.peer = info["jumpUrl"].asString.split("group_code=")[1]
}) })
@ -288,7 +291,8 @@ private object MsgConvertor {
"com.tencent.contact.lua" -> { "com.tencent.contact.lua" -> {
val info = data["meta"].asJsonObject["contact"].asJsonObject val info = data["meta"].asJsonObject["contact"].asJsonObject
elem.setContact(contactElement { elem.type = ElementType.CONTACT
elem.setContact(ContactElement.newBuilder().apply {
this.scene = Scene.FRIEND this.scene = Scene.FRIEND
this.peer = info["jumpUrl"].asString.split("uin=")[1] this.peer = info["jumpUrl"].asString.split("uin=")[1]
}) })
@ -296,7 +300,8 @@ private object MsgConvertor {
"com.tencent.map" -> { "com.tencent.map" -> {
val info = data["meta"].asJsonObject["Location.Search"].asJsonObject 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.lat = info["lat"].asString.toFloat()
this.lon = info["lng"].asString.toFloat() this.lon = info["lng"].asString.toFloat()
this.address = info["address"].asString this.address = info["address"].asString
@ -304,9 +309,12 @@ private object MsgConvertor {
}) })
} }
else -> elem.setJson(jsonElement { else -> {
this.json = data.toString() elem.type = ElementType.JSON
}) elem.setJson(JsonElement.newBuilder().apply {
this.json = data.toString()
})
}
} }
return Result.success(elem.build()) return Result.success(elem.build())
} }
@ -314,21 +322,23 @@ private object MsgConvertor {
suspend fun convertReply(record: MsgRecord, element: MsgElement): Result<Element> { suspend fun convertReply(record: MsgRecord, element: MsgElement): Result<Element> {
val reply = element.replyElement val reply = element.replyElement
val elem = Element.newBuilder() val elem = Element.newBuilder()
elem.setReply(replyElement { elem.type = ElementType.REPLY
elem.setReply(ReplyElement.newBuilder().apply {
val msgSeq = reply.replayMsgSeq val msgSeq = reply.replayMsgSeq
val contact = MessageHelper.generateContact(record) val contact = MessageHelper.generateContact(record)
val sourceRecords = withTimeoutOrNull(3000) { val sourceRecords = withTimeoutOrNull(3000) {
suspendCancellableCoroutine { suspendCancellableCoroutine {
QRoute.api(IMsgService::class.java).getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records -> QRoute.api(IMsgService::class.java)
it.resume(records) .getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records ->
} it.resume(records)
}
} }
} }
if (sourceRecords.isNullOrEmpty()) { if (sourceRecords.isNullOrEmpty()) {
LogCenter.log("无法查询到回复的消息ID: seq = $msgSeq", Level.WARN) LogCenter.log("无法查询到回复的消息ID: seq = $msgSeq", Level.WARN)
this.messageId = reply.replayMsgId this.messageId = reply.replayMsgId.toString()
} else { } else {
this.messageId = sourceRecords.first().msgId this.messageId = sourceRecords.first().msgId.toString()
} }
}) })
return Result.success(elem.build()) return Result.success(elem.build())
@ -344,11 +354,18 @@ private object MsgConvertor {
val fileSubId = fileMsg.fileSubId ?: "" val fileSubId = fileMsg.fileSubId ?: ""
val url = when (record.chatType) { val url = when (record.chatType) {
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId) 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) else -> RichProtoSvc.getGroupFileDownUrl(record.peerUin, fileId, bizId)
} }
val elem = Element.newBuilder() val elem = Element.newBuilder()
elem.setFile(io.kritor.event.fileElement { elem.type = ElementType.FILE
elem.setFile(FileElement.newBuilder().apply {
this.name = fileName this.name = fileName
this.size = fileSize this.size = fileSize
this.url = url this.url = url
@ -363,7 +380,8 @@ private object MsgConvertor {
suspend fun convertMarkdown(record: MsgRecord, element: MsgElement): Result<Element> { suspend fun convertMarkdown(record: MsgRecord, element: MsgElement): Result<Element> {
val markdown = element.markdownElement val markdown = element.markdownElement
val elem = Element.newBuilder() val elem = Element.newBuilder()
elem.setMarkdown(io.kritor.event.markdownElement { elem.type = ElementType.MARKDOWN
elem.setMarkdown(MarkdownElement.newBuilder().apply {
this.markdown = markdown.content this.markdown = markdown.content
}) })
return Result.success(elem.build()) return Result.success(elem.build())
@ -372,7 +390,8 @@ private object MsgConvertor {
suspend fun convertBubbleFace(record: MsgRecord, element: MsgElement): Result<Element> { suspend fun convertBubbleFace(record: MsgRecord, element: MsgElement): Result<Element> {
val bubbleFace = element.faceBubbleElement val bubbleFace = element.faceBubbleElement
val elem = Element.newBuilder() 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.id = bubbleFace.yellowFaceInfo.index
this.count = bubbleFace.faceCount ?: 1 this.count = bubbleFace.faceCount ?: 1
}) })
@ -382,34 +401,35 @@ private object MsgConvertor {
suspend fun convertInlineKeyboard(record: MsgRecord, element: MsgElement): Result<Element> { suspend fun convertInlineKeyboard(record: MsgRecord, element: MsgElement): Result<Element> {
val inlineKeyboard = element.inlineKeyboardElement val inlineKeyboard = element.inlineKeyboardElement
val elem = Element.newBuilder() val elem = Element.newBuilder()
elem.setButton(io.kritor.event.buttonElement { elem.type = ElementType.KEYBOARD
elem.setKeyboard(KeyboardElement.newBuilder().apply {
inlineKeyboard.rows.forEach { row -> inlineKeyboard.rows.forEach { row ->
this.rows.add(io.kritor.event.row { this.addRows(KeyboardRow.newBuilder().apply {
row.buttons.forEach buttonsLoop@ { button -> row.buttons.forEach buttonsLoop@{ button ->
if (button == null) return@buttonsLoop if (button == null) return@buttonsLoop
this.buttons.add(io.kritor.event.button { this.addButtons(Button.newBuilder().apply {
this.id = button.id this.id = button.id
this.action = buttonAction { this.action = ButtonAction.newBuilder().apply {
this.type = button.type this.type = button.type
this.permission = buttonActionPermission { this.permission = ButtonActionPermission.newBuilder().apply {
this.type = button.permissionType this.type = button.permissionType
button.specifyRoleIds?.let { button.specifyRoleIds?.let {
this.roleIds.addAll(it) this.addAllRoleIds(it)
} }
button.specifyTinyids?.let { button.specifyTinyids?.let {
this.userIds.addAll(it) this.addAllUserIds(it)
} }
} }.build()
this.unsupportedTips = button.unsupportTips ?: "" this.unsupportedTips = button.unsupportTips ?: ""
this.data = button.data ?: "" this.data = button.data ?: ""
this.reply = button.isReply this.reply = button.isReply
this.enter = button.enter this.enter = button.enter
} }.build()
this.renderData = buttonRender { this.renderData = ButtonRender.newBuilder().apply {
this.label = button.label ?: "" this.label = button.label ?: ""
this.visitedLabel = button.visitedLabel ?: "" this.visitedLabel = button.visitedLabel ?: ""
this.style = button.style 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
}

View 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
/**
* 将合并转发PBprotobuf.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
}

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

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

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

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