123 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
3a07116093 Shamrock: 实现事件推送
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-15 01:37:28 +08:00
be58c368e9 Merge pull request #295 from tobycroft/master
notice类消息,新增source字段
2024-03-15 00:34:42 +08:00
a95d8d85e8 Shamrock: 完成消息推送
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-14 20:33:30 +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
7bacea3288 Shamrock: イベントプッシュの実装
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-12 21:02:36 +08:00
ca47f9dbdf Shamrock: グループファイルサービスの実装
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-12 18:46:05 +08:00
cb4268edef Shamrock: 实现Kritor核心服务
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-11 12:48:11 +08:00
c16f9d543c Shamrock: 实现联系人服务
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-11 12:30:42 +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
a528030cbb Shamrock: 实现鉴权操作
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 16:05:20 +08:00
bbdfb7c04e Shamrock: 实现鉴权
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 16:04:52 +08:00
1afc0ac6a6 Shamrock: 修复服务器TLS异常
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 07:57:23 +08:00
638bf72392 Shamrock: 修复配置文件下发
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 07:44:02 +08:00
07364c8298 Shamrock: 修复kritor构建错误
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 02:26:53 +08:00
ee6e13a5bb Shamrock: 修正频道proto文件错误
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 01:58:18 +08:00
c3934778c7 Shamrock: 修正频道proto文件错误
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 01:56:02 +08:00
13a49dd70b Shamrock: 添加对kritor的依赖
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 01:00:20 +08:00
5637db43be Shamrock: 重构收包起,减少拷贝
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 00:33:26 +08:00
69cdbad643 Shamrock: rm CIO
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-08 01:19:26 +08:00
a06708bf95 Shamrock: build for kritor
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-08 01:11:24 +08:00
2ac0003166 Shamrock: fix wrong check field name 2024-03-05 13:25:56 +08:00
d92d1daffb 统一发送图片时的参数 (#276)
* 统一发送图片时的请求参数

* 修改另一个 ElemMaker
2024-03-04 01:13:21 +08:00
27f837adbe Shamrock: 支持NT图片合并转发
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-02 18:03:13 +08:00
661680e60b Shamrock: 绕过资源上传检测
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-01 20:29:28 +08:00
54b7eb95a8 Shamrock: 修复真机反检测异常
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-01 20:29:13 +08:00
265fff3cd2 Merge pull request #271 from PisLuanyao/master
fix termination by PullConfig for Emu
2024-03-01 18:04:46 +08:00
8ca0a3815a Merge branch 'whitechi73:master' into master 2024-03-01 17:50:16 +09:00
da6d34c53e fix Crash for Emu
强制初始化配置
2024-03-01 16:48:49 +08:00
61ffb37951 Merge pull request #269 from PisLuanyao/master
fix NativeLoader for Emu
2024-03-01 15:40:44 +08:00
593f461ffe fix NativeLoader for Emu
发现 isEmu 里反射的有问题,会出现 java.lang.NoSuchFieldException: No field vmInstructionSet in class Ldalvik/system/VMRuntime; (declaration of 'dalvik.system.VMRuntime' appears in /system/framework/core-libart.jar)
2024-03-01 14:57:21 +08:00
12d594697d fix buttons 2024-03-01 09:40:31 +08:00
352aa5f737 Merge pull request #268 from huankong233/master
修正错误部分错误
2024-03-01 01:34:47 +08:00
9546e90bec 修正报错无法通过 Websocket 返回 2024-03-01 00:23:46 +08:00
26b4d95ad8 修正错误的拼写 2024-02-29 23:56:00 +08:00
4a6109fbe6 修正 websocket 中 alias 的名称
修正 QuickOperation 中 错误的返回
2024-02-29 23:35:23 +08:00
d6142173c9 Merge pull request #267 from PisLuanyao/master
NativeLoader
2024-02-29 22:42:01 +08:00
38cf806b40 Shamrock: review
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-29 22:40:36 +08:00
82269bb171 NativeLoader
应该是这样关?
猫脑过载.png
2024-02-29 20:43:18 +08:00
fc0d7a62af NativeLoader
修复isEmu里因手残导致的问题
2024-02-29 19:32:36 +08:00
60fdfd9071 Shamrock: fix #265
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-29 11:34:32 +08:00
1f620bcc06 NativeLoader
现在,可以在HMA做了隐藏的情况下正确加载库文件

借鉴过来的,貌似会导致亿点问题

感觉isEmu也会翻车呢  ┭┮﹏┭┮
2024-02-29 11:29:26 +08:00
737acfa41b Shamrock: Rollback 52ec43abf80d6595d1fe3b490ebe1bfc23bf0ab8 commit
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-29 10:42:36 +08:00
3619cba33c Shamrock: Fix interface(get_img) support for nt
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-29 09:58:21 +08:00
52ec43abf8 Shamrock: fix nt image error in very old qq
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-29 09:41:56 +08:00
e96c356de4 禁止版本9.0.20基于getPackageGids的检测 (#263)
* 禁止基于getPackageGids的检测
2024-02-29 01:19:49 +08:00
bbdb0a65fb add alias for websocket (#262)
Co-authored-by: Simplxs <simplxsa@gmail.com>
2024-02-28 21:58:58 +08:00
ec56e32be1 refactor send_forward_msg 2024-02-28 21:51:06 +08:00
4dc83fdeba Shamrock: add real_id for long msg
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-28 00:03:47 +08:00
541422a43e Shamrock: fix #260
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-27 22:35:31 +08:00
cb7bf00e17 Shamrock: Automatically remove group forward messages
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-27 22:25:38 +08:00
a3171b3111 Shamrock: fix #259
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-27 16:53:32 +08:00
02626489eb Shamrock: fix remove redundant dollar 2024-02-27 13:05:36 +08:00
a9a2e9a3dd Shamrock: fix #258
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-26 20:59:29 +08:00
964c55de31 Shamrock: fix #256
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-26 20:51:45 +08:00
befb4a2bef Merge pull request #257 from huankong233/master
add `delete_delay` option in QuickOperation
2024-02-26 20:50:04 +08:00
210609bd7b use coroutines 2024-02-26 18:51:05 +08:00
3e03d4782d fix 2024-02-26 18:31:10 +08:00
675a7a5321 refactor InlineKeyboard 2024-02-26 17:11:48 +08:00
a78b5cab23 add delete_delay option in QuickOperation 2024-02-26 15:05:13 +08:00
252a3527a8 Merge remote-tracking branch 'origin/master' 2024-02-25 17:32:07 +08:00
ea4cf06edf Shamrock: 修复download_file指定名称失败
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-25 17:31:57 +08:00
1424efd7f8 send_forward_msg(support image) 2024-02-25 14:33:59 +08:00
eb807a0332 Shamrock: support upload resource by NtKernel x3
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-25 12:46:40 +08:00
e9a3a82b68 Shamrock: support upload resource by NtKernel x2
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-25 12:40:39 +08:00
fca66f3259 Shamrock: support upload resource by NtKernel
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-25 11:42:42 +08:00
92ebe0c6a8 send_forward_msg(support markdown, button...) 2024-02-25 04:10:29 +08:00
6b1147d065 Shamrock: fix #252
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-25 03:38:09 +08:00
720313124c Shamrock: support requestUploadGroupPic
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-25 03:27:55 +08:00
68ea62ea0b fix get_group_files_by_folder 2024-02-24 19:27:09 +08:00
5584a41af0 fix get_group_msg_history 2024-02-24 18:58:35 +08:00
0bca46bba3 upgrade kotlin 2024-02-24 17:51:02 +08:00
4f1d19fcbd upgrade kotlin version to 1.9.22 2024-02-24 16:25:31 +08:00
9a85e4d537 replace all group_id and user_id to Long 2024-02-24 16:25:31 +08:00
fac92d8094 Merge remote-tracking branch 'origin/master' 2024-02-24 12:32:02 +08:00
605f58da47 Shamrock: Using the NT kernel to upload resources
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-24 12:31:51 +08:00
393 changed files with 10334 additions and 22071 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

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "kritor"]
path = kritor/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

@ -9,6 +9,6 @@ java {
} }
dependencies { dependencies {
implementation(DEPENDENCY_PROTOBUF) //implementation(DEPENDENCY_PROTOBUF)
implementation(kotlinx("serialization-protobuf", "1.6.2")) implementation(kotlinx("serialization-protobuf", "1.6.2"))
} }

View File

@ -0,0 +1,9 @@
package kritor.service
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FUNCTION)
annotation class Grpc(
val serviceName: 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

@ -5,6 +5,8 @@ import kotlinx.serialization.protobuf.ProtoBuf
import kotlin.reflect.KClass import kotlin.reflect.KClass
val EMPTY_BYTE_ARRAY = ByteArray(0)
interface Protobuf<T: Protobuf<T>> interface Protobuf<T: Protobuf<T>>
inline fun <reified T: Protobuf<T>> ByteArray.decodeProtobuf(to: KClass<T>? = null): T { inline fun <reified T: Protobuf<T>> ByteArray.decodeProtobuf(to: KClass<T>? = null): T {

View File

@ -4,7 +4,7 @@ import java.io.ByteArrayOutputStream
plugins { plugins {
id("com.android.application") id("com.android.application")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
kotlin("plugin.serialization") version "1.9.21" kotlin("plugin.serialization") version "1.9.22"
} }
android { android {
@ -17,7 +17,7 @@ android {
minSdk = 27 minSdk = 27
targetSdk = 34 targetSdk = 34
versionCode = getVersionCode() versionCode = getVersionCode()
versionName = "1.0.8" + ".r${getGitCommitCount()}." + getVersionName() versionName = "1.1.0" + ".r${getGitCommitCount()}." + getVersionName()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@ -92,7 +92,7 @@ android {
compose = true compose = true
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = "1.5.4" kotlinCompilerExtensionVersion = "1.5.10"
} }
packaging { packaging {
jniLibs { jniLibs {
@ -127,11 +127,6 @@ android {
} }
configureAppSigningConfigsForRelease(project) configureAppSigningConfigsForRelease(project)
packagingOptions {
jniLibs {
useLegacyPackaging = true
}
}
} }
fun configureAppSigningConfigsForRelease(project: Project) { fun configureAppSigningConfigsForRelease(project: Project) {
@ -206,14 +201,8 @@ dependencies {
implementation("io.coil-kt:coil-compose:2.4.0") implementation("io.coil-kt:coil-compose:2.4.0")
implementation(kotlinx("io-jvm", "0.1.16")) implementation(kotlinx("io-jvm", "0.1.16"))
implementation(ktor("server", "core"))
implementation(ktor("server", "host-common"))
implementation(ktor("server", "status-pages"))
implementation(ktor("server", "netty"))
implementation(ktor("server", "content-negotiation"))
implementation(ktor("client", "core")) implementation(ktor("client", "core"))
implementation(ktor("client", "content-negotiation")) implementation(ktor("client", "okhttp"))
implementation(ktor("client", "cio"))
implementation(ktor("serialization", "kotlinx-json")) implementation(ktor("serialization", "kotlinx-json"))
implementation(project(":xposed")) implementation(project(":xposed"))

View File

@ -199,17 +199,36 @@
-keep class com.arthenica.ffmpegkit.NativeLoader { *; } -keep class com.arthenica.ffmpegkit.NativeLoader { *; }
-keep class moe.fuqiuluo.** { *; } -keep class moe.fuqiuluo.shamrock.app.** { *; }
-keep class moe.fuqiuluo.shamrock.ui.** { *; }
-keep class moe.fuqiuluo.shamrock.MainActivity { *; }
-keep class moe.fuqiuluo.shamrock.MainActivityKt { *; }
-keep class moe.fuqiuluo.shamrock.Manifest { *; }
-keep class moe.fuqiuluo.shamrock.xposed.** { *; }
-keep class moe.fuqiuluo.shamrock.helper.** { *; }
# tencent interfaces rules
-keep class com.tencent.** { *; } -keep class com.tencent.** { *; }
-keep class com.qq.** { *; } -keep class com.qq.** { *; }
-keep class com.google.gson.** { *; } -keep class com.google.gson.** { *; }
-keep class de.** { *; } -keep class de.** { *; }
-keep class epic.** { *; }
-keep class friendlist.** { *; }
-keep class KQQ.** { *; }
-keep class mqq.** { *; } -keep class mqq.** { *; }
-keep class msf.** { *; }
-keep class oicq.** { *; }
-keep class QQService.** { *; } -keep class QQService.** { *; }
-keep class SummaryCard.** { *; } -keep class SummaryCard.** { *; }
-keep class tencent.** { *; } -keep class tencent.** { *; }
-keep class VIP.** { *; }
-keepclassmembers class * { -keepclassmembers class * {
native <methods>; native <methods>;
} }
-keep class io.netty.** { *; } -keep class io.netty.** { *; }
-keepclasseswithmembernames class * {
native <methods>;
}

View File

@ -37,9 +37,7 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt. # List C/C++ source files with relative paths to this CMakeLists.txt.
${SRC_DIR} ${SRC_DIR}
md5.cpp md5.cpp
cqcode.cpp
silk.cpp silk.cpp
group_honor.cpp
message.cpp message.cpp
shamrock.cpp) shamrock.cpp)

View File

@ -1,138 +0,0 @@
#include <stdexcept>
#include "cqcode.h"
inline void replace_string(std::string& str, const std::string& from, const std::string& to) {
size_t startPos = 0;
while ((startPos = str.find(from, startPos)) != std::string::npos) {
str.replace(startPos, from.length(), to);
startPos += to.length();
}
}
inline int utf8_next_len(const std::string& str, size_t offset)
{
uint8_t c = (uint8_t)str[offset];
if (c >= 0xFC)
return 6;
else if (c >= 0xF8)
return 5;
else if (c >= 0xF0)
return 4;
else if (c >= 0xE0)
return 3;
else if (c >= 0xC0)
return 2;
else if (c > 0x00)
return 1;
else
return 0;
}
void decode_cqcode(const std::string& code, std::vector<std::unordered_map<std::string, std::string>>& dest) {
std::string cache;
bool is_start = false;
std::string key_tmp;
std::unordered_map<std::string, std::string> kv;
for(size_t i = 0; i < code.size(); i++) {
int utf8_char_len = utf8_next_len(code, i);
if(utf8_char_len == 0) {
continue;
}
std::string_view c(&code[i],utf8_char_len);
if (c == "[") {
if (is_start) {
throw illegal_code();
} else {
if (!cache.empty()) {
std::unordered_map<std::string, std::string> kv;
replace_string(cache, "&#91;", "[");
replace_string(cache, "&#93;", "]");
replace_string(cache, "&amp;", "&");
kv.emplace("_type", "text");
kv.emplace("text", cache);
dest.push_back(kv);
cache.clear();
}
std::string_view cq_flag(&code[i],4);
if(cq_flag == "[CQ:"){
is_start = true;
i += 3;
}else{
cache += c;
}
}
}
else if (c == "=") {
if (is_start) {
if (cache.empty()) {
throw illegal_code();
} else {
if (key_tmp.empty()) {
key_tmp.append(cache);
cache.clear();
} else {
cache += c;
}
}
} else {
cache += c;
}
}
else if (c == ",") {
if (is_start) {
if (kv.count("_type") == 0 && !cache.empty()) {
kv.emplace("_type", cache);
cache.clear();
} else {
if (!key_tmp.empty()) {
replace_string(cache, "&#91;", "[");
replace_string(cache, "&#93;", "]");
replace_string(cache, "&#44;", ",");
replace_string(cache, "&amp;", "&");
kv.emplace(key_tmp, cache);
cache.clear();
key_tmp.clear();
}
}
} else {
cache += c;
}
}
else if (c == "]") {
if (is_start) {
if (!cache.empty()) {
if (!key_tmp.empty()) {
replace_string(cache, "&#91;", "[");
replace_string(cache, "&#93;", "]");
replace_string(cache, "&#44;", ",");
replace_string(cache, "&amp;", "&");
kv.emplace(key_tmp, cache);
} else {
kv.emplace("_type", cache);
}
dest.push_back(kv);
kv.clear();
key_tmp.clear();
cache.clear();
is_start = false;
}
} else {
cache += c;
}
}
else {
cache += c;
i += (utf8_char_len - 1);
}
}
if (!cache.empty()) {
std::unordered_map<std::string, std::string> kv;
replace_string(cache, "&#91;", "[");
replace_string(cache, "&#93;", "]");
replace_string(cache, "&amp;", "&");
kv.emplace("_type", "text");
kv.emplace("text", cache);
dest.push_back(kv);
}
}

View File

@ -1,87 +0,0 @@
#include "jni.h"
#include <vector>
#include <string>
#include <algorithm>
struct Honor {
int id;
std::string name;
std::string icon_url;
int priority;
};
int calc_honor_flag(int honor_id, char honor_flag);
jobject make_honor_object(JNIEnv *env, jobject user_id, const Honor& honor);
extern "C"
JNIEXPORT jobject JNICALL
Java_moe_fuqiuluo_shamrock_remote_action_handlers_GetTroopHonor_nativeDecodeHonor(JNIEnv *env, jobject thiz,
jstring user_id,
jint honor_id,
jbyte honor_flag) {
static std::vector<Honor> honor_list = {
Honor{1, "龙王", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150116_n4PxCiurbm.png", 1},
Honor{2, "群聊之火", "https://qzonestyle.gtimg.cn/aoi/sola/20200217190136_92JEGFKC5k.png", 3},
Honor{3, "群聊炽焰", "https://qzonestyle.gtimg.cn/aoi/sola/20200217190204_zgCTeSrMq1.png", 4},
Honor{5, "冒尖小春笋", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150335_tUJCAtoKVP.png", 5},
Honor{6, "快乐源泉", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150434_3tDmsJExCP.png", 7},
Honor{7, "学术新星", "https://sola.gtimg.cn/aoi/sola/20200515140645_j0X6gbuHNP.png", 8},
Honor{8, "顶尖学霸", "https://sola.gtimg.cn/aoi/sola/20200515140639_0CtWOpfVzK.png", 9},
Honor{9, "至尊学神", "https://sola.gtimg.cn/aoi/sola/20200515140628_P8UEYBjMBT.png", 10},
Honor{10, "一笔当先", "https://sola.gtimg.cn/aoi/sola/20200515140654_4r94tSCdaB.png", 11},
Honor{11, "奋进小翠竹", "https://sola.gtimg.cn/aoi/sola/20200812151819_wbj6z2NGoB.png", 6},
Honor{12, "氛围魔杖", "https://sola.gtimg.cn/aoi/sola/20200812151831_4ZJgQCaD1H.png", 2},
Honor{13, "壕礼皇冠", "https://sola.gtimg.cn/aoi/sola/20200930154050_juZOAMg7pt.png", 12},
};
int flag = calc_honor_flag(honor_id, honor_flag);
if ((honor_id != 1 && honor_id != 2 && honor_id != 3) || flag != 1) {
auto honor = *std::find_if(honor_list.begin(), honor_list.end(), [&honor_id](auto &honor) {
return honor.id == honor_id;
});
return make_honor_object(env, user_id, honor);
} else {
auto honor = *std::find_if(honor_list.begin(), honor_list.end(), [&honor_id](auto &honor) {
return honor.id == honor_id;
});
std::string url = "https://static-res.qq.com/static-res/groupInteract/vas/a/" + std::to_string(honor_id) + "_1.png";
honor = Honor{honor_id, honor.name, url, honor.priority};
return make_honor_object(env, user_id, honor);
}
}
int calc_honor_flag(int honor_id, char honor_flag) {
int flag;
if (honor_flag == 0) {
return 0;
}
if (honor_id == 1) {
flag = honor_flag;
} else if (honor_id == 2 || honor_id == 3) {
flag = honor_flag >> 2;
} else if (honor_id != 4) {
return 0;
} else {
flag = honor_flag >> 4;
}
return flag & 3;
}
jobject make_honor_object(JNIEnv *env, jobject user_id, const Honor& honor) {
jclass GroupMemberHonor = env->FindClass("moe/fuqiuluo/shamrock/remote/service/data/GroupMemberHonor");
jmethodID GroupMemberHonor_init = env->GetMethodID(GroupMemberHonor, "<init>",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;)V");
auto user_id_str = (jstring) user_id;
jstring honor_desc = env->NewStringUTF(honor.name.c_str());
jstring uin_name = env->NewStringUTF("");
jstring honor_icon_url = env->NewStringUTF(honor.icon_url.c_str());
jobject ret = env->NewObject(GroupMemberHonor, GroupMemberHonor_init, user_id_str, uin_name, honor_icon_url, 0, honor.id, honor_desc);
env->DeleteLocalRef(GroupMemberHonor);
env->DeleteLocalRef(user_id_str);
env->DeleteLocalRef(honor_desc);
env->DeleteLocalRef(honor_icon_url);
return ret;
}

View File

@ -1,20 +0,0 @@
#ifndef UNTITLED_CQCODE_H
#define UNTITLED_CQCODE_H
#include <string>
#include <unordered_map>
#include <vector>
#include <exception>
class illegal_code: std::exception {
public:
[[nodiscard]] const char * what() const noexcept override {
return "Error cq code.";
}
};
void decode_cqcode(const std::string& code, std::vector<std::unordered_map<std::string, std::string>>& dest);
void encode_cqcode(const std::vector<std::unordered_map<std::string, std::string>>& segment, std::string& dest);
#endif //UNTITLED_CQCODE_H

View File

@ -1,5 +1,4 @@
#include "jni.h" #include "jni.h"
#include "cqcode.h"
#include <random> #include <random>
inline void replace_string(std::string& str, const std::string& from, const std::string& to) { inline void replace_string(std::string& str, const std::string& from, const std::string& to) {
@ -12,7 +11,7 @@ inline void replace_string(std::string& str, const std::string& from, const std:
extern "C" extern "C"
JNIEXPORT jlong JNICALL JNIEXPORT jlong JNICALL
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_createMessageUniseq(JNIEnv *env, jobject thiz, Java_qq_service_msg_MessageHelper_createMessageUniseq(JNIEnv *env, jobject thiz,
jint chat_type, jint chat_type,
jlong time) { jlong time) {
static std::random_device rd; static std::random_device rd;
@ -32,123 +31,6 @@ Java_moe_fuqiuluo_shamrock_helper_MessageHelper_getChatType(JNIEnv *env, jobject
return (int32_t) ((int64_t) msg_id & 0xffL); return (int32_t) ((int64_t) msg_id & 0xffL);
} }
extern "C"
JNIEXPORT jobject JNICALL
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_nativeDecodeCQCode(JNIEnv *env, jobject thiz,
jstring code) {
jclass ArrayList = env->FindClass("java/util/ArrayList");
jmethodID NewArrayList = env->GetMethodID(ArrayList, "<init>", "()V");
jmethodID ArrayListAdd = env->GetMethodID(ArrayList, "add", "(Ljava/lang/Object;)Z");
jobject arrayList = env->NewObject(ArrayList, NewArrayList);
const char* cCode = env->GetStringUTFChars(code, nullptr);
std::string cppCode = cCode;
std::vector<std::unordered_map<std::string, std::string>> dest;
try {
decode_cqcode(cppCode, dest);
} catch (illegal_code& code) {
return arrayList;
}
jclass HashMap = env->FindClass("java/util/HashMap");
jmethodID NewHashMap = env->GetMethodID(HashMap, "<init>", "()V");
jclass String = env->FindClass("java/lang/String");
jmethodID NewString = env->GetMethodID(String, "<init>", "([BLjava/lang/String;)V");
jstring charset = env->NewStringUTF("UTF-8");
jmethodID put = env->GetMethodID(HashMap, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
for (auto& map : dest) {
jobject hashMap = env->NewObject(HashMap, NewHashMap);
for (const auto& pair : map) {
jbyteArray keyArray = env->NewByteArray((int) pair.first.size());
jbyteArray valueArray = env->NewByteArray((int) pair.second.size());
env->SetByteArrayRegion(keyArray, 0, (int) pair.first.size(), (jbyte*)pair.first.c_str());
env->SetByteArrayRegion(valueArray, 0, (int) pair.second.size(), (jbyte*)pair.second.c_str());
auto key = (jstring) env->NewObject(String, NewString, keyArray, charset);
auto value = (jstring) env->NewObject(String, NewString, valueArray, charset);
env->CallObjectMethod(hashMap, put, key, value);
}
env->CallBooleanMethod(arrayList, ArrayListAdd, hashMap);
}
env->DeleteLocalRef(ArrayList);
env->DeleteLocalRef(HashMap);
env->DeleteLocalRef(String);
env->DeleteLocalRef(charset);
env->ReleaseStringUTFChars(code, cCode);
return arrayList;
}
extern "C"
JNIEXPORT jstring JNICALL
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_nativeEncodeCQCode(JNIEnv *env, jobject thiz,
jobject segment_list) {
jclass List = env->FindClass("java/util/List");
jmethodID ListSize = env->GetMethodID(List, "size", "()I");
jmethodID ListGet = env->GetMethodID(List, "get", "(I)Ljava/lang/Object;");
jclass Map = env->FindClass("java/util/Map");
jmethodID MapGet = env->GetMethodID(Map, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
jmethodID entrySetMethod = env->GetMethodID(Map, "entrySet", "()Ljava/util/Set;");
jclass setClass = env->FindClass("java/util/Set");
jmethodID iteratorMethod = env->GetMethodID(setClass, "iterator", "()Ljava/util/Iterator;");
jclass entryClass = env->FindClass("java/util/Map$Entry");
jmethodID getKeyMethod = env->GetMethodID(entryClass, "getKey", "()Ljava/lang/Object;");
jmethodID getValueMethod = env->GetMethodID(entryClass, "getValue", "()Ljava/lang/Object;");
std::string result;
jint size = env->CallIntMethod(segment_list, ListSize);
for (int i = 0; i < size; i++ ) {
jobject segment = env->CallObjectMethod(segment_list, ListGet, i);
jobject entrySet = env->CallObjectMethod(segment, entrySetMethod);
jobject iterator = env->CallObjectMethod(entrySet, iteratorMethod);
auto type = (jstring) env->CallObjectMethod(segment, MapGet, env->NewStringUTF("_type"));
auto typeString = env->GetStringUTFChars(type, nullptr);
if (strcmp(typeString, "text") == 0) {
auto text = (jstring) env->CallObjectMethod(segment, MapGet, env->NewStringUTF("text"));
auto textString = env->GetStringUTFChars(text, nullptr);
std::string tmpValue = textString;
replace_string(tmpValue, "&", "&amp;");
replace_string(tmpValue, "[", "&#91;");
replace_string(tmpValue, "]", "&#93;");
replace_string(tmpValue, ",", "&#44;");
result.append(tmpValue);
env->ReleaseStringUTFChars(text, textString);
} else {
result.append("[CQ:");
result.append(typeString);
while (env->CallBooleanMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "hasNext", "()Z"))) {
jobject entry = env->CallObjectMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "next", "()Ljava/lang/Object;"));
auto key = (jstring) env->CallObjectMethod(entry, getKeyMethod);
auto value = (jstring) env->CallObjectMethod(entry, getValueMethod);
auto keyString = env->GetStringUTFChars(key, nullptr);
auto valueString = env->GetStringUTFChars(value, nullptr);
if (strcmp(keyString, "_type") != 0) {
std::string tmpValue = valueString;
replace_string(tmpValue, "&", "&amp;");
replace_string(tmpValue, "[", "&#91;");
replace_string(tmpValue, "]", "&#93;");
replace_string(tmpValue, ",", "&#44;");
result.append(",").append(keyString).append("=").append(tmpValue);
}
env->ReleaseStringUTFChars(key, keyString);
env->ReleaseStringUTFChars(value, valueString);
env->DeleteLocalRef(entry);
env->DeleteLocalRef(key);
env->DeleteLocalRef(value);
}
result.append("]");
}
env->ReleaseStringUTFChars(type, typeString);
}
env->DeleteLocalRef(List);
env->DeleteLocalRef(Map);
env->DeleteLocalRef(setClass);
env->DeleteLocalRef(entryClass);
return env->NewStringUTF(result.c_str());
}
extern "C" extern "C"
JNIEXPORT jlong JNICALL JNIEXPORT jlong JNICALL
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_insertChatTypeToMsgId(JNIEnv *env, jobject thiz, Java_moe_fuqiuluo_shamrock_helper_MessageHelper_insertChatTypeToMsgId(JNIEnv *env, jobject thiz,

View File

@ -12,7 +12,7 @@
extern "C" extern "C"
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_moe_fuqiuluo_shamrock_xposed_hooks_PullConfig_testNativeLibrary(JNIEnv *env, jobject thiz) { Java_moe_fuqiuluo_shamrock_xposed_actions_interacts_Init_testNativeLibrary(JNIEnv *env, jobject thiz) {
return env->NewStringUTF("加载Shamrock库成功~"); return env->NewStringUTF("加载Shamrock库成功~");
} }

View File

@ -4,6 +4,7 @@ package moe.fuqiuluo.shamrock
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@ -64,7 +65,9 @@ import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import moe.fuqiuluo.shamrock.tools.GlobalUi
import moe.fuqiuluo.shamrock.ui.app.AppRuntime import moe.fuqiuluo.shamrock.ui.app.AppRuntime
import moe.fuqiuluo.shamrock.ui.app.Logger import moe.fuqiuluo.shamrock.ui.app.Logger
import moe.fuqiuluo.shamrock.ui.app.RuntimeState import moe.fuqiuluo.shamrock.ui.app.RuntimeState
@ -85,8 +88,16 @@ import moe.fuqiuluo.shamrock.ui.tools.getShamrockVersion
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
LaunchedEffect(Unit) {
while (true) {
delay(5_000) // Delay in milliseconds
broadcastToModule {
putExtra("__cmd", "switch_status")
}
}
}
CompositionLocalProvider( CompositionLocalProvider(
LocalIndication provides NoIndication LocalIndication provides NoIndication
) { ) {
@ -96,8 +107,9 @@ class MainActivity : ComponentActivity() {
isAppearanceLightStatusBars = true isAppearanceLightStatusBars = true
} }
WindowCompat.setDecorFitsSystemWindows(window, true) WindowCompat.setDecorFitsSystemWindows(window, true)
broadcastToModule { putExtra("__cmd", "fetchPort") }
} }
GlobalUi = Handler(mainLooper)
} }
} }
@ -153,7 +165,7 @@ private fun AppMainView() {
} }
val ctx = LocalContext.current val ctx = LocalContext.current
LaunchedEffect(isFined.value) { LaunchedEffect(isFined) {
if (isFined.value) { if (isFined.value) {
AppRuntime.log(LocalString.logCentralLoadSuccessfully) AppRuntime.log(LocalString.logCentralLoadSuccessfully)
Toast.makeText(ctx, LocalString.frameworkYes, Toast.LENGTH_SHORT).show() Toast.makeText(ctx, LocalString.frameworkYes, Toast.LENGTH_SHORT).show()
@ -284,58 +296,11 @@ private fun AnimatedTab(
val lastSelectedState = remember { val lastSelectedState = remember {
mutableIntStateOf(0) mutableIntStateOf(0)
} }
val enter = remember {
scaleIn(animationSpec = TweenSpec(150, easing = FastOutLinearInEasing))
}
val exit = remember {
scaleOut(animationSpec = TweenSpec(150, easing = FastOutSlowInEasing))
}
val defaultConst = SELECTED_TABLE[index * 2] val defaultConst = SELECTED_TABLE[index * 2]
val selectedConst = SELECTED_TABLE[(index * 2) + 1] val selectedConst = SELECTED_TABLE[(index * 2) + 1]
val isFirst: Boolean = (lastSelectedState.value and defaultConst) != defaultConst val isFirst: Boolean = (lastSelectedState.value and defaultConst) != defaultConst
var icon: @Composable (() -> Unit)? = null
var text: @Composable (() -> Unit)? = null
if (curSelected) {
text = {
AnimatedVisibility(visibleState = MutableTransitionState(false).also {
it.targetState =
isFirst || lastSelectedState.value and selectedConst == selectedConst
}, enter = enter, exit = exit, modifier = Modifier) {
Text(
text = titleWithIcon.first,
color = GlobalColor.TabItem,
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(bottom = 5.dp)
.indication(
remember { MutableInteractionSource() },
rememberRipple(color = Color.Transparent)
)
)
}
}
} else {
icon = {
Icon(
painter = painterResource(id = titleWithIcon.second),
contentDescription = titleWithIcon.first,
tint = Color.Unspecified,
modifier = Modifier
.height(24.dp)
.width(24.dp)
.padding(bottom = 5.dp)
.indication(
remember { MutableInteractionSource() },
rememberRipple(color = Color.Transparent)
)
)
}
}
ShamrockTab( ShamrockTab(
selected = curSelected, selected = curSelected,
onClick = { onClick = {
@ -343,11 +308,13 @@ private fun AnimatedTab(
state.scrollToPage(index, 0f) state.scrollToPage(index, 0f)
} }
}, },
text = text,
icon = icon,
selectedContentColor = Color.Transparent, selectedContentColor = Color.Transparent,
unselectedContentColor = Color.Transparent, unselectedContentColor = Color.Transparent,
indication = null indication = null,
titleWithIcon = titleWithIcon,
visibleState = MutableTransitionState(false).also {
it.targetState = isFirst || lastSelectedState.value and selectedConst == selectedConst
}
) )
lastSelectedState.value.let { lastSelectedState.value.let {
var tmp = it var tmp = it

View File

@ -0,0 +1,2 @@
package moe.fuqiuluo.shamrock.app.config

View File

@ -0,0 +1,33 @@
package moe.fuqiuluo.shamrock.app.config
import android.content.Context
import moe.fuqiuluo.shamrock.config.ConfigKey
import moe.fuqiuluo.shamrock.ui.service.internal.broadcastToModule
object ShamrockConfig {
internal fun getConfigPref(ctx: Context) = ctx.getSharedPreferences("config", 0)
internal inline operator fun <reified Type> get(ctx: Context, key: ConfigKey<Type>): Type {
val preferences = getConfigPref(ctx)
return when(Type::class) {
Int::class -> preferences.getInt(key.name(), key.default() as Int) as Type
Long::class -> preferences.getLong(key.name(), key.default() as Long) as Type
String::class -> preferences.getString(key.name(), key.default() as String) as Type
Boolean::class -> preferences.getBoolean(key.name(), key.default() as Boolean) as Type
else -> throw IllegalArgumentException("Unsupported type")
}
}
internal inline operator fun <reified Type> set(ctx: Context, key: ConfigKey<Type>, value: Type) {
val preferences = getConfigPref(ctx)
val editor = preferences.edit()
when(Type::class) {
Int::class -> editor.putInt(key.name(), value as Int)
Long::class -> editor.putLong(key.name(), value as Long)
String::class -> editor.putString(key.name(), value as String)
Boolean::class -> editor.putBoolean(key.name(), value as Boolean)
else -> throw IllegalArgumentException("Unsupported type")
}
editor.apply()
}
}

View File

@ -1,373 +0,0 @@
package moe.fuqiuluo.shamrock.ui.app
import android.content.Context
import moe.fuqiuluo.shamrock.ui.service.internal.broadcastToModule
object ShamrockConfig {
fun getSSLKeyPath(ctx: Context): String {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getString("key_store", "")!!
}
fun setSSLKeyPath(ctx: Context, path: String) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putString("key_store", path).apply()
pushUpdate(ctx)
}
fun getSSLPort(ctx: Context): Int {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getInt("ssl_port", 5701)
}
fun setSSLPort(ctx: Context, port: Int) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putInt("ssl_port", port).apply()
pushUpdate(ctx)
}
fun getSSLAlias(ctx: Context): String {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getString("ssl_alias", "")!!
}
fun setSSLAlias(ctx: Context, alias: String) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putString("ssl_alias", alias).apply()
pushUpdate(ctx)
}
fun getSSLPwd(ctx: Context): String {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getString("ssl_pwd", "")!!
}
fun setSSLPwd(ctx: Context, alias: String) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putString("ssl_pwd", alias).apply()
pushUpdate(ctx)
}
fun getSSLPrivatePwd(ctx: Context): String {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getString("ssl_private_pwd", "")!!
}
fun setSSLPrivatePwd(ctx: Context, alias: String) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putString("ssl_private_pwd", alias).apply()
pushUpdate(ctx)
}
fun getHttpAddr(ctx: Context): String {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getString("http_addr", "")!!
}
fun setHttpAddr(ctx: Context, v: String) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putString("http_addr", v).apply()
pushUpdate(ctx)
}
fun isPro(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("pro_api", false)
}
fun setPro(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("pro_api", v).apply()
ctx.broadcastToModule {
putExtra("type", "restart")
putExtra("__cmd", "change_port")
}
pushUpdate(ctx)
}
fun getToken(ctx: Context): String {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getString("token", null) ?: ""
}
fun setToken(ctx: Context, v: String?) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putString("token", v).apply()
pushUpdate(ctx)
}
fun isWs(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("ws", false)
}
fun setWs(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("ws", v).apply()
pushUpdate(ctx)
}
fun isWsClient(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("ws_client", false)
}
fun setWsClient(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("ws_client", v).apply()
pushUpdate(ctx)
}
fun isTablet(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("tablet", false)
}
fun setTablet(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("tablet", v).apply()
pushUpdate(ctx)
}
fun isUseCQCode(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("use_cqcode", false)
}
fun setUseCQCode(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("use_cqcode", v).apply()
pushUpdate(ctx)
}
fun isWebhook(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("webhook", false)
}
fun setWebhook(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("webhook", v).apply()
pushUpdate(ctx)
}
fun getWsAddr(ctx: Context): String {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getString("ws_addr", "")!!
}
fun setWsAddr(ctx: Context, v: String) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putString("ws_addr", v).apply()
pushUpdate(ctx)
}
fun getHttpPort(ctx: Context): Int {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getInt("port", 5700)
}
fun setHttpPort(ctx: Context, v: Int) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putInt("port", v).apply()
ctx.broadcastToModule {
putExtra("type", "port")
putExtra("port", v)
putExtra("__cmd", "change_port")
}
pushUpdate(ctx)
}
fun getWsPort(ctx: Context): Int {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getInt("ws_port", 5800)
}
fun setWsPort(ctx: Context, v: Int) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putInt("ws_port", v).apply()
ctx.broadcastToModule {
putExtra("type", "ws_port")
putExtra("port", v)
putExtra("__cmd", "change_port")
}
pushUpdate(ctx)
}
fun is2B(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("2B", false)
}
fun set2B(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("2B", v).apply()
}
fun setAutoClean(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("auto_clear", v).apply()
}
fun isAutoClean(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("auto_clear", false)
}
fun isDebug(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("debug", false)
}
fun setDebug(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("debug", v).apply()
}
fun isAntiTrace(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("anti_qq_trace", true)
}
fun isForbidUselessProcess(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("forbid_useless_process", false)
}
fun setForbidUselessProcess(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("forbid_useless_process", v).apply()
}
fun setAntiTrace(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("anti_qq_trace", v).apply()
}
fun isInjectPacket(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("inject_packet", false)
}
fun setInjectPacket(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("inject_packet", v).apply()
}
fun enableAutoStart(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("enable_auto_start", false)
}
fun disableAutoSyncSetting(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("disable_auto_sync_setting", false)
}
fun enableAliveReply(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("alive_reply", false)
}
fun allowShell(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("shell", false)
}
fun setAutoStart(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("enable_auto_start", v).apply()
}
fun setDisableAutoSyncSetting(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("disable_auto_sync_setting", v).apply()
}
fun setAliveReply(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("alive_reply", v).apply()
}
fun setShellStatus(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("shell", v).apply()
}
fun enableSelfMsg(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("enable_self_msg", false)
}
fun enableSyncMsgAsSentMsg(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("enable_sync_msg_as_sent_msg", false)
}
fun setEnableSelfMsg(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("enable_self_msg", v).apply()
}
fun setEnableSyncMsgAsSentMsg(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("enable_sync_msg_as_sent_msg", v).apply()
}
fun getConfigMap(ctx: Context): Map<String, Any?> {
val preferences = ctx.getSharedPreferences("config", 0)
return mapOf(
"tablet" to preferences.getBoolean("tablet", false),
"port" to preferences.getInt("port", 5700),
"ws" to preferences.getBoolean("ws", false),
"ws_port" to preferences.getInt("ws_port", 5800),
"ssl_port" to preferences.getInt("ssl_port", 5701),
"http" to preferences.getBoolean("webhook", false),
"http_addr" to preferences.getString("http_addr", ""),
"ws_client" to preferences.getBoolean("ws_client", false),
"use_cqcode" to preferences.getBoolean("use_cqcode", false),
"ws_addr" to preferences.getString("ws_addr", ""),
"ssl_alias" to preferences.getString("ssl_alias", ""),
"pro_api" to preferences.getBoolean("pro_api", false),
"token" to preferences.getString("token", null),
"ssl_pwd" to preferences.getString("ssl_pwd", ""),
"inject_packet" to preferences.getBoolean("inject_packet", false),
"debug" to preferences.getBoolean("debug", false),
"anti_qq_trace" to preferences.getBoolean("anti_qq_trace", true),
//"auto_clear" to preferences.getBoolean("auto_clear", false),
"ssl_private_pwd" to preferences.getString("ssl_private_pwd", ""),
"key_store" to preferences.getString("key_store", ""),
"enable_self_msg" to preferences.getBoolean("enable_self_msg", false),
"echo_number" to preferences.getBoolean("echo_number", false),
"shell" to preferences.getBoolean("shell", false),
"alive_reply" to preferences.getBoolean("alive_reply", false),
"enable_sync_msg_as_sent_msg" to preferences.getBoolean("enable_sync_msg_as_sent_msg", false),
"disable_auto_sync_setting" to preferences.getBoolean("disable_auto_sync_setting", false),
"forbid_useless_process" to preferences.getBoolean("forbid_useless_process", false)
)
}
fun pushUpdate(ctx: Context) {
ctx.broadcastToModule {
getConfigMap(ctx).forEach { (key, value) ->
if (value == null) {
val v: String? = null
this.putExtra(key, v)
} else {
when (value) {
is Int -> this.putExtra(key, value)
is Long -> this.putExtra(key, value)
is Short -> this.putExtra(key, value)
is Byte -> this.putExtra(key, value)
is String -> this.putExtra(key, value)
is ByteArray -> this.putExtra(key, value)
is Boolean -> this.putExtra(key, value)
is Float -> this.putExtra(key, value)
is Double -> this.putExtra(key, value)
}
}
}
putExtra("__cmd", "push_config")
}
}
}

View File

@ -5,7 +5,6 @@ import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
@ -25,6 +24,7 @@ import androidx.compose.material3.Divider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -45,10 +45,12 @@ import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.size.Size import coil.size.Size
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import moe.fuqiuluo.shamrock.R import moe.fuqiuluo.shamrock.R
import moe.fuqiuluo.shamrock.ui.app.AppRuntime import moe.fuqiuluo.shamrock.ui.app.AppRuntime
import moe.fuqiuluo.shamrock.ui.app.Level import moe.fuqiuluo.shamrock.ui.app.Level
import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig import moe.fuqiuluo.shamrock.app.config.ShamrockConfig
import moe.fuqiuluo.shamrock.config.*
import moe.fuqiuluo.shamrock.ui.theme.GlobalColor import moe.fuqiuluo.shamrock.ui.theme.GlobalColor
import moe.fuqiuluo.shamrock.ui.theme.LocalString import moe.fuqiuluo.shamrock.ui.theme.LocalString
import moe.fuqiuluo.shamrock.ui.theme.ThemeColor import moe.fuqiuluo.shamrock.ui.theme.ThemeColor
@ -72,110 +74,6 @@ fun DashboardFragment(
InformationCard(ctx) InformationCard(ctx)
APIInfoCard(ctx) APIInfoCard(ctx)
FunctionCard(scope, ctx, LocalString.functionSetting) FunctionCard(scope, ctx, LocalString.functionSetting)
SSLCard(ctx)
}
}
@Composable
private fun SSLCard(ctx: Context) {
ActionBox(
modifier = Modifier.padding(top = 12.dp),
painter = painterResource(id = R.drawable.baseline_security_24),
title = LocalString.sslSetting
) {
Column {
Divider(
modifier = Modifier,
color = GlobalColor.Divider,
thickness = 0.2.dp
)
val sslPort = remember { mutableStateOf(ShamrockConfig.getSSLPort(ctx).toString()) }
TextItem(
title = "SSL端口",
desc = "端口范围在0~65565并确保可用。",
text = sslPort,
hint = "请输入端口号",
error = "端口范围应在0~65565",
checker = {
it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }.getOrDefault(false)
},
confirm = {
val newPort = sslPort.value.toInt()
ShamrockConfig.setSSLPort(ctx, newPort)
AppRuntime.log("设置SSL(HTTP)端口为$newPort,立即生效尝试中。")
}
)
val keyStore = remember { mutableStateOf(ShamrockConfig.getSSLKeyPath(ctx)) }
TextItem(
title = "SSL证书",
desc = "BKS签名的证书。",
text = keyStore,
hint = "输入证书路径",
error = "证书路径不合法或不存在",
checker = {
it.isNotBlank()
},
confirm = {
val new = keyStore.value
ShamrockConfig.setSSLKeyPath(ctx, new)
AppRuntime.log("设置SSL证书为[$new]。")
}
)
val alias = remember { mutableStateOf(ShamrockConfig.getSSLAlias(ctx)) }
TextItem(
title = "SSL别名",
desc = "BKS签名的别名确保大小写区分正确。",
text = alias,
hint = "输入签名别名",
error = "别名不合法",
checker = {
it.isNotBlank()
},
confirm = {
val new = alias.value
ShamrockConfig.setSSLAlias(ctx, new)
AppRuntime.log("设置SSL别名为[$new]。")
}
)
val sslPwd = remember { mutableStateOf(ShamrockConfig.getSSLPwd(ctx)) }
TextItem(
title = "SSL密码",
desc = "BKS签名的密码。",
text = sslPwd,
hint = "输入签名密码",
error = "密码不合法",
checker = {
it.isNotBlank()
},
confirm = {
val new = sslPwd.value
ShamrockConfig.setSSLPwd(ctx, new)
AppRuntime.log("设置SSL密码为[$new]。")
}
)
val sslPrivatePwd = remember { mutableStateOf(ShamrockConfig.getSSLPrivatePwd(ctx)) }
TextItem(
title = "SSL Private密码",
desc = "BKS签名的Private密码。",
text = sslPrivatePwd,
hint = "输入Private密码",
error = "密码不合法",
checker = {
it.isNotBlank()
},
confirm = {
val new = sslPrivatePwd.value
ShamrockConfig.setSSLPrivatePwd(ctx, new)
AppRuntime.log("设置SSL Private密码为[$new]。")
}
)
}
} }
} }
@ -195,93 +93,35 @@ private fun APIInfoCard(
thickness = 0.2.dp thickness = 0.2.dp
) )
val wsPort = remember { mutableStateOf(ShamrockConfig.getWsPort(ctx).toString()) } val rpcPort = remember { mutableStateOf(ShamrockConfig[ctx, RPCPort].toString()) }
val port = remember { mutableStateOf(ShamrockConfig.getHttpPort(ctx).toString()) }
TextItem( TextItem(
title = "主动HTTP端口", title = "RPC服务端口",
desc = "端口范围在0~65565并确保可用。", desc = "端口范围在0~65565并确保可用。",
text = port, text = rpcPort,
hint = "请输入端口号", hint = "请输入端口号",
error = "端口范围应在0~65565", error = "端口范围应在0~65565",
checker = { checker = {
it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }.getOrDefault(false) && wsPort.value != it it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }
.getOrDefault(false) && rpcPort.value != it
}, },
confirm = { confirm = {
val newPort = port.value.toInt() val newPort = rpcPort.value.toInt()
ShamrockConfig.setHttpPort(ctx, newPort) ShamrockConfig[ctx, RPCPort] = newPort
AppRuntime.log("设置主动HTTP监听端口为$newPort,立即生效尝试中。") AppRuntime.log("设置主动HTTP监听端口为$newPort,立即生效尝试中。")
} }
) )
val rpcAddress = remember { mutableStateOf(ShamrockConfig[ctx, RPCAddress]) }
TextItem( TextItem(
title = "主动WebSocket端口", title = "回调RPC地址",
desc = "端口范围在0~65565并确保可用。", desc = "例如kritor.support:8081",
text = wsPort, text = rpcAddress,
hint = "请输入端口号",
error = "端口范围应在0~65565",
checker = {
it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }.getOrDefault(false) && it != port.value
},
confirm = {
val newPort = wsPort.value.toInt()
ShamrockConfig.setWsPort(ctx, newPort)
AppRuntime.log("设置主动WebSocket监听端口为$newPort")
}
)
val webHookAddress = remember { mutableStateOf(ShamrockConfig.getHttpAddr(ctx)) }
TextItem(
title = "回调HTTP地址",
desc = "例如http://shamrock.moe:80。",
text = webHookAddress,
hint = "请输入回调地址", hint = "请输入回调地址",
error = "输入的地址不合法", error = "输入的地址不合法",
checker = {
it.isNotBlank()
},
confirm = {
if (it.startsWith("http://") || it.startsWith("https://")) {
ShamrockConfig.setHttpAddr(ctx, webHookAddress.value)
AppRuntime.log("设置回调HTTP地址为[${webHookAddress.value}]。")
} else {
Toast.makeText(ctx, "回调地址不合法", Toast.LENGTH_SHORT).show()
webHookAddress.value = ""
}
}
)
val wsAddress = remember { mutableStateOf(ShamrockConfig.getWsAddr(ctx)) }
TextItem(
title = "被动WebSocket地址",
desc = "例如ws://shamrock.moe:81多个使用逗号分隔。",
text = wsAddress,
hint = "请输入被动地址",
error = "输入的地址不合法",
checker = {
true
},
confirm = {
if (it.startsWith("ws://") || it.startsWith("wss://") || it.isBlank()) {
ShamrockConfig.setWsAddr(ctx, wsAddress.value)
AppRuntime.log("设置被动WebSocket地址为[${wsAddress.value}]。")
} else {
Toast.makeText(ctx, "被动WebSocket地址不合法", Toast.LENGTH_SHORT).show()
wsAddress.value = ""
}
}
)
val authToken = remember { mutableStateOf(ShamrockConfig.getToken(ctx)) }
TextItem(
title = "鉴权Token",
desc = "用于鉴权的Token。",
text = authToken,
hint = "请填写鉴权token",
error = "输入的参数不合法",
checker = { true }, checker = { true },
confirm = { confirm = {
ShamrockConfig.setToken(ctx, authToken.value) ShamrockConfig[ctx, RPCAddress] = rpcAddress.value
AppRuntime.log("设置鉴权Token为[${authToken.value}]。") AppRuntime.log("设置回调RPC地址为[${rpcAddress.value}]。")
} }
) )
@ -314,59 +154,59 @@ private fun FunctionCard(
Function( Function(
title = "强制平板模式", title = "强制平板模式",
desc = "强制QQ使用平板模式实现共存登录。", desc = "强制QQ使用平板模式实现共存登录。",
isSwitch = ShamrockConfig.isTablet(ctx) isSwitch = ShamrockConfig[ctx, ForceTablet]
) { ) {
ShamrockConfig.setTablet(ctx, it) ShamrockConfig[ctx, ForceTablet] = it
return@Function true return@Function true
} }
Function( Function(
title = "HTTP回调", title = "主动RPC",
desc = "OneBot标准的HTTPAPI回调Shamrock作为Client。", desc = "Kritor协议实现RPC",
isSwitch = ShamrockConfig.isWebhook(ctx) isSwitch = ShamrockConfig[ctx, ActiveRPC]
) { ) {
ShamrockConfig.setWebhook(ctx, it) ShamrockConfig[ctx, ActiveRPC] = it
return@Function true return@Function true
} }
Function( Function(
title = "消息格式为CQ码", title = "被动RPC",
desc = "HTTPAPI回调的消息格式关闭则为消息段。", desc = "Kritor协议实现RPC",
isSwitch = ShamrockConfig.isUseCQCode(ctx) isSwitch = ShamrockConfig[ctx, PassiveRPC]
) { ) {
ShamrockConfig.setUseCQCode(ctx, it) ShamrockConfig[ctx, PassiveRPC] = it
return@Function true return@Function true
} }
Function( run {
title = "主动WebSocket", val uploadResourceGroup = remember { mutableStateOf(ShamrockConfig[ctx, ResourceGroup]) }
desc = "OneBot标准WebSocketShamrock作为Server。", Column(
isSwitch = ShamrockConfig.isWs(ctx) modifier = Modifier
) { .absolutePadding(left = 8.dp, right = 8.dp, top = 12.dp, bottom = 0.dp)
ShamrockConfig.setWs(ctx, it) ) {
return@Function true Text(
modifier = Modifier.padding(2.dp),
text = "用来上传资源的群聊,错误的资源上传终点可能导致封禁,请自建一个群聊并填写在下方。",
color = Color.Red,
fontSize = 11.sp
)
}
TextItem(
title = "接受资源群聊",
desc = "用来上传资源的群聊,请自建一个群聊并填写在下方。",
text = uploadResourceGroup,
hint = "请输入群号",
error = "群号不合法",
checker = {
it.isNotBlank() && it.toULongOrNull() != null
},
confirm = {
val groupId = uploadResourceGroup.value
ShamrockConfig[ctx, ResourceGroup] = groupId
AppRuntime.log("设置接受资源群聊为[$groupId]。")
}
)
} }
Function(
title = "被动WebSocket",
desc = "OneBot标准WebSocketShamrock作为Client。",
isSwitch = ShamrockConfig.isWsClient(ctx)
) {
ShamrockConfig.setWsClient(ctx, it)
return@Function true
}
/*
Function(
title = "专业级接口",
desc = "如果你不知道你在做什么,请不要开启本功能。",
descColor = Color.Red,
isSwitch = ShamrockConfig.isPro(ctx)
) {
ShamrockConfig.setPro(ctx, it)
AppRuntime.log("专业级API = $it", Level.WARN)
return@Function true
}*/
} }
} }
} }
@ -445,9 +285,7 @@ private fun InfoItem(
.fillMaxWidth() .fillMaxWidth()
.combinedClickable(onDoubleClick = { .combinedClickable(onDoubleClick = {
doubleClick?.invoke(content) doubleClick?.invoke(content)
}) { }) { true }
true
}
, ,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {

View File

@ -23,7 +23,14 @@ import androidx.compose.ui.unit.sp
import moe.fuqiuluo.shamrock.R import moe.fuqiuluo.shamrock.R
import moe.fuqiuluo.shamrock.ui.app.AppRuntime import moe.fuqiuluo.shamrock.ui.app.AppRuntime
import moe.fuqiuluo.shamrock.ui.app.Level import moe.fuqiuluo.shamrock.ui.app.Level
import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig import moe.fuqiuluo.shamrock.app.config.ShamrockConfig
import moe.fuqiuluo.shamrock.config.AliveReply
import moe.fuqiuluo.shamrock.config.AntiJvmTrace
import moe.fuqiuluo.shamrock.config.B2Mode
import moe.fuqiuluo.shamrock.config.DebugMode
import moe.fuqiuluo.shamrock.config.EnableOldBDH
import moe.fuqiuluo.shamrock.config.EnableSelfMessage
import moe.fuqiuluo.shamrock.ui.service.handlers.InitHandler
import moe.fuqiuluo.shamrock.ui.theme.GlobalColor import moe.fuqiuluo.shamrock.ui.theme.GlobalColor
import moe.fuqiuluo.shamrock.ui.theme.LocalString import moe.fuqiuluo.shamrock.ui.theme.LocalString
import moe.fuqiuluo.shamrock.ui.tools.NoticeTextDialog import moe.fuqiuluo.shamrock.ui.tools.NoticeTextDialog
@ -68,9 +75,9 @@ fun LabFragment() {
title = LocalString.b2Mode, title = LocalString.b2Mode,
desc = LocalString.b2ModeDesc, desc = LocalString.b2ModeDesc,
descColor = it, descColor = it,
isSwitch = ShamrockConfig.is2B(ctx) isSwitch = ShamrockConfig[ctx, B2Mode]
) { ) {
ShamrockConfig.set2B(ctx, it) ShamrockConfig[ctx, B2Mode] = it
scope.toast(ctx, LocalString.restartToast) scope.toast(ctx, LocalString.restartToast)
return@Function true return@Function true
} }
@ -79,10 +86,10 @@ fun LabFragment() {
title = LocalString.showDebugLog, title = LocalString.showDebugLog,
desc = LocalString.showDebugLogDesc, desc = LocalString.showDebugLogDesc,
descColor = it, descColor = it,
isSwitch = ShamrockConfig.isDebug(ctx) isSwitch = ShamrockConfig[ctx, DebugMode]
) { ) {
ShamrockConfig.setDebug(ctx, it) ShamrockConfig[ctx, DebugMode] = it
ShamrockConfig.pushUpdate(ctx) InitHandler.update(ctx)
return@Function true return@Function true
} }
} }
@ -100,54 +107,13 @@ fun LabFragment() {
thickness = 0.2.dp thickness = 0.2.dp
) )
Function(
title = "禁止无用进程",
desc = "禁止QQ生成无用进程浪费内存",
descColor = color,
isSwitch = ShamrockConfig.isForbidUselessProcess(ctx)
) {
ShamrockConfig.setForbidUselessProcess(ctx, it)
ShamrockConfig.pushUpdate(ctx)
return@Function true
}
Function( Function(
title = "自回复测试", title = "自回复测试",
desc = "发送[ping],机器人发送一个具有调试信息的返回。", desc = "发送[ping],机器人发送一个具有调试信息的返回。",
descColor = color, descColor = color,
isSwitch = ShamrockConfig.enableAliveReply(ctx) isSwitch = ShamrockConfig[ctx, AliveReply]
) { ) {
ShamrockConfig.setAliveReply(ctx, it) ShamrockConfig[ctx, AliveReply] = it
return@Function true
}
Function(
title = "开启Shell接口",
desc = "可能导致设备被入侵,请勿随意开启。",
descColor = color,
isSwitch = ShamrockConfig.allowShell(ctx)
) {
ShamrockConfig.setShellStatus(ctx, it)
return@Function true
}
Function(
title = "自动唤醒QQ",
desc = "QQ进程死亡时重新打开QQ进程前提本进程存活。",
descColor = color,
isSwitch = ShamrockConfig.enableAutoStart(ctx)
) {
ShamrockConfig.setAutoStart(ctx, it)
return@Function true
}
Function(
title = "禁止Shamrock同步设置",
desc = "禁止Shamrock同步设置防止恢复手动修改后的配置文件。",
descColor = color,
isSwitch = ShamrockConfig.disableAutoSyncSetting(ctx)
) {
ShamrockConfig.setDisableAutoSyncSetting(ctx, it)
return@Function true return@Function true
} }
@ -194,25 +160,14 @@ fun LabFragment() {
thickness = 0.2.dp thickness = 0.2.dp
) )
Function(
title = LocalString.injectPacket,
desc = LocalString.injectPacketDesc,
descColor = color,
isSwitch = ShamrockConfig.isInjectPacket(ctx)
) {
ShamrockConfig.setInjectPacket(ctx, it)
ShamrockConfig.pushUpdate(ctx)
return@Function true
}
Function( Function(
title = LocalString.antiTrace, title = LocalString.antiTrace,
desc = LocalString.antiTraceDesc, desc = LocalString.antiTraceDesc,
descColor = color, descColor = color,
isSwitch = ShamrockConfig.isAntiTrace(ctx) isSwitch = ShamrockConfig[ctx, AntiJvmTrace]
) { ) {
ShamrockConfig.setAntiTrace(ctx, it) ShamrockConfig[ctx, AntiJvmTrace] = it
ShamrockConfig.pushUpdate(ctx) scope.toast(ctx, LocalString.restartToast)
return@Function true return@Function true
} }
@ -221,7 +176,7 @@ fun LabFragment() {
}.onSuccess { }.onSuccess {
Function( Function(
title = "反检测加强", title = "反检测加强",
desc = "可能导致某些设备频繁闪退", desc = "可能导致某些设备频繁闪退,将拦截环境包上报。",
descColor = color, descColor = color,
isSwitch = it.getBoolean("super_anti", false) isSwitch = it.getBoolean("super_anti", false)
) { v -> ) { v ->
@ -277,35 +232,23 @@ fun LabFragment() {
title = "自发消息推送", title = "自发消息推送",
desc = "推送Bot发送的消息未做特殊处理请勿打开。", desc = "推送Bot发送的消息未做特殊处理请勿打开。",
descColor = it, descColor = it,
isSwitch = ShamrockConfig.enableSelfMsg(ctx) isSwitch = ShamrockConfig[ctx, EnableSelfMessage]
) { ) {
ShamrockConfig.setEnableSelfMsg(ctx, it) ShamrockConfig[ctx, EnableSelfMessage] = it
ShamrockConfig.pushUpdate(ctx) InitHandler.update(ctx)
return@Function true return@Function true
} }
Function( Function(
title = "同步消息推送类型异换", title = "启用旧版资源上传系统",
desc = "推送来自同号异设备消息,将同步消息作为自发消息推送", desc = "如果NT内核无法上传资源请打开本开关",
descColor = it, descColor = it,
isSwitch = ShamrockConfig.enableSyncMsgAsSentMsg(ctx) isSwitch = ShamrockConfig[ctx, EnableOldBDH]
) { ) {
ShamrockConfig.setEnableSyncMsgAsSentMsg(ctx, it) ShamrockConfig[ctx, EnableOldBDH] = it
ShamrockConfig.pushUpdate(ctx) InitHandler.update(ctx)
return@Function true return@Function true
} }
/*
Function(
title = "使用纯数字ECHO",
desc = "在部分强类型语言框架,需要打开此开关。",
descColor = it,
isSwitch = ShamrockConfig.isEchoNumber(ctx)
) {
ShamrockConfig.setEchoNumber(ctx, it)
ShamrockConfig.pushUpdate(ctx)
return@Function true
}*/
} }
} }
} }

