mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 05:12:17 +00:00
Compare commits
56 Commits
36ed55d220
...
1.1.1
Author | SHA1 | Date | |
---|---|---|---|
40d2911135 | |||
823d9911a0 | |||
4adbc12a0b | |||
114fbfdd23 | |||
0a563d60a1 | |||
5fb1d0aeb9 | |||
012ecaa85d | |||
7ea127a279 | |||
02f83a3c48 | |||
4016c05882 | |||
59d762eecf | |||
e891bc8512 | |||
494c70f2f8 | |||
7baf459b2a | |||
36a09ca088 | |||
926c4659f6 | |||
cb7c68f36c | |||
72af39208c | |||
042f4bd330 | |||
9aef71b09f | |||
9cbe755520 | |||
df02f9f872 | |||
5cbb695a66 | |||
c014e85faa | |||
4a396b0935 | |||
d59fcf9f6a | |||
cdc664f44a | |||
ad313f384c | |||
b6a510ce05 | |||
bed5947909 | |||
fb6578d243 | |||
d33cace7aa | |||
659d4e5da4 | |||
ac2aee8c0e | |||
0faada7b5a | |||
680317da13 | |||
d66358a1f3 | |||
ee5fcc3403 | |||
6c9b282c6a | |||
3a07116093 | |||
a95d8d85e8 | |||
7bacea3288 | |||
ca47f9dbdf | |||
cb4268edef | |||
c16f9d543c | |||
a528030cbb | |||
bbdfb7c04e | |||
1afc0ac6a6 | |||
638bf72392 | |||
07364c8298 | |||
ee6e13a5bb | |||
c3934778c7 | |||
13a49dd70b | |||
5637db43be | |||
69cdbad643 | |||
a06708bf95 |
4
.github/workflows/build-apk.yml
vendored
4
.github/workflows/build-apk.yml
vendored
@ -3,7 +3,7 @@ name: Build Shamrock APK
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [ v1.0.9 ]
|
branches: [ master ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- '**.txt'
|
- '**.txt'
|
||||||
@ -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
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "kritor"]
|
||||||
|
path = kritor/kritor
|
||||||
|
url = https://github.com/KarinJS/kritor
|
@ -16,19 +16,20 @@
|
|||||||
|
|
||||||
## 简介
|
## 简介
|
||||||
|
|
||||||
☘ 基于 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/)!
|
>
|
||||||
|
> 社区地址:[discord](https://discord.gg/MKR2wz863h)
|
||||||
|
|
||||||
## 兼容|迁移|替代 说明
|
## 兼容|迁移|替代 说明
|
||||||
|
|
||||||
- 一键移植:本项目基于 go-cqhttp 的文档进行开发实现。
|
- 一键移植:本项目基于 go-cqhttp 的文档进行开发实现。
|
||||||
- 平行部署:可多平台部署,未来将会支持 Docker 部署的教程。
|
- 平行部署:可多平台部署。
|
||||||
|
|
||||||
## 相关项目
|
## 相关项目
|
||||||
|
|
||||||
|
@ -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"))
|
||||||
}
|
}
|
9
annotations/src/main/java/kritor/service/Grpc.kt
Normal file
9
annotations/src/main/java/kritor/service/Grpc.kt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
@Target(AnnotationTarget.FUNCTION)
|
||||||
|
annotation class Grpc(
|
||||||
|
val serviceName: String,
|
||||||
|
val funcName: String,
|
||||||
|
|
||||||
|
)
|
@ -1,8 +0,0 @@
|
|||||||
package moe.fuqiuluo.symbols
|
|
||||||
|
|
||||||
@Retention(AnnotationRetention.SOURCE)
|
|
||||||
@Target(AnnotationTarget.CLASS)
|
|
||||||
annotation class OneBotHandler(
|
|
||||||
val actionName: String,
|
|
||||||
val alias: Array<String> = []
|
|
||||||
)
|
|
@ -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 {
|
||||||
|
@ -17,7 +17,7 @@ android {
|
|||||||
minSdk = 27
|
minSdk = 27
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = getVersionCode()
|
versionCode = getVersionCode()
|
||||||
versionName = "1.1.1.onebot" + ".r${getGitCommitCount()}." + getVersionName()
|
versionName = "1.1.1" + ".r${getGitCommitCount()}." + getVersionName()
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@ -201,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"))
|
||||||
|
@ -37,7 +37,6 @@ 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
|
||||||
message.cpp
|
message.cpp
|
||||||
shamrock.cpp)
|
shamrock.cpp)
|
||||||
|
@ -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, "[", "[");
|
|
||||||
replace_string(cache, "]", "]");
|
|
||||||
replace_string(cache, "&", "&");
|
|
||||||
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, "[", "[");
|
|
||||||
replace_string(cache, "]", "]");
|
|
||||||
replace_string(cache, ",", ",");
|
|
||||||
replace_string(cache, "&", "&");
|
|
||||||
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, "[", "[");
|
|
||||||
replace_string(cache, "]", "]");
|
|
||||||
replace_string(cache, ",", ",");
|
|
||||||
replace_string(cache, "&", "&");
|
|
||||||
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, "[", "[");
|
|
||||||
replace_string(cache, "]", "]");
|
|
||||||
replace_string(cache, "&", "&");
|
|
||||||
kv.emplace("_type", "text");
|
|
||||||
kv.emplace("text", cache);
|
|
||||||
dest.push_back(kv);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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
|
|
@ -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, "&", "&");
|
|
||||||
replace_string(tmpValue, "[", "[");
|
|
||||||
replace_string(tmpValue, "]", "]");
|
|
||||||
replace_string(tmpValue, ",", ",");
|
|
||||||
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, "&", "&");
|
|
||||||
replace_string(tmpValue, "[", "[");
|
|
||||||
replace_string(tmpValue, "]", "]");
|
|
||||||
replace_string(tmpValue, ",", ",");
|
|
||||||
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,
|
||||||
|
@ -12,8 +12,7 @@
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jstring JNICALL
|
JNIEXPORT jstring JNICALL
|
||||||
Java_moe_fuqiuluo_shamrock_xposed_hooks_PullConfig_00024Companion_testNativeLibrary(JNIEnv *env,
|
Java_moe_fuqiuluo_shamrock_xposed_actions_interacts_Init_testNativeLibrary(JNIEnv *env, jobject thiz) {
|
||||||
jobject thiz) {
|
|
||||||
return env->NewStringUTF("加载Shamrock库成功~");
|
return env->NewStringUTF("加载Shamrock库成功~");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
@ -52,6 +53,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
@ -65,6 +67,7 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
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
|
||||||
@ -79,13 +82,12 @@ import moe.fuqiuluo.shamrock.ui.theme.RANDOM_SUB_TITLE
|
|||||||
import moe.fuqiuluo.shamrock.ui.theme.RANDOM_TITLE
|
import moe.fuqiuluo.shamrock.ui.theme.RANDOM_TITLE
|
||||||
import moe.fuqiuluo.shamrock.ui.theme.ShamrockTheme
|
import moe.fuqiuluo.shamrock.ui.theme.ShamrockTheme
|
||||||
import moe.fuqiuluo.shamrock.ui.tools.NoIndication
|
import moe.fuqiuluo.shamrock.ui.tools.NoIndication
|
||||||
import moe.fuqiuluo.shamrock.ui.tools.ShamrockTabV2
|
import moe.fuqiuluo.shamrock.ui.tools.ShamrockTab
|
||||||
import moe.fuqiuluo.shamrock.ui.tools.getShamrockVersion
|
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) {
|
LaunchedEffect(Unit) {
|
||||||
while (true) {
|
while (true) {
|
||||||
@ -105,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,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()
|
||||||
@ -293,70 +296,25 @@ 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
|
ShamrockTab(
|
||||||
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)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ShamrockTabV2(
|
|
||||||
selected = curSelected,
|
selected = curSelected,
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
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
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.app.config
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -1,395 +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 getUploadResourceGroup(ctx: Context): String {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getString("up_res_group", "100000000")!!
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setUploadResourceGroup(ctx: Context, v: String) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putString("up_res_group", 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 enableOldBDH(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("enable_old_bdh", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setEnableOldBDH(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("enable_old_bdh", v).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
"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),
|
|
||||||
"enable_old_bdh" to preferences.getBoolean("enable_old_bdh", false),
|
|
||||||
"up_res_group" to preferences.getString("up_res_group", ""),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,50 +154,32 @@ 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,由Shamrock放出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,由客户端提供反向的rpc服务",
|
||||||
isSwitch = ShamrockConfig.isUseCQCode(ctx)
|
isSwitch = ShamrockConfig[ctx, PassiveRPC]
|
||||||
) {
|
) {
|
||||||
ShamrockConfig.setUseCQCode(ctx, it)
|
ShamrockConfig[ctx, PassiveRPC] = it
|
||||||
return@Function true
|
|
||||||
}
|
|
||||||
|
|
||||||
Function(
|
|
||||||
title = "主动WebSocket",
|
|
||||||
desc = "OneBot标准WebSocket,Shamrock作为Server。",
|
|
||||||
isSwitch = ShamrockConfig.isWs(ctx)
|
|
||||||
) {
|
|
||||||
ShamrockConfig.setWs(ctx, it)
|
|
||||||
return@Function true
|
|
||||||
}
|
|
||||||
|
|
||||||
Function(
|
|
||||||
title = "被动WebSocket",
|
|
||||||
desc = "OneBot标准WebSocket,Shamrock作为Client。",
|
|
||||||
isSwitch = ShamrockConfig.isWsClient(ctx)
|
|
||||||
) {
|
|
||||||
ShamrockConfig.setWsClient(ctx, it)
|
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
|
|
||||||
run {
|
run {
|
||||||
val uploadResourceGroup = remember { mutableStateOf(ShamrockConfig.getUploadResourceGroup(ctx)) }
|
val uploadResourceGroup = remember { mutableStateOf(ShamrockConfig[ctx, ResourceGroup]) }
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.absolutePadding(left = 8.dp, right = 8.dp, top = 12.dp, bottom = 0.dp)
|
.absolutePadding(left = 8.dp, right = 8.dp, top = 12.dp, bottom = 0.dp)
|
||||||
@ -380,23 +202,11 @@ private fun FunctionCard(
|
|||||||
},
|
},
|
||||||
confirm = {
|
confirm = {
|
||||||
val groupId = uploadResourceGroup.value
|
val groupId = uploadResourceGroup.value
|
||||||
ShamrockConfig.setUploadResourceGroup(ctx, groupId)
|
ShamrockConfig[ctx, ResourceGroup] = groupId
|
||||||
AppRuntime.log("设置接受资源群聊为[$groupId]。")
|
AppRuntime.log("设置接受资源群聊为[$groupId]。")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Function(
|
|
||||||
title = "专业级接口",
|
|
||||||
desc = "如果你不知道你在做什么,请不要开启本功能。",
|
|
||||||
descColor = Color.Red,
|
|
||||||
isSwitch = ShamrockConfig.isPro(ctx)
|
|
||||||
) {
|
|
||||||
ShamrockConfig.setPro(ctx, it)
|
|
||||||
AppRuntime.log("专业级API = $it", Level.WARN)
|
|
||||||
return@Function true
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,21 +232,10 @@ 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
|
|
||||||
}
|
|
||||||
|
|
||||||
Function(
|
|
||||||
title = "同步消息推送类型异换",
|
|
||||||
desc = "推送来自同号异设备消息,将同步消息作为自发消息推送。",
|
|
||||||
descColor = it,
|
|
||||||
isSwitch = ShamrockConfig.enableSyncMsgAsSentMsg(ctx)
|
|
||||||
) {
|
|
||||||
ShamrockConfig.setEnableSyncMsgAsSentMsg(ctx, it)
|
|
||||||
ShamrockConfig.pushUpdate(ctx)
|
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,10 +243,10 @@ fun LabFragment() {
|
|||||||
title = "启用旧版资源上传系统",
|
title = "启用旧版资源上传系统",
|
||||||
desc = "如果NT内核无法上传资源,请打开本开关。",
|
desc = "如果NT内核无法上传资源,请打开本开关。",
|
||||||
descColor = it,
|
descColor = it,
|
||||||
isSwitch = ShamrockConfig.enableOldBDH(ctx)
|
isSwitch = ShamrockConfig[ctx, EnableOldBDH]
|
||||||
) {
|
) {
|
||||||
ShamrockConfig.setEnableOldBDH(ctx, it)
|
ShamrockConfig[ctx, EnableOldBDH] = it
|
||||||
ShamrockConfig.pushUpdate(ctx)
|
InitHandler.update(ctx)
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"))
|
|
||||||
}
|
|
||||||
}
|
|
@ -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]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -29,8 +29,8 @@ abstract class ModuleHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
putExtra("__hash", callbackId)
|
|
||||||
putExtra("__cmd", cmd)
|
putExtra("__cmd", cmd)
|
||||||
|
putExtra("__hash", callbackId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.onebot.dynamic"
|
intent.action = "moe.fuqiuluo.kritor.dynamic"
|
||||||
intent.intentBuilder()
|
intent.intentBuilder()
|
||||||
sendBroadcast(intent)
|
sendBroadcast(intent)
|
||||||
}
|
}
|
@ -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,8 +149,7 @@ 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
|
||||||
@ -147,8 +160,7 @@ private fun TabBaselineLayout(
|
|||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
Box(Modifier.layoutId("icon")) { icon() }
|
Box(Modifier.layoutId("icon")) { icon() }
|
||||||
}
|
}
|
||||||
}
|
}) { measurables, constraints ->
|
||||||
) { 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
|
||||||
@ -242,27 +254,72 @@ private fun Placeable.PlacementScope.placeTextAndIcon(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ShamrockTabV2(
|
fun ShamrockTab(
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
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) {
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text = {
|
||||||
|
val style = MaterialTheme.typography
|
||||||
|
.fromToken(PrimaryNavigationTabTokens.LabelTextFont)
|
||||||
.copy(textAlign = TextAlign.Center)
|
.copy(textAlign = TextAlign.Center)
|
||||||
ProvideTextStyle(style, content = text)
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ShamrockTabV2(
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ShamrockTab(
|
||||||
selected,
|
selected,
|
||||||
onClick,
|
onClick,
|
||||||
modifier,
|
modifier,
|
||||||
@ -272,12 +329,15 @@ fun ShamrockTabV2(
|
|||||||
interactionSource,
|
interactionSource,
|
||||||
indication
|
indication
|
||||||
) {
|
) {
|
||||||
TabBaselineLayout(icon = icon, text = styledText)
|
TabBaselineLayout(
|
||||||
|
icon = icon,
|
||||||
|
text = text
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ShamrockTabV2(
|
fun ShamrockTab(
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -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.1" apply false
|
id("com.android.application") version "8.2.0" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
|
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
|
||||||
id("com.android.library") version "8.2.1" apply false
|
id("com.android.library") version "8.2.0" apply false
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.9.21"
|
kotlin("jvm") version "1.9.22"
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -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
42
kritor/.gitignore
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea/modules.xml
|
||||||
|
.idea/jarRepositories.xml
|
||||||
|
.idea/compiler.xml
|
||||||
|
.idea/libraries/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
|
||||||
|
### Eclipse ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
bin/
|
||||||
|
!**/src/main/**/bin/
|
||||||
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Mac OS ###
|
||||||
|
.DS_Store
|
76
kritor/build.gradle.kts
Normal file
76
kritor/build.gradle.kts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
id("org.jetbrains.kotlin.android")
|
||||||
|
id("com.google.protobuf") version "0.9.4"
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "moe.whitechi73.kritor"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
|
||||||
|
consumerProguardFiles("consumer-rules.pro")
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = false
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
protobuf(files("kritor/protos"))
|
||||||
|
|
||||||
|
implementation("com.google.protobuf:protobuf-java:4.26.0")
|
||||||
|
|
||||||
|
implementation(kotlinx("coroutines-core", "1.8.0"))
|
||||||
|
|
||||||
|
implementation(grpc("stub", "1.62.2"))
|
||||||
|
implementation(grpc("protobuf", "1.62.2"))
|
||||||
|
implementation(grpc("kotlin-stub", "1.4.1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
protobuf {
|
||||||
|
protoc {
|
||||||
|
artifact = "com.google.protobuf:protoc:4.26.0"
|
||||||
|
}
|
||||||
|
plugins {
|
||||||
|
create("grpc") {
|
||||||
|
artifact = "io.grpc:protoc-gen-grpc-java:1.62.2"
|
||||||
|
}
|
||||||
|
create("grpckt") {
|
||||||
|
artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generateProtoTasks {
|
||||||
|
all().forEach {
|
||||||
|
it.plugins {
|
||||||
|
create("grpc")
|
||||||
|
create("grpckt")
|
||||||
|
}
|
||||||
|
it.builtins {
|
||||||
|
create("java")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<KotlinCompile>().configureEach {
|
||||||
|
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
|
||||||
|
}
|
0
kritor/consumer-rules.pro
Normal file
0
kritor/consumer-rules.pro
Normal file
1
kritor/kritor
Submodule
1
kritor/kritor
Submodule
Submodule kritor/kritor added at 3dec747a8e
21
kritor/proguard-rules.pro
vendored
Normal file
21
kritor/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
@ -15,7 +15,7 @@ dependencies {
|
|||||||
implementation("com.google.devtools.ksp:symbol-processing-api:1.9.21-1.0.15")
|
implementation("com.google.devtools.ksp:symbol-processing-api:1.9.21-1.0.15")
|
||||||
implementation("com.squareup:kotlinpoet:1.14.2")
|
implementation("com.squareup:kotlinpoet:1.14.2")
|
||||||
|
|
||||||
implementation(DEPENDENCY_PROTOBUF)
|
//implementation(DEPENDENCY_PROTOBUF)
|
||||||
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
||||||
|
|
||||||
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
|
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
|
||||||
|
@ -0,0 +1,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()
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -32,7 +32,7 @@ class ProtobufProcessor(
|
|||||||
}.toList()
|
}.toList()
|
||||||
|
|
||||||
if (actions.isNotEmpty()) {
|
if (actions.isNotEmpty()) {
|
||||||
actions.forEachIndexed { index, clz ->
|
actions.forEachIndexed { _, clz ->
|
||||||
if (clz.isInternal()) return@forEachIndexed
|
if (clz.isInternal()) return@forEachIndexed
|
||||||
if (clz.isPrivate()) return@forEachIndexed
|
if (clz.isPrivate()) return@forEachIndexed
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
@file:Suppress("UNCHECKED_CAST", "LocalVariableName", "PrivatePropertyName")
|
@file:Suppress("UNCHECKED_CAST", "LocalVariableName", "PrivatePropertyName")
|
||||||
@file:OptIn(KspExperimental::class)
|
@file:OptIn(KspExperimental::class, KspExperimental::class)
|
||||||
|
|
||||||
package moe.fuqiuluo.ksp.impl
|
package moe.fuqiuluo.ksp.impl
|
||||||
|
|
||||||
@ -27,10 +27,14 @@ class XposedHookProcessor(
|
|||||||
private val logger: KSPLogger
|
private val logger: KSPLogger
|
||||||
): SymbolProcessor {
|
): SymbolProcessor {
|
||||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||||
val symbols = resolver.getSymbolsWithAnnotation(XposedHook::class.qualifiedName!!)
|
val symbols = resolver.getSymbolsWithAnnotation(
|
||||||
|
annotationName = XposedHook::class.qualifiedName!!,
|
||||||
|
inDepth = true
|
||||||
|
)
|
||||||
|
logger.warn("Found ${symbols.count()} classes annotated with XposedHook")
|
||||||
val unableToProcess = symbols.filterNot { it.validate() }
|
val unableToProcess = symbols.filterNot { it.validate() }
|
||||||
val actions = (symbols.filter {
|
val actions = (symbols.filter {
|
||||||
it is KSClassDeclaration && it.validate() && it.classKind == ClassKind.CLASS
|
it is KSClassDeclaration && it.classKind == ClassKind.CLASS
|
||||||
} as Sequence<KSClassDeclaration>).toList()
|
} as Sequence<KSClassDeclaration>).toList()
|
||||||
|
|
||||||
if (actions.isNotEmpty()) {
|
if (actions.isNotEmpty()) {
|
||||||
@ -46,7 +50,7 @@ class XposedHookProcessor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val context = ClassName("android.content", "Context")
|
val context = ClassName("android.content", "Context")
|
||||||
val packageName = "moe.fuqiuluo.shamrock.xposed.hooks"
|
val packageName = "moe.fuqiuluo.shamrock.xposed.actions"
|
||||||
val fileSpec = FileSpec.builder(packageName, "AutoActionLoader").addFunction(FunSpec.builder("runFirstActions")
|
val fileSpec = FileSpec.builder(packageName, "AutoActionLoader").addFunction(FunSpec.builder("runFirstActions")
|
||||||
.addParameter("ctx", context)
|
.addParameter("ctx", context)
|
||||||
.apply {
|
.apply {
|
||||||
@ -96,16 +100,6 @@ class XposedHookProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return unableToProcess.toList()
|
return unableToProcess.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ActionLoaderVisitor(
|
|
||||||
private val firstActions: List<KSClassDeclaration>,
|
|
||||||
private val serviceActions: List<KSClassDeclaration>,
|
|
||||||
): KSVisitorVoid() {
|
|
||||||
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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
|
||||||
)
|
)
|
@ -37,7 +37,6 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(DEPENDENCY_PROTOBUF)
|
|
||||||
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
||||||
implementation(kotlinx("serialization-json", "1.6.2"))
|
implementation(kotlinx("serialization-json", "1.6.2"))
|
||||||
|
|
||||||
@ -47,5 +46,5 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile>().configureEach {
|
tasks.withType<KotlinCompile>().configureEach {
|
||||||
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
|
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -8,7 +8,9 @@ import moe.fuqiuluo.symbols.Protobuf
|
|||||||
data class TrpcOidb(
|
data class TrpcOidb(
|
||||||
@ProtoNumber(1) val cmd: Int = Int.MIN_VALUE,
|
@ProtoNumber(1) val cmd: Int = Int.MIN_VALUE,
|
||||||
@ProtoNumber(2) val service: Int = Int.MIN_VALUE,
|
@ProtoNumber(2) val service: Int = Int.MIN_VALUE,
|
||||||
@ProtoNumber(4) val buffer: ByteArray,
|
@ProtoNumber(3) val result: UInt? = null,
|
||||||
|
@ProtoNumber(4) val buffer: ByteArray? = null,
|
||||||
|
@ProtoNumber(5) val msg: String? = null,
|
||||||
//@ProtoNumber(11) val traceParams: Map<String, String> = mapOf(),
|
//@ProtoNumber(11) val traceParams: Map<String, String> = mapOf(),
|
||||||
@ProtoNumber(12) val flag: Int = Int.MIN_VALUE,
|
@ProtoNumber(12) val flag: Int = Int.MIN_VALUE,
|
||||||
): Protobuf<TrpcOidb>
|
): Protobuf<TrpcOidb>
|
@ -1,7 +1,7 @@
|
|||||||
@file:OptIn(ExperimentalSerializationApi::class)
|
@file:OptIn(ExperimentalSerializationApi::class)
|
||||||
package protobuf.oidb.cmd0x11c5
|
package protobuf.oidb.cmd0x11c5
|
||||||
|
|
||||||
import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY
|
import moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
@ -9,7 +9,7 @@ import moe.fuqiuluo.symbols.Protobuf
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class NtV2RichMediaRsp(
|
data class NtV2RichMediaRsp(
|
||||||
@ProtoNumber(1) val head: RspHead,
|
@ProtoNumber(1) val head: RspHead?,
|
||||||
@ProtoNumber(2) val upload: UploadRsp?,
|
@ProtoNumber(2) val upload: UploadRsp?,
|
||||||
@ProtoNumber(3) val download: DownloadRsp?,
|
@ProtoNumber(3) val download: DownloadRsp?,
|
||||||
@ProtoNumber(4) val downloadRkeyRsp: DownloadRkeyRsp?,
|
@ProtoNumber(4) val downloadRkeyRsp: DownloadRkeyRsp?,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package protobuf.oidb.cmd0x388
|
package protobuf.oidb.cmd0x388
|
||||||
|
|
||||||
import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY
|
import moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
import moe.fuqiuluo.symbols.Protobuf
|
import moe.fuqiuluo.symbols.Protobuf
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
package protobuf.oidb.cmd0x388
|
package protobuf.oidb.cmd0x388
|
||||||
|
|
||||||
import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY
|
import moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
@ -6,9 +6,13 @@ import com.tencent.mobileqq.app.BusinessObserver;
|
|||||||
import com.tencent.mobileqq.app.MessageHandler;
|
import com.tencent.mobileqq.app.MessageHandler;
|
||||||
import com.tencent.qphone.base.remote.ToServiceMsg;
|
import com.tencent.qphone.base.remote.ToServiceMsg;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import mqq.app.AppRuntime;
|
import mqq.app.AppRuntime;
|
||||||
|
|
||||||
public abstract class AppInterface extends AppRuntime {
|
public abstract class AppInterface extends AppRuntime {
|
||||||
|
private final ConcurrentHashMap<String, BusinessHandler> allHandler = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public String getCurrentNickname() {
|
public String getCurrentNickname() {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,10 @@ public abstract class BaseBusinessHandler extends OidbWrapper {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addBusinessObserver(ToServiceMsg toServiceMsg, BusinessObserver businessObserver, boolean z) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public final <T> T decodePacket(byte[] data, String name, T obj) {
|
public final <T> T decodePacket(byte[] data, String name, T obj) {
|
||||||
UniPacket uniPacket = new UniPacket(true);
|
UniPacket uniPacket = new UniPacket(true);
|
||||||
try {
|
try {
|
||||||
@ -24,6 +28,10 @@ public abstract class BaseBusinessHandler extends OidbWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean msgCmdFilter(String str) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract Set<String> getCommandList();
|
protected abstract Set<String> getCommandList();
|
||||||
|
|
||||||
protected abstract Set<String> getPushCommandList();
|
protected abstract Set<String> getPushCommandList();
|
||||||
|
@ -8,6 +8,8 @@ public abstract class BusinessHandler extends BaseBusinessHandler {
|
|||||||
public BusinessHandler(AppInterface appInterface) {
|
public BusinessHandler(AppInterface appInterface) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract Class<? extends BusinessObserver> observerClass();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<String> getCommandList() {
|
public Set<String> getCommandList() {
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
package com.tencent.mobileqq.data.troop;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
public class TroopMemberNickInfo {
|
|
||||||
@NotNull
|
|
||||||
public final String getAutoRemark() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public final String getColorNick() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final int getColorNickId() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public final String getFriendNick() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public final String getHBShowName() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public final String getRemarkFromFriend() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public final String getRemarkFromFriendV2() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public final String getShowName() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public final String getTroopNick() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public final String getTroopUin() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package com.tencent.mobileqq.l0.b;
|
|
||||||
|
|
||||||
public class a {
|
|
||||||
public void b () {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void c (c cVar) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onUpdate(int progress, boolean z, Object obj) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
package com.tencent.mobileqq.l0.b;
|
|
||||||
|
|
||||||
import android.graphics.Point;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
public class b {
|
|
||||||
// text
|
|
||||||
public String a;
|
|
||||||
|
|
||||||
// confidence
|
|
||||||
public int b;
|
|
||||||
|
|
||||||
// coordinates
|
|
||||||
public ArrayList<Point> c;
|
|
||||||
|
|
||||||
/* renamed from: d */
|
|
||||||
public int d;
|
|
||||||
|
|
||||||
/* renamed from: e */
|
|
||||||
public boolean e;
|
|
||||||
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
package com.tencent.mobileqq.l0.b;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
public class c {
|
|
||||||
// image
|
|
||||||
public String a;
|
|
||||||
|
|
||||||
// width
|
|
||||||
public int b;
|
|
||||||
|
|
||||||
// height
|
|
||||||
public int c;
|
|
||||||
|
|
||||||
// lang
|
|
||||||
public String d;
|
|
||||||
|
|
||||||
// url
|
|
||||||
public String e;
|
|
||||||
|
|
||||||
// results
|
|
||||||
public ArrayList<b> f;
|
|
||||||
|
|
||||||
public ArrayList<String> g;
|
|
||||||
|
|
||||||
public HashMap<String, String> h;
|
|
||||||
|
|
||||||
public int i;
|
|
||||||
|
|
||||||
public int j;
|
|
||||||
|
|
||||||
public int k;
|
|
||||||
|
|
||||||
public String l;
|
|
||||||
|
|
||||||
public int m;
|
|
||||||
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
package com.tencent.mobileqq.msf.core;
|
package com.tencent.mobileqq.msf.core;
|
||||||
|
|
||||||
public class MsfCore {
|
public class MsfCore {
|
||||||
public synchronized int getNextSeq() {
|
public static synchronized int getNextSeq() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
package com.tencent.mobileqq.msf.service;
|
|
||||||
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.IBinder;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import com.tencent.mobileqq.msf.core.MsfCore;
|
|
||||||
|
|
||||||
public class MsfService extends Service {
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MsfCore getCore() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
package com.tencent.mobileqq.ocr.a;
|
|
||||||
|
|
||||||
public class a {
|
|
||||||
public String a;
|
|
||||||
|
|
||||||
// 1
|
|
||||||
public int b;
|
|
||||||
|
|
||||||
// file location
|
|
||||||
public String c;
|
|
||||||
|
|
||||||
// null
|
|
||||||
public String d;
|
|
||||||
|
|
||||||
// 0
|
|
||||||
public long e;
|
|
||||||
|
|
||||||
// md5
|
|
||||||
public String f;
|
|
||||||
|
|
||||||
// null
|
|
||||||
public String g;
|
|
||||||
|
|
||||||
// false
|
|
||||||
public boolean h;
|
|
||||||
|
|
||||||
// 0
|
|
||||||
public int i;
|
|
||||||
|
|
||||||
// 0
|
|
||||||
public int j;
|
|
||||||
|
|
||||||
// 0
|
|
||||||
public long k;
|
|
||||||
|
|
||||||
// null
|
|
||||||
public String l;
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
package com.tencent.mobileqq.ocr.api;
|
|
||||||
|
|
||||||
import mqq.app.api.IRuntimeService;
|
|
||||||
|
|
||||||
public interface IPicOcrService extends IRuntimeService {
|
|
||||||
void putOcrResult(String str, com.tencent.mobileqq.l0.b.c cVar);
|
|
||||||
|
|
||||||
void requestOcr(com.tencent.mobileqq.ocr.a.a aVar, com.tencent.mobileqq.l0.b.a callback);
|
|
||||||
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
package com.tencent.mobileqq.ocr.api.impl;
|
|
||||||
|
|
||||||
public class OcrServiceImpl {
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
package com.tencent.mobileqq.ocr.api.impl;
|
|
||||||
|
|
||||||
public class PicOcrServiceImpl {
|
|
||||||
}
|
|
@ -1,8 +1,8 @@
|
|||||||
package com.tencent.qqnt.aio.api;
|
package com.tencent.qqnt.aio.api;
|
||||||
|
|
||||||
import com.tencent.mobileqq.qroute.QRouteApi;
|
import com.tencent.mobileqq.qroute.QRouteApi;
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback;
|
|
||||||
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
|
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
|
||||||
|
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback;
|
||||||
|
|
||||||
public interface IAIOFileTransfer extends QRouteApi {
|
public interface IAIOFileTransfer extends QRouteApi {
|
||||||
void sendLocalFile(Contact contact, String path, IOperateCallback cb);
|
void sendLocalFile(Contact contact, String path, IOperateCallback cb);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
package com.tencent.qqnt.kernel.nativeinterface;
|
||||||
|
|
||||||
|
public class GProGuildTopFeedMsg {
|
||||||
|
}
|
@ -1,7 +1,5 @@
|
|||||||
package com.tencent.qqnt.kernel.nativeinterface;
|
package com.tencent.qqnt.kernel.nativeinterface;
|
||||||
|
|
||||||
import com.tencent.qqnt.kernelpublic.nativeinterface.MemberRole;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
public final class GrayTipGroupMember implements Serializable {
|
public final class GrayTipGroupMember implements Serializable {
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
package com.tencent.qqnt.kernel.nativeinterface;
|
package com.tencent.qqnt.kernel.nativeinterface;
|
||||||
|
|
||||||
import com.tencent.qqnt.kernelpublic.nativeinterface.MemberRole;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
package com.tencent.qqnt.kernel.nativeinterface;
|
package com.tencent.qqnt.kernel.nativeinterface;
|
||||||
|
|
||||||
import com.tencent.qqnt.kernelpublic.nativeinterface.MemberRole;
|
|
||||||
|
|
||||||
public final class GroupSimpleInfo implements IKernelModel {
|
public final class GroupSimpleInfo implements IKernelModel {
|
||||||
String avatarUrl;
|
String avatarUrl;
|
||||||
int discussToGroupMaxMsgSeq;
|
int discussToGroupMaxMsgSeq;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
package com.tencent.qqnt.kernel.nativeinterface;
|
package com.tencent.qqnt.kernel.nativeinterface;
|
||||||
|
|
||||||
|
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
|
|
||||||
|
|
||||||
public interface IKernelMsgListener {
|
public interface IKernelMsgListener {
|
||||||
void onAddSendMsg(MsgRecord msgRecord);
|
void onAddSendMsg(MsgRecord msgRecord);
|
||||||
@ -17,7 +18,7 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onCustomWithdrawConfigUpdate(CustomWithdrawConfig customWithdrawConfig);
|
void onCustomWithdrawConfigUpdate(CustomWithdrawConfig customWithdrawConfig);
|
||||||
|
|
||||||
void onDraftUpdate(Contact contact, ArrayList<MsgElement> arrayList, long j2);
|
void onDraftUpdate(Contact contact, ArrayList<MsgElement> arrayList, long j);
|
||||||
|
|
||||||
void onEmojiDownloadComplete(EmojiNotifyInfo emojiNotifyInfo);
|
void onEmojiDownloadComplete(EmojiNotifyInfo emojiNotifyInfo);
|
||||||
|
|
||||||
@ -31,7 +32,7 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onFirstViewGroupGuildMapping(ArrayList<FirstViewGroupGuildInfo> arrayList);
|
void onFirstViewGroupGuildMapping(ArrayList<FirstViewGroupGuildInfo> arrayList);
|
||||||
|
|
||||||
void onGrabPasswordRedBag(int i2, String str, int i3, RecvdOrder recvdOrder, MsgRecord msgRecord);
|
void onGrabPasswordRedBag(int i, String str, int i2, RecvdOrder recvdOrder, MsgRecord msgRecord);
|
||||||
|
|
||||||
void onGroupFileInfoAdd(GroupItem groupItem);
|
void onGroupFileInfoAdd(GroupItem groupItem);
|
||||||
|
|
||||||
@ -49,6 +50,8 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onGuildNotificationAbstractUpdate(GuildNotificationAbstractInfo guildNotificationAbstractInfo);
|
void onGuildNotificationAbstractUpdate(GuildNotificationAbstractInfo guildNotificationAbstractInfo);
|
||||||
|
|
||||||
|
void onGuildTopFeedUpdate(GProGuildTopFeedMsg gProGuildTopFeedMsg);
|
||||||
|
|
||||||
void onHitCsRelatedEmojiResult(DownloadRelateEmojiResultInfo downloadRelateEmojiResultInfo);
|
void onHitCsRelatedEmojiResult(DownloadRelateEmojiResultInfo downloadRelateEmojiResultInfo);
|
||||||
|
|
||||||
void onHitEmojiKeywordResult(HitRelatedEmojiWordsResult hitRelatedEmojiWordsResult);
|
void onHitEmojiKeywordResult(HitRelatedEmojiWordsResult hitRelatedEmojiWordsResult);
|
||||||
@ -63,7 +66,7 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onLineDev(ArrayList<DevInfo> arrayList);
|
void onLineDev(ArrayList<DevInfo> arrayList);
|
||||||
|
|
||||||
void onLogLevelChanged(long j2);
|
void onLogLevelChanged(long j);
|
||||||
|
|
||||||
void onMsgAbstractUpdate(ArrayList<MsgAbstract> arrayList);
|
void onMsgAbstractUpdate(ArrayList<MsgAbstract> arrayList);
|
||||||
|
|
||||||
@ -77,14 +80,16 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onMsgInfoListUpdate(ArrayList<MsgRecord> arrayList);
|
void onMsgInfoListUpdate(ArrayList<MsgRecord> arrayList);
|
||||||
|
|
||||||
void onMsgQRCodeStatusChanged(int i2);
|
void onMsgQRCodeStatusChanged(int i);
|
||||||
|
|
||||||
void onMsgRecall(int i2, String str, long j2);
|
void onMsgRecall(int i, String str, long j);
|
||||||
|
|
||||||
void onMsgSecurityNotify(MsgRecord msgRecord);
|
void onMsgSecurityNotify(MsgRecord msgRecord);
|
||||||
|
|
||||||
void onMsgSettingUpdate(MsgSetting msgSetting);
|
void onMsgSettingUpdate(MsgSetting msgSetting);
|
||||||
|
|
||||||
|
void onMsgWithRichLinkInfoUpdate(ArrayList<MsgRecord> arrayList);
|
||||||
|
|
||||||
void onNtFirstViewMsgSyncEnd();
|
void onNtFirstViewMsgSyncEnd();
|
||||||
|
|
||||||
void onNtMsgSyncEnd();
|
void onNtMsgSyncEnd();
|
||||||
@ -93,11 +98,11 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onReadFeedEventUpdate(FirstViewDirectMsgNotifyInfo firstViewDirectMsgNotifyInfo);
|
void onReadFeedEventUpdate(FirstViewDirectMsgNotifyInfo firstViewDirectMsgNotifyInfo);
|
||||||
|
|
||||||
void onRecvGroupGuildFlag(int i2);
|
void onRecvGroupGuildFlag(int i);
|
||||||
|
|
||||||
void onRecvMsg(ArrayList<MsgRecord> arrayList);
|
void onRecvMsg(ArrayList<MsgRecord> arrayList);
|
||||||
|
|
||||||
void onRecvMsgSvrRspTransInfo(long j2, Contact contact, int i2, int i3, String str, byte[] bArr);
|
void onRecvMsgSvrRspTransInfo(long j, Contact contact, int i, int i2, String str, byte[] bArr);
|
||||||
|
|
||||||
void onRecvOnlineFileMsg(ArrayList<MsgRecord> arrayList);
|
void onRecvOnlineFileMsg(ArrayList<MsgRecord> arrayList);
|
||||||
|
|
||||||
@ -105,7 +110,9 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onRecvSysMsg(ArrayList<Byte> arrayList);
|
void onRecvSysMsg(ArrayList<Byte> arrayList);
|
||||||
|
|
||||||
void onRecvUDCFlag(int i2);
|
void onRecvUDCFlag(int i);
|
||||||
|
|
||||||
|
void onRedTouchChanged();
|
||||||
|
|
||||||
void onRichMediaDownloadComplete(FileTransNotifyInfo fileTransNotifyInfo);
|
void onRichMediaDownloadComplete(FileTransNotifyInfo fileTransNotifyInfo);
|
||||||
|
|
||||||
@ -115,9 +122,9 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onSearchGroupFileInfoUpdate(SearchGroupFileResult searchGroupFileResult);
|
void onSearchGroupFileInfoUpdate(SearchGroupFileResult searchGroupFileResult);
|
||||||
|
|
||||||
void onSendMsgError(long j2, Contact contact, int i2, String str);
|
void onSendMsgError(long j, Contact contact, int i, String str);
|
||||||
|
|
||||||
void onSysMsgNotification(int i2, long j2, long j3, boolean z, ArrayList<Byte> arrayList);
|
void onSysMsgNotification(int i, long j, long j2, boolean z, ArrayList<Byte> arrayList);
|
||||||
|
|
||||||
void onTempChatInfoUpdate(TempChatInfo tempChatInfo);
|
void onTempChatInfoUpdate(TempChatInfo tempChatInfo);
|
||||||
|
|
||||||
@ -129,9 +136,11 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onUserOnlineStatusChanged(boolean z);
|
void onUserOnlineStatusChanged(boolean z);
|
||||||
|
|
||||||
|
void onUserSecQualityChanged(QueryUserSecQualityRsp queryUserSecQualityRsp);
|
||||||
|
|
||||||
void onUserTabStatusChanged(ArrayList<TabStatusInfo> arrayList);
|
void onUserTabStatusChanged(ArrayList<TabStatusInfo> arrayList);
|
||||||
|
|
||||||
void onlineStatusBigIconDownloadPush(int i2, long j2, String str);
|
void onlineStatusBigIconDownloadPush(int i, long j, String str);
|
||||||
|
|
||||||
void onlineStatusSmallIconDownloadPush(int i2, long j2, String str);
|
void onlineStatusSmallIconDownloadPush(int i, long j, String str);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
package com.tencent.qqnt.kernel.nativeinterface;
|
||||||
|
|
||||||
|
public enum MemberRole {
|
||||||
|
UNSPECIFIED,
|
||||||
|
STRANGER,
|
||||||
|
MEMBER,
|
||||||
|
ADMIN,
|
||||||
|
OWNER
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package com.tencent.qqnt.kernel.nativeinterface;
|
||||||
|
|
||||||
|
public class QueryUserSecQualityRsp {
|
||||||
|
}
|
@ -10,7 +10,6 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
||||||
import kotlin.Pair;
|
|
||||||
import kotlinx.coroutines.flow.Flow;
|
import kotlinx.coroutines.flow.Flow;
|
||||||
|
|
||||||
public interface IMsgService extends QRouteApi {
|
public interface IMsgService extends QRouteApi {
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
package com.tencent.qqnt.troopmemberlist;
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
|
||||||
|
|
||||||
import com.tencent.mobileqq.data.troop.TroopMemberInfo;
|
|
||||||
import com.tencent.mobileqq.data.troop.TroopMemberNickInfo;
|
|
||||||
import com.tencent.mobileqq.qroute.QRouteApi;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import kotlin.Deprecated;
|
|
||||||
import kotlin.Unit;
|
|
||||||
import kotlin.jvm.functions.Function1;
|
|
||||||
import kotlin.jvm.functions.Function2;
|
|
||||||
|
|
||||||
public interface ITroopMemberListRepoApi extends QRouteApi {
|
|
||||||
//void fetchGagTroopMemberInfo(@Nullable String str, @Nullable LifecycleOwner lifecycleOwner, boolean z, @NotNull String str2, @Nullable f fVar);
|
|
||||||
|
|
||||||
//void fetchTroopMemberInfo(@Nullable String str, @Nullable String str2, boolean z, @Nullable LifecycleOwner lifecycleOwner, @NotNull String str3, @Nullable g gVar);
|
|
||||||
|
|
||||||
//void fetchTroopMemberInfoWithExtInfo(@Nullable String str, @Nullable String str2, boolean z, @Nullable LifecycleOwner lifecycleOwner, @NotNull String str3, @Nullable g gVar);
|
|
||||||
|
|
||||||
//void fetchTroopMemberList(@Nullable String str, @Nullable LifecycleOwner lifecycleOwner, boolean z, @NotNull String str2, @Nullable f fVar);
|
|
||||||
|
|
||||||
//void fetchTroopMemberListWithExtInfo(@Nullable String str, @Nullable LifecycleOwner lifecycleOwner, boolean z, @NotNull String str2, @Nullable f fVar);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void fetchTroopMemberName(@Nullable String str, @Nullable String str2, @Nullable LifecycleOwner lifecycleOwner, @NotNull String str3, @Nullable Function1<? super TroopMemberNickInfo, Unit> cb);
|
|
||||||
|
|
||||||
void fetchTroopMemberName(@Nullable String str, @Nullable String str2, @NotNull String str3, @Nullable Function1<? super TroopMemberNickInfo, Unit> cb);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void fetchTroopMemberUid(@Nullable String str, @NotNull Function2<? super Boolean, ? super String, Unit> function2);
|
|
||||||
|
|
||||||
void fetchTroopMemberUid(@NotNull List<String> list, @NotNull Function2<? super Boolean, ? super Map<String, String>, Unit> function2);
|
|
||||||
|
|
||||||
void fetchTroopMemberUin(@Nullable String str, @NotNull Function2<? super Boolean, ? super String, Unit> function2);
|
|
||||||
|
|
||||||
void fetchTroopMemberUin(@NotNull List<String> list, @NotNull Function2<? super Boolean, ? super Map<String, String>, Unit> function2);
|
|
||||||
|
|
||||||
//void fetchTroopMemberUinListInfo(@Nullable String str, @Nullable List<String> list, boolean z, @Nullable LifecycleOwner lifecycleOwner, @NotNull String str2, @Nullable f fVar);
|
|
||||||
|
|
||||||
//void fetchTroopMemberUinListInfoWithExtInfo(@Nullable String str, @Nullable List<String> list, boolean z, @Nullable LifecycleOwner lifecycleOwner, @NotNull String str2, @Nullable f fVar);
|
|
||||||
|
|
||||||
//@Nullable
|
|
||||||
//TroopMemberInfo getTroopMemberFromCacheOrFetchAsync(@Nullable String str, @Nullable String str2, @Nullable LifecycleOwner lifecycleOwner, @NotNull String str3, @Nullable g gVar);
|
|
||||||
|
|
||||||
int getTroopMemberInfoDBVersion();
|
|
||||||
|
|
||||||
@Deprecated(message = "兼容旧逻辑,过渡用,新逻辑不要使用")
|
|
||||||
@Nullable
|
|
||||||
TroopMemberInfo getTroopMemberInfoSync(@Nullable String groupId, @Nullable String userId, @Nullable LifecycleOwner lifecycleOwner, @NotNull String from);
|
|
||||||
|
|
||||||
//@Deprecated(message = "兼容旧逻辑,过渡用,新逻辑不要使用")
|
|
||||||
//@Nullable
|
|
||||||
//TroopMemberInfo getTroopMemberInfoSync(@Nullable String str, @Nullable String str2, @Nullable LifecycleOwner lifecycleOwner, @NotNull String str3, long j);
|
|
||||||
|
|
||||||
//@Deprecated(message = "兼容旧逻辑,过渡用,新逻辑不要使用")
|
|
||||||
//@Nullable
|
|
||||||
//TroopMemberInfo getTroopMemberWithExtFromCacheOrFetchAsync(@Nullable String str, @Nullable String str2, @Nullable LifecycleOwner lifecycleOwner, @NotNull String str3, @Nullable g gVar);
|
|
||||||
|
|
||||||
//@Deprecated(message = "兼容旧逻辑,过渡用,新逻辑不要使用")
|
|
||||||
//@Nullable
|
|
||||||
//TroopMemberInfo getTroopMemberWithExtInfoSync(@Nullable String str, @Nullable String str2, @Nullable LifecycleOwner lifecycleOwner, @NotNull String str3);
|
|
||||||
|
|
||||||
boolean isTroopMemberInfoDBInited(@NotNull String str);
|
|
||||||
|
|
||||||
//void preLoadTroopMemberUinListInfo(@Nullable String str, @Nullable List<String> list, boolean z, @Nullable LifecycleOwner lifecycleOwner, @NotNull String str2, @Nullable f fVar);
|
|
||||||
}
|
|
@ -66,6 +66,13 @@ public abstract class AppRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MobileQQ getApplication() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startServlet(NewIntent newIntent) {
|
||||||
|
}
|
||||||
|
|
||||||
public <T extends IRuntimeService> T getRuntimeService(Class<T> cls, String namespace) {
|
public <T extends IRuntimeService> T getRuntimeService(Class<T> cls, String namespace) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
29
qqinterface/src/main/java/mqq/app/NewIntent.java
Normal file
29
qqinterface/src/main/java/mqq/app/NewIntent.java
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package mqq.app;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import com.tencent.mobileqq.app.BusinessObserver;
|
||||||
|
|
||||||
|
public class NewIntent extends Intent {
|
||||||
|
public boolean runNow;
|
||||||
|
|
||||||
|
public NewIntent(Context context, Class<? extends Servlet> cls) {
|
||||||
|
super(context, cls);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BusinessObserver getObserver() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isWithouLogin() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setObserver(BusinessObserver businessObserver) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWithouLogin(boolean z) {
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.android.tools:r8:8.2.47")
|
classpath("com.android.tools:r8:8.3.37")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,8 +34,9 @@ rootProject.name = "Shamrock"
|
|||||||
include(
|
include(
|
||||||
":app",
|
":app",
|
||||||
":xposed",
|
":xposed",
|
||||||
":qqinterface"
|
":qqinterface",
|
||||||
|
":protobuf",
|
||||||
|
":processor",
|
||||||
|
":annotations",
|
||||||
|
":kritor"
|
||||||
)
|
)
|
||||||
include(":protobuf")
|
|
||||||
include(":processor")
|
|
||||||
include(":annotations")
|
|
||||||
|
@ -63,6 +63,7 @@ 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"))
|
||||||
@ -72,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")
|
||||||
@ -101,6 +95,8 @@ dependencies {
|
|||||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile>().configureEach {
|
tasks.withType<KotlinCompile>().all {
|
||||||
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
|
kotlinOptions {
|
||||||
|
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn")
|
||||||
|
}
|
||||||
}
|
}
|
@ -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);
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
// IByteDataSign.aidl
|
|
||||||
package moe.fuqiuluo.shamrock.xposed.ipc.bytedata;
|
|
||||||
|
|
||||||
parcelable IByteDataSign;
|
|
@ -1,4 +0,0 @@
|
|||||||
// IQSign.aidl
|
|
||||||
package moe.fuqiuluo.shamrock.xposed.ipc.qsign;
|
|
||||||
|
|
||||||
parcelable IQSign;
|
|
@ -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();
|
|
||||||
}
|
|
43
xposed/src/main/assets/config.properties
Normal file
43
xposed/src/main/assets/config.properties
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Shamrock Config
|
||||||
|
|
||||||
|
# 资源上传群组
|
||||||
|
resource_group=883536416
|
||||||
|
|
||||||
|
# 强制使用平板模式
|
||||||
|
force_tablet=false
|
||||||
|
|
||||||
|
# 被动(反向)RPC开关
|
||||||
|
passive_rpc=false
|
||||||
|
# 被动(反向)RPC地址
|
||||||
|
rpc_address=
|
||||||
|
# 第一个被动RPC鉴权token
|
||||||
|
rpc_address.ticket=
|
||||||
|
# 如果有多个请使用
|
||||||
|
# 我是第二个地址
|
||||||
|
#rpc_address.1=
|
||||||
|
# 第二个被动RPC鉴权token
|
||||||
|
#rpc_address.1.ticket=
|
||||||
|
|
||||||
|
# 主动(正向)RPC开关
|
||||||
|
active_rpc=false
|
||||||
|
# 主动(正向)RPC端口
|
||||||
|
rpc_port=5700
|
||||||
|
# 主动RPC鉴权token
|
||||||
|
active_ticket=
|
||||||
|
# 多鉴权token支持
|
||||||
|
# 第二个主动RPC鉴权token
|
||||||
|
#active_ticket.1=
|
||||||
|
|
||||||
|
# 自回复开关
|
||||||
|
#alive_reply=false
|
||||||
|
# 自回复消息
|
||||||
|
enable_self_message=false
|
||||||
|
|
||||||
|
# 旧BDH兼容开关
|
||||||
|
enable_old_bdh=true
|
||||||
|
|
||||||
|
# 反JVM调用栈跟踪
|
||||||
|
anti_jvm_trace=true
|
||||||
|
|
||||||
|
# 调试模式
|
||||||
|
#debug=false
|
@ -173,7 +173,7 @@ NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) {
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_moe_fuqiuluo_shamrock_xposed_hooks_AntiDetection_antiNativeDetections(JNIEnv *env,
|
Java_moe_fuqiuluo_shamrock_xposed_actions_AntiDetection_antiNativeDetections(JNIEnv *env,
|
||||||
jobject thiz) {
|
jobject thiz) {
|
||||||
if (hook_function == nullptr) return false;
|
if (hook_function == nullptr) return false;
|
||||||
hook_function((void*) __system_property_get, (void *)fake_system_property_get, (void **) &backup_system_property_get);
|
hook_function((void*) __system_property_get, (void *)fake_system_property_get, (void **) &backup_system_property_get);
|
||||||
|
64
xposed/src/main/java/kritor/auth/AuthInterceptor.kt
Normal file
64
xposed/src/main/java/kritor/auth/AuthInterceptor.kt
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package kritor.auth
|
||||||
|
|
||||||
|
import io.grpc.ForwardingServerCallListener
|
||||||
|
import io.grpc.Metadata
|
||||||
|
import io.grpc.ServerCall
|
||||||
|
import io.grpc.ServerCallHandler
|
||||||
|
import io.grpc.ServerInterceptor
|
||||||
|
import moe.fuqiuluo.shamrock.config.ActiveTicket
|
||||||
|
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||||
|
|
||||||
|
object AuthInterceptor: ServerInterceptor {
|
||||||
|
/**
|
||||||
|
* Intercept [ServerCall] dispatch by the `next` [ServerCallHandler]. General
|
||||||
|
* semantics of [ServerCallHandler.startCall] apply and the returned
|
||||||
|
* [io.grpc.ServerCall.Listener] must not be `null`.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* If the implementation throws an exception, `call` will be closed with an error.
|
||||||
|
* Implementations must not throw an exception if they started processing that may use `call` on another thread.
|
||||||
|
*
|
||||||
|
* @param call object to receive response messages
|
||||||
|
* @param headers which can contain extra call metadata from [ClientCall.start],
|
||||||
|
* e.g. authentication credentials.
|
||||||
|
* @param next next processor in the interceptor chain
|
||||||
|
* @return listener for processing incoming messages for `call`, never `null`.
|
||||||
|
*/
|
||||||
|
override fun <ReqT : Any?, RespT : Any?> interceptCall(
|
||||||
|
call: ServerCall<ReqT, RespT>,
|
||||||
|
headers: Metadata?,
|
||||||
|
next: ServerCallHandler<ReqT, RespT>
|
||||||
|
): ServerCall.Listener<ReqT> {
|
||||||
|
val methodName = call.methodDescriptor.fullMethodName
|
||||||
|
val ticket = getAllTicket()
|
||||||
|
if (ticket.isNotEmpty() && !methodName.startsWith("Auth")) {
|
||||||
|
val ticketHeader = headers?.get(Metadata.Key.of("ticket", Metadata.ASCII_STRING_MARSHALLER))
|
||||||
|
if (ticketHeader == null || ticketHeader !in ticket) {
|
||||||
|
call.close(io.grpc.Status.UNAUTHENTICATED.withDescription("Invalid ticket"), Metadata())
|
||||||
|
return object: ServerCall.Listener<ReqT>() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object: ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(next.startCall(call, headers)) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllTicket(): List<String> {
|
||||||
|
val result = arrayListOf<String>()
|
||||||
|
val activeTicketName = ActiveTicket.name()
|
||||||
|
var index = 0
|
||||||
|
while (true) {
|
||||||
|
val ticket = ShamrockConfig.getProperty(activeTicketName + if (index == 0) "" else ".$index", null)
|
||||||
|
if (ticket.isNullOrEmpty()) {
|
||||||
|
if (index == 0) {
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.add(ticket)
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
164
xposed/src/main/java/kritor/client/KritorClient.kt
Normal file
164
xposed/src/main/java/kritor/client/KritorClient.kt
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
@file:OptIn(DelicateCoroutinesApi::class)
|
||||||
|
package kritor.client
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import io.grpc.CallOptions
|
||||||
|
import io.grpc.Channel
|
||||||
|
import io.grpc.ClientCall
|
||||||
|
import io.grpc.ClientInterceptor
|
||||||
|
import io.grpc.ForwardingClientCall
|
||||||
|
import io.grpc.Metadata
|
||||||
|
import io.grpc.ManagedChannel
|
||||||
|
import io.grpc.ManagedChannelBuilder
|
||||||
|
import io.grpc.MethodDescriptor
|
||||||
|
import io.kritor.common.Request
|
||||||
|
import io.kritor.common.Response
|
||||||
|
import io.kritor.event.EventServiceGrpcKt
|
||||||
|
import io.kritor.event.EventStructure
|
||||||
|
import io.kritor.event.EventType
|
||||||
|
import io.kritor.reverse.ReverseServiceGrpcKt
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.asExecutor
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kritor.handlers.handleGrpc
|
||||||
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter
|
||||||
|
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
|
||||||
|
import qq.service.ticket.TicketHelper
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
internal class KritorClient(
|
||||||
|
val host: String,
|
||||||
|
val port: Int
|
||||||
|
) {
|
||||||
|
private lateinit var channel: ManagedChannel
|
||||||
|
|
||||||
|
private lateinit var channelJob: Job
|
||||||
|
private val senderChannel = MutableSharedFlow<Response>()
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
runCatching {
|
||||||
|
if (::channel.isInitialized && isActive()){
|
||||||
|
channel.shutdown()
|
||||||
|
}
|
||||||
|
val interceptor = object : ClientInterceptor {
|
||||||
|
override fun <ReqT, RespT> interceptCall(method: MethodDescriptor<ReqT, RespT>, callOptions: CallOptions, next: Channel): ClientCall<ReqT, RespT> {
|
||||||
|
return object : ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
|
||||||
|
override fun start(responseListener: Listener<RespT>, headers: Metadata) {
|
||||||
|
headers.merge(Metadata().apply {
|
||||||
|
put(Metadata.Key.of("kritor-self-uin", Metadata.ASCII_STRING_MARSHALLER), TicketHelper.getUin())
|
||||||
|
put(Metadata.Key.of("kritor-self-uid", Metadata.ASCII_STRING_MARSHALLER), TicketHelper.getUid())
|
||||||
|
put(Metadata.Key.of("kritor-self-version", Metadata.ASCII_STRING_MARSHALLER), "OpenShamrock-$ShamrockVersion")
|
||||||
|
})
|
||||||
|
super.start(responseListener, headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channel = ManagedChannelBuilder
|
||||||
|
.forAddress(host, port)
|
||||||
|
.usePlaintext()
|
||||||
|
.enableRetry() // 允许尝试
|
||||||
|
.executor(Dispatchers.IO.asExecutor()) // 使用协程的调度器
|
||||||
|
.intercept(interceptor)
|
||||||
|
.build()
|
||||||
|
}.onFailure {
|
||||||
|
LogCenter.log("KritorClient start failed: ${it.stackTraceToString()}", Level.ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listen(retryCnt: Int = -1) {
|
||||||
|
if (::channelJob.isInitialized && channelJob.isActive) {
|
||||||
|
channelJob.cancel()
|
||||||
|
}
|
||||||
|
channelJob = GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
val stub = ReverseServiceGrpcKt.ReverseServiceCoroutineStub(channel)
|
||||||
|
registerEvent(EventType.EVENT_TYPE_MESSAGE)
|
||||||
|
registerEvent(EventType.EVENT_TYPE_CORE_EVENT)
|
||||||
|
registerEvent(EventType.EVENT_TYPE_REQUEST)
|
||||||
|
registerEvent(EventType.EVENT_TYPE_NOTICE)
|
||||||
|
stub.reverseStream(channelFlow {
|
||||||
|
senderChannel.collect { send(it) }
|
||||||
|
}).collect {
|
||||||
|
onReceive(it)
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
LogCenter.log("KritorClient listen failed, retry after 15s: ${it.stackTraceToString()}", Level.WARN)
|
||||||
|
}
|
||||||
|
delay(15.seconds)
|
||||||
|
LogCenter.log("KritorClient listen retrying, retryCnt = $retryCnt", Level.WARN)
|
||||||
|
if (retryCnt != 0) listen(retryCnt - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerEvent(eventType: EventType) {
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
EventServiceGrpcKt.EventServiceCoroutineStub(channel).registerPassiveListener(channelFlow {
|
||||||
|
when(eventType) {
|
||||||
|
EventType.EVENT_TYPE_MESSAGE -> GlobalEventTransmitter.onMessageEvent {
|
||||||
|
send(EventStructure.newBuilder().apply {
|
||||||
|
this.type = EventType.EVENT_TYPE_MESSAGE
|
||||||
|
this.message = it.second
|
||||||
|
}.build())
|
||||||
|
}
|
||||||
|
EventType.EVENT_TYPE_CORE_EVENT -> {}
|
||||||
|
EventType.EVENT_TYPE_NOTICE -> GlobalEventTransmitter.onNoticeEvent {
|
||||||
|
send(EventStructure.newBuilder().apply {
|
||||||
|
this.type = EventType.EVENT_TYPE_NOTICE
|
||||||
|
this.notice = it
|
||||||
|
}.build())
|
||||||
|
}
|
||||||
|
EventType.EVENT_TYPE_REQUEST -> GlobalEventTransmitter.onRequestEvent {
|
||||||
|
send(EventStructure.newBuilder().apply {
|
||||||
|
this.type = EventType.EVENT_TYPE_REQUEST
|
||||||
|
this.request = it
|
||||||
|
}.build())
|
||||||
|
}
|
||||||
|
EventType.UNRECOGNIZED -> {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}.onFailure {
|
||||||
|
LogCenter.log("KritorClient registerEvent failed: ${it.stackTraceToString()}", Level.ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun onReceive(request: Request) = GlobalScope.launch {
|
||||||
|
//LogCenter.log("KritorClient onReceive: $request")
|
||||||
|
runCatching {
|
||||||
|
val rsp = handleGrpc(request.cmd, request.buf.toByteArray())
|
||||||
|
senderChannel.emit(Response.newBuilder()
|
||||||
|
.setCmd(request.cmd)
|
||||||
|
.setCode(Response.ResponseCode.SUCCESS)
|
||||||
|
.setMsg("success")
|
||||||
|
.setSeq(request.seq)
|
||||||
|
.setBuf(ByteString.copyFrom(rsp))
|
||||||
|
.build())
|
||||||
|
}.onFailure {
|
||||||
|
senderChannel.emit(Response.newBuilder()
|
||||||
|
.setCmd(request.cmd)
|
||||||
|
.setCode(Response.ResponseCode.INTERNAL)
|
||||||
|
.setMsg(it.stackTraceToString())
|
||||||
|
.setSeq(request.seq)
|
||||||
|
.setBuf(ByteString.EMPTY)
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isActive(): Boolean {
|
||||||
|
return !channel.isShutdown
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
channel.shutdown()
|
||||||
|
}
|
||||||
|
}
|
6
xposed/src/main/java/kritor/handlers/GrpcHandlers.kt
Normal file
6
xposed/src/main/java/kritor/handlers/GrpcHandlers.kt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package kritor.handlers
|
||||||
|
|
||||||
|
internal object GrpcHandlers {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
70
xposed/src/main/java/kritor/server/KritorServer.kt
Normal file
70
xposed/src/main/java/kritor/server/KritorServer.kt
Normal 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)
|
||||||
|
}
|
60
xposed/src/main/java/kritor/service/AuthenticationService.kt
Normal file
60
xposed/src/main/java/kritor/service/AuthenticationService.kt
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
import io.grpc.Status
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
|
import io.kritor.authentication.*
|
||||||
|
import io.kritor.authentication.AuthenticateResponse.AuthenticateResponseCode
|
||||||
|
import kritor.auth.AuthInterceptor
|
||||||
|
import moe.fuqiuluo.shamrock.config.ActiveTicket
|
||||||
|
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
|
||||||
|
internal object AuthenticationService: AuthenticationServiceGrpcKt.AuthenticationServiceCoroutineImplBase() {
|
||||||
|
@Grpc("AuthenticationService", "Authenticate")
|
||||||
|
override suspend fun authenticate(request: AuthenticateRequest): AuthenticateResponse {
|
||||||
|
if (QQInterfaces.app.account != request.account) {
|
||||||
|
return AuthenticateResponse.newBuilder().apply {
|
||||||
|
code = AuthenticateResponseCode.NO_ACCOUNT
|
||||||
|
msg = "No such account"
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
val activeTicketName = ActiveTicket.name()
|
||||||
|
var index = 0
|
||||||
|
while (true) {
|
||||||
|
val ticket = ShamrockConfig.getProperty(activeTicketName + if (index == 0) "" else ".$index", null)
|
||||||
|
if (ticket.isNullOrEmpty()) {
|
||||||
|
if (index == 0) {
|
||||||
|
return AuthenticateResponse.newBuilder().apply {
|
||||||
|
code = AuthenticateResponseCode.OK
|
||||||
|
msg = "OK"
|
||||||
|
}.build()
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if (ticket == request.ticket) {
|
||||||
|
return AuthenticateResponse.newBuilder().apply {
|
||||||
|
code = AuthenticateResponseCode.OK
|
||||||
|
msg = "OK"
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthenticateResponse.newBuilder().apply {
|
||||||
|
code = AuthenticateResponseCode.NO_TICKET
|
||||||
|
msg = "Invalid ticket"
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("AuthenticationService", "GetAuthenticationState")
|
||||||
|
override suspend fun getAuthenticationState(request: GetAuthenticationStateRequest): GetAuthenticationStateResponse {
|
||||||
|
if (request.account != QQInterfaces.app.account) {
|
||||||
|
throw StatusRuntimeException(Status.CANCELLED.withDescription("No such account"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetAuthenticationStateResponse.newBuilder().apply {
|
||||||
|
isRequired = AuthInterceptor.getAllTicket().isNotEmpty()
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
101
xposed/src/main/java/kritor/service/CoreService.kt
Normal file
101
xposed/src/main/java/kritor/service/CoreService.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
67
xposed/src/main/java/kritor/service/DeveloperService.kt
Normal file
67
xposed/src/main/java/kritor/service/DeveloperService.kt
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import io.kritor.developer.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import moe.fuqiuluo.shamrock.utils.FileUtils
|
||||||
|
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
|
||||||
|
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||||
|
import qq.service.QQInterfaces
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
internal object DeveloperService: DeveloperServiceGrpcKt.DeveloperServiceCoroutineImplBase() {
|
||||||
|
@Grpc("DeveloperService", "Shell")
|
||||||
|
override suspend fun shell(request: ShellRequest): ShellResponse {
|
||||||
|
if (request.commandList.isEmpty()) return ShellResponse.newBuilder().setIsSuccess(false).build()
|
||||||
|
val runtime = Runtime.getRuntime()
|
||||||
|
val result = withTimeoutOrNull(5000L) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
runtime.exec(request.commandList.toTypedArray(), null, File(request.directory)).apply { waitFor() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ShellResponse.newBuilder().apply {
|
||||||
|
if (result == null) {
|
||||||
|
isSuccess = false
|
||||||
|
} else {
|
||||||
|
isSuccess = true
|
||||||
|
result.inputStream.use {
|
||||||
|
data = it.readBytes().toString(Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("DeveloperService", "ClearCache")
|
||||||
|
override suspend fun clearCache(request: ClearCacheRequest): ClearCacheResponse {
|
||||||
|
FileUtils.clearCache()
|
||||||
|
MMKVFetcher.mmkvWithId("audio2silk")
|
||||||
|
.clear()
|
||||||
|
return ClearCacheResponse.newBuilder().build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("DeveloperService", "GetDeviceBattery")
|
||||||
|
override suspend fun getDeviceBattery(request: GetDeviceBatteryRequest): GetDeviceBatteryResponse {
|
||||||
|
return GetDeviceBatteryResponse.newBuilder().apply {
|
||||||
|
PlatformUtils.getDeviceBattery().let {
|
||||||
|
this.battery = it.battery
|
||||||
|
this.scale = it.scale
|
||||||
|
this.status = it.status
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("DeveloperService", "SendPacket")
|
||||||
|
override suspend fun sendPacket(request: SendPacketRequest): SendPacketResponse {
|
||||||
|
return SendPacketResponse.newBuilder().apply {
|
||||||
|
val fromServiceMsg = QQInterfaces.sendBufferAW(request.command, request.isProtobuf, request.requestBuffer.toByteArray())
|
||||||
|
if (fromServiceMsg?.wupBuffer == null) {
|
||||||
|
this.isSuccess = false
|
||||||
|
} else {
|
||||||
|
this.isSuccess = true
|
||||||
|
this.responseBuffer = ByteString.copyFrom(fromServiceMsg.wupBuffer)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
43
xposed/src/main/java/kritor/service/EventService.kt
Normal file
43
xposed/src/main/java/kritor/service/EventService.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
244
xposed/src/main/java/kritor/service/FriendService.kt
Normal file
244
xposed/src/main/java/kritor/service/FriendService.kt
Normal 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.hasNick()) {
|
||||||
|
bundle.putString(IProfileProtocolConst.KEY_NICK, request.nick)
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
133
xposed/src/main/java/kritor/service/GroupFileService.kt
Normal file
133
xposed/src/main/java/kritor/service/GroupFileService.kt
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
import io.grpc.Status
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
|
import io.kritor.file.*
|
||||||
|
import moe.fuqiuluo.shamrock.tools.decodeToOidb
|
||||||
|
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 = fromServiceMsg.decodeToOidb()
|
||||||
|
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 = fromServiceMsg.decodeToOidb()
|
||||||
|
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 = fromServiceMsg.decodeToOidb()
|
||||||
|
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 = fromServiceMsg.decodeToOidb()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
403
xposed/src/main/java/kritor/service/GroupService.kt
Normal file
403
xposed/src/main/java/kritor/service/GroupService.kt
Normal 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
|
||||||
|
import tencent.im.troop.honor.troop_honor
|
||||||
|
|
||||||
|
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.toLong()
|
||||||
|
card = memberInfo.troopnick.ifNullOrEmpty { memberInfo.friendnick } ?: ""
|
||||||
|
joinTime = memberInfo.join_time
|
||||||
|
lastActiveTime = memberInfo.last_active_time
|
||||||
|
level = memberInfo.level
|
||||||
|
shutUpTime = 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,
|
||||||
|
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.values.forEach { memberInfo ->
|
||||||
|
this.addGroupMembersInfo(GroupMemberInfo.newBuilder().apply {
|
||||||
|
uid = memberInfo.uid
|
||||||
|
uin = memberInfo.uin
|
||||||
|
nick = memberInfo.nick ?: ""
|
||||||
|
age = 0
|
||||||
|
uniqueTitle = memberInfo.memberSpecialTitle ?: ""
|
||||||
|
uniqueTitleExpireTime = memberInfo.specialTitleExpireTime
|
||||||
|
card = memberInfo.cardName.ifNullOrEmpty { memberInfo.nick } ?: ""
|
||||||
|
joinTime = memberInfo.joinTime.toLong()
|
||||||
|
lastActiveTime = memberInfo.lastSpeakTime.toLong()
|
||||||
|
level = memberInfo.memberLevel
|
||||||
|
shutUpTime = memberInfo.shutUpTime.toLong()
|
||||||
|
|
||||||
|
distance = 0
|
||||||
|
addAllHonors(memberInfo.groupHonor.let { bytes ->
|
||||||
|
val honor = troop_honor.GroupUserCardHonor()
|
||||||
|
honor.mergeFrom(bytes)
|
||||||
|
honor.id.get()
|
||||||
|
})
|
||||||
|
unfriendly = false
|
||||||
|
cardChangeable = memberInfo.role == com.tencent.qqnt.kernelpublic.nativeinterface.MemberRole.ADMIN
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}.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.toLong()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}.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
|
||||||
|
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, true).onFailure {
|
||||||
|
throw StatusRuntimeException(
|
||||||
|
Status.INTERNAL.withDescription("unable to get group member list").withCause(it)
|
||||||
|
)
|
||||||
|
}.onSuccess { memberList ->
|
||||||
|
memberList.values.forEach { member ->
|
||||||
|
member.groupHonor.let { bytes ->
|
||||||
|
val honor = troop_honor.GroupUserCardHonor()
|
||||||
|
honor.mergeFrom(bytes)
|
||||||
|
honor.id.get()
|
||||||
|
}.forEach {
|
||||||
|
val honor = decodeHonor(member.uin, it, 0)
|
||||||
|
if (honor != null) {
|
||||||
|
addGroupHonorsInfo(GroupHonorInfo.newBuilder().apply {
|
||||||
|
uid = member.uid
|
||||||
|
uin = member.uin
|
||||||
|
nick = member.nick.ifEmpty {
|
||||||
|
member.cardName
|
||||||
|
} ?: ""
|
||||||
|
honorName = honor.honorName
|
||||||
|
avatar = honor.honorIconUrl
|
||||||
|
id = honor.honorId
|
||||||
|
description = honor.honorUrl
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
475
xposed/src/main/java/kritor/service/MessageService.kt
Normal file
475
xposed/src/main/java/kritor/service/MessageService.kt
Normal file
@ -0,0 +1,475 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
import com.tencent.mobileqq.qroute.QRoute
|
||||||
|
import com.tencent.qqnt.kernelpublic.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.toLong()
|
||||||
|
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(
|
||||||
|
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
|
||||||
|
this.senderNick = it.senderNick
|
||||||
|
this.senderUin = it.senderId
|
||||||
|
this.operationTime = it.operatorTime
|
||||||
|
this.operatorNick = it.operatorNick
|
||||||
|
this.operatorUin = it.operatorId
|
||||||
|
this.jsonElements = it.messageContent.toString()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "SetEssenceMessage")
|
||||||
|
override suspend fun setEssenceMessage(request: SetEssenceMessageRequest): SetEssenceMessageResponse {
|
||||||
|
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString())
|
||||||
|
val msg: MsgRecord = withTimeoutOrNull(5000) {
|
||||||
|
val service = QRoute.api(IMsgService::class.java)
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
service.getMsgsByMsgId(contact, arrayListOf(request.messageId.toLong())) { code, _, msgRecords ->
|
||||||
|
if (code == 0 && msgRecords.isNotEmpty()) {
|
||||||
|
continuation.resume(msgRecords.first())
|
||||||
|
} else {
|
||||||
|
continuation.resume(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
continuation.resume(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
|
||||||
|
if (MessageHelper.setEssenceMessage(request.groupId, msg.msgSeq, msg.msgRandom) == null) {
|
||||||
|
throw StatusRuntimeException(Status.NOT_FOUND.withDescription("set essence message failed"))
|
||||||
|
}
|
||||||
|
return SetEssenceMessageResponse.newBuilder().build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("MessageService", "ReactMessageWithEmoji")
|
||||||
|
override suspend fun reactMessageWithEmoji(request: ReactMessageWithEmojiRequest): ReactMessageWithEmojiResponse {
|
||||||
|
val contact = request.contact.let {
|
||||||
|
MessageHelper.generateContact(
|
||||||
|
when (it.scene!!) {
|
||||||
|
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
|
||||||
|
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
|
||||||
|
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
|
||||||
|
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
|
||||||
|
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||||
|
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||||
|
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
|
||||||
|
}, it.peer, it.subPeer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val msg: MsgRecord = withTimeoutOrNull(5000) {
|
||||||
|
val service = QRoute.api(IMsgService::class.java)
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
service.getMsgsByMsgId(contact, arrayListOf(request.messageId.toLong())) { code, _, msgRecords ->
|
||||||
|
if (code == 0 && msgRecords.isNotEmpty()) {
|
||||||
|
continuation.resume(msgRecords.first())
|
||||||
|
} else {
|
||||||
|
continuation.resume(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
continuation.resume(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
|
||||||
|
MessageHelper.setGroupMessageCommentFace(
|
||||||
|
request.contact.longPeer(),
|
||||||
|
msg.msgSeq.toULong(),
|
||||||
|
request.faceId.toString(),
|
||||||
|
request.isSet
|
||||||
|
)
|
||||||
|
return ReactMessageWithEmojiResponse.newBuilder().build()
|
||||||
|
}
|
||||||
|
}
|
33
xposed/src/main/java/kritor/service/QsignService.kt
Normal file
33
xposed/src/main/java/kritor/service/QsignService.kt
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import com.tencent.mobileqq.fe.FEKit
|
||||||
|
import com.tencent.mobileqq.qsec.qsecdandelionsdk.Dandelion
|
||||||
|
import io.kritor.developer.*
|
||||||
|
|
||||||
|
|
||||||
|
internal object QsignService: QsignServiceGrpcKt.QsignServiceCoroutineImplBase() {
|
||||||
|
@Grpc("QsignService", "Sign")
|
||||||
|
override suspend fun sign(request: SignRequest): SignResponse {
|
||||||
|
return SignResponse.newBuilder().apply {
|
||||||
|
val result = FEKit.getInstance().getSign(request.command, request.buffer.toByteArray(), request.seq, request.uin)
|
||||||
|
this.secSig = ByteString.copyFrom(result.sign)
|
||||||
|
this.secDeviceToken = ByteString.copyFrom(result.token)
|
||||||
|
this.secExtra = ByteString.copyFrom(result.extra)
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("QsignService", "Energy")
|
||||||
|
override suspend fun energy(request: EnergyRequest): EnergyResponse {
|
||||||
|
return EnergyResponse.newBuilder().apply {
|
||||||
|
this.result = ByteString.copyFrom(Dandelion.getInstance().fly(request.data, request.salt.toByteArray()))
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("QsignService", "GetCmdWhitelist")
|
||||||
|
override suspend fun getCmdWhitelist(request: GetCmdWhitelistRequest): GetCmdWhitelistResponse {
|
||||||
|
return GetCmdWhitelistResponse.newBuilder().apply {
|
||||||
|
addAllCommands(FEKit.getInstance().cmdWhiteList)
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
58
xposed/src/main/java/kritor/service/WebService.kt
Normal file
58
xposed/src/main/java/kritor/service/WebService.kt
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package kritor.service
|
||||||
|
|
||||||
|
import io.grpc.Status
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
|
import io.kritor.web.*
|
||||||
|
import qq.service.ticket.TicketHelper
|
||||||
|
|
||||||
|
internal object WebService: WebServiceGrpcKt.WebServiceCoroutineImplBase() {
|
||||||
|
@Grpc("WebService", "GetCookies")
|
||||||
|
override suspend fun getCookies(request: GetCookiesRequest): GetCookiesResponse {
|
||||||
|
return GetCookiesResponse.newBuilder().apply {
|
||||||
|
if (request.domain.isNullOrEmpty()) {
|
||||||
|
this.cookie = TicketHelper.getCookie()
|
||||||
|
} else {
|
||||||
|
this.cookie = TicketHelper.getCookie(request.domain)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("WebService", "GetCredentials")
|
||||||
|
override suspend fun getCredentials(request: GetCredentialsRequest): GetCredentialsResponse {
|
||||||
|
return GetCredentialsResponse.newBuilder().apply {
|
||||||
|
if (request.domain.isNullOrEmpty()) {
|
||||||
|
val uin = TicketHelper.getUin()
|
||||||
|
val skey = TicketHelper.getRealSkey(uin)
|
||||||
|
val pskey = TicketHelper.getPSKey(uin)
|
||||||
|
this.cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey;"
|
||||||
|
this.bkn = TicketHelper.getCSRF(pskey)
|
||||||
|
} else {
|
||||||
|
val uin = TicketHelper.getUin()
|
||||||
|
val skey = TicketHelper.getRealSkey(uin)
|
||||||
|
val pskey = TicketHelper.getPSKey(uin, request.domain) ?: ""
|
||||||
|
val pt4token = TicketHelper.getPt4Token(uin, request.domain) ?: ""
|
||||||
|
this.cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey; pt4_token=$pt4token;"
|
||||||
|
this.bkn = TicketHelper.getCSRF(pskey)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("WebService", "GetCSRFToken")
|
||||||
|
override suspend fun getCSRFToken(request: GetCSRFTokenRequest): GetCSRFTokenResponse {
|
||||||
|
return GetCSRFTokenResponse.newBuilder().apply {
|
||||||
|
if (request.domain.isNullOrEmpty()) {
|
||||||
|
this.bkn = TicketHelper.getCSRF()
|
||||||
|
} else {
|
||||||
|
this.bkn = TicketHelper.getCSRF(TicketHelper.getUin(), request.domain)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Grpc("WebService", "GetHttpCookies")
|
||||||
|
override suspend fun getHttpCookies(request: GetHttpCookiesRequest): GetHttpCookiesResponse {
|
||||||
|
return GetHttpCookiesResponse.newBuilder().apply {
|
||||||
|
this.cookie = TicketHelper.getHttpCookies(request.appid, request.daid, request.jumpUrl)
|
||||||
|
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get http cookies"))
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
@ -1,137 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet
|
|
||||||
|
|
||||||
import VIP.GetCustomOnlineStatusReq
|
|
||||||
import VIP.GetCustomOnlineStatusRsp
|
|
||||||
import com.qq.jce.wup.UniPacket
|
|
||||||
import com.tencent.mobileqq.data.Card
|
|
||||||
import com.tencent.mobileqq.profilecard.api.IProfileDataService
|
|
||||||
import com.tencent.mobileqq.profilecard.api.IProfileProtocolService
|
|
||||||
import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver
|
|
||||||
import io.ktor.client.request.header
|
|
||||||
import io.ktor.client.request.post
|
|
||||||
import io.ktor.client.request.setBody
|
|
||||||
import io.ktor.client.statement.bodyAsText
|
|
||||||
import io.ktor.http.ContentType.Application.Json
|
|
||||||
import io.ktor.http.contentType
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import moe.fuqiuluo.shamrock.tools.GlobalClient
|
|
||||||
import moe.fuqiuluo.shamrock.tools.json
|
|
||||||
import moe.fuqiuluo.shamrock.tools.slice
|
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
|
||||||
import moe.fuqiuluo.shamrock.tools.decodeToOidb
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.QQInterfaces
|
|
||||||
import mqq.app.Packet
|
|
||||||
import tencent.im.oidb.cmd0x11b2.oidb_0x11b2
|
|
||||||
import tencent.im.oidb.oidb_sso
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
internal object CardSvc: QQInterfaces() {
|
|
||||||
private val GetModelShowLock by lazy {
|
|
||||||
Mutex()
|
|
||||||
}
|
|
||||||
private val refreshCardLock by lazy {
|
|
||||||
Mutex()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getModelShow(uin: Long = app.longAccountUin): String {
|
|
||||||
return GetModelShowLock.withLock {
|
|
||||||
val uniPacket = UniPacket()
|
|
||||||
uniPacket.servantName = "VIP.CustomOnlineStatusServer.CustomOnlineStatusObj"
|
|
||||||
uniPacket.funcName = "GetCustomOnlineStatus"
|
|
||||||
val getCustomOnlineStatusReq = GetCustomOnlineStatusReq()
|
|
||||||
getCustomOnlineStatusReq.lUin = uin
|
|
||||||
getCustomOnlineStatusReq.sIMei = ""
|
|
||||||
uniPacket.put("req", getCustomOnlineStatusReq)
|
|
||||||
|
|
||||||
val resp = sendBufferAW("VipCustom.GetCustomOnlineStatus", false, uniPacket.encode())
|
|
||||||
?: error("unable to fetch contact model_show")
|
|
||||||
val buffer = resp.wupBuffer
|
|
||||||
Packet.decodePacket(buffer, "rsp", GetCustomOnlineStatusRsp()).sBuffer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun setModelShow(model: String, manu: String, modelShow: String, imei: String, show: Boolean) {
|
|
||||||
val cookie = TicketSvc.getCookie("vip.qq.com")
|
|
||||||
val csrf = TicketSvc.getCSRF(TicketSvc.getUin(), "vip.qq.com")
|
|
||||||
val p4token = TicketSvc.getPt4Token(TicketSvc.getUin(), "vip.qq.com") ?: ""
|
|
||||||
GlobalClient.post("https://club.vip.qq.com/srf-cgi-node?srfname=VIP.CustomOnlineStatusServer.CustomOnlineStatusObj.SetCustomOnlineStatus&ts=${System.currentTimeMillis()}&daid=18&g_tk=$csrf&pt4_token=$p4token") {
|
|
||||||
header("Cookie", cookie)
|
|
||||||
contentType(Json)
|
|
||||||
setBody(mapOf(
|
|
||||||
"servicesName" to "VIP.CustomOnlineStatusServer.CustomOnlineStatusObj",
|
|
||||||
"cmd" to "SetCustomOnlineStatus",
|
|
||||||
"args" to listOf(mapOf(
|
|
||||||
"sIMei" to imei,
|
|
||||||
"sModel" to model,
|
|
||||||
"sManu" to manu,
|
|
||||||
"lUin" to app.currentUin.toLong(),
|
|
||||||
"bShowInfo" to show,
|
|
||||||
"sModelShow" to modelShow
|
|
||||||
))
|
|
||||||
).json.toString())
|
|
||||||
}.bodyAsText().let {
|
|
||||||
LogCenter.log({ "setModelShow() => $it" }, Level.DEBUG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getSharePrivateArkMsg(peerId: Long): String {
|
|
||||||
val reqBody = oidb_0x11b2.BusinessCardV3Req()
|
|
||||||
reqBody.uin.set(peerId)
|
|
||||||
reqBody.jump_url.set("mqqapi://card/show_pslcard?src_type=internal&source=sharecard&version=1&uin=$peerId")
|
|
||||||
|
|
||||||
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x11ca_0", 4790, 0, reqBody.toByteArray())
|
|
||||||
?: error("unable to fetch contact ark_json_text")
|
|
||||||
val body = fromServiceMsg.decodeToOidb()
|
|
||||||
val rsp = oidb_0x11b2.BusinessCardV3Rsp()
|
|
||||||
rsp.mergeFrom(body.bytes_bodybuffer.get().toByteArray())
|
|
||||||
return rsp.signed_ark_msg.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getProfileCard(uin: Long): Result<Card> {
|
|
||||||
return getProfileCardFromCache(uin).onFailure {
|
|
||||||
return refreshAndGetProfileCard(uin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getProfileCardFromCache(uin: Long): Result<Card> {
|
|
||||||
val profileDataService = app
|
|
||||||
.getRuntimeService(IProfileDataService::class.java, "all")
|
|
||||||
val card = profileDataService.getProfileCard(uin.toString(), true)
|
|
||||||
return if (card == null || card.strNick.isNullOrEmpty()) {
|
|
||||||
Result.failure(Exception("unable to fetch profile card"))
|
|
||||||
} else {
|
|
||||||
Result.success(card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun refreshAndGetProfileCard(uin: Long): Result<Card> {
|
|
||||||
val dataService = app
|
|
||||||
.getRuntimeService(IProfileDataService::class.java, "all")
|
|
||||||
val card = refreshCardLock.withLock {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
app.addObserver(object: ProfileCardObserver() {
|
|
||||||
override fun onGetProfileCard(success: Boolean, obj: Any) {
|
|
||||||
app.removeObserver(this)
|
|
||||||
if (!success || obj !is Card) {
|
|
||||||
it.resume(null)
|
|
||||||
} else {
|
|
||||||
dataService.saveProfileCard(obj)
|
|
||||||
it.resume(obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
app.getRuntimeService(IProfileProtocolService::class.java, "all")
|
|
||||||
.requestProfileCard(app.currentUin, uin.toString(), 12, 0L, 0.toByte(), 0L, 0L, null, "", 0L, 10004, null, 0.toByte())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return if (card == null || card.strNick.isNullOrEmpty()) {
|
|
||||||
Result.failure(Exception("unable to fetch profile card"))
|
|
||||||
} else {
|
|
||||||
Result.success(card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet
|
|
||||||
|
|
||||||
import kotlinx.serialization.encodeToByteArray
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.QQInterfaces
|
|
||||||
import protobuf.auto.toByteArray
|
|
||||||
|
|
||||||
import protobuf.oidb.cmd0x9082.Oidb0x9082
|
|
||||||
|
|
||||||
internal object ChatSvc: QQInterfaces() {
|
|
||||||
fun setGroupMessageCommentFace(peer: Long, msgSeq: ULong, faceIndex: String, isSet: Boolean) {
|
|
||||||
val serviceId = if (isSet) 1 else 2
|
|
||||||
sendOidb("OidbSvcTrpcTcp.0x9082_$serviceId", 36994, serviceId, Oidb0x9082(
|
|
||||||
peer = peer.toULong(),
|
|
||||||
msgSeq = msgSeq,
|
|
||||||
faceIndex = faceIndex,
|
|
||||||
flag = 1u,
|
|
||||||
u1 = 0u,
|
|
||||||
u2 = 0u
|
|
||||||
).toByteArray())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,245 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet
|
|
||||||
|
|
||||||
import com.tencent.mobileqq.pb.ByteStringMicro
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.structures.*
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
|
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
|
|
||||||
import moe.fuqiuluo.shamrock.tools.decodeToOidb
|
|
||||||
import moe.fuqiuluo.shamrock.tools.slice
|
|
||||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
|
||||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.QQInterfaces
|
|
||||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
|
||||||
import protobuf.oidb.cmd0x6d7.CreateFolderReq
|
|
||||||
import protobuf.oidb.cmd0x6d7.DeleteFolderReq
|
|
||||||
import protobuf.oidb.cmd0x6d7.MoveFolderReq
|
|
||||||
import protobuf.oidb.cmd0x6d7.Oidb0x6d7ReqBody
|
|
||||||
import protobuf.oidb.cmd0x6d7.Oidb0x6d7RespBody
|
|
||||||
import protobuf.oidb.cmd0x6d7.RenameFolderReq
|
|
||||||
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
|
|
||||||
import tencent.im.oidb.cmd0x6d8.oidb_0x6d8
|
|
||||||
import tencent.im.oidb.oidb_sso
|
|
||||||
import protobuf.group_file_common.FolderInfo as GroupFileCommonFolderInfo
|
|
||||||
import protobuf.auto.toByteArray
|
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
internal object FileSvc: QQInterfaces() {
|
|
||||||
suspend fun createFileFolder(groupId: Long, folderName: String, parentFolderId: String = "/"): Result<GroupFileCommonFolderInfo> {
|
|
||||||
val data = Oidb0x6d7ReqBody(
|
|
||||||
createFolder = CreateFolderReq(
|
|
||||||
groupCode = groupId.toULong(),
|
|
||||||
appId = 3u,
|
|
||||||
parentFolderId = parentFolderId,
|
|
||||||
folderName = folderName
|
|
||||||
)
|
|
||||||
).toByteArray()
|
|
||||||
val fromServiceMsg = sendOidbAW("OidbSvc.0x6d7_0", 1751, 0, data)
|
|
||||||
?: return Result.failure(Exception("unable to fetch result"))
|
|
||||||
val oidbPkg = fromServiceMsg.decodeToOidb()
|
|
||||||
val rsp = oidbPkg.bytes_bodybuffer.get()
|
|
||||||
.toByteArray()
|
|
||||||
.decodeProtobuf<Oidb0x6d7RespBody>()
|
|
||||||
if (rsp.createFolder?.retCode != 0) {
|
|
||||||
return Result.failure(Exception("unable to create folder: ${rsp.createFolder?.retCode}"))
|
|
||||||
}
|
|
||||||
return Result.success(rsp.createFolder!!.folderInfo!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun deleteGroupFolder(groupId: Long, folderUid: String): Boolean {
|
|
||||||
val fromServiceMsg = sendOidbAW("OidbSvc.0x6d7_1", 1751, 1, Oidb0x6d7ReqBody(
|
|
||||||
deleteFolder = DeleteFolderReq(
|
|
||||||
groupCode = groupId.toULong(),
|
|
||||||
appId = 3u,
|
|
||||||
folderId = folderUid
|
|
||||||
)
|
|
||||||
).toByteArray()) ?: return false
|
|
||||||
val oidbPkg = fromServiceMsg.decodeToOidb()
|
|
||||||
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
|
|
||||||
return rsp.deleteFolder?.retCode == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun moveGroupFolder(groupId: Long, folderUid: String, newParentFolderUid: String): Boolean {
|
|
||||||
val fromServiceMsg = sendOidbAW("OidbSvc.0x6d7_2", 1751, 2, Oidb0x6d7ReqBody(
|
|
||||||
moveFolder = MoveFolderReq(
|
|
||||||
groupCode = groupId.toULong(),
|
|
||||||
appId = 3u,
|
|
||||||
folderId = folderUid,
|
|
||||||
parentFolderId = "/"
|
|
||||||
)
|
|
||||||
).toByteArray()) ?: return false
|
|
||||||
val oidbPkg = fromServiceMsg.decodeToOidb()
|
|
||||||
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
|
|
||||||
return rsp.moveFolder?.retCode == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun renameFolder(groupId: Long, folderUid: String, name: String): Boolean {
|
|
||||||
val fromServiceMsg = sendOidbAW("OidbSvc.0x6d7_3", 1751, 3, Oidb0x6d7ReqBody(
|
|
||||||
renameFolder = RenameFolderReq(
|
|
||||||
groupCode = groupId.toULong(),
|
|
||||||
appId = 3u,
|
|
||||||
folderId = folderUid,
|
|
||||||
folderName = name
|
|
||||||
)
|
|
||||||
).toByteArray()) ?: return false
|
|
||||||
val oidbPkg = fromServiceMsg.decodeToOidb()
|
|
||||||
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
|
|
||||||
return rsp.renameFolder?.retCode == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun deleteGroupFile(groupId: Long, bizId: Int, fileUid: String): Boolean {
|
|
||||||
val oidb0x6d6ReqBody = oidb_0x6d6.ReqBody().apply {
|
|
||||||
delete_file_req.set(oidb_0x6d6.DeleteFileReqBody().apply {
|
|
||||||
uint64_group_code.set(groupId)
|
|
||||||
uint32_app_id.set(3)
|
|
||||||
uint32_bus_id.set(bizId)
|
|
||||||
str_parent_folder_id.set("/")
|
|
||||||
str_file_id.set(fileUid)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
val result = sendOidbAW("OidbSvc.0x6d6_3", 1750, 3, oidb0x6d6ReqBody.toByteArray())
|
|
||||||
?: return false
|
|
||||||
val oidbPkg = result.decodeToOidb()
|
|
||||||
val rsp = oidb_0x6d6.RspBody().apply {
|
|
||||||
mergeFrom(oidbPkg.bytes_bodybuffer.get().toByteArray())
|
|
||||||
}
|
|
||||||
return rsp.delete_file_rsp.int32_ret_code.get() == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGroupFileSystemInfo(groupId: Long): FileSystemInfo {
|
|
||||||
val rspGetFileCntBuffer = sendOidbAW("OidbSvc.0x6d8_1", 1752, 2, oidb_0x6d8.ReqBody().also {
|
|
||||||
it.group_file_cnt_req.set(oidb_0x6d8.GetFileCountReqBody().also {
|
|
||||||
it.uint64_group_code.set(groupId)
|
|
||||||
it.uint32_app_id.set(3)
|
|
||||||
it.uint32_bus_id.set(0)
|
|
||||||
})
|
|
||||||
}.toByteArray())
|
|
||||||
val fileCnt: Int
|
|
||||||
val limitCnt: Int
|
|
||||||
if (rspGetFileCntBuffer != null) {
|
|
||||||
oidb_0x6d8.RspBody().mergeFrom(
|
|
||||||
rspGetFileCntBuffer.decodeToOidb()
|
|
||||||
.bytes_bodybuffer.get()
|
|
||||||
.toByteArray()
|
|
||||||
).group_file_cnt_rsp.apply {
|
|
||||||
fileCnt = uint32_all_file_count.get()
|
|
||||||
limitCnt = uint32_limit_count.get()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw RuntimeException("获取群文件数量失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
val rspGetFileSpaceBuffer = sendOidbAW("OidbSvc.0x6d8_1", 1752, 3, oidb_0x6d8.ReqBody().also {
|
|
||||||
it.group_space_req.set(oidb_0x6d8.GetSpaceReqBody().apply {
|
|
||||||
uint64_group_code.set(groupId)
|
|
||||||
uint32_app_id.set(3)
|
|
||||||
})
|
|
||||||
}.toByteArray())
|
|
||||||
val totalSpace: Long
|
|
||||||
val usedSpace: Long
|
|
||||||
if (rspGetFileSpaceBuffer != null) {
|
|
||||||
oidb_0x6d8.RspBody().mergeFrom(
|
|
||||||
rspGetFileSpaceBuffer.decodeToOidb()
|
|
||||||
.bytes_bodybuffer.get()
|
|
||||||
.toByteArray()).group_space_rsp.apply {
|
|
||||||
totalSpace = uint64_total_space.get()
|
|
||||||
usedSpace = uint64_used_space.get()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw RuntimeException("获取群文件空间失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
return FileSystemInfo(
|
|
||||||
fileCnt, limitCnt, usedSpace, totalSpace
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGroupRootFiles(groupId: Long): Result<GroupFileList> {
|
|
||||||
return getGroupFiles(groupId, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGroupFileInfo(groupId: Long, fileId: String, busid: Int): FileUrl {
|
|
||||||
return FileUrl(RichProtoSvc.getGroupFileDownUrl(groupId, fileId, busid))
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGroupFiles(groupId: Long, folderId: String): Result<GroupFileList> {
|
|
||||||
val fileSystemInfo = getGroupFileSystemInfo(groupId)
|
|
||||||
val rspGetFileListBuffer = sendOidbAW("OidbSvc.0x6d8_1", 1752, 1, oidb_0x6d8.ReqBody().also {
|
|
||||||
it.file_list_info_req.set(oidb_0x6d8.GetFileListReqBody().apply {
|
|
||||||
uint64_group_code.set(groupId)
|
|
||||||
uint32_app_id.set(3)
|
|
||||||
str_folder_id.set(folderId)
|
|
||||||
|
|
||||||
uint32_file_count.set(fileSystemInfo.fileCount)
|
|
||||||
uint32_all_file_count.set(0)
|
|
||||||
uint32_req_from.set(3)
|
|
||||||
uint32_sort_by.set(oidb_0x6d8.GetFileListReqBody.SORT_BY_TIMESTAMP)
|
|
||||||
|
|
||||||
uint32_filter_code.set(0)
|
|
||||||
uint64_uin.set(0)
|
|
||||||
|
|
||||||
uint32_start_index.set(0)
|
|
||||||
|
|
||||||
bytes_context.set(ByteStringMicro.copyFrom(EMPTY_BYTE_ARRAY))
|
|
||||||
|
|
||||||
uint32_show_onlinedoc_folder.set(0)
|
|
||||||
})
|
|
||||||
}.toByteArray(), timeout = 15.seconds)
|
|
||||||
|
|
||||||
return kotlin.runCatching {
|
|
||||||
val files = arrayListOf<FileInfo>()
|
|
||||||
val dirs = arrayListOf<FolderInfo>()
|
|
||||||
if (rspGetFileListBuffer != null) {
|
|
||||||
val oidb = rspGetFileListBuffer.decodeToOidb()
|
|
||||||
|
|
||||||
oidb_0x6d8.RspBody().mergeFrom(oidb.bytes_bodybuffer.get().toByteArray())
|
|
||||||
.file_list_info_rsp.apply {
|
|
||||||
rpt_item_list.get().forEach { file ->
|
|
||||||
if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FILE) {
|
|
||||||
val fileInfo = file.file_info
|
|
||||||
files.add(FileInfo(
|
|
||||||
groupId = groupId,
|
|
||||||
fileId = fileInfo.str_file_id.get(),
|
|
||||||
fileName = fileInfo.str_file_name.get(),
|
|
||||||
fileSize = fileInfo.uint64_file_size.get(),
|
|
||||||
busid = fileInfo.uint32_bus_id.get(),
|
|
||||||
uploadTime = fileInfo.uint32_upload_time.get(),
|
|
||||||
deadTime = fileInfo.uint32_dead_time.get(),
|
|
||||||
modifyTime = fileInfo.uint32_modify_time.get(),
|
|
||||||
downloadTimes = fileInfo.uint32_download_times.get(),
|
|
||||||
uploadUin = fileInfo.uint64_uploader_uin.get(),
|
|
||||||
uploadNick = fileInfo.str_uploader_name.get(),
|
|
||||||
md5 = fileInfo.bytes_md5.get().toByteArray().toHexString(),
|
|
||||||
sha = fileInfo.bytes_sha.get().toByteArray().toHexString(),
|
|
||||||
// 根本没有
|
|
||||||
sha3 = fileInfo.bytes_sha3.get().toByteArray().toHexString(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
else if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FOLDER) {
|
|
||||||
val folderInfo = file.folder_info
|
|
||||||
dirs.add(FolderInfo(
|
|
||||||
groupId = groupId,
|
|
||||||
folderId = folderInfo.str_folder_id.get(),
|
|
||||||
folderName = folderInfo.str_folder_name.get(),
|
|
||||||
totalFileCount = folderInfo.uint32_total_file_count.get(),
|
|
||||||
createTime = folderInfo.uint32_create_time.get(),
|
|
||||||
creator = folderInfo.uint64_create_uin.get(),
|
|
||||||
creatorNick = folderInfo.str_creator_name.get()
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
LogCenter.log("未知文件类型: ${file.uint32_type.get()}", Level.WARN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw RuntimeException("获取群文件列表失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupFileList(files, dirs)
|
|
||||||
}.onFailure {
|
|
||||||
LogCenter.log(it.message + ", buffer: ${rspGetFileListBuffer?.wupBuffer?.toHexString()}", Level.ERROR)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,356 +0,0 @@
|
|||||||
@file:OptIn(ExperimentalSerializationApi::class)
|
|
||||||
|
|
||||||
package moe.fuqiuluo.qqinterface.servlet
|
|
||||||
|
|
||||||
import com.tencent.mobileqq.qqguildsdk.api.IGPSService
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.GProGuildRole
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.GProRoleCreateInfo
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.GProRoleMemberList
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.GProRolePermission
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.structures.GProChannelInfo
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.structures.GetGuildMemberListNextToken
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.structures.GuildInfo
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.structures.GuildStatus
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.structures.SlowModeInfo
|
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
|
|
||||||
import moe.fuqiuluo.shamrock.tools.decodeToObject
|
|
||||||
import moe.fuqiuluo.shamrock.tools.decodeToOidb
|
|
||||||
import moe.fuqiuluo.shamrock.tools.slice
|
|
||||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.QQInterfaces
|
|
||||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
|
||||||
import protobuf.auto.toByteArray
|
|
||||||
import protobuf.guild.GetGuildFeedsReq
|
|
||||||
import protobuf.guild.GetGuildFeedsRsp
|
|
||||||
import protobuf.oidb.cmd0xf88.GProFilter
|
|
||||||
import protobuf.oidb.cmd0xf88.GProUserInfo
|
|
||||||
import protobuf.oidb.cmd0xf88.Oidb0xf88Req
|
|
||||||
import protobuf.oidb.cmd0xf88.Oidb0xf88Rsp
|
|
||||||
import protobuf.oidb.cmx0xf57.Oidb0xf57Filter
|
|
||||||
import protobuf.oidb.cmx0xf57.Oidb0xf57GuildInfo
|
|
||||||
import protobuf.oidb.cmx0xf57.Oidb0xf57MetaInfo
|
|
||||||
import protobuf.oidb.cmx0xf57.Oidb0xf57Req
|
|
||||||
import protobuf.oidb.cmx0xf57.Oidb0xf57Rsp
|
|
||||||
import protobuf.oidb.cmx0xf57.Oidb0xf57U1
|
|
||||||
import protobuf.oidb.cmx0xf57.Oidb0xf57U2
|
|
||||||
import protobuf.qweb.DEFAULT_DEVICE_INFO
|
|
||||||
import protobuf.qweb.QWebExtInfo
|
|
||||||
import protobuf.qweb.QWebReq
|
|
||||||
import protobuf.qweb.QWebRsp
|
|
||||||
import tencent.im.oidb.oidb_sso
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
internal object GProSvc: QQInterfaces() {
|
|
||||||
fun getSelfTinyId(): ULong {
|
|
||||||
val service = app.getRuntimeService(IGPSService::class.java, "all")
|
|
||||||
return service.selfTinyId.toULong()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGuildInfo(guildId: ULong): Result<Oidb0xf57MetaInfo> {
|
|
||||||
val respBuffer = sendOidbAW("OidbSvcTrpcTcp.0xf57_9", 0xf57, 9, Oidb0xf57Req(
|
|
||||||
filter = Oidb0xf57Filter(
|
|
||||||
u1 = Oidb0xf57U1(1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u),
|
|
||||||
u2 = Oidb0xf57U2(1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u)
|
|
||||||
),
|
|
||||||
guildInfo = Oidb0xf57GuildInfo(guildId = guildId)
|
|
||||||
).toByteArray()) ?: return Result.failure(Exception("unable to send packet"))
|
|
||||||
val body = respBuffer.decodeToOidb()
|
|
||||||
return runCatching {
|
|
||||||
body.bytes_bodybuffer.get()
|
|
||||||
.toByteArray()
|
|
||||||
.decodeProtobuf<Oidb0xf57Rsp>().metaInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGuildFeeds(guildId: ULong, channelId: ULong, startIndex: Int): Result<GetGuildFeedsRsp> {
|
|
||||||
val fromServiceMsg = sendBufferAW("QChannelSvr.trpc.qchannel.commreader.ComReader.GetGuildFeeds", true, QWebReq(
|
|
||||||
seq = 10,
|
|
||||||
qua = PlatformUtils.getQUA(),
|
|
||||||
deviceInfo = DEFAULT_DEVICE_INFO,
|
|
||||||
buffer = GetGuildFeedsReq(
|
|
||||||
count = 12,
|
|
||||||
from = startIndex,
|
|
||||||
feedAttchInfo = EMPTY_BYTE_ARRAY,
|
|
||||||
guildId = guildId,
|
|
||||||
getType = 1,
|
|
||||||
u7 = 0,
|
|
||||||
u8 = 1,
|
|
||||||
u9 = EMPTY_BYTE_ARRAY
|
|
||||||
).toByteArray(),
|
|
||||||
traceId = app.account + "_0_0",
|
|
||||||
extinfo = listOf(
|
|
||||||
QWebExtInfo("fc-appid", "96"),
|
|
||||||
QWebExtInfo("environment_id", "shamrock"),
|
|
||||||
QWebExtInfo("tiny_id", getSelfTinyId().toString()),
|
|
||||||
)
|
|
||||||
).toByteArray()) ?: return Result.failure(Exception("unable to send packet"))
|
|
||||||
val webRsp = fromServiceMsg.decodeToObject<QWebRsp>()
|
|
||||||
if(webRsp.buffer == null) return Result.failure(Exception("server error"))
|
|
||||||
val wupBuffer = webRsp.buffer!!
|
|
||||||
val feeds = wupBuffer.decodeProtobuf<GetGuildFeedsRsp>()
|
|
||||||
return Result.success(feeds)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getChannelList(guildId: ULong, refresh: Boolean = false): Result<ArrayList<GProChannelInfo>> {
|
|
||||||
if (refresh) {
|
|
||||||
refreshGuildInfo(guildId)
|
|
||||||
}
|
|
||||||
val result = arrayListOf<GProChannelInfo>()
|
|
||||||
app.getRuntimeService(IGPSService::class.java, "all").getChannelList(guildId.toString()).forEach {
|
|
||||||
result.add(GProChannelInfo(
|
|
||||||
ownerGuildId = guildId,
|
|
||||||
guildId = it.guildId,
|
|
||||||
channelId = it.channelUin.toLong(),
|
|
||||||
channelUin = it.channelUin.toLong(),
|
|
||||||
channelName = it.channelName ?: "",
|
|
||||||
channelType = it.type,
|
|
||||||
createTime = it.createTime,
|
|
||||||
creatorTinyId = it.creatorId.toLong(),
|
|
||||||
talkPermission = it.talkPermission,
|
|
||||||
visibleType = it.visibleType,
|
|
||||||
currentSlowMode = it.slowModeKey,
|
|
||||||
slowModes = it.gProSlowModeInfoList.map {
|
|
||||||
SlowModeInfo(it.slowModeKey, it.slowModeText, it.speakFrequency, it.slowModeCircle)
|
|
||||||
},
|
|
||||||
appIconUrl = it.iconUrl,
|
|
||||||
jumpType = it.appChannelJumpType,
|
|
||||||
jumpSwitch = it.jumpSwitch,
|
|
||||||
jumpUrl = it.appChannelJumpUrl,
|
|
||||||
categoryId = it.categoryId,
|
|
||||||
myTalkPermission = it.myTalkPermissionType,
|
|
||||||
maxMemberCount = it.channelMemberMax
|
|
||||||
))
|
|
||||||
}
|
|
||||||
return Result.success(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshGuildInfo(guildId: ULong) {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
kernelGProService.refreshGuildInfo(guildId.toLong(), true, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGuildMemberList(
|
|
||||||
guildId: ULong,
|
|
||||||
startIndex: Long = 0,
|
|
||||||
roleIndex: Long = 1,
|
|
||||||
count: Int = 50,
|
|
||||||
fetchAll: Boolean = false,
|
|
||||||
result: ArrayList<GProRoleMemberList> = arrayListOf()
|
|
||||||
): Result<Pair<GetGuildMemberListNextToken, ArrayList<GProRoleMemberList>>> {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
|
|
||||||
val fetchGuildMemberListResult: Pair<GetGuildMemberListNextToken, ArrayList<GProRoleMemberList>> = (withTimeoutOrNull(5000) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
kernelGProService.fetchMemberListWithRole(guildId.toLong(), 0, startIndex, roleIndex, count, 0) { code, reason, finish, nextIndex, nextRoleIdIndex, _, seq, roleList ->
|
|
||||||
if (code == 0) {
|
|
||||||
it.resume(GetGuildMemberListNextToken(nextIndex, nextRoleIdIndex, seq, finish) to roleList)
|
|
||||||
} else {
|
|
||||||
LogCenter.log("fetchMemberListWithRole failed: $code($reason)", Level.WARN)
|
|
||||||
it.resume(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) ?: return Result.failure(Exception("unable to fetch guild member list"))
|
|
||||||
|
|
||||||
val nextToken = fetchGuildMemberListResult.first
|
|
||||||
val roleList = fetchGuildMemberListResult.second
|
|
||||||
result.addAll(roleList)
|
|
||||||
return if (fetchAll) {
|
|
||||||
if (!fetchGuildMemberListResult.first.finish) {
|
|
||||||
getGuildMemberList(guildId, nextToken.startIndex, nextToken.roleIndex, count, true, result)
|
|
||||||
} else {
|
|
||||||
Result.success(nextToken.copy(finish = true) to result)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Result.success(nextToken to result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getSelfGuildInfo(): Result<GProUserInfo> {
|
|
||||||
val selfTinyId = getSelfTinyId()
|
|
||||||
return getUserGuildInfo(0u, selfTinyId)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getUserGuildInfo(
|
|
||||||
guildId: ULong,
|
|
||||||
memberTinyId: ULong
|
|
||||||
): Result<GProUserInfo> {
|
|
||||||
val respBuffer = sendOidbAW("OidbSvcTrpcTcp.0xf88_1", 0xf88, 1, Oidb0xf88Req(
|
|
||||||
filter = GProFilter(1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u),
|
|
||||||
memberId = 0uL,
|
|
||||||
tinyId = memberTinyId,
|
|
||||||
guildId = guildId
|
|
||||||
).toByteArray()) ?: return Result.failure(Exception("unable to send packet"))
|
|
||||||
val body = respBuffer.decodeToOidb()
|
|
||||||
return runCatching {
|
|
||||||
body.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0xf88Rsp>().userInfo!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getGuildListByOldApi(result: ArrayList<GuildInfo>) {
|
|
||||||
app.getRuntimeService(IGPSService::class.java, "all").guildList?.forEach {
|
|
||||||
result.add(GuildInfo(
|
|
||||||
guildId = it.guildID.toLong(),
|
|
||||||
guildName = it.guildName ?: "",
|
|
||||||
guildDisplayId = it.guildNumber ?: "",
|
|
||||||
profile = it.profile ?: "",
|
|
||||||
status = GuildStatus(
|
|
||||||
isEnable = !it.isFrozen && !it.isBanned,
|
|
||||||
isBanned = it.isBanned,
|
|
||||||
isFrozen = it.isFrozen
|
|
||||||
),
|
|
||||||
ownerId = 0,
|
|
||||||
shutUpTime = it.shutUpExpireTime,
|
|
||||||
allowSearch = it.allowSearch
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getGuildListByNt(result: ArrayList<GuildInfo>) {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
kernelGProService.guildListFromCache.forEach {
|
|
||||||
if (it.result != 0) return@forEach
|
|
||||||
val guildInfo = it.guildInfo
|
|
||||||
result.add(GuildInfo(
|
|
||||||
guildId = it.guildId,
|
|
||||||
guildName = guildInfo.guildName ?: "",
|
|
||||||
guildDisplayId = guildInfo.guildNumber ?: "",
|
|
||||||
profile = guildInfo.profile ?: "",
|
|
||||||
status = GuildStatus(
|
|
||||||
isEnable = guildInfo.guildStatus?.isEnable == 1,
|
|
||||||
isBanned = guildInfo.guildStatus?.isBanned == 1,
|
|
||||||
isFrozen = guildInfo.guildStatus?.isFrozen == 1
|
|
||||||
),
|
|
||||||
ownerId = guildInfo.ownerTinyid,
|
|
||||||
shutUpTime = guildInfo.shutupExpireTime,
|
|
||||||
allowSearch = guildInfo.allowSearch == 1
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun fetchGuildMemberRoles(guildId: ULong, tinyId: ULong, refresh: Boolean = false): Result<ArrayList<GProGuildRole>> {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
if (refresh) {
|
|
||||||
kernelGProService.refreshGuildUserProfileInfo(guildId.toLong(), tinyId.toLong(), 1)
|
|
||||||
}
|
|
||||||
val result: ArrayList<GProGuildRole> = withTimeoutOrNull(5000) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
kernelGProService.fetchMemberRoles(guildId.toLong(), 0, tinyId.toLong(), 2) { code, reason, roles ->
|
|
||||||
it.resume(roles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: return Result.failure(Exception("unable to fetch guild member roles"))
|
|
||||||
return Result.success(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getGuildList(refresh: Boolean = false, forceOldApi: Boolean): ArrayList<GuildInfo> {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
if (refresh) {
|
|
||||||
kernelGProService.refreshGuildList(true)
|
|
||||||
kernelGProService.guildListFromCache.forEach {
|
|
||||||
refreshGuildInfo(it.guildId.toULong())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val result = arrayListOf<GuildInfo>()
|
|
||||||
if (PlatformUtils.getQQVersionCode() < PlatformUtils.QQ_9_0_8_VER || forceOldApi) {
|
|
||||||
getGuildListByOldApi(result)
|
|
||||||
} else {
|
|
||||||
runCatching {
|
|
||||||
getGuildListByNt(result)
|
|
||||||
}.onFailure {
|
|
||||||
LogCenter.log("GetGuildListByNt failed: ${it.stackTraceToString()}", Level.ERROR)
|
|
||||||
getGuildListByOldApi(result) // 防止QQ更新API导致无法获取
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGuildRoles(guildId: ULong): Result<List<GProGuildRole>> {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
val roles: List<GProGuildRole> = withTimeoutOrNull(5000) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
kernelGProService.fetchRoleListWithPermission(guildId.toLong(), 1) { code, _, roles, _, _, _ ->
|
|
||||||
if (code != 0) it.resume(null) else it.resume(roles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: return Result.failure(Exception("unable to fetch guild roles"))
|
|
||||||
return Result.success(roles)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteGuildRole(guildId: ULong, roleId: ULong) {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
kernelGProService.deleteRole(guildId.toLong(), roleId.toLong()) { code, msg, result ->
|
|
||||||
if (code != 0) {
|
|
||||||
LogCenter.log("deleteGuildRole failed: $code($msg) => $result", Level.WARN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setMemberRole(guildId: ULong, tinyId: ULong, roleId: ULong, isSet: Boolean) {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
val addList = arrayListOf<Long>()
|
|
||||||
val rmList = arrayListOf<Long>()
|
|
||||||
(if (isSet) addList else rmList).add(roleId.toLong())
|
|
||||||
kernelGProService.setMemberRoles(guildId.toLong(), 0, 0, tinyId.toLong(), addList, rmList) { code, msg, result ->
|
|
||||||
if (code != 0) {
|
|
||||||
LogCenter.log("setMemberRole failed: $code($msg) => $result", Level.WARN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGuildRolePermission(guildId: ULong, roleId: ULong): Result<GProGuildRole> {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
val role:GProGuildRole = withTimeoutOrNull(5000) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
kernelGProService.fetchRoleWithPermission(guildId.toLong(), roleId.toLong(), 1) { code, msg, role, _, _, _ ->
|
|
||||||
if (code != 0) {
|
|
||||||
LogCenter.log("getGuildRolePermission failed: $code($msg)", Level.WARN)
|
|
||||||
it.resume(null)
|
|
||||||
} else it.resume(role)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: return Result.failure(Exception("unable to fetch guild role permission"))
|
|
||||||
return Result.success(role)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateGuildRole(guildId: ULong, roleId: ULong, name: String, color: Long): Result<Unit> {
|
|
||||||
val oldInfo = getGuildRolePermission(guildId, roleId).onFailure {
|
|
||||||
return Result.failure(it)
|
|
||||||
}.getOrThrow()
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
val info = GProRoleCreateInfo(
|
|
||||||
name, color, oldInfo.bHoist, oldInfo.rolePermissions
|
|
||||||
)
|
|
||||||
kernelGProService.setRoleInfo(guildId.toLong(), roleId.toLong(), info) { code, msg, result ->
|
|
||||||
if (code != 0) {
|
|
||||||
LogCenter.log("updateGuildRole failed: $code($msg) => $result", Level.WARN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Result.success(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun createGuildRole(guildId: ULong, name: String, color: Long, initialUsers: ArrayList<Long>): Result<GProGuildRole> {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
val permission = GProRolePermission(false, arrayListOf())
|
|
||||||
val info = GProRoleCreateInfo(name, color, false, permission)
|
|
||||||
val role: GProGuildRole = withTimeoutOrNull(5000) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
kernelGProService.createRole(guildId.toLong(), info, initialUsers) { code, msg, result, role ->
|
|
||||||
if (code != 0) {
|
|
||||||
LogCenter.log("createGuildRole failed: $code($msg) => $result", Level.WARN)
|
|
||||||
it.resume(null)
|
|
||||||
} else it.resume(role)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: return Result.failure(Exception("unable to create guild role"))
|
|
||||||
return Result.success(role)
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user