mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 05:12:17 +00:00
Compare commits
11 Commits
master
...
36ed55d220
Author | SHA1 | Date | |
---|---|---|---|
36ed55d220 | |||
e2f27cb36a | |||
65ddca2ea4 | |||
a6f570e4f0 | |||
29dfc1590b | |||
ffeda0a472 | |||
0e5add2146 | |||
be9ff46134 | |||
1d6ac3e022 | |||
2db187e3d5 | |||
18ec586b12 |
1
.github/ISSUE_TEMPLATE/bug.md
vendored
1
.github/ISSUE_TEMPLATE/bug.md
vendored
@ -21,7 +21,6 @@ labels: bug
|
|||||||
## 系统信息
|
## 系统信息
|
||||||
|
|
||||||
- Shamrock 版本:
|
- Shamrock 版本:
|
||||||
- QQ 版本:
|
|
||||||
- Android 版本:
|
- Android 版本:
|
||||||
- LSPosed 框架版本:
|
- LSPosed 框架版本:
|
||||||
- 设备的制造商和型号:
|
- 设备的制造商和型号:
|
||||||
|
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: [ master ]
|
branches: [ v1.0.9 ]
|
||||||
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:
|
||||||
submodules: recursive
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup JDK 17
|
- name: Setup JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
|||||||
[submodule "kritor"]
|
|
||||||
path = kritor/kritor
|
|
||||||
url = https://github.com/KarinJS/kritor
|
|
@ -16,22 +16,19 @@
|
|||||||
|
|
||||||
## 简介
|
## 简介
|
||||||
|
|
||||||
☘ 基于 Lsposed(**Non**-Riru) 实现 Kritor 标准的 QQ 机器人框架!
|
☘ 基于 Lsposed(**Non**-Riru) 实现 OneBot 标准的 QQ 机器人框架,原作者[**fuqiuluo**](https://github.com/fuqiuluo)已脱离开发,接下来由白池接手哦!本项目为OpenShamrock,不会有任何收费行为,欢迎大家的加入!
|
||||||
|
|
||||||
> 本项目仅提供学习与交流用途,请在24小时内删除。
|
> 本项目仅提供学习与交流用途,请在24小时内删除。
|
||||||
> 本项目目的是研究 Xposed 和 LSPosed 框架的使用。 Epic 框架开发相关知识。
|
> 本项目目的是研究 Xposed 和 LSPosed 框架的使用。 Epic 框架开发相关知识。
|
||||||
> Riru可能导致封禁,请减少使用。
|
> Riru可能导致封禁,请减少使用。
|
||||||
> 如有违反法律,请联系删除。
|
> 如有违反法律,请联系删除。
|
||||||
> 请勿在任何平台宣传,宣扬,转发本项目,请勿恶意修改企业安装包造成相关企业产生损失,如有违背,必将追责到底。
|
> 请勿在任何平台宣传,宣扬,转发本项目,请勿恶意修改企业安装包造成相关企业产生损失,如有违背,必将追责到底。
|
||||||
>
|
> 官方论坛,[点我直达](https://forum.libfekit.so/)!
|
||||||
> 社区地址:[discord](https://discord.gg/MKR2wz863h)
|
|
||||||
|
|
||||||
## 兼容|迁移|替代 说明
|
## 兼容|迁移|替代 说明
|
||||||
|
|
||||||
仅支持QQ9.0.70以上的版本,低版本问题将不再修复与处理。
|
|
||||||
|
|
||||||
- 一键移植:本项目基于 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"))
|
||||||
}
|
}
|
@ -1,9 +0,0 @@
|
|||||||
package kritor.service
|
|
||||||
|
|
||||||
@Retention(AnnotationRetention.SOURCE)
|
|
||||||
@Target(AnnotationTarget.FUNCTION)
|
|
||||||
annotation class Grpc(
|
|
||||||
val serviceName: String,
|
|
||||||
val funcName: String,
|
|
||||||
|
|
||||||
)
|
|
@ -0,0 +1,8 @@
|
|||||||
|
package moe.fuqiuluo.symbols
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class OneBotHandler(
|
||||||
|
val actionName: String,
|
||||||
|
val alias: Array<String> = []
|
||||||
|
)
|
@ -5,8 +5,6 @@ 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" + ".r${getGitCommitCount()}." + getVersionName()
|
versionName = "1.1.1.onebot" + ".r${getGitCommitCount()}." + getVersionName()
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@ -201,8 +201,14 @@ 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", "okhttp"))
|
implementation(ktor("client", "content-negotiation"))
|
||||||
|
implementation(ktor("client", "cio"))
|
||||||
implementation(ktor("serialization", "kotlinx-json"))
|
implementation(ktor("serialization", "kotlinx-json"))
|
||||||
|
|
||||||
implementation(project(":xposed"))
|
implementation(project(":xposed"))
|
||||||
|
@ -37,6 +37,7 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
|
|||||||
# List C/C++ source files with relative paths to this CMakeLists.txt.
|
# List C/C++ source files with relative paths to this CMakeLists.txt.
|
||||||
${SRC_DIR}
|
${SRC_DIR}
|
||||||
md5.cpp
|
md5.cpp
|
||||||
|
cqcode.cpp
|
||||||
silk.cpp
|
silk.cpp
|
||||||
message.cpp
|
message.cpp
|
||||||
shamrock.cpp)
|
shamrock.cpp)
|
||||||
|
138
app/src/main/cpp/cqcode.cpp
Normal file
138
app/src/main/cpp/cqcode.cpp
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
|
}
|
87
app/src/main/cpp/group_honor.cpp
Normal file
87
app/src/main/cpp/group_honor.cpp
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
#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;
|
||||||
|
}
|
20
app/src/main/cpp/interface/cqcode.h
Normal file
20
app/src/main/cpp/interface/cqcode.h
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#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,4 +1,5 @@
|
|||||||
#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) {
|
||||||
@ -11,7 +12,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_qq_service_msg_MessageHelper_createMessageUniseq(JNIEnv *env, jobject thiz,
|
Java_moe_fuqiuluo_shamrock_helper_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;
|
||||||
@ -31,6 +32,123 @@ 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,7 +12,8 @@
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jstring JNICALL
|
JNIEXPORT jstring JNICALL
|
||||||
Java_moe_fuqiuluo_shamrock_xposed_actions_interacts_Init_testNativeLibrary(JNIEnv *env, jobject thiz) {
|
Java_moe_fuqiuluo_shamrock_xposed_hooks_PullConfig_00024Companion_testNativeLibrary(JNIEnv *env,
|
||||||
|
jobject thiz) {
|
||||||
return env->NewStringUTF("加载Shamrock库成功~");
|
return env->NewStringUTF("加载Shamrock库成功~");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ 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
|
||||||
@ -53,7 +52,6 @@ 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
|
||||||
@ -67,7 +65,6 @@ 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
|
||||||
@ -82,12 +79,13 @@ 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.ShamrockTab
|
import moe.fuqiuluo.shamrock.ui.tools.ShamrockTabV2
|
||||||
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) {
|
||||||
@ -107,9 +105,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
isAppearanceLightStatusBars = true
|
isAppearanceLightStatusBars = true
|
||||||
}
|
}
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||||
|
broadcastToModule { putExtra("__cmd", "fetchPort") }
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalUi = Handler(mainLooper)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,7 +162,7 @@ private fun AppMainView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val ctx = LocalContext.current
|
val ctx = LocalContext.current
|
||||||
LaunchedEffect(isFined) {
|
LaunchedEffect(isFined.value) {
|
||||||
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()
|
||||||
@ -296,25 +293,70 @@ 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
|
||||||
|
|
||||||
ShamrockTab(
|
var icon: @Composable (() -> Unit)? = null
|
||||||
|
var text: @Composable (() -> Unit)? = null
|
||||||
|
|
||||||
|
if (curSelected) {
|
||||||
|
text = {
|
||||||
|
AnimatedVisibility(visibleState = MutableTransitionState(false).also {
|
||||||
|
it.targetState =
|
||||||
|
isFirst || lastSelectedState.value and selectedConst == selectedConst
|
||||||
|
}, enter = enter, exit = exit, modifier = Modifier) {
|
||||||
|
Text(
|
||||||
|
text = titleWithIcon.first,
|
||||||
|
color = GlobalColor.TabItem,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 5.dp)
|
||||||
|
.indication(
|
||||||
|
remember { MutableInteractionSource() },
|
||||||
|
rememberRipple(color = Color.Transparent)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = titleWithIcon.second),
|
||||||
|
contentDescription = titleWithIcon.first,
|
||||||
|
tint = Color.Unspecified,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(24.dp)
|
||||||
|
.width(24.dp)
|
||||||
|
.padding(bottom = 5.dp)
|
||||||
|
.indication(
|
||||||
|
remember { MutableInteractionSource() },
|
||||||
|
rememberRipple(color = Color.Transparent)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.app.config
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
395
app/src/main/java/moe/fuqiuluo/shamrock/ui/app/ShamrockConfig.kt
Normal file
395
app/src/main/java/moe/fuqiuluo/shamrock/ui/app/ShamrockConfig.kt
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
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,6 +5,7 @@ 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
|
||||||
@ -24,7 +25,6 @@ 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,12 +45,10 @@ 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.app.config.ShamrockConfig
|
import moe.fuqiuluo.shamrock.ui.app.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
|
||||||
@ -74,6 +72,110 @@ 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]。")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,35 +195,93 @@ private fun APIInfoCard(
|
|||||||
thickness = 0.2.dp
|
thickness = 0.2.dp
|
||||||
)
|
)
|
||||||
|
|
||||||
val rpcPort = remember { mutableStateOf(ShamrockConfig[ctx, RPCPort].toString()) }
|
val wsPort = remember { mutableStateOf(ShamrockConfig.getWsPort(ctx).toString()) }
|
||||||
|
val port = remember { mutableStateOf(ShamrockConfig.getHttpPort(ctx).toString()) }
|
||||||
TextItem(
|
TextItem(
|
||||||
title = "RPC服务端口",
|
title = "主动HTTP端口",
|
||||||
desc = "端口范围在0~65565,并确保可用。",
|
desc = "端口范围在0~65565,并确保可用。",
|
||||||
text = rpcPort,
|
text = port,
|
||||||
hint = "请输入端口号",
|
hint = "请输入端口号",
|
||||||
error = "端口范围应在0~65565",
|
error = "端口范围应在0~65565",
|
||||||
checker = {
|
checker = {
|
||||||
it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }
|
it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }.getOrDefault(false) && wsPort.value != it
|
||||||
.getOrDefault(false) && rpcPort.value != it
|
|
||||||
},
|
},
|
||||||
confirm = {
|
confirm = {
|
||||||
val newPort = rpcPort.value.toInt()
|
val newPort = port.value.toInt()
|
||||||
ShamrockConfig[ctx, RPCPort] = newPort
|
ShamrockConfig.setHttpPort(ctx, newPort)
|
||||||
AppRuntime.log("设置主动HTTP监听端口为$newPort,立即生效尝试中。")
|
AppRuntime.log("设置主动HTTP监听端口为$newPort,立即生效尝试中。")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val rpcAddress = remember { mutableStateOf(ShamrockConfig[ctx, RPCAddress]) }
|
|
||||||
TextItem(
|
TextItem(
|
||||||
title = "回调RPC地址",
|
title = "主动WebSocket端口",
|
||||||
desc = "例如:kritor.support:8081",
|
desc = "端口范围在0~65565,并确保可用。",
|
||||||
text = rpcAddress,
|
text = wsPort,
|
||||||
|
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[ctx, RPCAddress] = rpcAddress.value
|
ShamrockConfig.setToken(ctx, authToken.value)
|
||||||
AppRuntime.log("设置回调RPC地址为[${rpcAddress.value}]。")
|
AppRuntime.log("设置鉴权Token为[${authToken.value}]。")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -154,32 +314,50 @@ private fun FunctionCard(
|
|||||||
Function(
|
Function(
|
||||||
title = "强制平板模式",
|
title = "强制平板模式",
|
||||||
desc = "强制QQ使用平板模式,实现共存登录。",
|
desc = "强制QQ使用平板模式,实现共存登录。",
|
||||||
isSwitch = ShamrockConfig[ctx, ForceTablet]
|
isSwitch = ShamrockConfig.isTablet(ctx)
|
||||||
) {
|
) {
|
||||||
ShamrockConfig[ctx, ForceTablet] = it
|
ShamrockConfig.setTablet(ctx, it)
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
|
|
||||||
Function(
|
Function(
|
||||||
title = "主动RPC",
|
title = "HTTP回调",
|
||||||
desc = "Kritor协议实现RPC,由Shamrock放出rpc服务",
|
desc = "OneBot标准的HTTPAPI回调,Shamrock作为Client。",
|
||||||
isSwitch = ShamrockConfig[ctx, ActiveRPC]
|
isSwitch = ShamrockConfig.isWebhook(ctx)
|
||||||
) {
|
) {
|
||||||
ShamrockConfig[ctx, ActiveRPC] = it
|
ShamrockConfig.setWebhook(ctx, it)
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
|
|
||||||
Function(
|
Function(
|
||||||
title = "被动RPC",
|
title = "消息格式为CQ码",
|
||||||
desc = "Kritor协议实现RPC,由客户端提供反向的rpc服务",
|
desc = "HTTPAPI回调的消息格式,关闭则为消息段。",
|
||||||
isSwitch = ShamrockConfig[ctx, PassiveRPC]
|
isSwitch = ShamrockConfig.isUseCQCode(ctx)
|
||||||
) {
|
) {
|
||||||
ShamrockConfig[ctx, PassiveRPC] = it
|
ShamrockConfig.setUseCQCode(ctx, 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[ctx, ResourceGroup]) }
|
val uploadResourceGroup = remember { mutableStateOf(ShamrockConfig.getUploadResourceGroup(ctx)) }
|
||||||
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)
|
||||||
@ -202,11 +380,23 @@ private fun FunctionCard(
|
|||||||
},
|
},
|
||||||
confirm = {
|
confirm = {
|
||||||
val groupId = uploadResourceGroup.value
|
val groupId = uploadResourceGroup.value
|
||||||
ShamrockConfig[ctx, ResourceGroup] = groupId
|
ShamrockConfig.setUploadResourceGroup(ctx, 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,14 +23,7 @@ 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.app.config.ShamrockConfig
|
import moe.fuqiuluo.shamrock.ui.app.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
|
||||||
@ -75,9 +68,9 @@ fun LabFragment() {
|
|||||||
title = LocalString.b2Mode,
|
title = LocalString.b2Mode,
|
||||||
desc = LocalString.b2ModeDesc,
|
desc = LocalString.b2ModeDesc,
|
||||||
descColor = it,
|
descColor = it,
|
||||||
isSwitch = ShamrockConfig[ctx, B2Mode]
|
isSwitch = ShamrockConfig.is2B(ctx)
|
||||||
) {
|
) {
|
||||||
ShamrockConfig[ctx, B2Mode] = it
|
ShamrockConfig.set2B(ctx, it)
|
||||||
scope.toast(ctx, LocalString.restartToast)
|
scope.toast(ctx, LocalString.restartToast)
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
@ -86,10 +79,10 @@ fun LabFragment() {
|
|||||||
title = LocalString.showDebugLog,
|
title = LocalString.showDebugLog,
|
||||||
desc = LocalString.showDebugLogDesc,
|
desc = LocalString.showDebugLogDesc,
|
||||||
descColor = it,
|
descColor = it,
|
||||||
isSwitch = ShamrockConfig[ctx, DebugMode]
|
isSwitch = ShamrockConfig.isDebug(ctx)
|
||||||
) {
|
) {
|
||||||
ShamrockConfig[ctx, DebugMode] = it
|
ShamrockConfig.setDebug(ctx, it)
|
||||||
InitHandler.update(ctx)
|
ShamrockConfig.pushUpdate(ctx)
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,13 +100,54 @@ 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[ctx, AliveReply]
|
isSwitch = ShamrockConfig.enableAliveReply(ctx)
|
||||||
) {
|
) {
|
||||||
ShamrockConfig[ctx, AliveReply] = it
|
ShamrockConfig.setAliveReply(ctx, 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,14 +194,25 @@ 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[ctx, AntiJvmTrace]
|
isSwitch = ShamrockConfig.isAntiTrace(ctx)
|
||||||
) {
|
) {
|
||||||
ShamrockConfig[ctx, AntiJvmTrace] = it
|
ShamrockConfig.setAntiTrace(ctx, it)
|
||||||
scope.toast(ctx, LocalString.restartToast)
|
ShamrockConfig.pushUpdate(ctx)
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,10 +277,21 @@ fun LabFragment() {
|
|||||||
title = "自发消息推送",
|
title = "自发消息推送",
|
||||||
desc = "推送Bot发送的消息,未做特殊处理请勿打开。",
|
desc = "推送Bot发送的消息,未做特殊处理请勿打开。",
|
||||||
descColor = it,
|
descColor = it,
|
||||||
isSwitch = ShamrockConfig[ctx, EnableSelfMessage]
|
isSwitch = ShamrockConfig.enableSelfMsg(ctx)
|
||||||
) {
|
) {
|
||||||
ShamrockConfig[ctx, EnableSelfMessage] = it
|
ShamrockConfig.setEnableSelfMsg(ctx, it)
|
||||||
InitHandler.update(ctx)
|
ShamrockConfig.pushUpdate(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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,10 +299,10 @@ fun LabFragment() {
|
|||||||
title = "启用旧版资源上传系统",
|
title = "启用旧版资源上传系统",
|
||||||
desc = "如果NT内核无法上传资源,请打开本开关。",
|
desc = "如果NT内核无法上传资源,请打开本开关。",
|
||||||
descColor = it,
|
descColor = it,
|
||||||
isSwitch = ShamrockConfig[ctx, EnableOldBDH]
|
isSwitch = ShamrockConfig.enableOldBDH(ctx)
|
||||||
) {
|
) {
|
||||||
ShamrockConfig[ctx, EnableOldBDH] = it
|
ShamrockConfig.setEnableOldBDH(ctx, it)
|
||||||
InitHandler.update(ctx)
|
ShamrockConfig.pushUpdate(ctx)
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,108 @@
|
|||||||
|
@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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
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,37 +3,13 @@ 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.app.config.ShamrockConfig
|
import moe.fuqiuluo.shamrock.ui.app.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("__cmd", cmd)
|
|
||||||
putExtra("__hash", callbackId)
|
putExtra("__hash", callbackId)
|
||||||
|
putExtra("__cmd", cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,49 +0,0 @@
|
|||||||
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.kritor.dynamic"
|
intent.action = "moe.fuqiuluo.onebot.dynamic"
|
||||||
intent.intentBuilder()
|
intent.intentBuilder()
|
||||||
sendBroadcast(intent)
|
sendBroadcast(intent)
|
||||||
}
|
}
|
@ -1,35 +1,23 @@
|
|||||||
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
|
||||||
@ -43,10 +31,8 @@ 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
|
||||||
@ -149,18 +135,20 @@ private fun TabBaselineLayout(
|
|||||||
text: @Composable (() -> Unit)?,
|
text: @Composable (() -> Unit)?,
|
||||||
icon: @Composable (() -> Unit)?
|
icon: @Composable (() -> Unit)?
|
||||||
) {
|
) {
|
||||||
Layout({
|
Layout(
|
||||||
if (text != null) {
|
{
|
||||||
Box(
|
if (text != null) {
|
||||||
Modifier
|
Box(
|
||||||
.layoutId("text")
|
Modifier
|
||||||
.padding(horizontal = HorizontalTextPadding)
|
.layoutId("text")
|
||||||
) { text() }
|
.padding(horizontal = HorizontalTextPadding)
|
||||||
|
) { text() }
|
||||||
|
}
|
||||||
|
if (icon != null) {
|
||||||
|
Box(Modifier.layoutId("icon")) { icon() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (icon != null) {
|
) { measurables, constraints ->
|
||||||
Box(Modifier.layoutId("icon")) { icon() }
|
|
||||||
}
|
|
||||||
}) { measurables, constraints ->
|
|
||||||
val textPlaceable = text?.let {
|
val textPlaceable = text?.let {
|
||||||
measurables.first { it.layoutId == "text" }.measure(
|
measurables.first { it.layoutId == "text" }.measure(
|
||||||
// Measure with loose constraints for height as we don't want the text to take up more
|
// Measure with loose constraints for height as we don't want the text to take up more
|
||||||
@ -254,72 +242,27 @@ private fun Placeable.PlacementScope.placeTextAndIcon(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ShamrockTab(
|
fun ShamrockTabV2(
|
||||||
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>
|
|
||||||
) {
|
) {
|
||||||
var text: @Composable (() -> Unit)? = null
|
val styledText: @Composable (() -> Unit)? = text?.let {
|
||||||
var icon: @Composable (() -> Unit)? = null
|
@Composable {
|
||||||
|
val style =
|
||||||
if (!selected) {
|
MaterialTheme.typography.fromToken(PrimaryNavigationTabTokens.LabelTextFont)
|
||||||
icon = {
|
.copy(textAlign = TextAlign.Center)
|
||||||
Icon(
|
ProvideTextStyle(style, content = text)
|
||||||
painter = painterResource(id = titleWithIcon.second),
|
|
||||||
contentDescription = titleWithIcon.first,
|
|
||||||
tint = Color.Unspecified,
|
|
||||||
modifier = Modifier
|
|
||||||
.height(24.dp)
|
|
||||||
.width(24.dp)
|
|
||||||
.padding(bottom = 5.dp)
|
|
||||||
.indication(
|
|
||||||
remember { MutableInteractionSource() },
|
|
||||||
rememberRipple(color = Color.Transparent)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
text = {
|
|
||||||
val style = MaterialTheme.typography
|
|
||||||
.fromToken(PrimaryNavigationTabTokens.LabelTextFont)
|
|
||||||
.copy(textAlign = TextAlign.Center)
|
|
||||||
|
|
||||||
ProvideTextStyle(style) {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visibleState = visibleState,
|
|
||||||
enter = remember {
|
|
||||||
scaleIn(animationSpec = TweenSpec(150, easing = FastOutLinearInEasing))
|
|
||||||
},
|
|
||||||
exit = remember {
|
|
||||||
scaleOut(animationSpec = TweenSpec(150, easing = FastOutSlowInEasing))
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = titleWithIcon.first,
|
|
||||||
color = GlobalColor.TabItem,
|
|
||||||
fontSize = 15.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(bottom = 5.dp)
|
|
||||||
.indication(
|
|
||||||
remember { MutableInteractionSource() },
|
|
||||||
rememberRipple(color = Color.Transparent)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ShamrockTabV2(
|
||||||
ShamrockTab(
|
|
||||||
selected,
|
selected,
|
||||||
onClick,
|
onClick,
|
||||||
modifier,
|
modifier,
|
||||||
@ -329,15 +272,12 @@ fun ShamrockTab(
|
|||||||
interactionSource,
|
interactionSource,
|
||||||
indication
|
indication
|
||||||
) {
|
) {
|
||||||
TabBaselineLayout(
|
TabBaselineLayout(icon = icon, text = styledText)
|
||||||
icon = icon,
|
|
||||||
text = text
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ShamrockTab(
|
fun ShamrockTabV2(
|
||||||
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.0" apply false
|
id("com.android.application") version "8.2.1" 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.0" apply false
|
id("com.android.library") version "8.2.1" apply false
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.9.22"
|
kotlin("jvm") version "1.9.21"
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -7,6 +7,10 @@ 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"
|
||||||
@ -15,9 +19,8 @@ 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
42
kritor/.gitignore
vendored
@ -1,42 +0,0 @@
|
|||||||
.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
|
|
@ -1,76 +0,0 @@
|
|||||||
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"
|
|
||||||
}
|
|
Submodule kritor/kritor deleted from 3dec747a8e
21
kritor/proguard-rules.pro
vendored
21
kritor/proguard-rules.pro
vendored
@ -1,21 +0,0 @@
|
|||||||
# 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")
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
@file:Suppress("UNCHECKED_CAST")
|
|
||||||
@file:OptIn(KspExperimental::class)
|
|
||||||
|
|
||||||
package moe.fuqiuluo.ksp.impl
|
|
||||||
|
|
||||||
import com.google.devtools.ksp.KspExperimental
|
|
||||||
import com.google.devtools.ksp.getAnnotationsByType
|
|
||||||
import com.google.devtools.ksp.processing.*
|
|
||||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
|
||||||
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
|
|
||||||
import com.squareup.kotlinpoet.FileSpec
|
|
||||||
import com.squareup.kotlinpoet.FunSpec
|
|
||||||
import com.squareup.kotlinpoet.KModifier
|
|
||||||
import kritor.service.Grpc
|
|
||||||
|
|
||||||
class GrpcProcessor(
|
|
||||||
private val codeGenerator: CodeGenerator,
|
|
||||||
private val logger: KSPLogger
|
|
||||||
): SymbolProcessor {
|
|
||||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
|
||||||
val symbols = resolver.getSymbolsWithAnnotation(Grpc::class.qualifiedName!!)
|
|
||||||
val actions = (symbols as Sequence<KSFunctionDeclaration>).toList()
|
|
||||||
|
|
||||||
if (actions.isEmpty()) return emptyList()
|
|
||||||
|
|
||||||
// 怎么返回nullable的结果
|
|
||||||
val packageName = "kritor.handlers"
|
|
||||||
val funcBuilder = FunSpec.builder("handleGrpc")
|
|
||||||
.addModifiers(KModifier.SUSPEND)
|
|
||||||
.addParameter("cmd", String::class)
|
|
||||||
.addParameter("data", ByteArray::class)
|
|
||||||
.returns(ByteArray::class)
|
|
||||||
val fileSpec = FileSpec.scriptBuilder("AutoGrpcHandlers", packageName)
|
|
||||||
|
|
||||||
logger.warn("Found ${actions.size} grpc-actions")
|
|
||||||
|
|
||||||
//logger.error(resolver.getClassDeclarationByName("io.kritor.AuthReq").toString())
|
|
||||||
//logger.error(resolver.getJavaClassByName("io.kritor.AuthReq").toString())
|
|
||||||
//logger.error(resolver.getKotlinClassByName("io.kritor.AuthReq").toString())
|
|
||||||
|
|
||||||
actions.forEach { action ->
|
|
||||||
val methodName = action.qualifiedName?.asString()!!
|
|
||||||
val grpcMethod = action.getAnnotationsByType(Grpc::class).first()
|
|
||||||
val service = grpcMethod.serviceName
|
|
||||||
val funcName = grpcMethod.funcName
|
|
||||||
funcBuilder.addStatement("if (cmd == \"${service}.${funcName}\") {\t")
|
|
||||||
|
|
||||||
val reqType = action.parameters[0].type.toString()
|
|
||||||
val rspType = action.returnType.toString()
|
|
||||||
funcBuilder.addStatement("val resp: $rspType = $methodName($reqType.parseFrom(data))")
|
|
||||||
funcBuilder.addStatement("return resp.toByteArray()")
|
|
||||||
|
|
||||||
funcBuilder.addStatement("}")
|
|
||||||
}
|
|
||||||
funcBuilder.addStatement("return EMPTY_BYTE_ARRAY")
|
|
||||||
fileSpec
|
|
||||||
.addStatement("import io.kritor.authentication.*")
|
|
||||||
.addStatement("import io.kritor.core.*")
|
|
||||||
.addStatement("import io.kritor.customization.*")
|
|
||||||
.addStatement("import io.kritor.developer.*")
|
|
||||||
.addStatement("import io.kritor.file.*")
|
|
||||||
.addStatement("import io.kritor.friend.*")
|
|
||||||
.addStatement("import io.kritor.group.*")
|
|
||||||
.addStatement("import io.kritor.guild.*")
|
|
||||||
.addStatement("import io.kritor.message.*")
|
|
||||||
.addStatement("import io.kritor.web.*")
|
|
||||||
.addFunction(funcBuilder.build())
|
|
||||||
.addImport("moe.fuqiuluo.symbols", "EMPTY_BYTE_ARRAY")
|
|
||||||
runCatching {
|
|
||||||
codeGenerator.createNewFile(
|
|
||||||
dependencies = Dependencies(aggregating = false),
|
|
||||||
packageName = packageName,
|
|
||||||
fileName = fileSpec.name
|
|
||||||
).use { outputStream ->
|
|
||||||
outputStream.writer().use {
|
|
||||||
fileSpec.build().writeTo(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,85 @@
|
|||||||
|
@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 { _, clz ->
|
actions.forEachIndexed { index, 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, KspExperimental::class)
|
@file:OptIn(KspExperimental::class)
|
||||||
|
|
||||||
package moe.fuqiuluo.ksp.impl
|
package moe.fuqiuluo.ksp.impl
|
||||||
|
|
||||||
@ -27,14 +27,10 @@ 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(
|
val symbols = resolver.getSymbolsWithAnnotation(XposedHook::class.qualifiedName!!)
|
||||||
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.classKind == ClassKind.CLASS
|
it is KSClassDeclaration && it.validate() && it.classKind == ClassKind.CLASS
|
||||||
} as Sequence<KSClassDeclaration>).toList()
|
} as Sequence<KSClassDeclaration>).toList()
|
||||||
|
|
||||||
if (actions.isNotEmpty()) {
|
if (actions.isNotEmpty()) {
|
||||||
@ -50,7 +46,7 @@ class XposedHookProcessor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val context = ClassName("android.content", "Context")
|
val context = ClassName("android.content", "Context")
|
||||||
val packageName = "moe.fuqiuluo.shamrock.xposed.actions"
|
val packageName = "moe.fuqiuluo.shamrock.xposed.hooks"
|
||||||
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 {
|
||||||
@ -100,6 +96,16 @@ 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.GrpcProcessor
|
import moe.fuqiuluo.ksp.impl.OneBotHandlerProcessor
|
||||||
|
|
||||||
@AutoService(SymbolProcessorProvider::class)
|
@AutoService(SymbolProcessorProvider::class)
|
||||||
class GrpcProvider: SymbolProcessorProvider {
|
class OneBotHandlerProcessorProvider: SymbolProcessorProvider {
|
||||||
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
|
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
|
||||||
return GrpcProcessor(
|
return OneBotHandlerProcessor(
|
||||||
environment.codeGenerator,
|
environment.codeGenerator,
|
||||||
environment.logger
|
environment.logger
|
||||||
)
|
)
|
@ -37,6 +37,7 @@ 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"))
|
||||||
|
|
||||||
@ -46,5 +47,5 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile>().configureEach {
|
tasks.withType<KotlinCompile>().configureEach {
|
||||||
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
|
kotlinOptions.freeCompilerArgs += "-Xopt-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: ULong? = null,
|
@ProtoNumber(2) val appid: Int? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -8,9 +8,7 @@ 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(3) val result: UInt? = null,
|
@ProtoNumber(4) val buffer: ByteArray,
|
||||||
@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 moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY
|
import com.google.protobuf.Internal.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 moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY
|
import com.google.protobuf.Internal.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 moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY
|
import com.google.protobuf.Internal.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,13 +6,9 @@ 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,10 +13,6 @@ 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 {
|
||||||
@ -28,10 +24,6 @@ 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,8 +8,6 @@ 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;
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
15
qqinterface/src/main/java/com/tencent/mobileqq/l0/b/a.java
Normal file
15
qqinterface/src/main/java/com/tencent/mobileqq/l0/b/a.java
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
23
qqinterface/src/main/java/com/tencent/mobileqq/l0/b/b.java
Normal file
23
qqinterface/src/main/java/com/tencent/mobileqq/l0/b/b.java
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
}
|
39
qqinterface/src/main/java/com/tencent/mobileqq/l0/b/c.java
Normal file
39
qqinterface/src/main/java/com/tencent/mobileqq/l0/b/c.java
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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 static synchronized int getNextSeq() {
|
public synchronized int getNextSeq() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
38
qqinterface/src/main/java/com/tencent/mobileqq/ocr/a/a.java
Normal file
38
qqinterface/src/main/java/com/tencent/mobileqq/ocr/a/a.java
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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;
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package com.tencent.mobileqq.ocr.api.impl;
|
||||||
|
|
||||||
|
public class OcrServiceImpl {
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
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.kernelpublic.nativeinterface.Contact;
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback;
|
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback;
|
||||||
|
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
|
||||||
|
|
||||||
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,11 +18,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
package com.tencent.qqnt.kernel.nativeinterface;
|
|
||||||
|
|
||||||
public class GProGuildTopFeedMsg {
|
|
||||||
}
|
|
@ -1,5 +1,7 @@
|
|||||||
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,5 +1,7 @@
|
|||||||
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,5 +1,7 @@
|
|||||||
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,9 +1,8 @@
|
|||||||
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);
|
||||||
@ -18,7 +17,7 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onCustomWithdrawConfigUpdate(CustomWithdrawConfig customWithdrawConfig);
|
void onCustomWithdrawConfigUpdate(CustomWithdrawConfig customWithdrawConfig);
|
||||||
|
|
||||||
void onDraftUpdate(Contact contact, ArrayList<MsgElement> arrayList, long j);
|
void onDraftUpdate(Contact contact, ArrayList<MsgElement> arrayList, long j2);
|
||||||
|
|
||||||
void onEmojiDownloadComplete(EmojiNotifyInfo emojiNotifyInfo);
|
void onEmojiDownloadComplete(EmojiNotifyInfo emojiNotifyInfo);
|
||||||
|
|
||||||
@ -32,7 +31,7 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onFirstViewGroupGuildMapping(ArrayList<FirstViewGroupGuildInfo> arrayList);
|
void onFirstViewGroupGuildMapping(ArrayList<FirstViewGroupGuildInfo> arrayList);
|
||||||
|
|
||||||
void onGrabPasswordRedBag(int i, String str, int i2, RecvdOrder recvdOrder, MsgRecord msgRecord);
|
void onGrabPasswordRedBag(int i2, String str, int i3, RecvdOrder recvdOrder, MsgRecord msgRecord);
|
||||||
|
|
||||||
void onGroupFileInfoAdd(GroupItem groupItem);
|
void onGroupFileInfoAdd(GroupItem groupItem);
|
||||||
|
|
||||||
@ -50,8 +49,6 @@ 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);
|
||||||
@ -66,7 +63,7 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onLineDev(ArrayList<DevInfo> arrayList);
|
void onLineDev(ArrayList<DevInfo> arrayList);
|
||||||
|
|
||||||
void onLogLevelChanged(long j);
|
void onLogLevelChanged(long j2);
|
||||||
|
|
||||||
void onMsgAbstractUpdate(ArrayList<MsgAbstract> arrayList);
|
void onMsgAbstractUpdate(ArrayList<MsgAbstract> arrayList);
|
||||||
|
|
||||||
@ -80,16 +77,14 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onMsgInfoListUpdate(ArrayList<MsgRecord> arrayList);
|
void onMsgInfoListUpdate(ArrayList<MsgRecord> arrayList);
|
||||||
|
|
||||||
void onMsgQRCodeStatusChanged(int i);
|
void onMsgQRCodeStatusChanged(int i2);
|
||||||
|
|
||||||
void onMsgRecall(int i, String str, long j);
|
void onMsgRecall(int i2, String str, long j2);
|
||||||
|
|
||||||
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();
|
||||||
@ -98,11 +93,11 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onReadFeedEventUpdate(FirstViewDirectMsgNotifyInfo firstViewDirectMsgNotifyInfo);
|
void onReadFeedEventUpdate(FirstViewDirectMsgNotifyInfo firstViewDirectMsgNotifyInfo);
|
||||||
|
|
||||||
void onRecvGroupGuildFlag(int i);
|
void onRecvGroupGuildFlag(int i2);
|
||||||
|
|
||||||
void onRecvMsg(ArrayList<MsgRecord> arrayList);
|
void onRecvMsg(ArrayList<MsgRecord> arrayList);
|
||||||
|
|
||||||
void onRecvMsgSvrRspTransInfo(long j, Contact contact, int i, int i2, String str, byte[] bArr);
|
void onRecvMsgSvrRspTransInfo(long j2, Contact contact, int i2, int i3, String str, byte[] bArr);
|
||||||
|
|
||||||
void onRecvOnlineFileMsg(ArrayList<MsgRecord> arrayList);
|
void onRecvOnlineFileMsg(ArrayList<MsgRecord> arrayList);
|
||||||
|
|
||||||
@ -110,9 +105,7 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onRecvSysMsg(ArrayList<Byte> arrayList);
|
void onRecvSysMsg(ArrayList<Byte> arrayList);
|
||||||
|
|
||||||
void onRecvUDCFlag(int i);
|
void onRecvUDCFlag(int i2);
|
||||||
|
|
||||||
void onRedTouchChanged();
|
|
||||||
|
|
||||||
void onRichMediaDownloadComplete(FileTransNotifyInfo fileTransNotifyInfo);
|
void onRichMediaDownloadComplete(FileTransNotifyInfo fileTransNotifyInfo);
|
||||||
|
|
||||||
@ -122,9 +115,9 @@ public interface IKernelMsgListener {
|
|||||||
|
|
||||||
void onSearchGroupFileInfoUpdate(SearchGroupFileResult searchGroupFileResult);
|
void onSearchGroupFileInfoUpdate(SearchGroupFileResult searchGroupFileResult);
|
||||||
|
|
||||||
void onSendMsgError(long j, Contact contact, int i, String str);
|
void onSendMsgError(long j2, Contact contact, int i2, String str);
|
||||||
|
|
||||||
void onSysMsgNotification(int i, long j, long j2, boolean z, ArrayList<Byte> arrayList);
|
void onSysMsgNotification(int i2, long j2, long j3, boolean z, ArrayList<Byte> arrayList);
|
||||||
|
|
||||||
void onTempChatInfoUpdate(TempChatInfo tempChatInfo);
|
void onTempChatInfoUpdate(TempChatInfo tempChatInfo);
|
||||||
|
|
||||||
@ -136,11 +129,9 @@ 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 i, long j, String str);
|
void onlineStatusBigIconDownloadPush(int i2, long j2, String str);
|
||||||
|
|
||||||
void onlineStatusSmallIconDownloadPush(int i, long j, String str);
|
void onlineStatusSmallIconDownloadPush(int i2, long j2, String str);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
package com.tencent.qqnt.kernel.nativeinterface;
|
|
||||||
|
|
||||||
public enum MemberRole {
|
|
||||||
UNSPECIFIED,
|
|
||||||
STRANGER,
|
|
||||||
MEMBER,
|
|
||||||
ADMIN,
|
|
||||||
OWNER
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
package com.tencent.qqnt.kernel.nativeinterface;
|
|
||||||
|
|
||||||
public class QueryUserSecQualityRsp {
|
|
||||||
}
|
|
@ -10,6 +10,7 @@ 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 {
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
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,13 +66,6 @@ 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();
|
||||||
}
|
}
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
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.3.37")
|
classpath("com.android.tools:r8:8.2.47")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,9 +34,8 @@ rootProject.name = "Shamrock"
|
|||||||
include(
|
include(
|
||||||
":app",
|
":app",
|
||||||
":xposed",
|
":xposed",
|
||||||
":qqinterface",
|
":qqinterface"
|
||||||
":protobuf",
|
|
||||||
":processor",
|
|
||||||
":annotations",
|
|
||||||
":kritor"
|
|
||||||
)
|
)
|
||||||
|
include(":protobuf")
|
||||||
|
include(":processor")
|
||||||
|
include(":annotations")
|
||||||
|
@ -60,10 +60,9 @@ kotlin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly("de.robv.android.xposed:api:82")
|
compileOnly ("de.robv.android.xposed:api:82")
|
||||||
compileOnly(project(":qqinterface"))
|
compileOnly (project(":qqinterface"))
|
||||||
|
|
||||||
implementation(project(":kritor"))
|
|
||||||
implementation(project(":protobuf"))
|
implementation(project(":protobuf"))
|
||||||
implementation(project(":annotations"))
|
implementation(project(":annotations"))
|
||||||
ksp(project(":processor"))
|
ksp(project(":processor"))
|
||||||
@ -73,20 +72,27 @@ 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", "okhttp"))
|
implementation(ktor("client", "content-negotiation"))
|
||||||
|
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")
|
||||||
@ -95,8 +101,6 @@ dependencies {
|
|||||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile>().all {
|
tasks.withType<KotlinCompile>().configureEach {
|
||||||
kotlinOptions {
|
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
|
||||||
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn")
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
// IByteDataSign.aidl
|
||||||
|
package moe.fuqiuluo.shamrock.xposed.ipc.bytedata;
|
||||||
|
|
||||||
|
parcelable IByteDataSign;
|
@ -0,0 +1,4 @@
|
|||||||
|
// IQSign.aidl
|
||||||
|
package moe.fuqiuluo.shamrock.xposed.ipc.qsign;
|
||||||
|
|
||||||
|
parcelable IQSign;
|
@ -0,0 +1,14 @@
|
|||||||
|
// 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();
|
||||||
|
}
|
@ -1,43 +0,0 @@
|
|||||||
# 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_actions_AntiDetection_antiNativeDetections(JNIEnv *env,
|
Java_moe_fuqiuluo_shamrock_xposed_hooks_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);
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,164 +0,0 @@
|
|||||||
@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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package kritor.handlers
|
|
||||||
|
|
||||||
internal object GrpcHandlers {
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
@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)
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
package kritor.service
|
|
||||||
|
|
||||||
import io.grpc.Status
|
|
||||||
import io.grpc.StatusRuntimeException
|
|
||||||
import io.kritor.authentication.*
|
|
||||||
import io.kritor.authentication.AuthenticateResponse.AuthenticateResponseCode
|
|
||||||
import kritor.auth.AuthInterceptor
|
|
||||||
import moe.fuqiuluo.shamrock.config.ActiveTicket
|
|
||||||
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
|
||||||
import qq.service.QQInterfaces
|
|
||||||
|
|
||||||
internal object AuthenticationService: AuthenticationServiceGrpcKt.AuthenticationServiceCoroutineImplBase() {
|
|
||||||
@Grpc("AuthenticationService", "Authenticate")
|
|
||||||
override suspend fun authenticate(request: AuthenticateRequest): AuthenticateResponse {
|
|
||||||
if (QQInterfaces.app.account != request.account) {
|
|
||||||
return AuthenticateResponse.newBuilder().apply {
|
|
||||||
code = AuthenticateResponseCode.NO_ACCOUNT
|
|
||||||
msg = "No such account"
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
val activeTicketName = ActiveTicket.name()
|
|
||||||
var index = 0
|
|
||||||
while (true) {
|
|
||||||
val ticket = ShamrockConfig.getProperty(activeTicketName + if (index == 0) "" else ".$index", null)
|
|
||||||
if (ticket.isNullOrEmpty()) {
|
|
||||||
if (index == 0) {
|
|
||||||
return AuthenticateResponse.newBuilder().apply {
|
|
||||||
code = AuthenticateResponseCode.OK
|
|
||||||
msg = "OK"
|
|
||||||
}.build()
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else if (ticket == request.ticket) {
|
|
||||||
return AuthenticateResponse.newBuilder().apply {
|
|
||||||
code = AuthenticateResponseCode.OK
|
|
||||||
msg = "OK"
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
|
|
||||||
return AuthenticateResponse.newBuilder().apply {
|
|
||||||
code = AuthenticateResponseCode.NO_TICKET
|
|
||||||
msg = "Invalid ticket"
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Grpc("AuthenticationService", "GetAuthenticationState")
|
|
||||||
override suspend fun getAuthenticationState(request: GetAuthenticationStateRequest): GetAuthenticationStateResponse {
|
|
||||||
if (request.account != QQInterfaces.app.account) {
|
|
||||||
throw StatusRuntimeException(Status.CANCELLED.withDescription("No such account"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetAuthenticationStateResponse.newBuilder().apply {
|
|
||||||
isRequired = AuthInterceptor.getAllTicket().isNotEmpty()
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,101 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,244 +0,0 @@
|
|||||||
package kritor.service
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.tencent.mobileqq.profilecard.api.IProfileCardBlacklistApi
|
|
||||||
import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst
|
|
||||||
import com.tencent.mobileqq.profilecard.api.IProfileProtocolService
|
|
||||||
import com.tencent.mobileqq.qroute.QRoute
|
|
||||||
import io.grpc.Status
|
|
||||||
import io.grpc.StatusRuntimeException
|
|
||||||
import io.kritor.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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,133 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,403 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,475 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
137
xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/CardSvc.kt
Normal file
137
xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/CardSvc.kt
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
245
xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FileSvc.kt
Normal file
245
xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FileSvc.kt
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
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,24 +1,33 @@
|
|||||||
package qq.service.friend
|
@file:OptIn(DelicateCoroutinesApi::class)
|
||||||
|
@file:Suppress("IllegalIdentifier")
|
||||||
|
package moe.fuqiuluo.qqinterface.servlet
|
||||||
|
|
||||||
|
import com.tencent.common.app.AppInterface
|
||||||
import com.tencent.mobileqq.data.Friends
|
import com.tencent.mobileqq.data.Friends
|
||||||
import com.tencent.mobileqq.friend.api.IFriendDataService
|
import com.tencent.mobileqq.friend.api.IFriendDataService
|
||||||
import com.tencent.mobileqq.friend.api.IFriendHandlerService
|
import com.tencent.mobileqq.friend.api.IFriendHandlerService
|
||||||
import com.tencent.mobileqq.qroute.QRoute
|
import com.tencent.mobileqq.qroute.QRoute
|
||||||
import com.tencent.mobileqq.relation.api.IAddFriendTempApi
|
import com.tencent.mobileqq.relation.api.IAddFriendTempApi
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import moe.fuqiuluo.shamrock.tools.decodeToObject
|
||||||
import moe.fuqiuluo.shamrock.tools.slice
|
import moe.fuqiuluo.shamrock.tools.slice
|
||||||
import qq.service.QQInterfaces
|
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
|
||||||
|
import moe.fuqiuluo.shamrock.xposed.helper.QQInterfaces
|
||||||
|
import mqq.app.AppRuntime
|
||||||
import tencent.mobileim.structmsg.structmsg
|
import tencent.mobileim.structmsg.structmsg
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
internal object FriendHelper: QQInterfaces() {
|
internal object FriendSvc: QQInterfaces() {
|
||||||
|
|
||||||
suspend fun getFriendList(refresh: Boolean): Result<List<Friends>> {
|
suspend fun getFriendList(refresh: Boolean): Result<List<Friends>> {
|
||||||
val service = app.getRuntimeService(IFriendDataService::class.java, "all")
|
val runtime = AppRuntimeFetcher.appRuntime
|
||||||
|
val service = runtime.getRuntimeService(IFriendDataService::class.java, "all")
|
||||||
if(refresh || !service.isInitFinished) {
|
if(refresh || !service.isInitFinished) {
|
||||||
if(!requestFriendList(service)) {
|
if(!requestFriendList(runtime, service)) {
|
||||||
return Result.failure(Exception("获取好友列表失败"))
|
return Result.failure(Exception("获取好友列表失败"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -27,6 +36,9 @@ internal object FriendHelper: QQInterfaces() {
|
|||||||
|
|
||||||
// ProfileService.Pb.ReqSystemMsgAction.Friend
|
// ProfileService.Pb.ReqSystemMsgAction.Friend
|
||||||
fun requestFriendRequest(msgSeq: Long, uin: Long, remark: String = "", approve: Boolean? = true, notSee: Boolean? = false) {
|
fun requestFriendRequest(msgSeq: Long, uin: Long, remark: String = "", approve: Boolean? = true, notSee: Boolean? = false) {
|
||||||
|
val app = AppRuntimeFetcher.appRuntime
|
||||||
|
if (app !is AppInterface)
|
||||||
|
throw RuntimeException("AppRuntime cannot cast to AppInterface")
|
||||||
val service = QRoute.api(IAddFriendTempApi::class.java)
|
val service = QRoute.api(IAddFriendTempApi::class.java)
|
||||||
val action = structmsg.SystemMsgActionInfo()
|
val action = structmsg.SystemMsgActionInfo()
|
||||||
action.type.set(if (approve != false) 2 else 3)
|
action.type.set(if (approve != false) 2 else 3)
|
||||||
@ -36,7 +48,10 @@ internal object FriendHelper: QQInterfaces() {
|
|||||||
snInfo.uint32_not_see_dynamic.set(if (notSee != false) 1 else 0)
|
snInfo.uint32_not_see_dynamic.set(if (notSee != false) 1 else 0)
|
||||||
snInfo.uint32_set_sn.set(0)
|
snInfo.uint32_set_sn.set(0)
|
||||||
action.addFrdSNInfo.set(snInfo)
|
action.addFrdSNInfo.set(snInfo)
|
||||||
service.sendFriendSystemMsgAction(1, msgSeq, uin, 1, 2004, 11, 0, action, 0, structmsg.StructMsg(), false, app)
|
service.sendFriendSystemMsgAction(1, msgSeq, uin, 1, 2004, 11, 0, action, 0,
|
||||||
|
structmsg.StructMsg(), false,
|
||||||
|
app
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun requestFriendSystemMsgNew(msgNum: Int, latestFriendSeq: Long = 0, latestGroupSeq: Long = 0, retryCnt: Int = 3): List<structmsg.StructMsg>? {
|
suspend fun requestFriendSystemMsgNew(msgNum: Int, latestFriendSeq: Long = 0, latestGroupSeq: Long = 0, retryCnt: Int = 3): List<structmsg.StructMsg>? {
|
||||||
@ -73,13 +88,12 @@ internal object FriendHelper: QQInterfaces() {
|
|||||||
req.friend_msg_type_flag.set(1)
|
req.friend_msg_type_flag.set(1)
|
||||||
req.uint32_req_msg_type.set(1)
|
req.uint32_req_msg_type.set(1)
|
||||||
req.uint32_need_uid.set(1)
|
req.uint32_need_uid.set(1)
|
||||||
val fromServiceMsg = sendBufferAW("ProfileService.Pb.ReqSystemMsgNew.Friend", true, req.toByteArray())
|
val respBuffer = sendBufferAW("ProfileService.Pb.ReqSystemMsgNew.Friend", true, req.toByteArray())
|
||||||
return if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
return if (respBuffer == null) {
|
||||||
ArrayList()
|
ArrayList()
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
val msg = structmsg.RspSystemMsgNew()
|
val msg = respBuffer.decodeToObject(structmsg.RspSystemMsgNew())
|
||||||
msg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
|
||||||
return msg.friendmsgs.get()
|
return msg.friendmsgs.get()
|
||||||
} catch (err: Throwable) {
|
} catch (err: Throwable) {
|
||||||
requestFriendSystemMsgNew(msgNum, latestFriendSeq, latestGroupSeq, retryCnt - 1)
|
requestFriendSystemMsgNew(msgNum, latestFriendSeq, latestGroupSeq, retryCnt - 1)
|
||||||
@ -88,8 +102,9 @@ internal object FriendHelper: QQInterfaces() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun requestFriendList(dataService: IFriendDataService): Boolean {
|
|
||||||
val service = app.getRuntimeService(IFriendHandlerService::class.java, "all")
|
private suspend fun requestFriendList(runtime: AppRuntime, dataService: IFriendDataService): Boolean {
|
||||||
|
val service = runtime.getRuntimeService(IFriendHandlerService::class.java, "all")
|
||||||
service.requestFriendList(true, 0)
|
service.requestFriendList(true, 0)
|
||||||
return suspendCancellableCoroutine { continuation ->
|
return suspendCancellableCoroutine { continuation ->
|
||||||
val waiter = GlobalScope.launch {
|
val waiter = GlobalScope.launch {
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user