View File

@ -1,108 +0,0 @@
@file:OptIn(DelicateCoroutinesApi::class)
package moe.fuqiuluo.shamrock.ui.service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.content.ContextCompat.startActivity
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.request.url
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import moe.fuqiuluo.shamrock.remote.structures.CommonResult
import moe.fuqiuluo.shamrock.remote.structures.CurrentAccount
import moe.fuqiuluo.shamrock.remote.structures.Status
import moe.fuqiuluo.shamrock.tools.GlobalClient
import moe.fuqiuluo.shamrock.ui.app.AppRuntime.AccountInfo
import moe.fuqiuluo.shamrock.ui.app.AppRuntime.log
import moe.fuqiuluo.shamrock.ui.app.AppRuntime.state
import moe.fuqiuluo.shamrock.ui.app.Level
import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig
import moe.fuqiuluo.shamrock.ui.service.internal.broadcastToModule
import java.net.ConnectException
import java.util.Timer
import kotlin.concurrent.timer
object DashboardInitializer {
private var servicePort: Int = 0
private lateinit var heartbeatTimer: Timer
operator fun invoke(context: Context, port: Int) {
servicePort = port
initHeartbeat(true, context)
}
private fun initHeartbeat(reload: Boolean, context: Context) {
if (::heartbeatTimer.isInitialized && !reload) {
return
}
if (::heartbeatTimer.isInitialized) {
heartbeatTimer.cancel()
}
heartbeatTimer = timer("heartbeat", false, 0, 1000L * 30) {
checkService(context)
}
}
private fun checkService(context: Context) {
GlobalScope.launch {
try {
GlobalClient.get {
url("http://127.0.0.1:$servicePort/get_account_info")
val token = ShamrockConfig.getToken(context)
if (token.isNotBlank()) {
//header("Authorization", "Bearer $token")
parameter("access_token", token)
}
}.let {
if (it.status == HttpStatusCode.OK) {
val result: CommonResult<CurrentAccount> = Json.decodeFromString(it.bodyAsText())
state.isFined.value = result.retcode == 0
if (result.retcode == Status.InternalHandlerError.code) {
log("账号未登录。", Level.WARN)
} else if (result.retcode != 0) {
log("尝试从接口获取账号信息失败,未知错误。", Level.ERROR)
} else {
AccountInfo.let { account ->
account.uin.value = result.data.uin.toString()
account.nick.value = result.data.nick
}
}
} else {
state.isFined.value = false
log("尝试从接口获取账号信息失败,服务运行异常。", Level.ERROR)
}
}
} catch (e: ConnectException) {
state.isFined.value = false
context.broadcastToModule {
putExtra("__cmd", "checkAndStartService")
}
if (ShamrockConfig.enableAutoStart(context)) {
log("检测到Service死亡正在尝试重新启动")
GlobalScope.launch(Dispatchers.Main) {
val packageName = "com.tencent.mobileqq"
val className = "com.tencent.mobileqq.activity.SplashActivity"
val intent = Intent()
intent.component = ComponentName(packageName, className)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.putExtra("from", "shamrock")
startActivity(context, intent, Bundle.EMPTY)
}
}
} catch (e: Throwable) {
state.isFined.value = false
log(e.stackTraceToString(), Level.ERROR)
}
}
}
}

View File

@ -1,15 +0,0 @@
package moe.fuqiuluo.shamrock.ui.service.handlers
import android.content.ContentValues
import android.content.Context
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
import moe.fuqiuluo.shamrock.ui.service.DashboardInitializer
object FetchPortHandler: ModuleHandler() {
override val cmd: String = "success"
override fun onReceive(callbackId: Int, values: ContentValues, context: Context) {
AppRuntime.state.supportVoice.value = values.getAsBoolean("voice")
DashboardInitializer(context, values.getAsInteger("port"))
}
}

View File

@ -3,13 +3,37 @@ package moe.fuqiuluo.shamrock.ui.service.handlers
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import moe.fuqiuluo.shamrock.ui.app.AppRuntime import moe.fuqiuluo.shamrock.ui.app.AppRuntime
import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig import moe.fuqiuluo.shamrock.app.config.ShamrockConfig
import moe.fuqiuluo.shamrock.config.*
internal object InitHandler: ModuleHandler() { internal object InitHandler: ModuleHandler() {
override val cmd: String = "init" override val cmd: String = "init"
override fun onReceive(callbackId: Int, values: ContentValues, context: Context) { override fun onReceive(callbackId: Int, values: ContentValues, context: Context) {
update(context)
}
fun update(context: Context) {
AppRuntime.log("推送QQ进程初始化设置数据包成功...") AppRuntime.log("推送QQ进程初始化设置数据包成功...")
callback(context, callbackId, ShamrockConfig.getConfigMap(context))
val maps = hashMapOf<String, Any?>()
ActiveRPC.update(context, maps)
AliveReply.update(context, maps)
AntiJvmTrace.update(context, maps)
DebugMode.update(context, maps)
EnableOldBDH.update(context, maps)
EnableSelfMessage.update(context, maps)
ForceTablet.update(context, maps)
PassiveRPC.update(context, maps)
ResourceGroup.update(context, maps)
RPCAddress.update(context, maps)
RPCPort.update(context, maps)
callback(context, 1, maps)
}
private inline fun <reified T> ConfigKey<T>.update(context: Context, map: HashMap<String, Any?>) {
map[name()] = ShamrockConfig[context, this]
} }
} }

View File

@ -29,6 +29,7 @@ abstract class ModuleHandler {
} }
} }
} }
putExtra("__cmd", cmd)
putExtra("__hash", callbackId) putExtra("__hash", callbackId)
} }
} }

View File

@ -0,0 +1,49 @@
package moe.fuqiuluo.shamrock.ui.service.handlers
import android.content.ContentValues
import android.content.Context
import android.widget.Toast
import moe.fuqiuluo.shamrock.tools.GlobalUi
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
import java.util.Timer
import kotlin.concurrent.timer
import kotlin.concurrent.timerTask
object SwitchStatus: ModuleHandler() {
override val cmd: String
get() = "switch_status"
private var lastActiveTime = 0L
private var timer: Timer? = null
override fun onReceive(callbackId: Int, values: ContentValues, context: Context) {
val voiceSwitch = values.getAsBoolean("voice")
val nickname = values.getAsString("nickname")
val account = values.getAsString("account")
if (lastActiveTime == 0L) GlobalUi.post {
Toast.makeText(context, "激活成功", Toast.LENGTH_SHORT).show()
}
AppRuntime.state.apply {
isFined.value = true
coreVersion.value = values.getAsString("core_version")
coreName.value = "LSPosed"
supportVoice.value = voiceSwitch
}
AppRuntime.AccountInfo.apply {
uin.value = account
nick.value = nickname
}
lastActiveTime = System.currentTimeMillis()
startTimer()
}
private fun startTimer() {
timer?.cancel()
timer = timer("SwitchStatus", true, 0, 5_000) {
if (lastActiveTime != 0L && System.currentTimeMillis() - lastActiveTime > 30 * 1000) {
AppRuntime.state.isFined.value = false
lastActiveTime = 0
}
}
}
}

View File

@ -5,9 +5,9 @@ import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.database.Cursor import android.database.Cursor
import android.net.Uri
import moe.fuqiuluo.shamrock.ui.service.ModuleTalker import moe.fuqiuluo.shamrock.ui.service.ModuleTalker
import moe.fuqiuluo.shamrock.ui.service.handlers.* import moe.fuqiuluo.shamrock.ui.service.handlers.*
import android.net.Uri
class MultifunctionalProvider: ContentProvider() { class MultifunctionalProvider: ContentProvider() {
override fun insert(uri: Uri, content: ContentValues?): Uri { override fun insert(uri: Uri, content: ContentValues?): Uri {
@ -28,8 +28,8 @@ class MultifunctionalProvider: ContentProvider() {
override fun onCreate(): Boolean { override fun onCreate(): Boolean {
ModuleTalker.register(InitHandler) ModuleTalker.register(InitHandler)
ModuleTalker.register(FetchPortHandler)
ModuleTalker.register(LogHandler) ModuleTalker.register(LogHandler)
ModuleTalker.register(SwitchStatus)
return true return true
} }
@ -58,7 +58,7 @@ class MultifunctionalProvider: ContentProvider() {
inline fun Context.broadcastToModule(intentBuilder: Intent.() -> Unit) { inline fun Context.broadcastToModule(intentBuilder: Intent.() -> Unit) {
val intent = Intent() val intent = Intent()
intent.action = "moe.fuqiuluo.xqbot.dynamic" intent.action = "moe.fuqiuluo.kritor.dynamic"
intent.intentBuilder() intent.intentBuilder()
sendBroadcast(intent) sendBroadcast(intent)
} }

View File

@ -1,23 +1,35 @@
package moe.fuqiuluo.shamrock.ui.tools package moe.fuqiuluo.shamrock.ui.tools
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColor import androidx.compose.animation.animateColor
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.Indication import androidx.compose.foundation.Indication
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
@ -31,8 +43,10 @@ import androidx.compose.ui.layout.LastBaseline
import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -135,20 +149,18 @@ private fun TabBaselineLayout(
text: @Composable (() -> Unit)?, text: @Composable (() -> Unit)?,
icon: @Composable (() -> Unit)? icon: @Composable (() -> Unit)?
) { ) {
Layout( Layout({
{ if (text != null) {
if (text != null) { Box(
Box( Modifier
Modifier .layoutId("text")
.layoutId("text") .padding(horizontal = HorizontalTextPadding)
.padding(horizontal = HorizontalTextPadding) ) { text() }
) { text() }
}
if (icon != null) {
Box(Modifier.layoutId("icon")) { icon() }
}
} }
) { measurables, constraints -> if (icon != null) {
Box(Modifier.layoutId("icon")) { icon() }
}
}) { measurables, constraints ->
val textPlaceable = text?.let { val textPlaceable = text?.let {
measurables.first { it.layoutId == "text" }.measure( measurables.first { it.layoutId == "text" }.measure(
// Measure with loose constraints for height as we don't want the text to take up more // Measure with loose constraints for height as we don't want the text to take up more
@ -247,21 +259,66 @@ fun ShamrockTab(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
text: @Composable (() -> Unit)? = null,
icon: @Composable (() -> Unit)? = null,
selectedContentColor: Color = GlobalColor.TabSelected, selectedContentColor: Color = GlobalColor.TabSelected,
unselectedContentColor: Color = selectedContentColor, unselectedContentColor: Color = selectedContentColor,
indication: Indication? = rememberRipple(bounded = true, color = selectedContentColor), indication: Indication? = rememberRipple(bounded = true, color = selectedContentColor),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
titleWithIcon: Pair<String, Int>,
visibleState: MutableTransitionState<Boolean>
) { ) {
val styledText: @Composable (() -> Unit)? = text?.let { var text: @Composable (() -> Unit)? = null
@Composable { var icon: @Composable (() -> Unit)? = null
val style =
MaterialTheme.typography.fromToken(PrimaryNavigationTabTokens.LabelTextFont) if (!selected) {
.copy(textAlign = TextAlign.Center) icon = {
ProvideTextStyle(style, content = text) Icon(
painter = painterResource(id = titleWithIcon.second),
contentDescription = titleWithIcon.first,
tint = Color.Unspecified,
modifier = Modifier
.height(24.dp)
.width(24.dp)
.padding(bottom = 5.dp)
.indication(
remember { MutableInteractionSource() },
rememberRipple(color = Color.Transparent)
)
)
}
} else {
text = {
val style = MaterialTheme.typography
.fromToken(PrimaryNavigationTabTokens.LabelTextFont)
.copy(textAlign = TextAlign.Center)
ProvideTextStyle(style) {
AnimatedVisibility(
visibleState = visibleState,
enter = remember {
scaleIn(animationSpec = TweenSpec(150, easing = FastOutLinearInEasing))
},
exit = remember {
scaleOut(animationSpec = TweenSpec(150, easing = FastOutSlowInEasing))
},
modifier = Modifier
) {
Text(
text = titleWithIcon.first,
color = GlobalColor.TabItem,
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.padding(bottom = 5.dp)
.indication(
remember { MutableInteractionSource() },
rememberRipple(color = Color.Transparent)
)
)
}
}
} }
} }
ShamrockTab( ShamrockTab(
selected, selected,
onClick, onClick,
@ -272,7 +329,10 @@ fun ShamrockTab(
interactionSource, interactionSource,
indication indication
) { ) {
TabBaselineLayout(icon = icon, text = styledText) TabBaselineLayout(
icon = icon,
text = text
)
} }
} }

View File

@ -2,16 +2,14 @@ package moe.fuqiuluo.shamrock
import org.junit.Test import org.junit.Test
import org.junit.Assert.*
/** /**
* Example local unit test, which will execute on the development machine (host). * Example local unit test, which will execute on the development machine (host).
* *
* See [testing documentation](http://d.android.com/tools/testing). * See [testing documentation](http://d.android.com/tools/testing).
*/ */
class ExampleUnitTest { class ExampleUnitTest {
@Test @Test
fun addition_isCorrect() { fun test() {
assertEquals(4, 2 + 2)
} }
} }

View File

@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id("com.android.application") version "8.2.0" apply false id("com.android.application") version "8.2.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.20" apply false id("org.jetbrains.kotlin.android") version "1.9.22" apply false
id("com.android.library") version "8.2.0" apply false id("com.android.library") version "8.2.0" apply false
} }

View File

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

View File

@ -7,10 +7,6 @@ val DEPENDENCY_ANDROIDX = arrayOf(
"androidx.activity:activity-compose:1.7.2", "androidx.activity:activity-compose:1.7.2",
) )
const val DEPENDENCY_JSON5K = "io.github.xn32:json5k:0.3.0"
const val DEPENDENCY_PROTOBUF = "com.google.protobuf:protobuf-java:3.24.0"
const val DEPENDENCY_JAVA_WEBSOCKET = "org.java-websocket:Java-WebSocket:1.5.4"
fun room(name: String) = "androidx.room:room-$name:${Versions.roomVersion}" fun room(name: String) = "androidx.room:room-$name:${Versions.roomVersion}"
fun kotlinx(name: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$name:$version" fun kotlinx(name: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$name:$version"
@ -19,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"
} }

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

@ -1,7 +1,7 @@
plugins { plugins {
kotlin("jvm") kotlin("jvm")
id("com.google.devtools.ksp") version "1.9.21-1.0.15" id("com.google.devtools.ksp") version "1.9.22-1.0.17"
kotlin("plugin.serialization") version "1.9.21" kotlin("plugin.serialization") version "1.9.22"
} }
ksp { ksp {
@ -15,7 +15,7 @@ dependencies {
implementation("com.google.devtools.ksp:symbol-processing-api:1.9.21-1.0.15") implementation("com.google.devtools.ksp:symbol-processing-api:1.9.21-1.0.15")
implementation("com.squareup:kotlinpoet:1.14.2") implementation("com.squareup:kotlinpoet:1.14.2")
implementation(DEPENDENCY_PROTOBUF) //implementation(DEPENDENCY_PROTOBUF)
implementation(kotlinx("serialization-protobuf", "1.6.2")) implementation(kotlinx("serialization-protobuf", "1.6.2"))
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")

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

@ -1,85 +0,0 @@
@file:OptIn(KspExperimental::class)
@file:Suppress("LocalVariableName", "UNCHECKED_CAST")
package moe.fuqiuluo.ksp.impl
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getAnnotationsByType
import com.google.devtools.ksp.getClassDeclarationByName
import com.google.devtools.ksp.getKotlinClassByName
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSVisitorVoid
import com.google.devtools.ksp.validate
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import moe.fuqiuluo.symbols.OneBotHandler
class OneBotHandlerProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
): SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val ActionManagerNode = resolver.getClassDeclarationByName("moe.fuqiuluo.shamrock.remote.action.ActionManager")
?: resolver.getKotlinClassByName("moe.fuqiuluo.shamrock.remote.action.ActionManager")
?: resolver.getClassDeclarationByName("ActionManager")
val symbols = resolver.getSymbolsWithAnnotation(OneBotHandler::class.qualifiedName!!)
val unableToProcess = symbols.filterNot { it.validate() }
if (ActionManagerNode != null) {
val oneBotHandlers = (symbols.filter {
it is KSClassDeclaration && it.validate() && it.classKind == ClassKind.OBJECT
} as Sequence<KSClassDeclaration>).toList()
if (oneBotHandlers.isNotEmpty()) {
ActionManagerNode.accept(ActionManagerVisitor(oneBotHandlers), Unit)
}
}
return unableToProcess.toList()
}
inner class ActionManagerVisitor(
private val actionHandlers: List<KSClassDeclaration>
): KSVisitorVoid() {
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
val packageName = classDeclaration.packageName.asString()
// generate kotlin `init { }`
val fileSpec = FileSpec.builder(packageName, classDeclaration.qualifiedName?.asString() ?: run {
throw IllegalStateException("ActionManagerVisitor: classDeclaration.qualifiedName is null")
}).addFunction(FunSpec.builder("initManager").apply {
actionHandlers.forEach { handler ->
// fetch the params of the annotation
val annotation = handler.getAnnotationsByType(OneBotHandler::class).first()
val actionName = annotation.actionName
val alias = annotation.alias
alias.forEach { name ->
addStatement("actionMap[\"$name\"] = ${handler.simpleName.asString()}")
}
addStatement("actionMap[\"$actionName\"] = ${handler.simpleName.asString()}")
}
}.build()).apply {
addImport("moe.fuqiuluo.shamrock.remote.action.ActionManager", "actionMap")
actionHandlers.forEach {
addImport(it.packageName.asString(), it.simpleName.asString())
}
}.build()
codeGenerator.createNewFile(
dependencies = Dependencies(aggregating = false),
packageName = packageName,
fileName = "Auto" + classDeclaration.simpleName.asString()
).use { outputStream ->
outputStream.writer().use {
fileSpec.writeTo(it)
}
}
}
}
}

View File

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

View File

@ -1,5 +1,5 @@
@file:Suppress("UNCHECKED_CAST", "LocalVariableName", "PrivatePropertyName") @file:Suppress("UNCHECKED_CAST", "LocalVariableName", "PrivatePropertyName")
@file:OptIn(KspExperimental::class) @file:OptIn(KspExperimental::class, KspExperimental::class)
package moe.fuqiuluo.ksp.impl package moe.fuqiuluo.ksp.impl
@ -27,10 +27,14 @@ class XposedHookProcessor(
private val logger: KSPLogger private val logger: KSPLogger
): SymbolProcessor { ): SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> { override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation(XposedHook::class.qualifiedName!!) val symbols = resolver.getSymbolsWithAnnotation(
annotationName = XposedHook::class.qualifiedName!!,
inDepth = true
)
logger.warn("Found ${symbols.count()} classes annotated with XposedHook")
val unableToProcess = symbols.filterNot { it.validate() } val unableToProcess = symbols.filterNot { it.validate() }
val actions = (symbols.filter { val actions = (symbols.filter {
it is KSClassDeclaration && it.validate() && it.classKind == ClassKind.CLASS it is KSClassDeclaration && it.classKind == ClassKind.CLASS
} as Sequence<KSClassDeclaration>).toList() } as Sequence<KSClassDeclaration>).toList()
if (actions.isNotEmpty()) { if (actions.isNotEmpty()) {
@ -46,7 +50,7 @@ class XposedHookProcessor(
} }
val context = ClassName("android.content", "Context") val context = ClassName("android.content", "Context")
val packageName = "moe.fuqiuluo.shamrock.xposed.hooks" val packageName = "moe.fuqiuluo.shamrock.xposed.actions"
val fileSpec = FileSpec.builder(packageName, "AutoActionLoader").addFunction(FunSpec.builder("runFirstActions") val fileSpec = FileSpec.builder(packageName, "AutoActionLoader").addFunction(FunSpec.builder("runFirstActions")
.addParameter("ctx", context) .addParameter("ctx", context)
.apply { .apply {
@ -96,16 +100,6 @@ class XposedHookProcessor(
} }
} }
} }
return unableToProcess.toList() return unableToProcess.toList()
} }
inner class ActionLoaderVisitor(
private val firstActions: List<KSClassDeclaration>,
private val serviceActions: List<KSClassDeclaration>,
): KSVisitorVoid() {
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
}
}
} }

View File

@ -4,12 +4,12 @@ import com.google.auto.service.AutoService
import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider import com.google.devtools.ksp.processing.SymbolProcessorProvider
import moe.fuqiuluo.ksp.impl.OneBotHandlerProcessor import moe.fuqiuluo.ksp.impl.GrpcProcessor
@AutoService(SymbolProcessorProvider::class) @AutoService(SymbolProcessorProvider::class)
class OneBotHandlerProcessorProvider: SymbolProcessorProvider { class GrpcProvider: SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return OneBotHandlerProcessor( return GrpcProcessor(
environment.codeGenerator, environment.codeGenerator,
environment.logger environment.logger
) )

View File

@ -1,8 +1,10 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
id("com.android.library") id("com.android.library")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
kotlin("plugin.serialization") version "1.9.21" kotlin("plugin.serialization") version "1.9.22"
id("com.google.devtools.ksp") version "1.9.21-1.0.15" id("com.google.devtools.ksp") version "1.9.22-1.0.17"
} }
android { android {
@ -35,11 +37,14 @@ 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"))
implementation(project(":annotations")) implementation(project(":annotations"))
ksp(project(":processor")) ksp(project(":processor"))
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
} }

View File

@ -32,6 +32,11 @@ data class AdaptShareInfoReq(
@Serializable @Serializable
data class Template( data class Template(
@ProtoNumber(1) var templateId: UInt? = null, @ProtoNumber(1) var templateId: ULong? = null,
@ProtoNumber(2) var templateData: ByteArray? = null, @ProtoNumber(2) var templateData: ByteArray? = null,
) )
@Serializable
data class AdaptShareInfoResp(
@ProtoNumber(2) var json: String? = null,
): Protobuf<AdaptShareInfoResp>

View File

@ -15,7 +15,7 @@ data class ContentHead(
@ProtoNumber(8) val u6: Int? = null, @ProtoNumber(8) val u6: Int? = null,
@ProtoNumber(9) val u7: Int? = null, @ProtoNumber(9) val u7: Int? = null,
@ProtoNumber(11) val msgSeq: Long? = null, @ProtoNumber(11) val msgSeq: Long? = null,
@ProtoNumber(12) val msgRandom: Long = Long.MIN_VALUE, @ProtoNumber(12) val msgRandom: Long = Long.MIN_VALUE, // 0x0100000000000000L xor msgViaRandom
@ProtoNumber(14) val u4: Long? = null, @ProtoNumber(14) val u4: Long? = null,
@ProtoNumber(15) val forwardHead: ForwardHead? = null, @ProtoNumber(15) val forwardHead: ForwardHead? = null,
@ProtoNumber(28) val u5: Long? = null @ProtoNumber(28) val u5: Long? = null

View File

@ -7,9 +7,9 @@ import kotlinx.serialization.protobuf.ProtoNumber
data class Ptt( data class Ptt(
@ProtoNumber(1) var fileType: UInt?=null, @ProtoNumber(1) var fileType: UInt?=null,
@ProtoNumber(2) var srcUin: ULong?=null, @ProtoNumber(2) var srcUin: ULong?=null,
@ProtoNumber(3) var fileUuid: ByteArray?=null, @ProtoNumber(3) var fileUuid: String?=null,
@ProtoNumber(4) var fileMd5: ByteArray?=null, @ProtoNumber(4) var fileMd5: ByteArray?=null,
@ProtoNumber(5) var fileName: ByteArray?=null, @ProtoNumber(5) var fileName: String?=null,
@ProtoNumber(6) var fileSize: UInt?=null, @ProtoNumber(6) var fileSize: UInt?=null,
@ProtoNumber(7) var reserve: ByteArray?=null, @ProtoNumber(7) var reserve: ByteArray?=null,
@ProtoNumber(8) var fileId: UInt?=null, @ProtoNumber(8) var fileId: UInt?=null,
@ -22,11 +22,19 @@ data class Ptt(
@ProtoNumber(15) var magicPttIndex: UInt?=null, @ProtoNumber(15) var magicPttIndex: UInt?=null,
@ProtoNumber(16) var voiceSwitch: UInt?=null, @ProtoNumber(16) var voiceSwitch: UInt?=null,
@ProtoNumber(17) var pttUrl: ByteArray?=null, @ProtoNumber(17) var pttUrl: ByteArray?=null,
@ProtoNumber(18) var groupFileKey: ByteArray?=null, @ProtoNumber(18) var groupFileKey: String?=null,
@ProtoNumber(19) var time: UInt?=null, @ProtoNumber(19) var time: UInt?=null,
@ProtoNumber(20) var downPara: ByteArray?=null, @ProtoNumber(20) var downPara: ByteArray?=null,
@ProtoNumber(29) var format: UInt?=null, @ProtoNumber(29) var format: UInt?=null,
@ProtoNumber(30) var pbReserve: ByteArray?=null, @ProtoNumber(30) var pbReserve: PbReserve?=null,
@ProtoNumber(31) var rptPttUrls: List<String>? = null, @ProtoNumber(31) var rptPttUrls: List<String>? = null,
@ProtoNumber(32) var downloadFlag: UInt?=null, @ProtoNumber(32) var downloadFlag: UInt?=null,
) ){
companion object{
@Serializable
data class PbReserve(
@ProtoNumber(2) var magic: Int?=null,
@ProtoNumber(7) var reserve: Int?=null,
)
}
}

View File

@ -9,9 +9,9 @@ import kotlinx.serialization.protobuf.ProtoNumber
@Serializable @Serializable
data class RichText( data class RichText(
@ProtoNumber(1) val attr: Attr? = null, @ProtoNumber(1) val attr: Attr? = null,
@ProtoNumber(2) val elements: List<Elem>? = null, @ProtoNumber(2) var elements: List<Elem>? = null,
@ProtoNumber(3) val not_online_file: NotOnlineFile? = null, @ProtoNumber(3) var not_online_file: NotOnlineFile? = null,
@ProtoNumber(4) val ptt: Ptt? = null, @ProtoNumber(4) var ptt: Ptt? = null,
@ProtoNumber(5) val tmp_ptt: TmpPtt? = null, @ProtoNumber(5) val tmp_ptt: TmpPtt? = null,
@ProtoNumber(6) val trans_211_tmp_msg: Trans211TmpMsg? = null, @ProtoNumber(6) val trans_211_tmp_msg: Trans211TmpMsg? = null,
) )

View File

@ -30,7 +30,7 @@ data class CustomFace(
@ProtoNumber(23) var height: UInt? = null, @ProtoNumber(23) var height: UInt? = null,
@ProtoNumber(24) var source: UInt? = null, @ProtoNumber(24) var source: UInt? = null,
@ProtoNumber(25) var size: UInt? = null, @ProtoNumber(25) var size: UInt? = null,
@ProtoNumber(26) var origin: UInt? = null, @ProtoNumber(26) var origin: Boolean? = null,
@ProtoNumber(27) var thumbWidth: UInt? = null, @ProtoNumber(27) var thumbWidth: UInt? = null,
@ProtoNumber(28) var thumbHeight: UInt? = null, @ProtoNumber(28) var thumbHeight: UInt? = null,
@ProtoNumber(29) var showLen: UInt? = null, @ProtoNumber(29) var showLen: UInt? = null,
@ -39,11 +39,26 @@ data class CustomFace(
@ProtoNumber(32) var width400: UInt? = null, @ProtoNumber(32) var width400: UInt? = null,
@ProtoNumber(33) var height400: UInt? = null, @ProtoNumber(33) var height400: UInt? = null,
@ProtoNumber(34) var pbReserve: PbReserve? = null, @ProtoNumber(34) var pbReserve: PbReserve? = null,
){ ) {
companion object{ companion object {
@Serializable @Serializable
data class PbReserve( data class PbReserve(
@ProtoNumber(1) var field1: Int? = null @ProtoNumber(1) var field1: Int? = null,
@ProtoNumber(3) var field3: Int? = null,
@ProtoNumber(4) var field4: Int? = null,
@ProtoNumber(10) var field10: Int? = null,
@ProtoNumber(21) var field21: Object1? = null,
@ProtoNumber(31) var field31: String? = null
)
@Serializable
data class Object1(
@ProtoNumber(1) var field1: Int? = null,
@ProtoNumber(2) var field2: String? = null,
@ProtoNumber(3) var field3: Int? = null,
@ProtoNumber(4) var field4: Int? = null,
@ProtoNumber(5) var field5: Int? = null,
@ProtoNumber(7) var md5Str: String? = null
) )
} }
} }

View File

@ -5,8 +5,8 @@ import kotlinx.serialization.protobuf.ProtoNumber
@Serializable @Serializable
data class FaceMsg( data class FaceMsg(
@ProtoNumber(1) val id: Int? = null, @ProtoNumber(1) val index: Int? = null,
@ProtoNumber(2) var old: ByteArray? = null, @ProtoNumber(2) var old: ByteArray? = null,
@ProtoNumber(11) var buf: ByteArray? = null, @ProtoNumber(11) var buf: ByteArray? = null,
) )

View File

@ -12,7 +12,7 @@ data class GeneralFlags(
@ProtoNumber(4) val rpId: ByteArray? = null, @ProtoNumber(4) val rpId: ByteArray? = null,
@ProtoNumber(5) val prpFold: UInt? = null, @ProtoNumber(5) val prpFold: UInt? = null,
@ProtoNumber(6) val longTextFlag: UInt? = null, @ProtoNumber(6) val longTextFlag: UInt? = null,
@ProtoNumber(7) val longTextResid: ByteArray? = null, @ProtoNumber(7) val longTextResid: String? = null,
@ProtoNumber(8) val groupType: UInt? = null, @ProtoNumber(8) val groupType: UInt? = null,
@ProtoNumber(9) val toUinFlag: UInt? = null, @ProtoNumber(9) val toUinFlag: UInt? = null,
@ProtoNumber(10) val glamourLevel: UInt? = null, @ProtoNumber(10) val glamourLevel: UInt? = null,

View File

@ -5,19 +5,19 @@ import kotlinx.serialization.protobuf.ProtoNumber
@Serializable @Serializable
data class NotOnlineImage( data class NotOnlineImage(
@ProtoNumber(1) val filePath: ByteArray? = null, @ProtoNumber(1) val filePath: String? = null,
@ProtoNumber(2) val fileLen: UInt? = null, @ProtoNumber(2) val fileLen: UInt? = null,
@ProtoNumber(3) val downloadPath: ByteArray? = null, @ProtoNumber(3) val downloadPath: String? = null,
@ProtoNumber(4) val oldVerSendFile: ByteArray? = null, @ProtoNumber(4) val oldVerSendFile: ByteArray? = null,
@ProtoNumber(5) val imgType: UInt? = null, @ProtoNumber(5) val imgType: UInt? = null,
@ProtoNumber(6) val previewsImage: ByteArray? = null, @ProtoNumber(6) val previewsImage: ByteArray? = null,
@ProtoNumber(7) val picMd5: ByteArray? = null, @ProtoNumber(7) val picMd5: ByteArray? = null,
@ProtoNumber(8) val picHeight: UInt? = null, @ProtoNumber(8) val picHeight: UInt? = null,
@ProtoNumber(9) val picWidth: UInt? = null, @ProtoNumber(9) val picWidth: UInt? = null,
@ProtoNumber(10) val resId: ByteArray? = null, @ProtoNumber(10) val resId: String? = null, // md5 + ".jpg"
@ProtoNumber(11) val flag: ByteArray? = null, @ProtoNumber(11) val flag: ByteArray? = null,
@ProtoNumber(12) val thumbUrl: String? = null, @ProtoNumber(12) val thumbUrl: String? = null,
@ProtoNumber(13) val original: UInt? = null, @ProtoNumber(13) val original: Boolean? = null,
@ProtoNumber(14) val bigUrl: String? = null, @ProtoNumber(14) val bigUrl: String? = null,
@ProtoNumber(15) val origUrl: String? = null, @ProtoNumber(15) val origUrl: String? = null,
@ProtoNumber(16) val bizType: UInt? = null, @ProtoNumber(16) val bizType: UInt? = null,
@ -39,8 +39,23 @@ data class NotOnlineImage(
@Serializable @Serializable
data class PbReserve( data class PbReserve(
@ProtoNumber(1) var field1: Int? = null, @ProtoNumber(1) var field1: Int? = null,
@ProtoNumber(3) var field3: Int? = null,
@ProtoNumber(4) var field4: Int? = null,
@ProtoNumber(8) var field8: String? = null, @ProtoNumber(8) var field8: String? = null,
@ProtoNumber(30) var url: String? = null @ProtoNumber(10) var field10: Int? = null,
@ProtoNumber(20) var field20: Object1? = null,
@ProtoNumber(30) var url: String? = null,
@ProtoNumber(31) var md5Str: String? = null
)
@Serializable
data class Object1(
@ProtoNumber(1) var field1: Int? = null,
@ProtoNumber(2) var field2: String? = null,
@ProtoNumber(3) var field3: Int? = null,
@ProtoNumber(4) var field4: Int? = null,
@ProtoNumber(5) var field5: Int? = null,
@ProtoNumber(7) var field7: String? = null
) )
} }
} }

View File

@ -21,7 +21,7 @@ data class SourceMsg(
companion object { companion object {
@Serializable @Serializable
data class PbReserve( data class PbReserve(
@ProtoNumber(3) var field3: ULong? = null, @ProtoNumber(3) var msgRand: ULong? = null,
@ProtoNumber(6) var senderUid: String? = null, @ProtoNumber(6) var senderUid: String? = null,
@ProtoNumber(7) var receiverUid: String? = null, @ProtoNumber(7) var receiverUid: String? = null,
@ProtoNumber(8) var field8: Int? = null, @ProtoNumber(8) var field8: Int? = null,

View File

@ -10,5 +10,12 @@ data class TextMsg(
@ProtoNumber(3) val attr6Buf: ByteArray? = null, @ProtoNumber(3) val attr6Buf: ByteArray? = null,
@ProtoNumber(4) val attr7Buf: ByteArray? = null, @ProtoNumber(4) val attr7Buf: ByteArray? = null,
@ProtoNumber(11) val buf: ByteArray? = null, @ProtoNumber(11) val buf: ByteArray? = null,
@ProtoNumber(12) val pbReserve: ByteArray? = null, @ProtoNumber(12) val pbReserve: PbReserve? = null,
) ){
companion object {
@Serializable
data class PbReserve(
@ProtoNumber(1) val field1: String? = null, // [打 call]] 请使用最新版手机 QQ 体验新功能
)
}
}

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
@ -23,7 +23,7 @@ data class Row(
@Serializable @Serializable
data class Button( data class Button(
@ProtoNumber(1) val id: Int? = null, @ProtoNumber(1) val id: String? = null,
@ProtoNumber(2) val renderData: RenderData? = null, @ProtoNumber(2) val renderData: RenderData? = null,
@ProtoNumber(3) val action: Action? = null, @ProtoNumber(3) val action: Action? = null,
) )
@ -41,8 +41,8 @@ data class Action(
@ProtoNumber(2) val permission: Permission? = null, @ProtoNumber(2) val permission: Permission? = null,
@ProtoNumber(4) val unsupportTips: String? = null, @ProtoNumber(4) val unsupportTips: String? = null,
@ProtoNumber(5) val data: String? = null, @ProtoNumber(5) val data: String? = null,
@ProtoNumber(6) val reply: Boolean? = null, @ProtoNumber(7) val reply: Boolean? = null,
@ProtoNumber(7) val enter: Boolean? = null, @ProtoNumber(8) val enter: Boolean? = null,
) )
@Serializable @Serializable

View File

@ -11,7 +11,7 @@ data class QFaceExtra(
@ProtoNumber(3) val faceId: Int? = null, @ProtoNumber(3) val faceId: Int? = null,
@ProtoNumber(4) val field4: Int? = null, @ProtoNumber(4) val field4: Int? = null,
@ProtoNumber(5) val field5: Int? = null, @ProtoNumber(5) val field5: Int? = null,
@ProtoNumber(6) val field6: String? = null, @ProtoNumber(6) val result: String? = null,
@ProtoNumber(7) val faceText: String? = null, @ProtoNumber(7) val faceText: String? = null,
@ProtoNumber(9) val field9: Int? = null @ProtoNumber(9) val field9: Int? = null
) : Protobuf<QFaceExtra> ) : Protobuf<QFaceExtra>

View File

@ -31,7 +31,7 @@ data class RecvLongMsgInfo(
data class SendLongMsgInfo( data class SendLongMsgInfo(
@ProtoNumber(1) val type: Int? = null, @ProtoNumber(1) val type: Int? = null,
@ProtoNumber(2) val uid: LongMsgUid? = null, @ProtoNumber(2) val uid: LongMsgUid? = null,
@ProtoNumber(3) val groupUin: Int? = null, @ProtoNumber(3) val groupUin: ULong? = null,
@ProtoNumber(4) val payload: ByteArray? = null, @ProtoNumber(4) val payload: ByteArray? = null,
) )

View File

@ -1,55 +1 @@
@file:OptIn(ExperimentalSerializationApi::class)
package protobuf.message.multimedia package protobuf.message.multimedia
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import moe.fuqiuluo.symbols.Protobuf
@Serializable
data class RichMediaForPicData(
@ProtoNumber(1) val info: MediaInfo?,
@ProtoNumber(2) val display: DisplayMediaInfo?,
): Protobuf<RichMediaForPicData> {
companion object {
@Serializable
data class MediaInfo(
@ProtoNumber(1) val picture: Picture? = null,
)
@Serializable
data class Picture(
@ProtoNumber(1) val info: PictureInfo? = null,
@ProtoNumber(2) val fileId: String? = null,
@ProtoNumber(4) val time: ULong? = null,
)
@Serializable
data class PictureInfo(
@ProtoNumber(2) val md5Hex: String? = null,
@ProtoNumber(3) val sha: String? = null,
@ProtoNumber(4) val name: String? = null,
@ProtoNumber(6) val width: Int? = null,
@ProtoNumber(7) val height: Int? = null,
)
}
}
@Serializable
data class DisplayMediaInfo(
@ProtoNumber(1) val show: Show? = null,
) {
companion object {
@Serializable
data class Show(
@ProtoNumber(2) val text: String? = null,
@ProtoNumber(12) val download: Download? = null
)
@Serializable
data class Download(
@ProtoNumber(30) val url: String? = null,
)
}
}

View File

@ -49,8 +49,8 @@ data class UploadCompletedReq(
@Serializable @Serializable
data class MsgInfo( data class MsgInfo(
@ProtoNumber(1) val msgInfoBody: List<MsgInfoBody>, @ProtoNumber(1) val msgInfoBody: List<MsgInfoBody>,
@ProtoNumber(2) val extBizInfo: ExtBizInfo, @ProtoNumber(2) val extBizInfo: ExtBizInfo?,
) ): Protobuf<MsgInfo>
@Serializable @Serializable
data class MsgInfoBody( data class MsgInfoBody(
@ -106,7 +106,7 @@ data class UploadReq(
@ProtoNumber(5) val compatQMsgSceneType: UInt? = null, @ProtoNumber(5) val compatQMsgSceneType: UInt? = null,
@ProtoNumber(6) val extBizInfo: ExtBizInfo? = null, @ProtoNumber(6) val extBizInfo: ExtBizInfo? = null,
@ProtoNumber(7) val clientSeq: UInt? = null, @ProtoNumber(7) val clientSeq: UInt? = null,
@ProtoNumber(8) val noNeedCompatMsg: Boolean = false, @ProtoNumber(8) val noNeedCompatMsg: Boolean? = null,
) )
@Serializable @Serializable
@ -114,7 +114,7 @@ data class ExtBizInfo(
@ProtoNumber(1) val pic: PicExtBizInfo? = null, @ProtoNumber(1) val pic: PicExtBizInfo? = null,
@ProtoNumber(2) val video: VideoExtBizInfo? = null, @ProtoNumber(2) val video: VideoExtBizInfo? = null,
@ProtoNumber(3) val ptt: PttExtBizInfo? = null, @ProtoNumber(3) val ptt: PttExtBizInfo? = null,
@ProtoNumber(10) val busiType: UInt, @ProtoNumber(10) val busiType: UInt?,
) )
@Serializable @Serializable
@ -132,15 +132,15 @@ data class PttExtBizInfo(
@Serializable @Serializable
data class VideoExtBizInfo( data class VideoExtBizInfo(
@ProtoNumber(1) val fromScene: UInt, @ProtoNumber(1) val fromScene: UInt?,
@ProtoNumber(2) val toScene: UInt, @ProtoNumber(2) val toScene: UInt?,
@ProtoNumber(3) val bytesPbReserve: ByteArray, @ProtoNumber(3) val bytesPbReserve: ByteArray?,
) )
@Serializable @Serializable
data class PicExtBizInfo( data class PicExtBizInfo(
@ProtoNumber(1) val bizType: UInt, @ProtoNumber(1) val bizType: UInt?,
@ProtoNumber(2) val textSummary: String, @ProtoNumber(2) val textSummary: String?,
@ProtoNumber(11) val bytesPbReserveC2c: ByteArray? = null, @ProtoNumber(11) val bytesPbReserveC2c: ByteArray? = null,
@ProtoNumber(12) val bytesPbReserveTroop: ByteArray? = null, @ProtoNumber(12) val bytesPbReserveTroop: ByteArray? = null,
@ProtoNumber(1001) val fromScene: UInt? = null, @ProtoNumber(1001) val fromScene: UInt? = null,
@ -156,15 +156,15 @@ data class UploadInfo(
@Serializable @Serializable
data class FileInfo( data class FileInfo(
@ProtoNumber(1) val fileSize: ULong, @ProtoNumber(1) val fileSize: ULong?,
@ProtoNumber(2) val md5: String, @ProtoNumber(2) val md5: String?,
@ProtoNumber(3) val sha1: String, @ProtoNumber(3) val sha1: String?,
@ProtoNumber(4) val name: String, @ProtoNumber(4) val name: String?,
@ProtoNumber(5) val fileType: FileType, @ProtoNumber(5) val fileType: FileType?,
@ProtoNumber(6) val width: UInt, @ProtoNumber(6) val width: UInt?,
@ProtoNumber(7) val height: UInt, @ProtoNumber(7) val height: UInt?,
@ProtoNumber(8) val time: UInt, @ProtoNumber(8) val time: UInt?,
@ProtoNumber(9) val original: UInt, @ProtoNumber(9) val original: UInt?,
) )
@Serializable @Serializable
@ -217,7 +217,7 @@ data class IndexNode(
@ProtoNumber(3) val storeId: UInt, // 0为旧服务器 1为nt服务器 @ProtoNumber(3) val storeId: UInt, // 0为旧服务器 1为nt服务器
@ProtoNumber(4) val uploadTime: ULong, @ProtoNumber(4) val uploadTime: ULong,
@ProtoNumber(5) val ttl: ULong, @ProtoNumber(5) val ttl: ULong,
@ProtoNumber(6) val subType: UInt, @ProtoNumber(6) val subType: UInt? = null,
@ProtoNumber(7) val storeAppId: UInt? = null @ProtoNumber(7) val storeAppId: UInt? = null
) )

View File

@ -1,7 +1,7 @@
@file:OptIn(ExperimentalSerializationApi::class) @file:OptIn(ExperimentalSerializationApi::class)
package protobuf.oidb.cmd0x11c5 package protobuf.oidb.cmd0x11c5
import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY import moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
@ -9,7 +9,7 @@ import moe.fuqiuluo.symbols.Protobuf
@Serializable @Serializable
data class NtV2RichMediaRsp( data class NtV2RichMediaRsp(
@ProtoNumber(1) val head: RspHead, @ProtoNumber(1) val head: RspHead?,
@ProtoNumber(2) val upload: UploadRsp?, @ProtoNumber(2) val upload: UploadRsp?,
@ProtoNumber(3) val download: DownloadRsp?, @ProtoNumber(3) val download: DownloadRsp?,
@ProtoNumber(4) val downloadRkeyRsp: DownloadRkeyRsp?, @ProtoNumber(4) val downloadRkeyRsp: DownloadRkeyRsp?,
@ -26,8 +26,8 @@ class DownloadSafeRsp
@Serializable @Serializable
data class UploadKeyRenewalRsp( data class UploadKeyRenewalRsp(
@ProtoNumber(1) val ukey: String, @ProtoNumber(1) val ukey: String?,
@ProtoNumber(2) val ukeyTtlSec: ULong, @ProtoNumber(2) val ukeyTtlSec: ULong?,
) )
@Serializable @Serializable
@ -39,7 +39,7 @@ data class MsgInfoAuthRsp(
@Serializable @Serializable
data class UploadCompletedRsp( data class UploadCompletedRsp(
@ProtoNumber(1) val msgSeq: ULong @ProtoNumber(1) val msgSeq: ULong?
) )
@Serializable @Serializable
@ -47,13 +47,13 @@ class DeleteRsp
@Serializable @Serializable
data class DownloadRkeyRsp( data class DownloadRkeyRsp(
@ProtoNumber(1) val rkeys: List<RKeyInfo> @ProtoNumber(1) val rkeys: List<RKeyInfo>?
) )
@Serializable @Serializable
data class RKeyInfo( data class RKeyInfo(
@ProtoNumber(1) val rkey: String, @ProtoNumber(1) val rkey: String?,
@ProtoNumber(2) val rkeyTtlSec: ULong, @ProtoNumber(2) val rkeyTtlSec: ULong?,
@ProtoNumber(3) val storeId: UInt = 0u, @ProtoNumber(3) val storeId: UInt = 0u,
@ProtoNumber(4) val rkeyCreateTime: UInt?, @ProtoNumber(4) val rkeyCreateTime: UInt?,
@ProtoNumber(4) val type: UInt?, @ProtoNumber(4) val type: UInt?,
@ -61,8 +61,8 @@ data class RKeyInfo(
@Serializable @Serializable
data class DownloadRsp( data class DownloadRsp(
@ProtoNumber(1) val rkeyParam: String, @ProtoNumber(1) val rkeyParam: String?,
@ProtoNumber(2) val rkeyTtlSec: ULong, @ProtoNumber(2) val rkeyTtlSec: ULong?,
@ProtoNumber(3) val downloadInfo: DownloadInfo?, @ProtoNumber(3) val downloadInfo: DownloadInfo?,
@ProtoNumber(4) val rkeyCreateTime: UInt? @ProtoNumber(4) val rkeyCreateTime: UInt?
) )
@ -80,16 +80,16 @@ data class DownloadInfo(
@Serializable @Serializable
data class VideoExtInfo( data class VideoExtInfo(
@ProtoNumber(1) val videoCodecFormat: UInt, @ProtoNumber(1) val videoCodecFormat: UInt? = null,
) )
@Serializable @Serializable
data class UploadRsp( data class UploadRsp(
@ProtoNumber(1) val ukey: String, @ProtoNumber(1) val ukey: String?,
@ProtoNumber(2) val ukeyTtlSec: ULong, @ProtoNumber(2) val ukeyTtlSec: ULong?,
@ProtoNumber(3) val ipv4: List<Ipv4>, @ProtoNumber(3) val ipv4: List<Ipv4>?,
@ProtoNumber(4) val ipv6: List<Ipv6>, @ProtoNumber(4) val ipv6: List<Ipv6>?,
@ProtoNumber(5) val msgSeq: ULong, @ProtoNumber(5) val msgSeq: ULong?,
@ProtoNumber(6) val msgInfo: MsgInfo? = null, @ProtoNumber(6) val msgInfo: MsgInfo? = null,
@ProtoNumber(7) val ext: List<RichmediaStorageTransInfo>? = null, @ProtoNumber(7) val ext: List<RichmediaStorageTransInfo>? = null,
@ProtoNumber(8) val compatQMsg: ByteArray? = null, @ProtoNumber(8) val compatQMsg: ByteArray? = null,
@ -98,11 +98,11 @@ data class UploadRsp(
@Serializable @Serializable
data class SubFileInfo( data class SubFileInfo(
@ProtoNumber(1) val subType: UInt, @ProtoNumber(1) val subType: UInt?,
@ProtoNumber(2) val ukey: String, @ProtoNumber(2) val ukey: String?,
@ProtoNumber(3) val ukeyTTLSec: ULong, @ProtoNumber(3) val ukeyTTLSec: ULong?,
@ProtoNumber(4) val ipv4: List<Ipv4>, @ProtoNumber(4) val ipv4: List<Ipv4>?,
@ProtoNumber(5) val ipv6: List<Ipv6>, @ProtoNumber(5) val ipv6: List<Ipv6>?,
) )
@Serializable @Serializable
@ -132,8 +132,8 @@ data class Ipv6(
@Serializable @Serializable
data class RspHead( data class RspHead(
@ProtoNumber(1) val commonHead: CommonHead, @ProtoNumber(1) val commonHead: CommonHead?,
@ProtoNumber(2) val retCode: UInt = 0u, @ProtoNumber(2) val retCode: UInt = 0u,
@ProtoNumber(3) val msg: String @ProtoNumber(3) val msg: String?
) )

View File

@ -0,0 +1,63 @@
package protobuf.oidb.cmd0x388
import moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import moe.fuqiuluo.symbols.Protobuf
@Serializable
data class Cmd0x388ReqBody(
@ProtoNumber(1) var netType: Int = 0,
@ProtoNumber(2) var subCmd: Int = 0,
@ProtoNumber(3) var msgTryUpImg: ArrayList<TryUpImgReq>? = null,
// @ProtoNumber(4) var rpt_msg_getimg_url_req: ArrayList<GetImgUrlReq>? = null,
@ProtoNumber(5) var msgTryUpPttReq: ArrayList<TryUpPttReq>? = null,
// @ProtoNumber(6) var msgGetPttUrlReq: ArrayList<GetPttUrlReq>? = null,
@ProtoNumber(7) var commandId: Int = 0,
// @ProtoNumber(8) var rpt_msg_del_img_req: ArrayList<DelImgReq>? = null,
@ProtoNumber(1001) var extension: ByteArray = EMPTY_BYTE_ARRAY,
): Protobuf<Cmd0x388ReqBody>
@Serializable
data class TryUpImgReq(
@ProtoNumber(1) var groupCode: Long = 0,
@ProtoNumber(2) var srcUin: Long = 0,
@ProtoNumber(3) var fileId: Long? = null,
@ProtoNumber(4) var fileMd5: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(5) var fileSize: Long = 0,
@ProtoNumber(6) var fileName: String = "",
@ProtoNumber(7) var srcTerm: Int = 0,
@ProtoNumber(8) var platformType: Int = 0,
@ProtoNumber(9) var buType: Int = 0,
@ProtoNumber(10) var picWidth: Int = 0,
@ProtoNumber(11) var picHeight: Int = 0,
@ProtoNumber(12) var picType: Int = 0,
@ProtoNumber(13) var buildVer: String = "",
@ProtoNumber(14) var innerIp: Int = 0,
@ProtoNumber(15) var appPicType: Int = 0,
@ProtoNumber(16) var originalPic: Int = 0,
@ProtoNumber(17) var fileIndex: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(18) var dstUin: Long = 0,
@ProtoNumber(19) var srvUpload: Int? = null,
@ProtoNumber(20) var transferUrl: ByteArray = EMPTY_BYTE_ARRAY,
)
@Serializable
data class TryUpPttReq(
@ProtoNumber(1) var groupCode: Long = 0,
@ProtoNumber(2) var srcUin: Long = 0,
@ProtoNumber(3) var fileId: Long = 0,
@ProtoNumber(4) var fileMd5: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(5) var fileSize: Long = 0,
@ProtoNumber(6) var fileName: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(7) var srcTerm: Int = 0,
@ProtoNumber(8) var platformType: Int = 0,
@ProtoNumber(9) var buType: Int = 0,
@ProtoNumber(10) var buildVer: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(11) var innerIp: Int = 0,
@ProtoNumber(12) var voiceLength: Int = 0,
@ProtoNumber(13) var newUpChan: Boolean = false,
@ProtoNumber(14) var codec: Int = 0,
@ProtoNumber(15) var voiceType: Int = 0,
@ProtoNumber(16) var buId: Int = 0,
)

View File

@ -0,0 +1,78 @@
@file:OptIn(ExperimentalSerializationApi::class)
package protobuf.oidb.cmd0x388
import moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import moe.fuqiuluo.symbols.Protobuf
@Serializable
data class Cmd0x388RspBody(
@ProtoNumber(1) var clientIp: UInt = 0u,
@ProtoNumber(2) var subCmd: UInt = 0u,
@ProtoNumber(3) var msgTryUpImgRsp: ArrayList<TryUpImgRsp>? = null,
@ProtoNumber(5) var msgTryUpPttRsp: ArrayList<TryUpPttRsp>? = null,
): Protobuf<Cmd0x388RspBody>
@Serializable
data class TryUpPttRsp(
@ProtoNumber(1) var fileId: Long = 0,
@ProtoNumber(2) var result: Int = 0,
@ProtoNumber(3) var failMsg: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(4) var fileExit: Boolean = false,
@ProtoNumber(5) var upIp: List<Int>? = null,
@ProtoNumber(6) var upPort: List<Int>? = null,
@ProtoNumber(7) var upUkey: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(8) var fileid: Long = 0,
@ProtoNumber(9) var upOffset: Long = 0,
@ProtoNumber(10) var blockSize: Long = 0,
@ProtoNumber(11) var fileKey: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(12) var channelType: Int = 0,
@ProtoNumber(26) var msgUpIp6: ArrayList<IPv6Info>? = null,
@ProtoNumber(27) var clientIp6: ByteArray = EMPTY_BYTE_ARRAY,
): Protobuf<TryUpPttRsp>
@Serializable
data class TryUpImgRsp(
@ProtoNumber(1) var extFileId: ULong = 0u, // 没有实际作用
@ProtoNumber(2) var result: UInt = 0u,
@ProtoNumber(3) var faiMsg: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(4) var fileExist: Boolean = false,
@ProtoNumber(5) var msgImgInfo: ImgInfo? = null, // 里面只是一堆垃圾
@ProtoNumber(6) var upIp: ArrayList<Long>? = null,
@ProtoNumber(7) var upPort: ArrayList<Int>? = null,
@ProtoNumber(8) var ukey: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(9) var fileId: Long = 0,
@ProtoNumber(10) var upOffset: ULong = 0u,
@ProtoNumber(11) var blockSize: Long = 0,
@ProtoNumber(12) var bool_new_big_chan: Boolean = false,
@ProtoNumber(26) var rpt_msg_up_ip6: ArrayList<IPv6Info>? = null,
@ProtoNumber(27) var client_ip6: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(1001) var msg_info4busi: TryUpInfo4Busi? = null,
)
@Serializable
data class TryUpInfo4Busi(
@ProtoNumber(1) var down_domain: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) var thumb_down_url: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(3) var original_down_url: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(4) var big_down_url: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(5) var file_resid: ByteArray = EMPTY_BYTE_ARRAY,
)
@Serializable
data class IPv6Info(
@ProtoNumber(1) var ip6: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) var port: UInt = 0u,
)
@Serializable
data class ImgInfo(
@ProtoNumber(1) var file_md5: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) var file_type: UInt = 0u,
@ProtoNumber(3) var file_size: ULong = 0u,
@ProtoNumber(4) var file_width: UInt = 0u,
@ProtoNumber(5) var file_height: UInt = 0u,
)

View File

@ -7,6 +7,8 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
import moe.fuqiuluo.symbols.Protobuf import moe.fuqiuluo.symbols.Protobuf
const val DEFAULT_DEVICE_INFO = "i=&imsi=&mac=02:00:00:00:00:00&m=Shamrock&o=114514&a=1919810&sd=0&c64=1&sc=1&p=8000*8000&aid=123456789012345678901234567890abcdef&f=Tencent&mm=5610&cf=1726&cc=8&qimei=&qimei36=&sharpP=1&n=nether_world&support_xsj_live=false&client_mod=concise&timezone=America/La_Paz&material_sdk_version=&vh265=&refreshrate=10086&hwlevel=9&suphdr=1&is_teenager_mod=8&liveH265=&bmst=5&AV1=0"
@Serializable @Serializable
data class QWebReq( data class QWebReq(
@ProtoNumber(1) val seq: Int = 0, @ProtoNumber(1) val seq: Int = 0,

View File

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

View File

@ -13,6 +13,10 @@ public abstract class BaseBusinessHandler extends OidbWrapper {
return null; return null;
} }
public void addBusinessObserver(ToServiceMsg toServiceMsg, BusinessObserver businessObserver, boolean z) {
}
public final <T> T decodePacket(byte[] data, String name, T obj) { public final <T> T decodePacket(byte[] data, String name, T obj) {
UniPacket uniPacket = new UniPacket(true); UniPacket uniPacket = new UniPacket(true);
try { try {
@ -24,6 +28,10 @@ public abstract class BaseBusinessHandler extends OidbWrapper {
} }
} }
public boolean msgCmdFilter(String str) {
return false;
}
protected abstract Set<String> getCommandList(); protected abstract Set<String> getCommandList();
protected abstract Set<String> getPushCommandList(); protected abstract Set<String> getPushCommandList();

View File

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

View File

@ -0,0 +1,74 @@
package com.tencent.mobileqq.data;
import java.util.HashMap;
import java.util.Map;
public class MessageForPic extends MessageRecord {
public String SpeedInfo;
public String actMsgContentValue;
public String action;
public String bigMsgUrl;
public String bigThumbMsgUrl;
public int busiType;
public int fileSizeFlag;
public long groupFileID;
public long height;
public int imageType;
public boolean isInMixedMsg;
public boolean isMixed;
public boolean isRead;
public boolean isShareAppActionMsg;
public String localUUID;
public int mCurrlength;
public int mDownloadLength;
public long mPresendTransferedSize;
public int mShowLength;
public String md5;
//@RecordForTest
// public msg_ctrl$MsgCtrl msgCtrl;
public int msgVia;
public int ntChatType;
public String ntPeerUid;
public String path;
//public PicMessageExtraData picExtraData;
public int picExtraFlag;
public Object picExtraObject;
public int previewed;
public String rawMsgUrl;
/// public ReportInfo reportInfo;
//public MsgRecordParams rootMsgRecordParams;
public String serverStoreSource;
public long shareAppID;
public long size;
public long subTypeId;
public int thumbHeight;
public String thumbMsgUrl;
public int thumbWidth;
//public ThumbWidthHeightDP thumbWidthHeightDP;
public int type;
public String uuid;
public long width;
public boolean isDownStatusReady = false;
public int subMsgId = 0;
public int isReport = 0;
public int subVersion = 5;
public int preDownState = -1;
public int preDownNetworkType = -1;
public long DSKey = 0;
public int mNotPredownloadReason = 0;
public int subThumbWidth = -1;
public int subThumbHeight = -1;
public int aiofileType = -1;
public int subMsgType = -1;
public boolean bEnableEnc = false;
public int thumbSize = -1;
public boolean isBlessPic = false;
public boolean sync2Story = false;
public boolean isQzonePic = false;
public boolean isStoryPhoto = false;
public long replyRealSourceMsgId = -1;
public String toLogString() {
return "path:" + this.path + ",uuid:" + this.uuid + ",md5:" + this.md5 + ",size:" + this.size + ",groupFileID:" + this.groupFileID;
}
}

View File

@ -0,0 +1,18 @@
package com.tencent.mobileqq.msf.sdk;
import com.tencent.qphone.base.remote.FromServiceMsg;
import com.tencent.qphone.base.remote.ToServiceMsg;
public class MsfMessagePair {
public FromServiceMsg fromServiceMsg;
public String sendProcess;
public ToServiceMsg toServiceMsg;
public MsfMessagePair(String str, ToServiceMsg toServiceMsg, FromServiceMsg fromServiceMsg) {
}
public MsfMessagePair(ToServiceMsg toServiceMsg, FromServiceMsg fromServiceMsg) {
}
}

View File

@ -1,6 +1,8 @@
package com.tencent.mobileqq.transfile; package com.tencent.mobileqq.transfile;
public class BaseTransProcessor { import com.tencent.mobileqq.utils.httputils.IHttpCommunicatorListener;
public class BaseTransProcessor implements IHttpCommunicatorListener {
public FileMsg file; public FileMsg file;
public long getFileStatus() { public long getFileStatus() {

View File

@ -0,0 +1,5 @@
package com.tencent.mobileqq.transfile;
public class BaseUploadProcessor extends BaseTransProcessor {
}

View File

@ -1,5 +1,9 @@
package com.tencent.mobileqq.transfile; package com.tencent.mobileqq.transfile;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
public class FileMsg { public class FileMsg {
public static final int STATUS_FILE_EXPIRED = 5002; public static final int STATUS_FILE_EXPIRED = 5002;
public static final int STATUS_FILE_TRANSFERING = 5000; public static final int STATUS_FILE_TRANSFERING = 5000;
@ -107,4 +111,58 @@ public class FileMsg {
public static final int UIN_BUDDY = 0; public static final int UIN_BUDDY = 0;
public static final int UIN_DISCUSS = 2; public static final int UIN_DISCUSS = 2;
public static final int UIN_TROOP = 1; public static final int UIN_TROOP = 1;
public String domain;
public String downDomain;
public long endTime;
public int errorCode;
public String errorMessage;
public File file;
public long fileID;
public String fileKey;
public String fileMd5;
public String filePath;
public long fileSize;
public int fileType;
public String fileUrl;
public String forwardTaskKey;
public String friendUin;
public int isRead;
public boolean isStreamMode;
public int lastStatus;
public byte[] localFileMd5;
public String logTag;
public long mSecMsgId;
public long mSubMsgId;
public String mUin;
public String msgImageData;
public String msgTime;
public String orgiDownUrl;
public String peerUin;
public int picScale;
public long picThumbSize;
public BaseTransProcessor processor;
public boolean processorDoReportSelf;
public int pttSlicePos;
public String pttSliceText;
public OutputStream revStream;
public String selfUin;
public InputStream sendStream;
public String serverPath;
public long startTime;
public int status;
public long stepUrlCost;
public String suffixType;
public String thumbDownUrl;
public String thumbPath;
public String thumbUrl;
public String tmpFilePath;
public byte[] transferData;
public long transferedSize;
public String uKey;
public int uinType;
public long uniseq;
public String[] urls;
public byte[] userInfo;
public String uuidPath;
} }

View File

@ -1,4 +1,6 @@
package com.tencent.mobileqq.transfile; package com.tencent.mobileqq.transfile;
public class GroupPicUploadProcessor { import com.tencent.mobileqq.utils.httputils.IHttpCommunicatorListener;
public class GroupPicUploadProcessor extends BaseUploadProcessor {
} }

View File

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

View File

@ -6,7 +6,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
public interface IKernelMsgService { public interface IKernelMsgService {
void deleteMsg(Contact contact, ArrayList<Long> msgIdList, IOperateCallback callback); void deleteMsg(Contact contact, ArrayList<Long> msgIdList, IOperateCallback cb);
void fetchLongMsg(Contact contact, long msgId); void fetchLongMsg(Contact contact, long msgId);

View File

@ -2,17 +2,20 @@ package com.tencent.qqnt.kernel.nativeinterface;
import java.util.ArrayList; import java.util.ArrayList;
public final class InlineKeyboardButton {
public final class InlineKeyboardButton { int anchor;
boolean atBotShowChannelList; boolean atBotShowChannelList;
int clickLimit; int clickLimit;
String data; String data;
boolean enter;
String id; String id;
boolean isReply;
String label; String label;
int permissionType; int permissionType;
ArrayList<String> specifyRoleIds; ArrayList<String> specifyRoleIds;
ArrayList<String> specifyTinyids; ArrayList<String> specifyTinyids;
int style; int style;
ArrayList<SubscribeMsgTemplateID> subscribeDataTemplateIds;
int type; int type;
String unsupportTips; String unsupportTips;
String visitedLabel; String visitedLabel;
@ -25,6 +28,11 @@ public final class InlineKeyboardButton {
this.data = ""; this.data = "";
this.specifyRoleIds = new ArrayList<>(); this.specifyRoleIds = new ArrayList<>();
this.specifyTinyids = new ArrayList<>(); this.specifyTinyids = new ArrayList<>();
this.subscribeDataTemplateIds = new ArrayList<>();
}
public int getAnchor() {
return this.anchor;
} }
public boolean getAtBotShowChannelList() { public boolean getAtBotShowChannelList() {
@ -39,10 +47,18 @@ public final class InlineKeyboardButton {
return this.data; return this.data;
} }
public boolean getEnter() {
return this.enter;
}
public String getId() { public String getId() {
return this.id; return this.id;
} }
public boolean getIsReply() {
return this.isReply;
}
public String getLabel() { public String getLabel() {
return this.label; return this.label;
} }
@ -63,6 +79,10 @@ public final class InlineKeyboardButton {
return this.style; return this.style;
} }
public ArrayList<SubscribeMsgTemplateID> getSubscribeDataTemplateIds() {
return this.subscribeDataTemplateIds;
}
public int getType() { public int getType() {
return this.type; return this.type;
} }
@ -76,14 +96,14 @@ public final class InlineKeyboardButton {
} }
public String toString() { public String toString() {
return "InlineKeyboardButton{id=" + this.id + ",label=" + this.label + ",visitedLabel=" + this.visitedLabel + ",style=" + this.style + ",type=" + this.type + ",clickLimit=" + this.clickLimit + ",unsupportTips=" + this.unsupportTips + ",data=" + this.data + ",atBotShowChannelList=" + this.atBotShowChannelList + ",permissionType=" + this.permissionType + ",specifyRoleIds=" + this.specifyRoleIds + ",specifyTinyids=" + this.specifyTinyids + ",}"; return "InlineKeyboardButton{id=" + this.id + ",label=" + this.label + ",visitedLabel=" + this.visitedLabel + ",style=" + this.style + ",type=" + this.type + ",clickLimit=" + this.clickLimit + ",unsupportTips=" + this.unsupportTips + ",data=" + this.data + ",atBotShowChannelList=" + this.atBotShowChannelList + ",permissionType=" + this.permissionType + ",specifyRoleIds=" + this.specifyRoleIds + ",specifyTinyids=" + this.specifyTinyids + ",isReply=" + this.isReply + ",anchor=" + this.anchor + ",enter=" + this.enter + ",subscribeDataTemplateIds=" + this.subscribeDataTemplateIds + ",}";
} }
public InlineKeyboardButton(String str, String str2, String str3, int i, int i2, int i3, String str4, String str5, boolean z, int i4, ArrayList<String> arrayList, ArrayList<String> arrayList2, boolean z2, int i5, boolean z3, ArrayList<SubscribeMsgTemplateID> arrayList3) { public InlineKeyboardButton(String str, String str2, String str3, int i, int i2, int i3, String str4, String str5, boolean z, int i4, ArrayList<String> arrayList, ArrayList<String> arrayList2) {
} }
public InlineKeyboardButton(String str, String str2, String str3, int i2, int i3, int i4, String str4, String str5, boolean z, int i5, ArrayList<String> arrayList, ArrayList<String> arrayList2) { public InlineKeyboardButton(String str, String str2, String str3, int i2, int i3, int i4, String str4, String str5, boolean z, int i5, ArrayList<String> arrayList, ArrayList<String> arrayList2, boolean z2, int i6, boolean z3, ArrayList<SubscribeMsgTemplateID> arrayList3) {
this.id = ""; this.id = "";
this.label = ""; this.label = "";
this.visitedLabel = ""; this.visitedLabel = "";
@ -91,6 +111,7 @@ public final class InlineKeyboardButton {
this.data = ""; this.data = "";
this.specifyRoleIds = new ArrayList<>(); this.specifyRoleIds = new ArrayList<>();
this.specifyTinyids = new ArrayList<>(); this.specifyTinyids = new ArrayList<>();
this.subscribeDataTemplateIds = new ArrayList<>();
this.id = str; this.id = str;
this.label = str2; this.label = str2;
this.visitedLabel = str3; this.visitedLabel = str3;
@ -103,5 +124,9 @@ public final class InlineKeyboardButton {
this.permissionType = i5; this.permissionType = i5;
this.specifyRoleIds = arrayList; this.specifyRoleIds = arrayList;
this.specifyTinyids = arrayList2; this.specifyTinyids = arrayList2;
this.isReply = z2;
this.anchor = i6;
this.enter = z3;
this.subscribeDataTemplateIds = arrayList3;
} }
} }

View File

@ -39,6 +39,7 @@ public class MsgRecord {
boolean isOnlineMsg; boolean isOnlineMsg;
FromRoleInfo levelRoleInfo; FromRoleInfo levelRoleInfo;
HashMap<Integer, MsgAttributeInfo> msgAttrs; HashMap<Integer, MsgAttributeInfo> msgAttrs;
byte[] msgEventInfo;
long msgId; long msgId;
byte[] msgMeta; byte[] msgMeta;
long msgRandom; long msgRandom;
@ -231,6 +232,10 @@ public class MsgRecord {
return this.msgAttrs; return this.msgAttrs;
} }
public byte[] getMsgEventInfo() {
return this.msgEventInfo;
}
public long getMsgId() { public long getMsgId() {
return this.msgId; return this.msgId;
} }
@ -332,10 +337,10 @@ public class MsgRecord {
} }
public String toString() { public String toString() {
return "MsgRecord{msgId=" + this.msgId + ",msgRandom=" + this.msgRandom + ",msgSeq=" + this.msgSeq + ",cntSeq=" + this.cntSeq + ",chatType=" + this.chatType + ",msgType=" + this.msgType + ",subMsgType=" + this.subMsgType + ",sendType=" + this.sendType + ",senderUid=" + this.senderUid + ",peerUid=" + this.peerUid + ",channelId=" + this.channelId + ",guildId=" + this.guildId + ",guildCode=" + this.guildCode + ",fromUid=" + this.fromUid + ",fromAppid=" + this.fromAppid + ",msgTime=" + this.msgTime + ",msgMeta=" + this.msgMeta + ",sendStatus=" + this.sendStatus + ",sendRemarkName=" + this.sendRemarkName + ",sendMemberName=" + this.sendMemberName + ",sendNickName=" + this.sendNickName + ",guildName=" + this.guildName + ",channelName=" + this.channelName + ",elements=" + this.elements + ",records=" + this.records + ",emojiLikesList=" + this.emojiLikesList + ",commentCnt=" + this.commentCnt + ",directMsgFlag=" + this.directMsgFlag + ",directMsgMembers=" + this.directMsgMembers + ",peerName=" + this.peerName + ",freqLimitInfo=" + this.freqLimitInfo + ",editable=" + this.editable + ",avatarMeta=" + this.avatarMeta + ",avatarPendant=" + this.avatarPendant + ",feedId=" + this.feedId + ",roleId=" + this.roleId + ",timeStamp=" + this.timeStamp + ",clientIdentityInfo=" + this.clientIdentityInfo + ",isImportMsg=" + this.isImportMsg + ",atType=" + this.atType + ",roleType=" + this.roleType + ",fromChannelRoleInfo=" + this.fromChannelRoleInfo + ",fromGuildRoleInfo=" + this.fromGuildRoleInfo + ",levelRoleInfo=" + this.levelRoleInfo + ",recallTime=" + this.recallTime + ",isOnlineMsg=" + this.isOnlineMsg + ",generalFlags=" + this.generalFlags + ",clientSeq=" + this.clientSeq + ",fileGroupSize=" + this.fileGroupSize + ",foldingInfo=" + this.foldingInfo + ",multiTransInfo=" + this.multiTransInfo + ",senderUin=" + this.senderUin + ",peerUin=" + this.peerUin + ",msgAttrs=" + this.msgAttrs + ",anonymousExtInfo=" + this.anonymousExtInfo + ",nameType=" + this.nameType + ",avatarFlag=" + this.avatarFlag + ",extInfoForUI=" + this.extInfoForUI + ",personalMedal=" + this.personalMedal + ",categoryManage=" + this.categoryManage + ",}"; return "MsgRecord{msgId=" + this.msgId + ",msgRandom=" + this.msgRandom + ",msgSeq=" + this.msgSeq + ",cntSeq=" + this.cntSeq + ",chatType=" + this.chatType + ",msgType=" + this.msgType + ",subMsgType=" + this.subMsgType + ",sendType=" + this.sendType + ",senderUid=" + this.senderUid + ",peerUid=" + this.peerUid + ",channelId=" + this.channelId + ",guildId=" + this.guildId + ",guildCode=" + this.guildCode + ",fromUid=" + this.fromUid + ",fromAppid=" + this.fromAppid + ",msgTime=" + this.msgTime + ",msgMeta=" + this.msgMeta + ",sendStatus=" + this.sendStatus + ",sendRemarkName=" + this.sendRemarkName + ",sendMemberName=" + this.sendMemberName + ",sendNickName=" + this.sendNickName + ",guildName=" + this.guildName + ",channelName=" + this.channelName + ",elements=" + this.elements + ",records=" + this.records + ",emojiLikesList=" + this.emojiLikesList + ",commentCnt=" + this.commentCnt + ",directMsgFlag=" + this.directMsgFlag + ",directMsgMembers=" + this.directMsgMembers + ",peerName=" + this.peerName + ",freqLimitInfo=" + this.freqLimitInfo + ",editable=" + this.editable + ",avatarMeta=" + this.avatarMeta + ",avatarPendant=" + this.avatarPendant + ",feedId=" + this.feedId + ",roleId=" + this.roleId + ",timeStamp=" + this.timeStamp + ",clientIdentityInfo=" + this.clientIdentityInfo + ",isImportMsg=" + this.isImportMsg + ",atType=" + this.atType + ",roleType=" + this.roleType + ",fromChannelRoleInfo=" + this.fromChannelRoleInfo + ",fromGuildRoleInfo=" + this.fromGuildRoleInfo + ",levelRoleInfo=" + this.levelRoleInfo + ",recallTime=" + this.recallTime + ",isOnlineMsg=" + this.isOnlineMsg + ",generalFlags=" + this.generalFlags + ",clientSeq=" + this.clientSeq + ",fileGroupSize=" + this.fileGroupSize + ",foldingInfo=" + this.foldingInfo + ",multiTransInfo=" + this.multiTransInfo + ",senderUin=" + this.senderUin + ",peerUin=" + this.peerUin + ",msgAttrs=" + this.msgAttrs + ",anonymousExtInfo=" + this.anonymousExtInfo + ",nameType=" + this.nameType + ",avatarFlag=" + this.avatarFlag + ",extInfoForUI=" + this.extInfoForUI + ",personalMedal=" + this.personalMedal + ",categoryManage=" + this.categoryManage + ",msgEventInfo=" + this.msgEventInfo + ",}";
} }
public MsgRecord(long j2, long j3, long j4, long j5, int i2, int i3, int i4, int i5, String str, String str2, String str3, String str4, long j6, long j7, long j8, long j9, byte[] bArr, int i6, String str5, String str6, String str7, String str8, String str9, ArrayList<MsgElement> arrayList, ArrayList<MsgRecord> arrayList2, ArrayList<MsgEmojiLikes> arrayList3, long j10, int i7, ArrayList<DirectMsgMember> arrayList4, String str10, FreqLimitInfo freqLimitInfo, boolean z, String str11, String str12, String str13, long j11, long j12, GProClientIdentity gProClientIdentity, boolean z2, int i8, int i9, FromRoleInfo fromRoleInfo, FromRoleInfo fromRoleInfo2, FromRoleInfo fromRoleInfo3, long j13, boolean z3, byte[] bArr2, long j14, Integer num, FoldingInfo foldingInfo, MultiTransInfo multiTransInfo, long j15, long j16, HashMap<Integer, MsgAttributeInfo> hashMap, AnonymousExtInfo anonymousExtInfo, int i10, int i11, byte[] bArr3, GProMedal gProMedal, int i12) { public MsgRecord(long j2, long j3, long j4, long j5, int i2, int i3, int i4, int i5, String str, String str2, String str3, String str4, long j6, long j7, long j8, long j9, byte[] bArr, int i6, String str5, String str6, String str7, String str8, String str9, ArrayList<MsgElement> arrayList, ArrayList<MsgRecord> arrayList2, ArrayList<MsgEmojiLikes> arrayList3, long j10, int i7, ArrayList<DirectMsgMember> arrayList4, String str10, FreqLimitInfo freqLimitInfo, boolean z, String str11, String str12, String str13, long j11, long j12, GProClientIdentity gProClientIdentity, boolean z2, int i8, int i9, FromRoleInfo fromRoleInfo, FromRoleInfo fromRoleInfo2, FromRoleInfo fromRoleInfo3, long j13, boolean z3, byte[] bArr2, long j14, Integer num, FoldingInfo foldingInfo, MultiTransInfo multiTransInfo, long j15, long j16, HashMap<Integer, MsgAttributeInfo> hashMap, AnonymousExtInfo anonymousExtInfo, int i10, int i11, byte[] bArr3, GProMedal gProMedal, int i12, byte[] bArr4) {
this.senderUid = ""; this.senderUid = "";
this.peerUid = ""; this.peerUid = "";
this.channelId = ""; this.channelId = "";
@ -419,5 +424,6 @@ public class MsgRecord {
this.extInfoForUI = bArr3; this.extInfoForUI = bArr3;
this.personalMedal = gProMedal; this.personalMedal = gProMedal;
this.categoryManage = i12; this.categoryManage = i12;
this.msgEventInfo = bArr4;
} }
} }

View File

@ -208,6 +208,8 @@ public final class PicElement implements IKernelModel {
this.isFlashPic = bool; this.isFlashPic = bool;
} }
public void setStoreID(int i2) {
}
public void setMd5HexStr(String str) { public void setMd5HexStr(String str) {
this.md5HexStr = str; this.md5HexStr = str;
} }

View File

@ -66,6 +66,13 @@ public abstract class AppRuntime {
} }
} }
public MobileQQ getApplication() {
return null;
}
public void startServlet(NewIntent newIntent) {
}
public <T extends IRuntimeService> T getRuntimeService(Class<T> cls, String namespace) { public <T extends IRuntimeService> T getRuntimeService(Class<T> cls, String namespace) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

View File

@ -0,0 +1,29 @@
package mqq.app;
import android.content.Context;
import android.content.Intent;
import com.tencent.mobileqq.app.BusinessObserver;
public class NewIntent extends Intent {
public boolean runNow;
public NewIntent(Context context, Class<? extends Servlet> cls) {
super(context, cls);
}
public BusinessObserver getObserver() {
return null;
}
public boolean isWithouLogin() {
return false;
}
public void setObserver(BusinessObserver businessObserver) {
}
public void setWithouLogin(boolean z) {
}
}

View File

@ -26,7 +26,7 @@ buildscript {
} }
} }
dependencies { dependencies {
classpath("com.android.tools:r8:8.2.26") classpath("com.android.tools:r8:8.3.37")
} }
} }
@ -34,8 +34,9 @@ rootProject.name = "Shamrock"
include( include(
":app", ":app",
":xposed", ":xposed",
":qqinterface" ":qqinterface",
) ":protobuf",
include(":protobuf") ":processor",
include(":processor") ":annotations",
include(":annotations") ":kritor"
)

View File

@ -1,9 +1,11 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
id("com.android.library") id("com.android.library")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("kotlin-kapt") id("kotlin-kapt")
id("com.google.devtools.ksp") version "1.9.21-1.0.15" id("com.google.devtools.ksp") version "1.9.22-1.0.17"
kotlin("plugin.serialization") version "1.9.21" kotlin("plugin.serialization") version "1.9.22"
} }
android { android {
@ -58,9 +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"))
implementation(project(":kritor"))
implementation(project(":protobuf")) implementation(project(":protobuf"))
implementation(project(":annotations")) implementation(project(":annotations"))
ksp(project(":processor")) ksp(project(":processor"))
@ -70,27 +73,20 @@ dependencies {
DEPENDENCY_ANDROIDX.forEach { DEPENDENCY_ANDROIDX.forEach {
implementation(it) implementation(it)
} }
implementation(DEPENDENCY_JAVA_WEBSOCKET)
implementation(DEPENDENCY_PROTOBUF)
implementation(DEPENDENCY_JSON5K)
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("server", "core"))
implementation(ktor("server", "host-common"))
implementation(ktor("server", "status-pages"))
implementation(ktor("server", "netty"))
implementation(ktor("server", "content-negotiation"))
implementation(ktor("client", "core")) implementation(ktor("client", "core"))
implementation(ktor("client", "content-negotiation")) implementation(ktor("client", "okhttp"))
implementation(ktor("client", "cio"))
implementation(ktor("serialization", "kotlinx-json")) implementation(ktor("serialization", "kotlinx-json"))
implementation(ktor("network", "tls-certificates"))
implementation(grpc("protobuf", "1.62.2"))
implementation(grpc("kotlin-stub", "1.4.1"))
implementation(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")
@ -99,4 +95,8 @@ dependencies {
androidTestImplementation("androidx.compose.ui:ui-test-junit4") androidTestImplementation("androidx.compose.ui:ui-test-junit4")
} }
tasks.withType<KotlinCompile>().all {
kotlinOptions {
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn")
}
}

View File

@ -18,182 +18,4 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
# Never inline methods, but allow shrinking and obfuscation.
-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.view.ViewCompat$Api* {
<methods>;
}
-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.view.WindowInsetsCompat$*Impl* {
<methods>;
}
-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.app.NotificationCompat$*$Api*Impl {
<methods>;
}
-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.os.UserHandleCompat$Api*Impl {
<methods>;
}
-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.widget.EdgeEffectCompat$Api*Impl {
<methods>;
}
# Keep metadata about inner emulated classes. This helps with
# reflection on older platforms, but can be omitted if the
# metadata usage is not present in the app.
-keepclassmembers class * {
** CREATOR;
}
# Keep the special static methods that are required in all enumeration
# classes.
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keep public class androidx.appcompat.widget.** { *; }
-keep public class androidx.appcompat.app.** { *; }
-keep public class androidx.appcompat.view.menu.** { *; }
# Ensure that reflectively-loaded inflater is not obfuscated. This can be
# removed when we stop supporting AAPT1 builds.
-keepnames class androidx.appcompat.app.AppCompatViewInflater
# aapt is not able to read app::actionViewClass and app:actionProviderClass to produce proguard
# keep rules. Add a commonly used SearchView to the keep list until b/109831488 is resolved.
-keep class androidx.appcompat.widget.SearchView { <init>(...); }
# CoordinatorLayout resolves the behaviors of its child components with reflection.
-keep public class * extends androidx.coordinatorlayout.widget.CoordinatorLayout$Behavior {
public <init>(android.content.Context, android.util.AttributeSet);
public <init>();
}
# Make sure we keep annotations for CoordinatorLayout's DefaultBehavior
-keepattributes RuntimeVisible*Annotation*
-if class androidx.appcompat.app.AppCompatViewInflater
-keep class com.google.android.material.theme.MaterialComponentsViewInflater {
<init>();
}
# Keep `Companion` object fields of serializable classes.
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
-if @kotlinx.serialization.Serializable class **
-keepclassmembers class <1> {
static <1>$Companion Companion;
}
# Keep `serializer()` on companion objects (both default and named) of serializable classes.
-if @kotlinx.serialization.Serializable class ** {
static **$* *;
}
-keepclassmembers class <2>$<3> {
kotlinx.serialization.KSerializer serializer(...);
}
# Keep `INSTANCE.serializer()` of serializable objects.
-if @kotlinx.serialization.Serializable class ** {
public static ** INSTANCE;
}
-keepclassmembers class <1> {
public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...);
}
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
# Don't print notes about potential mistakes or omissions in the configuration for kotlinx-serialization classes
# See also https://github.com/Kotlin/kotlinx.serialization/issues/1900
-dontnote kotlinx.serialization.**
# Serialization core uses `java.lang.ClassValue` for caching inside these specified classes.
# If there is no `java.lang.ClassValue` (for example, in Android), then R8/ProGuard will print a warning.
# However, since in this case they will not be used, we can disable these warnings
-dontwarn kotlinx.serialization.internal.ClassValueReferences
# Rule to save runtime annotations on serializable class.
# If the R8 full mode is used, annotations are removed from classes-files.
#
# For the annotation serializer, it is necessary to read the `Serializable` annotation inside the serializer<T>() function - if it is present,
# then `SealedClassSerializer` is used, if absent, then `PolymorphicSerializer'.
#
# When using R8 full mode, all interfaces will be serialized using `PolymorphicSerializer`.
#
# see https://github.com/Kotlin/kotlinx.serialization/issues/2050
-if @kotlinx.serialization.Serializable class **
-keep, allowshrinking, allowoptimization, allowobfuscation, allowaccessmodification class <1>
# Entry point for retaining MainDispatcherLoader which uses a ServiceLoader.
-keep class kotlinx.coroutines.Dispatchers {
** getMain();
}
# Entry point for retaining CoroutineExceptionHandlerImpl.handlers which uses a ServiceLoader.
-keep class kotlinx.coroutines.CoroutineExceptionHandlerKt {
void handleCoroutineException(...);
}
# Entry point for the rest of coroutines machinery
-keep class kotlinx.coroutines.BuildersKt {
** runBlocking(...);
** launch(...);
}
# We are cheating a bit by not having android.jar on R8's library classpath. Ignore those warnings.
-ignorewarnings
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
-keep class kotlinx.coroutines.android.AndroidExceptionPreHandler {*;}
# Statically turn off all debugging facilities and assertions
-keepclassmembers class io.ktor.** { volatile <fields>; }
-keep class io.ktor.** { *; }
-keep class kotlinx.coroutines.** { *; }
-dontwarn kotlinx.atomicfu.**
-dontwarn io.netty.**
-dontwarn com.typesafe.**
-assumenosideeffects class * implements org.slf4j.Logger {
public *** trace(...);
public *** debug(...);
public *** info(...);
public *** warn(...);
public *** error(...);
}
-keep class kotlin.reflect.jvm.internal.** { *; }
-keep class com.arthenica.ffmpegkit.FFmpegKitConfig {
native <methods>;
void log(long, int, byte[]);
void statistics(long, int, float, float, long , int, double, double);
int safOpen(int);
int safClose(int);
}
-keep class com.arthenica.ffmpegkit.AbiDetect {
native <methods>;
}
-keep class com.arthenica.ffmpegkit.NativeLoader { *; }
-keep class moe.fuqiuluo.** { *; }
-keep class com.tencent.** { *; }
-keep class com.qq.** { *; }
-keep class com.google.gson.** { *; }
-keep class de.** { *; }
-keep class mqq.** { *; }
-keep class QQService.** { *; }
-keep class SummaryCard.** { *; }
-keep class tencent.** { *; }
-keepclassmembers class * {
native <methods>;
}
-keep class io.netty.** { *; }
-keep class moe.fuqiuluo.** implements com.tencent.qqnt.kernel.nativeinterface.** {
*;
}

View File

@ -1,8 +0,0 @@
// IByteData.aidl
package moe.fuqiuluo.shamrock.xposed.ipc.bytedata;
import moe.fuqiuluo.shamrock.xposed.ipc.bytedata.IByteDataSign;
interface IByteData {
IByteDataSign sign(String uin, String data, in byte[] salt);
}

View File

@ -1,4 +0,0 @@
// IByteDataSign.aidl
package moe.fuqiuluo.shamrock.xposed.ipc.bytedata;
parcelable IByteDataSign;

View File

@ -1,4 +0,0 @@
// IQSign.aidl
package moe.fuqiuluo.shamrock.xposed.ipc.qsign;
parcelable IQSign;

View File

@ -1,14 +0,0 @@
// IQSigner.aidl
package moe.fuqiuluo.shamrock.xposed.ipc.qsign;
import moe.fuqiuluo.shamrock.xposed.ipc.qsign.IQSign;
interface IQSigner {
IQSign sign(String cmd, int seq, String uin, in byte[] buffer);
byte[] energy(String module, in byte[] salt);
byte[] xwDebugId(String uin, String start, String end);
List<String> getCmdWhiteList();
}

View File

@ -0,0 +1,43 @@
# Shamrock Config
# 资源上传群组
resource_group=883536416
# 强制使用平板模式
force_tablet=false
# 被动反向RPC开关
passive_rpc=false
# 被动反向RPC地址
rpc_address=
# 第一个被动RPC鉴权token
rpc_address.ticket=
# 如果有多个请使用
# 我是第二个地址
#rpc_address.1=
# 第二个被动RPC鉴权token
#rpc_address.1.ticket=
# 主动正向RPC开关
active_rpc=false
# 主动正向RPC端口
rpc_port=5700
# 主动RPC鉴权token
active_ticket=
# 多鉴权token支持
# 第二个主动RPC鉴权token
#active_ticket.1=
# 自回复开关
#alive_reply=false
# 自回复消息
enable_self_message=false
# 旧BDH兼容开关
enable_old_bdh=true
# 反JVM调用栈跟踪
anti_jvm_trace=true
# 调试模式
#debug=false

View File

@ -173,7 +173,7 @@ NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) {
extern "C" extern "C"
JNIEXPORT jboolean JNICALL JNIEXPORT jboolean JNICALL
Java_moe_fuqiuluo_shamrock_xposed_hooks_AntiDetection_antiNativeDetections(JNIEnv *env, Java_moe_fuqiuluo_shamrock_xposed_actions_AntiDetection_antiNativeDetections(JNIEnv *env,
jobject thiz) { jobject thiz) {
if (hook_function == nullptr) return false; if (hook_function == nullptr) return false;
hook_function((void*) __system_property_get, (void *)fake_system_property_get, (void **) &backup_system_property_get); hook_function((void*) __system_property_get, (void *)fake_system_property_get, (void **) &backup_system_property_get);

View File

@ -0,0 +1,64 @@
package kritor.auth
import io.grpc.ForwardingServerCallListener
import io.grpc.Metadata
import io.grpc.ServerCall
import io.grpc.ServerCallHandler
import io.grpc.ServerInterceptor
import moe.fuqiuluo.shamrock.config.ActiveTicket
import moe.fuqiuluo.shamrock.config.ShamrockConfig
object AuthInterceptor: ServerInterceptor {
/**
* Intercept [ServerCall] dispatch by the `next` [ServerCallHandler]. General
* semantics of [ServerCallHandler.startCall] apply and the returned
* [io.grpc.ServerCall.Listener] must not be `null`.
*
*
* If the implementation throws an exception, `call` will be closed with an error.
* Implementations must not throw an exception if they started processing that may use `call` on another thread.
*
* @param call object to receive response messages
* @param headers which can contain extra call metadata from [ClientCall.start],
* e.g. authentication credentials.
* @param next next processor in the interceptor chain
* @return listener for processing incoming messages for `call`, never `null`.
*/
override fun <ReqT : Any?, RespT : Any?> interceptCall(
call: ServerCall<ReqT, RespT>,
headers: Metadata?,
next: ServerCallHandler<ReqT, RespT>
): ServerCall.Listener<ReqT> {
val methodName = call.methodDescriptor.fullMethodName
val ticket = getAllTicket()
if (ticket.isNotEmpty() && !methodName.startsWith("Auth")) {
val ticketHeader = headers?.get(Metadata.Key.of("ticket", Metadata.ASCII_STRING_MARSHALLER))
if (ticketHeader == null || ticketHeader !in ticket) {
call.close(io.grpc.Status.UNAUTHENTICATED.withDescription("Invalid ticket"), Metadata())
return object: ServerCall.Listener<ReqT>() {}
}
}
return object: ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(next.startCall(call, headers)) {
}
}
fun getAllTicket(): List<String> {
val result = arrayListOf<String>()
val activeTicketName = ActiveTicket.name()
var index = 0
while (true) {
val ticket = ShamrockConfig.getProperty(activeTicketName + if (index == 0) "" else ".$index", null)
if (ticket.isNullOrEmpty()) {
if (index == 0) {
return result
} else {
break
}
} else {
result.add(ticket)
}
index++
}
return result
}
}

View File

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

@ -0,0 +1,70 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
package kritor.server
import io.grpc.Grpc
import io.grpc.Metadata
import io.grpc.InsecureServerCredentials
import io.grpc.ServerCall
import io.grpc.ServerCallHandler
import io.grpc.ServerInterceptor
import io.grpc.ForwardingServerCall.SimpleForwardingServerCall;
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asExecutor
import kritor.auth.AuthInterceptor
import kritor.service.*
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
import qq.service.ticket.TicketHelper
import kotlin.coroutines.CoroutineContext
class KritorServer(
private val port: Int
): CoroutineScope {
private val serverInterceptor = object : ServerInterceptor {
override fun <ReqT, RespT> interceptCall(
call: ServerCall<ReqT, RespT>, headers: Metadata, next: ServerCallHandler<ReqT, RespT>
): ServerCall.Listener<ReqT> {
return next.startCall(object : SimpleForwardingServerCall<ReqT, RespT>(call) {
override fun sendHeaders(headers: Metadata?) {
headers?.apply {
put(Metadata.Key.of("kritor-self-uin", Metadata.ASCII_STRING_MARSHALLER), TicketHelper.getUin())
put(Metadata.Key.of("kritor-self-uid", Metadata.ASCII_STRING_MARSHALLER), TicketHelper.getUid())
put(Metadata.Key.of("kritor-self-version", Metadata.ASCII_STRING_MARSHALLER), "OpenShamrock-$ShamrockVersion")
}
super.sendHeaders(headers)
}
}, headers)
}
}
private val server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create())
.executor(Dispatchers.IO.asExecutor())
.intercept(AuthInterceptor)
.intercept(serverInterceptor)
.addService(AuthenticationService)
.addService(CoreService)
.addService(FriendService)
.addService(GroupService)
.addService(GroupFileService)
.addService(MessageService)
.addService(EventService)
.addService(WebService)
.addService(DeveloperService)
.addService(QsignService)
.build()!!
fun start(block: Boolean = false) {
LogCenter.log("KritorServer started at port $port.")
server.start()
if (block) {
server.awaitTermination()
}
}
override val coroutineContext: CoroutineContext =
Dispatchers.IO.limitedParallelism(12)
}

View File

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

@ -0,0 +1,101 @@
package kritor.service
import android.util.Base64
import com.tencent.mobileqq.app.QQAppInterface
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.core.*
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
import moe.fuqiuluo.shamrock.utils.DownloadUtils
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MD5
import mqq.app.MobileQQ
import qq.service.QQInterfaces.Companion.app
import qq.service.contact.ContactHelper
import java.io.File
internal object CoreService : CoreServiceGrpcKt.CoreServiceCoroutineImplBase() {
@Grpc("CoreService", "GetVersion")
override suspend fun getVersion(request: GetVersionRequest): GetVersionResponse {
return GetVersionResponse.newBuilder().apply {
this.version = ShamrockVersion
this.appName = "Shamrock"
}.build()
}
@Grpc("CoreService", "GetCurrentAccount")
override suspend fun getCurrentAccount(request: GetCurrentAccountRequest): GetCurrentAccountResponse {
return GetCurrentAccountResponse.newBuilder().apply {
this.accountName = if (app is QQAppInterface) app.currentNickname else "unknown"
this.accountUid = app.currentUid ?: ""
this.accountUin = (app.currentUin ?: "0").toLong()
}.build()
}
@Grpc("CoreService", "DownloadFile")
override suspend fun downloadFile(request: DownloadFileRequest): DownloadFileResponse {
val headerMap = mutableMapOf(
"User-Agent" to "Shamrock"
)
if (request.hasHeaders()) {
request.headers.split("[\r\n]").forEach {
val pair = it.split("=")
if (pair.size >= 2) {
val (k, v) = pair
headerMap[k] = v
}
}
}
var tmp = FileUtils.getTmpFile("cache")
if (request.hasBase64()) {
val bytes = Base64.decode(request.base64, Base64.DEFAULT)
tmp.writeBytes(bytes)
} else if (request.hasUrl()) {
if (!DownloadUtils.download(
urlAdr = request.url,
dest = tmp,
headers = headerMap,
threadCount = if (request.hasThreadCnt()) request.threadCnt else 3
)
) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("download failed"))
}
}
tmp = if (!request.hasFileName()) FileUtils.renameByMd5(tmp)
else tmp.parentFile!!.resolve(request.fileName).also {
tmp.renameTo(it)
}
if (request.hasRootPath()) {
tmp = File(request.rootPath).resolve(tmp.name).also {
tmp.renameTo(it)
}
}
return DownloadFileResponse.newBuilder().apply {
this.fileMd5 = MD5.genFileMd5Hex(tmp.absolutePath)
this.fileAbsolutePath = tmp.absolutePath
}.build()
}
@Grpc("CoreService", "SwitchAccount")
override suspend fun switchAccount(request: SwitchAccountRequest): SwitchAccountResponse {
val uin = when (request.accountCase!!) {
SwitchAccountRequest.AccountCase.ACCOUNT_UID -> ContactHelper.getUinByUidAsync(request.accountUid)
SwitchAccountRequest.AccountCase.ACCOUNT_UIN -> request.accountUin.toString()
SwitchAccountRequest.AccountCase.ACCOUNT_NOT_SET -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT.withDescription(
"account not found"
)
)
}
val account = MobileQQ.getMobileQQ().allAccounts.firstOrNull { it.uin == uin }
?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("account not found"))
runCatching {
app.switchAccount(account, null)
}.onFailure {
throw StatusRuntimeException(Status.INTERNAL.withCause(it).withDescription("failed to switch account"))
}
return SwitchAccountResponse.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

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

View File

@ -0,0 +1,244 @@
package kritor.service
import android.os.Bundle
import com.tencent.mobileqq.profilecard.api.IProfileCardBlacklistApi
import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst
import com.tencent.mobileqq.profilecard.api.IProfileProtocolService
import com.tencent.mobileqq.qroute.QRoute
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.friend.*
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import qq.service.QQInterfaces
import qq.service.contact.ContactHelper
import qq.service.friend.FriendHelper
import kotlin.coroutines.resume
internal object FriendService : FriendServiceGrpcKt.FriendServiceCoroutineImplBase() {
@Grpc("FriendService", "GetFriendList")
override suspend fun getFriendList(request: GetFriendListRequest): GetFriendListResponse {
val friendList = FriendHelper.getFriendList(if (request.hasRefresh()) request.refresh else false).onFailure {
throw StatusRuntimeException(
Status.INTERNAL
.withDescription(it.stackTraceToString())
)
}.getOrThrow()
return GetFriendListResponse.newBuilder().apply {
friendList.forEach {
this.addFriendsInfo(FriendInfo.newBuilder().apply {
uin = it.uin.toLong()
uid = ContactHelper.getUidByUinAsync(uin)
qid = ""
nick = it.name ?: ""
remark = it.remark ?: ""
age = it.age
level = 0
gender = it.gender.toInt()
groupId = it.groupid
ext = 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

@ -0,0 +1,136 @@
package kritor.service
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.file.*
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.auto.toByteArray
import protobuf.oidb.cmd0x6d7.CreateFolderReq
import protobuf.oidb.cmd0x6d7.DeleteFolderReq
import protobuf.oidb.cmd0x6d7.Oidb0x6d7ReqBody
import protobuf.oidb.cmd0x6d7.Oidb0x6d7RespBody
import protobuf.oidb.cmd0x6d7.RenameFolderReq
import qq.service.QQInterfaces
import qq.service.file.GroupFileHelper
import qq.service.file.GroupFileHelper.getGroupFileSystemInfo
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
import tencent.im.oidb.oidb_sso
internal object GroupFileService : GroupFileServiceGrpcKt.GroupFileServiceCoroutineImplBase() {
@Grpc("GroupFileService", "CreateFolder")
override suspend fun createFolder(request: CreateFolderRequest): CreateFolderResponse {
val data = Oidb0x6d7ReqBody(
createFolder = CreateFolderReq(
groupCode = request.groupId.toULong(),
appId = 3u,
parentFolderId = "/",
folderName = request.name
)
).toByteArray()
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_0", 1751, 0, data)
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
if (fromServiceMsg.wupBuffer == null) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
}
val oidbPkg = oidb_sso.OIDBSSOPkg()
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val rsp = oidbPkg.bytes_bodybuffer.get()
.toByteArray()
.decodeProtobuf<Oidb0x6d7RespBody>()
if (rsp.createFolder?.retCode != 0) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to create folder: ${rsp.createFolder?.retCode}"))
}
return CreateFolderResponse.newBuilder().apply {
this.id = rsp.createFolder?.folderInfo?.folderId ?: ""
this.usedSpace = 0
}.build()
}
@Grpc("GroupFileService", "DeleteFolder")
override suspend fun deleteFolder(request: DeleteFolderRequest): DeleteFolderResponse {
val fromServiceMsg = QQInterfaces.sendOidbAW(
"OidbSvc.0x6d7_1", 1751, 1, Oidb0x6d7ReqBody(
deleteFolder = DeleteFolderReq(
groupCode = request.groupId.toULong(),
appId = 3u,
folderId = request.folderId
)
).toByteArray()
) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
if (fromServiceMsg.wupBuffer == null) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
}
val oidbPkg = oidb_sso.OIDBSSOPkg()
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
if (rsp.deleteFolder?.retCode != 0) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to delete folder: ${rsp.deleteFolder?.retCode}"))
}
return DeleteFolderResponse.newBuilder().build()
}
@Grpc("GroupFileService", "DeleteFile")
override suspend fun deleteFile(request: DeleteFileRequest): DeleteFileResponse {
val oidb0x6d6ReqBody = oidb_0x6d6.ReqBody().apply {
delete_file_req.set(oidb_0x6d6.DeleteFileReqBody().apply {
uint64_group_code.set(request.groupId)
uint32_app_id.set(3)
uint32_bus_id.set(request.busId)
str_parent_folder_id.set("/")
str_file_id.set(request.fileId)
})
}
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d6_3", 1750, 3, oidb0x6d6ReqBody.toByteArray())
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
if (fromServiceMsg.wupBuffer == null) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
}
val oidbPkg = oidb_sso.OIDBSSOPkg()
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val rsp = oidb_0x6d6.RspBody().apply {
mergeFrom(oidbPkg.bytes_bodybuffer.get().toByteArray())
}
if (rsp.delete_file_rsp.int32_ret_code.get() != 0) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to delete file: ${rsp.delete_file_rsp.int32_ret_code.get()}"))
}
return DeleteFileResponse.newBuilder().build()
}
@Grpc("GroupFileService", "RenameFolder")
override suspend fun renameFolder(request: RenameFolderRequest): RenameFolderResponse {
val fromServiceMsg = QQInterfaces.sendOidbAW(
"OidbSvc.0x6d7_3", 1751, 3, Oidb0x6d7ReqBody(
renameFolder = RenameFolderReq(
groupCode = request.groupId.toULong(),
appId = 3u,
folderId = request.folderId,
folderName = request.name
)
).toByteArray()
) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
if (fromServiceMsg.wupBuffer == null) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
}
val oidbPkg = oidb_sso.OIDBSSOPkg()
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
if (rsp.renameFolder?.retCode != 0) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to rename folder: ${rsp.renameFolder?.retCode}"))
}
return RenameFolderResponse.newBuilder().build()
}
@Grpc("GroupFileService", "GetFileSystemInfo")
override suspend fun getFileSystemInfo(request: GetFileSystemInfoRequest): GetFileSystemInfoResponse {
return getGroupFileSystemInfo(request.groupId)
}
@Grpc("GroupFileService", "GetFileList")
override suspend fun getFileList(request: GetFileListRequest): GetFileListResponse {
return if (request.hasFolderId())
GroupFileHelper.getGroupFiles(request.groupId, request.folderId)
else
GroupFileHelper.getGroupFiles(request.groupId)
}
}

View File

@ -0,0 +1,403 @@
package kritor.service
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.group.*
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.TroopHonorHelper.decodeHonor
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
import qq.service.contact.ContactHelper
import qq.service.group.GroupHelper
internal object GroupService : GroupServiceGrpcKt.GroupServiceCoroutineImplBase() {
@Grpc("GroupService", "BanMember")
override suspend fun banMember(request: BanMemberRequest): BanMemberResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.banMember(
request.groupId, when (request.targetCase!!) {
BanMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
BanMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}, request.duration
)
return BanMemberResponse.newBuilder().apply {
groupId = request.groupId
}.build()
}
@Grpc("GroupService", "PokeMember")
override suspend fun pokeMember(request: PokeMemberRequest): PokeMemberResponse {
GroupHelper.pokeMember(
request.groupId, when (request.targetCase!!) {
PokeMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
PokeMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}
)
return PokeMemberResponse.newBuilder().build()
}
@Grpc("GroupService", "KickMember")
override suspend fun kickMember(request: KickMemberRequest): KickMemberResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.kickMember(
request.groupId,
request.rejectAddRequest,
if (request.hasKickReason()) request.kickReason else "",
when (request.targetCase!!) {
KickMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
KickMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}
)
return KickMemberResponse.newBuilder().build()
}
@Grpc("GroupService", "LeaveGroup")
override suspend fun leaveGroup(request: LeaveGroupRequest): LeaveGroupResponse {
GroupHelper.resignTroop(request.groupId.toString())
return LeaveGroupResponse.newBuilder().build()
}
@Grpc("GroupService", "ModifyMemberCard")
override suspend fun modifyMemberCard(request: ModifyMemberCardRequest): ModifyMemberCardResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.modifyGroupMemberCard(
request.groupId, when (request.targetCase!!) {
ModifyMemberCardRequest.TargetCase.TARGET_UIN -> request.targetUin
ModifyMemberCardRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid)
.toLong()
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}, request.card
)
return ModifyMemberCardResponse.newBuilder().build()
}
@Grpc("GroupService", "ModifyGroupName")
override suspend fun modifyGroupName(request: ModifyGroupNameRequest): ModifyGroupNameResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.modifyTroopName(request.groupId.toString(), request.groupName)
return ModifyGroupNameResponse.newBuilder().build()
}
@Grpc("GroupService", "ModifyGroupRemark")
override suspend fun modifyGroupRemark(request: ModifyGroupRemarkRequest): ModifyGroupRemarkResponse {
GroupHelper.modifyGroupRemark(request.groupId, request.remark)
return ModifyGroupRemarkResponse.newBuilder().build()
}
@Grpc("GroupService", "SetGroupAdmin")
override suspend fun setGroupAdmin(request: SetGroupAdminRequest): SetGroupAdminResponse {
if (!GroupHelper.isOwner(request.groupId.toString())) {
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.setGroupAdmin(
request.groupId, when (request.targetCase!!) {
SetGroupAdminRequest.TargetCase.TARGET_UIN -> request.targetUin
SetGroupAdminRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}, request.isAdmin
)
return SetGroupAdminResponse.newBuilder().build()
}
@Grpc("GroupService", "SetGroupUniqueTitle")
override suspend fun setGroupUniqueTitle(request: SetGroupUniqueTitleRequest): SetGroupUniqueTitleResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.setGroupUniqueTitle(
request.groupId.toString(), when (request.targetCase!!) {
SetGroupUniqueTitleRequest.TargetCase.TARGET_UIN -> request.targetUin
SetGroupUniqueTitleRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid)
.toLong()
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}.toString(), request.uniqueTitle
)
return SetGroupUniqueTitleResponse.newBuilder().build()
}
@Grpc("GroupService", "SetGroupWholeBan")
override suspend fun setGroupWholeBan(request: SetGroupWholeBanRequest): SetGroupWholeBanResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.setGroupWholeBan(request.groupId, request.isBan)
return SetGroupWholeBanResponse.newBuilder().build()
}
@Grpc("GroupService", "GetGroupInfo")
override suspend fun getGroupInfo(request: GetGroupInfoRequest): GetGroupInfoResponse {
val groupInfo = GroupHelper.getGroupInfo(request.groupId.toString(), true).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group info").withCause(it))
}.getOrThrow()
return GetGroupInfoResponse.newBuilder().apply {
this.groupInfo = GroupInfo.newBuilder().apply {
groupId = groupInfo.troopcode.toLong()
groupName =
groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName }
?: ""
groupRemark = groupInfo.troopRemark ?: ""
owner = groupInfo.troopowneruin?.toLong() ?: 0
addAllAdmins(GroupHelper.getAdminList(groupId))
maxMemberCount = groupInfo.wMemberMax
memberCount = groupInfo.wMemberNum
groupUin = groupInfo.troopuin?.toLong() ?: 0
}.build()
}.build()
}
@Grpc("GroupService", "GetGroupList")
override suspend fun getGroupList(request: GetGroupListRequest): GetGroupListResponse {
val groupList = GroupHelper.getGroupList(if (request.hasRefresh()) request.refresh else false).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group list").withCause(it))
}.getOrThrow()
return GetGroupListResponse.newBuilder().apply {
groupList.forEach { groupInfo ->
this.addGroupsInfo(GroupInfo.newBuilder().apply {
groupId = groupInfo.troopcode.ifNullOrEmpty { groupInfo.uin }.ifNullOrEmpty { groupInfo.troopuin }?.toLong() ?: 0
groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }
.ifNullOrEmpty { groupInfo.newTroopName }
?: ""
groupRemark = groupInfo.troopRemark ?: ""
owner = groupInfo.troopowneruin?.toLong() ?: 0
addAllAdmins(GroupHelper.getAdminList(groupId))
maxMemberCount = groupInfo.wMemberMax
memberCount = groupInfo.wMemberNum
groupUin = groupInfo.troopuin?.toLong() ?: 0
})
}
}.build()
}
@Grpc("GroupService", "GetGroupMemberInfo")
override suspend fun getGroupMemberInfo(request: GetGroupMemberInfoRequest): GetGroupMemberInfoResponse {
val memberInfo = GroupHelper.getTroopMemberInfoByUin(
request.groupId.toString(), when (request.targetCase!!) {
GetGroupMemberInfoRequest.TargetCase.TARGET_UID -> request.targetUin
GetGroupMemberInfoRequest.TargetCase.TARGET_UIN -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}.toString()
).onFailure {
throw StatusRuntimeException(
Status.INTERNAL.withDescription("unable to get group member info").withCause(it)
)
}.getOrThrow()
return GetGroupMemberInfoResponse.newBuilder().apply {
groupMemberInfo = GroupMemberInfo.newBuilder().apply {
uid =
if (request.targetCase == GetGroupMemberInfoRequest.TargetCase.TARGET_UID) request.targetUid else ContactHelper.getUidByUinAsync(
request.targetUin
)
uin = memberInfo.memberuin?.toLong() ?: 0
nick = memberInfo.troopnick
.ifNullOrEmpty { memberInfo.hwName }
.ifNullOrEmpty { memberInfo.troopColorNick }
.ifNullOrEmpty { memberInfo.friendnick } ?: ""
age = memberInfo.age.toInt()
uniqueTitle = memberInfo.mUniqueTitle ?: ""
uniqueTitleExpireTime = memberInfo.mUniqueTitleExpire
card = memberInfo.troopnick.ifNullOrEmpty { memberInfo.friendnick } ?: ""
joinTime = memberInfo.join_time
lastActiveTime = memberInfo.last_active_time
level = memberInfo.level
shutUpTimestamp = memberInfo.gagTimeStamp
distance = memberInfo.distance
addAllHonors((memberInfo.honorList ?: "")
.split("|")
.filter { it.isNotBlank() }
.map { it.toInt() })
unfriendly = false
cardChangeable = GroupHelper.isAdmin(request.groupId.toString())
}.build()
}.build()
}
@Grpc("GroupService", "GetGroupMemberList")
override suspend fun getGroupMemberList(request: GetGroupMemberListRequest): GetGroupMemberListResponse {
val memberList = GroupHelper.getGroupMemberList(
request.groupId.toString(),
if (request.hasRefresh()) request.refresh else false
).onFailure {
throw StatusRuntimeException(
Status.INTERNAL.withDescription("unable to get group member list").withCause(it)
)
}.getOrThrow()
return GetGroupMemberListResponse.newBuilder().apply {
memberList.forEach { memberInfo ->
this.addGroupMembersInfo(GroupMemberInfo.newBuilder().apply {
uid = ContactHelper.getUidByUinAsync(memberInfo.memberuin?.toLong() ?: 0)
uin = memberInfo.memberuin?.toLong() ?: 0
nick = memberInfo.troopnick
.ifNullOrEmpty { memberInfo.hwName }
.ifNullOrEmpty { memberInfo.troopColorNick }
.ifNullOrEmpty { memberInfo.friendnick } ?: ""
age = memberInfo.age.toInt()
uniqueTitle = memberInfo.mUniqueTitle ?: ""
uniqueTitleExpireTime = memberInfo.mUniqueTitleExpire
card = memberInfo.troopnick.ifNullOrEmpty { memberInfo.friendnick } ?: ""
joinTime = memberInfo.join_time
lastActiveTime = memberInfo.last_active_time
level = memberInfo.level
shutUpTimestamp = memberInfo.gagTimeStamp
distance = memberInfo.distance
addAllHonors((memberInfo.honorList ?: "")
.split("|")
.filter { it.isNotBlank() }
.map { it.toInt() })
unfriendly = false
cardChangeable = GroupHelper.isAdmin(request.groupId.toString())
})
}
}.build()
}
@Grpc("GroupService", "GetProhibitedUserList")
override suspend fun getProhibitedUserList(request: GetProhibitedUserListRequest): GetProhibitedUserListResponse {
val prohibitedList = GroupHelper.getProhibitedMemberList(request.groupId).onFailure {
throw StatusRuntimeException(
Status.INTERNAL.withDescription("unable to get prohibited user list").withCause(it)
)
}.getOrThrow()
return GetProhibitedUserListResponse.newBuilder().apply {
prohibitedList.forEach {
this.addProhibitedUsersInfo(ProhibitedUserInfo.newBuilder().apply {
uid = ContactHelper.getUidByUinAsync(it.memberUin)
uin = it.memberUin
prohibitedTime = it.shutuptimestap
})
}
}.build()
}
@Grpc("GroupService", "GetRemainCountAtAll")
override suspend fun getRemainCountAtAll(request: GetRemainCountAtAllRequest): GetRemainCountAtAllResponse {
val remainAtAllRsp = GroupHelper.getGroupRemainAtAllRemain(request.groupId).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get remain count").withCause(it))
}.getOrThrow()
return GetRemainCountAtAllResponse.newBuilder().apply {
accessAtAll = remainAtAllRsp.bool_can_at_all.get()
remainCountForGroup = remainAtAllRsp.uint32_remain_at_all_count_for_group.get()
remainCountForSelf = remainAtAllRsp.uint32_remain_at_all_count_for_uin.get()
}.build()
}
@Grpc("GroupService", "GetNotJoinedGroupInfo")
override suspend fun getNotJoinedGroupInfo(request: GetNotJoinedGroupInfoRequest): GetNotJoinedGroupInfoResponse {
val groupInfo = GroupHelper.getNotJoinedGroupInfo(request.groupId).onFailure {
throw StatusRuntimeException(
Status.INTERNAL.withDescription("unable to get not joined group info").withCause(it)
)
}.getOrThrow()
return GetNotJoinedGroupInfoResponse.newBuilder().apply {
this.groupInfo = NotJoinedGroupInfo.newBuilder().apply {
groupId = groupInfo.groupId
groupName = groupInfo.groupName
owner = groupInfo.owner
maxMemberCount = groupInfo.maxMember
memberCount = groupInfo.memberCount
groupDesc = groupInfo.groupDesc
createTime = groupInfo.createTime.toInt()
groupFlag = groupInfo.groupFlag
groupFlagExt = groupInfo.groupFlagExt
}.build()
}.build()
}
@Grpc("GroupService", "GetGroupHonor")
override suspend fun getGroupHonor(request: GetGroupHonorRequest): GetGroupHonorResponse {
return GetGroupHonorResponse.newBuilder().apply {
GroupHelper.getGroupMemberList(request.groupId.toString(), true).onFailure {
throw StatusRuntimeException(
Status.INTERNAL.withDescription("unable to get group member list").withCause(it)
)
}.onSuccess { memberList ->
memberList.forEach { member ->
(member.honorList ?: "").split("|")
.filter { it.isNotBlank() }
.map { it.toInt() }.forEach {
val honor = decodeHonor(member.memberuin.toLong(), it, member.mHonorRichFlag)
if (honor != null) {
addGroupHonorsInfo(GroupHonorInfo.newBuilder().apply {
uid = ContactHelper.getUidByUinAsync(member.memberuin.toLong())
uin = member.memberuin.toLong()
nick = member.troopnick
.ifNullOrEmpty { member.hwName }
.ifNullOrEmpty { member.troopColorNick }
.ifNullOrEmpty { member.friendnick } ?: ""
honorName = honor.honorName
avatar = honor.honorIconUrl
id = honor.honorId
description = honor.honorUrl
})
}
}
}
}
}.build()
}
}

View File

@ -0,0 +1,470 @@
package kritor.service
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import com.tencent.qqnt.msg.api.IMsgService
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.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() {
@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()
}
}

Some files were not shown because too many files have changed in this diff Show More