mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 05:12:17 +00:00
Compare commits
98 Commits
675a7a5321
...
1.1.0
Author | SHA1 | Date | |
---|---|---|---|
042f4bd330 | |||
9aef71b09f | |||
9cbe755520 | |||
df02f9f872 | |||
5cbb695a66 | |||
c014e85faa | |||
4a396b0935 | |||
d59fcf9f6a | |||
cdc664f44a | |||
ad313f384c | |||
b6a510ce05 | |||
bed5947909 | |||
fb6578d243 | |||
d33cace7aa | |||
659d4e5da4 | |||
ac2aee8c0e | |||
0faada7b5a | |||
680317da13 | |||
7782feb6ac | |||
d66358a1f3 | |||
824f280b3a | |||
6936262d62 | |||
0955267ee5 | |||
f3da62fa74 | |||
abbac6315c | |||
0cf10eabd6 | |||
8c33267887 | |||
f030104ff2 | |||
ee5fcc3403 | |||
5e819179b4 | |||
ea206faf4f | |||
5adfc544a2 | |||
bdb75841cf | |||
a3dc0d06b2 | |||
3664352f23 | |||
2770979fee | |||
6c9b282c6a | |||
3a07116093 | |||
be58c368e9 | |||
a95d8d85e8 | |||
1d035fa378 | |||
7d0b60271e | |||
d38777d06a | |||
7bacea3288 | |||
ca47f9dbdf | |||
cb4268edef | |||
c16f9d543c | |||
93c49953cf | |||
883e949cc1 | |||
a528030cbb | |||
bbdfb7c04e | |||
1afc0ac6a6 | |||
638bf72392 | |||
07364c8298 | |||
ee6e13a5bb | |||
c3934778c7 | |||
13a49dd70b | |||
5637db43be | |||
69cdbad643 | |||
a06708bf95 | |||
2ac0003166 | |||
d92d1daffb | |||
27f837adbe | |||
661680e60b | |||
54b7eb95a8 | |||
265fff3cd2 | |||
8ca0a3815a | |||
da6d34c53e | |||
61ffb37951 | |||
593f461ffe | |||
12d594697d | |||
352aa5f737 | |||
9546e90bec | |||
26b4d95ad8 | |||
4a6109fbe6 | |||
d6142173c9 | |||
38cf806b40 | |||
82269bb171 | |||
fc0d7a62af | |||
60fdfd9071 | |||
1f620bcc06 | |||
737acfa41b | |||
3619cba33c | |||
52ec43abf8 | |||
e96c356de4 | |||
bbdb0a65fb | |||
ec56e32be1 | |||
4dc83fdeba | |||
541422a43e | |||
cb7bf00e17 | |||
a3171b3111 | |||
02626489eb | |||
a9a2e9a3dd | |||
964c55de31 | |||
befb4a2bef | |||
210609bd7b | |||
3e03d4782d | |||
a78b5cab23 |
2
.github/workflows/build-apk.yml
vendored
2
.github/workflows/build-apk.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "kritor"]
|
||||
path = kritor/kritor
|
||||
url = https://github.com/KarinJS/kritor
|
23
README.md
23
README.md
@ -16,20 +16,35 @@
|
||||
|
||||
## 简介
|
||||
|
||||
☘ 基于 Lsposed(**Non**-Riru) 实现 OneBot 标准的 QQ 机器人框架,原作者[**fuqiuluo**](https://github.com/fuqiuluo)已脱离开发,接下来由白池接手哦!本项目为OpenShamrock,不会有任何收费行为,欢迎大家的加入!
|
||||
☘ 基于 Lsposed(**Non**-Riru) 实现 Kritor 标准的 QQ 机器人框架!
|
||||
|
||||
> 本项目仅提供学习与交流用途,请在24小时内删除。
|
||||
> 本项目目的是研究 Xposed 和 LSPosed 框架的使用。 Epic 框架开发相关知识。
|
||||
> Riru可能导致封禁,请减少使用。
|
||||
> 如有违反法律,请联系删除。
|
||||
> 请勿在任何平台宣传,宣扬,转发本项目,请勿恶意修改企业安装包造成相关企业产生损失,如有违背,必将追责到底。
|
||||
> 官方论坛,[点我直达](https://forum.libfekit.so/)!
|
||||
|
||||
## 兼容|迁移|替代 说明
|
||||
|
||||
- 一键移植:本项目基于 go-cqhttp 的文档进行开发实现。
|
||||
- 平行部署:可多平台部署,未来将会支持 Docker 部署的教程。
|
||||
- 替代方案:[Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core)
|
||||
- 平行部署:可多平台部署。
|
||||
|
||||
## 相关项目
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><a href="https://github.com/LagrangeDev/Lagrange.Core">Lagrange.Core</a></td>
|
||||
<td>NTQQ 的协议实现</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/whitechi73/OpenShamrock">OpenShamrock</a></td>
|
||||
<td>基于 Xposed 实现 OneBot 标准的机器人框架(👈你在这里</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/chrononeko/chronocat">Chronocat</a></td>
|
||||
<td>基于 Electron 的、模块化的 Satori 框架</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 权限声明
|
||||
|
||||
|
@ -9,6 +9,6 @@ java {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(DEPENDENCY_PROTOBUF)
|
||||
//implementation(DEPENDENCY_PROTOBUF)
|
||||
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
||||
}
|
9
annotations/src/main/java/kritor/service/Grpc.kt
Normal file
9
annotations/src/main/java/kritor/service/Grpc.kt
Normal file
@ -0,0 +1,9 @@
|
||||
package kritor.service
|
||||
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
annotation class Grpc(
|
||||
val serviceName: String,
|
||||
val funcName: String,
|
||||
|
||||
)
|
@ -1,8 +0,0 @@
|
||||
package moe.fuqiuluo.symbols
|
||||
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class OneBotHandler(
|
||||
val actionName: String,
|
||||
val alias: Array<String> = []
|
||||
)
|
@ -5,6 +5,8 @@ import kotlinx.serialization.protobuf.ProtoBuf
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
val EMPTY_BYTE_ARRAY = ByteArray(0)
|
||||
|
||||
interface Protobuf<T: Protobuf<T>>
|
||||
|
||||
inline fun <reified T: Protobuf<T>> ByteArray.decodeProtobuf(to: KClass<T>? = null): T {
|
||||
|
@ -17,7 +17,7 @@ android {
|
||||
minSdk = 27
|
||||
targetSdk = 34
|
||||
versionCode = getVersionCode()
|
||||
versionName = "1.0.9" + ".r${getGitCommitCount()}." + getVersionName()
|
||||
versionName = "1.1.0" + ".r${getGitCommitCount()}." + getVersionName()
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@ -201,14 +201,8 @@ dependencies {
|
||||
implementation("io.coil-kt:coil-compose:2.4.0")
|
||||
|
||||
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", "content-negotiation"))
|
||||
implementation(ktor("client", "cio"))
|
||||
implementation(ktor("client", "okhttp"))
|
||||
implementation(ktor("serialization", "kotlinx-json"))
|
||||
|
||||
implementation(project(":xposed"))
|
||||
|
@ -37,7 +37,6 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
# List C/C++ source files with relative paths to this CMakeLists.txt.
|
||||
${SRC_DIR}
|
||||
md5.cpp
|
||||
cqcode.cpp
|
||||
silk.cpp
|
||||
message.cpp
|
||||
shamrock.cpp)
|
||||
|
@ -1,138 +0,0 @@
|
||||
#include <stdexcept>
|
||||
#include "cqcode.h"
|
||||
|
||||
inline void replace_string(std::string& str, const std::string& from, const std::string& to) {
|
||||
size_t startPos = 0;
|
||||
while ((startPos = str.find(from, startPos)) != std::string::npos) {
|
||||
str.replace(startPos, from.length(), to);
|
||||
startPos += to.length();
|
||||
}
|
||||
}
|
||||
|
||||
inline int utf8_next_len(const std::string& str, size_t offset)
|
||||
{
|
||||
uint8_t c = (uint8_t)str[offset];
|
||||
if (c >= 0xFC)
|
||||
return 6;
|
||||
else if (c >= 0xF8)
|
||||
return 5;
|
||||
else if (c >= 0xF0)
|
||||
return 4;
|
||||
else if (c >= 0xE0)
|
||||
return 3;
|
||||
else if (c >= 0xC0)
|
||||
return 2;
|
||||
else if (c > 0x00)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void decode_cqcode(const std::string& code, std::vector<std::unordered_map<std::string, std::string>>& dest) {
|
||||
std::string cache;
|
||||
bool is_start = false;
|
||||
std::string key_tmp;
|
||||
std::unordered_map<std::string, std::string> kv;
|
||||
for(size_t i = 0; i < code.size(); i++) {
|
||||
int utf8_char_len = utf8_next_len(code, i);
|
||||
if(utf8_char_len == 0) {
|
||||
continue;
|
||||
}
|
||||
std::string_view c(&code[i],utf8_char_len);
|
||||
if (c == "[") {
|
||||
if (is_start) {
|
||||
throw illegal_code();
|
||||
} else {
|
||||
if (!cache.empty()) {
|
||||
std::unordered_map<std::string, std::string> kv;
|
||||
replace_string(cache, "[", "[");
|
||||
replace_string(cache, "]", "]");
|
||||
replace_string(cache, "&", "&");
|
||||
kv.emplace("_type", "text");
|
||||
kv.emplace("text", cache);
|
||||
dest.push_back(kv);
|
||||
cache.clear();
|
||||
}
|
||||
std::string_view cq_flag(&code[i],4);
|
||||
if(cq_flag == "[CQ:"){
|
||||
is_start = true;
|
||||
i += 3;
|
||||
}else{
|
||||
cache += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (c == "=") {
|
||||
if (is_start) {
|
||||
if (cache.empty()) {
|
||||
throw illegal_code();
|
||||
} else {
|
||||
if (key_tmp.empty()) {
|
||||
key_tmp.append(cache);
|
||||
cache.clear();
|
||||
} else {
|
||||
cache += c;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cache += c;
|
||||
}
|
||||
}
|
||||
else if (c == ",") {
|
||||
if (is_start) {
|
||||
if (kv.count("_type") == 0 && !cache.empty()) {
|
||||
kv.emplace("_type", cache);
|
||||
cache.clear();
|
||||
} else {
|
||||
if (!key_tmp.empty()) {
|
||||
replace_string(cache, "[", "[");
|
||||
replace_string(cache, "]", "]");
|
||||
replace_string(cache, ",", ",");
|
||||
replace_string(cache, "&", "&");
|
||||
kv.emplace(key_tmp, cache);
|
||||
cache.clear();
|
||||
key_tmp.clear();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cache += c;
|
||||
}
|
||||
}
|
||||
else if (c == "]") {
|
||||
if (is_start) {
|
||||
if (!cache.empty()) {
|
||||
if (!key_tmp.empty()) {
|
||||
replace_string(cache, "[", "[");
|
||||
replace_string(cache, "]", "]");
|
||||
replace_string(cache, ",", ",");
|
||||
replace_string(cache, "&", "&");
|
||||
kv.emplace(key_tmp, cache);
|
||||
} else {
|
||||
kv.emplace("_type", cache);
|
||||
}
|
||||
dest.push_back(kv);
|
||||
kv.clear();
|
||||
key_tmp.clear();
|
||||
cache.clear();
|
||||
is_start = false;
|
||||
}
|
||||
} else {
|
||||
cache += c;
|
||||
}
|
||||
}
|
||||
else {
|
||||
cache += c;
|
||||
i += (utf8_char_len - 1);
|
||||
}
|
||||
}
|
||||
if (!cache.empty()) {
|
||||
std::unordered_map<std::string, std::string> kv;
|
||||
replace_string(cache, "[", "[");
|
||||
replace_string(cache, "]", "]");
|
||||
replace_string(cache, "&", "&");
|
||||
kv.emplace("_type", "text");
|
||||
kv.emplace("text", cache);
|
||||
dest.push_back(kv);
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
#include "jni.h"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
|
||||
struct Honor {
|
||||
int id;
|
||||
std::string name;
|
||||
std::string icon_url;
|
||||
int priority;
|
||||
};
|
||||
|
||||
int calc_honor_flag(int honor_id, char honor_flag);
|
||||
|
||||
jobject make_honor_object(JNIEnv *env, jobject user_id, const Honor& honor);
|
||||
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_moe_fuqiuluo_shamrock_remote_action_handlers_GetTroopHonor_nativeDecodeHonor(JNIEnv *env, jobject thiz,
|
||||
jstring user_id,
|
||||
jint honor_id,
|
||||
jbyte honor_flag) {
|
||||
static std::vector<Honor> honor_list = {
|
||||
Honor{1, "龙王", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150116_n4PxCiurbm.png", 1},
|
||||
Honor{2, "群聊之火", "https://qzonestyle.gtimg.cn/aoi/sola/20200217190136_92JEGFKC5k.png", 3},
|
||||
Honor{3, "群聊炽焰", "https://qzonestyle.gtimg.cn/aoi/sola/20200217190204_zgCTeSrMq1.png", 4},
|
||||
Honor{5, "冒尖小春笋", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150335_tUJCAtoKVP.png", 5},
|
||||
Honor{6, "快乐源泉", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150434_3tDmsJExCP.png", 7},
|
||||
Honor{7, "学术新星", "https://sola.gtimg.cn/aoi/sola/20200515140645_j0X6gbuHNP.png", 8},
|
||||
Honor{8, "顶尖学霸", "https://sola.gtimg.cn/aoi/sola/20200515140639_0CtWOpfVzK.png", 9},
|
||||
Honor{9, "至尊学神", "https://sola.gtimg.cn/aoi/sola/20200515140628_P8UEYBjMBT.png", 10},
|
||||
Honor{10, "一笔当先", "https://sola.gtimg.cn/aoi/sola/20200515140654_4r94tSCdaB.png", 11},
|
||||
Honor{11, "奋进小翠竹", "https://sola.gtimg.cn/aoi/sola/20200812151819_wbj6z2NGoB.png", 6},
|
||||
Honor{12, "氛围魔杖", "https://sola.gtimg.cn/aoi/sola/20200812151831_4ZJgQCaD1H.png", 2},
|
||||
Honor{13, "壕礼皇冠", "https://sola.gtimg.cn/aoi/sola/20200930154050_juZOAMg7pt.png", 12},
|
||||
};
|
||||
int flag = calc_honor_flag(honor_id, honor_flag);
|
||||
if ((honor_id != 1 && honor_id != 2 && honor_id != 3) || flag != 1) {
|
||||
auto honor = *std::find_if(honor_list.begin(), honor_list.end(), [&honor_id](auto &honor) {
|
||||
return honor.id == honor_id;
|
||||
});
|
||||
return make_honor_object(env, user_id, honor);
|
||||
} else {
|
||||
auto honor = *std::find_if(honor_list.begin(), honor_list.end(), [&honor_id](auto &honor) {
|
||||
return honor.id == honor_id;
|
||||
});
|
||||
std::string url = "https://static-res.qq.com/static-res/groupInteract/vas/a/" + std::to_string(honor_id) + "_1.png";
|
||||
honor = Honor{honor_id, honor.name, url, honor.priority};
|
||||
return make_honor_object(env, user_id, honor);
|
||||
}
|
||||
}
|
||||
|
||||
int calc_honor_flag(int honor_id, char honor_flag) {
|
||||
int flag;
|
||||
if (honor_flag == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (honor_id == 1) {
|
||||
flag = honor_flag;
|
||||
} else if (honor_id == 2 || honor_id == 3) {
|
||||
flag = honor_flag >> 2;
|
||||
} else if (honor_id != 4) {
|
||||
return 0;
|
||||
} else {
|
||||
flag = honor_flag >> 4;
|
||||
}
|
||||
return flag & 3;
|
||||
}
|
||||
|
||||
jobject make_honor_object(JNIEnv *env, jobject user_id, const Honor& honor) {
|
||||
jclass GroupMemberHonor = env->FindClass("moe/fuqiuluo/shamrock/remote/service/data/GroupMemberHonor");
|
||||
jmethodID GroupMemberHonor_init = env->GetMethodID(GroupMemberHonor, "<init>",
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;)V");
|
||||
auto user_id_str = (jstring) user_id;
|
||||
jstring honor_desc = env->NewStringUTF(honor.name.c_str());
|
||||
jstring uin_name = env->NewStringUTF("");
|
||||
jstring honor_icon_url = env->NewStringUTF(honor.icon_url.c_str());
|
||||
jobject ret = env->NewObject(GroupMemberHonor, GroupMemberHonor_init, user_id_str, uin_name, honor_icon_url, 0, honor.id, honor_desc);
|
||||
|
||||
env->DeleteLocalRef(GroupMemberHonor);
|
||||
env->DeleteLocalRef(user_id_str);
|
||||
env->DeleteLocalRef(honor_desc);
|
||||
env->DeleteLocalRef(honor_icon_url);
|
||||
|
||||
return ret;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
#ifndef UNTITLED_CQCODE_H
|
||||
#define UNTITLED_CQCODE_H
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <exception>
|
||||
|
||||
class illegal_code: std::exception {
|
||||
public:
|
||||
[[nodiscard]] const char * what() const noexcept override {
|
||||
return "Error cq code.";
|
||||
}
|
||||
};
|
||||
|
||||
void decode_cqcode(const std::string& code, std::vector<std::unordered_map<std::string, std::string>>& dest);
|
||||
|
||||
void encode_cqcode(const std::vector<std::unordered_map<std::string, std::string>>& segment, std::string& dest);
|
||||
|
||||
#endif //UNTITLED_CQCODE_H
|
@ -1,5 +1,4 @@
|
||||
#include "jni.h"
|
||||
#include "cqcode.h"
|
||||
#include <random>
|
||||
|
||||
inline void replace_string(std::string& str, const std::string& from, const std::string& to) {
|
||||
@ -12,7 +11,7 @@ inline void replace_string(std::string& str, const std::string& from, const std:
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_createMessageUniseq(JNIEnv *env, jobject thiz,
|
||||
Java_qq_service_msg_MessageHelper_createMessageUniseq(JNIEnv *env, jobject thiz,
|
||||
jint chat_type,
|
||||
jlong time) {
|
||||
static std::random_device rd;
|
||||
@ -32,123 +31,6 @@ Java_moe_fuqiuluo_shamrock_helper_MessageHelper_getChatType(JNIEnv *env, jobject
|
||||
return (int32_t) ((int64_t) msg_id & 0xffL);
|
||||
}
|
||||
|
||||
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"
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_insertChatTypeToMsgId(JNIEnv *env, jobject thiz,
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_moe_fuqiuluo_shamrock_xposed_hooks_PullConfig_testNativeLibrary(JNIEnv *env, jobject thiz) {
|
||||
Java_moe_fuqiuluo_shamrock_xposed_actions_interacts_Init_testNativeLibrary(JNIEnv *env, jobject thiz) {
|
||||
return env->NewStringUTF("加载Shamrock库成功~");
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ package moe.fuqiuluo.shamrock
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
@ -64,7 +65,9 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.fuqiuluo.shamrock.tools.GlobalUi
|
||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
||||
import moe.fuqiuluo.shamrock.ui.app.Logger
|
||||
import moe.fuqiuluo.shamrock.ui.app.RuntimeState
|
||||
@ -85,8 +88,16 @@ import moe.fuqiuluo.shamrock.ui.tools.getShamrockVersion
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
delay(5_000) // Delay in milliseconds
|
||||
broadcastToModule {
|
||||
putExtra("__cmd", "switch_status")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalIndication provides NoIndication
|
||||
) {
|
||||
@ -96,8 +107,9 @@ class MainActivity : ComponentActivity() {
|
||||
isAppearanceLightStatusBars = true
|
||||
}
|
||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||
broadcastToModule { putExtra("__cmd", "fetchPort") }
|
||||
}
|
||||
|
||||
GlobalUi = Handler(mainLooper)
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,7 +165,7 @@ private fun AppMainView() {
|
||||
}
|
||||
|
||||
val ctx = LocalContext.current
|
||||
LaunchedEffect(isFined.value) {
|
||||
LaunchedEffect(isFined) {
|
||||
if (isFined.value) {
|
||||
AppRuntime.log(LocalString.logCentralLoadSuccessfully)
|
||||
Toast.makeText(ctx, LocalString.frameworkYes, Toast.LENGTH_SHORT).show()
|
||||
@ -284,58 +296,11 @@ private fun AnimatedTab(
|
||||
val lastSelectedState = remember {
|
||||
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 selectedConst = SELECTED_TABLE[(index * 2) + 1]
|
||||
val isFirst: Boolean = (lastSelectedState.value and defaultConst) != defaultConst
|
||||
|
||||
var icon: @Composable (() -> Unit)? = null
|
||||
var text: @Composable (() -> Unit)? = null
|
||||
|
||||
if (curSelected) {
|
||||
text = {
|
||||
AnimatedVisibility(visibleState = MutableTransitionState(false).also {
|
||||
it.targetState =
|
||||
isFirst || lastSelectedState.value and selectedConst == selectedConst
|
||||
}, enter = enter, exit = exit, modifier = Modifier) {
|
||||
Text(
|
||||
text = titleWithIcon.first,
|
||||
color = GlobalColor.TabItem,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.padding(bottom = 5.dp)
|
||||
.indication(
|
||||
remember { MutableInteractionSource() },
|
||||
rememberRipple(color = Color.Transparent)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(id = titleWithIcon.second),
|
||||
contentDescription = titleWithIcon.first,
|
||||
tint = Color.Unspecified,
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.width(24.dp)
|
||||
.padding(bottom = 5.dp)
|
||||
.indication(
|
||||
remember { MutableInteractionSource() },
|
||||
rememberRipple(color = Color.Transparent)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ShamrockTab(
|
||||
selected = curSelected,
|
||||
onClick = {
|
||||
@ -343,11 +308,13 @@ private fun AnimatedTab(
|
||||
state.scrollToPage(index, 0f)
|
||||
}
|
||||
},
|
||||
text = text,
|
||||
icon = icon,
|
||||
selectedContentColor = 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 {
|
||||
var tmp = it
|
||||
|
@ -0,0 +1,2 @@
|
||||
package moe.fuqiuluo.shamrock.app.config
|
||||
|
@ -0,0 +1,33 @@
|
||||
package moe.fuqiuluo.shamrock.app.config
|
||||
|
||||
import android.content.Context
|
||||
import moe.fuqiuluo.shamrock.config.ConfigKey
|
||||
import moe.fuqiuluo.shamrock.ui.service.internal.broadcastToModule
|
||||
|
||||
object ShamrockConfig {
|
||||
internal fun getConfigPref(ctx: Context) = ctx.getSharedPreferences("config", 0)
|
||||
|
||||
internal inline operator fun <reified Type> get(ctx: Context, key: ConfigKey<Type>): Type {
|
||||
val preferences = getConfigPref(ctx)
|
||||
return when(Type::class) {
|
||||
Int::class -> preferences.getInt(key.name(), key.default() as Int) as Type
|
||||
Long::class -> preferences.getLong(key.name(), key.default() as Long) as Type
|
||||
String::class -> preferences.getString(key.name(), key.default() as String) as Type
|
||||
Boolean::class -> preferences.getBoolean(key.name(), key.default() as Boolean) as Type
|
||||
else -> throw IllegalArgumentException("Unsupported type")
|
||||
}
|
||||
}
|
||||
|
||||
internal inline operator fun <reified Type> set(ctx: Context, key: ConfigKey<Type>, value: Type) {
|
||||
val preferences = getConfigPref(ctx)
|
||||
val editor = preferences.edit()
|
||||
when(Type::class) {
|
||||
Int::class -> editor.putInt(key.name(), value as Int)
|
||||
Long::class -> editor.putLong(key.name(), value as Long)
|
||||
String::class -> editor.putString(key.name(), value as String)
|
||||
Boolean::class -> editor.putBoolean(key.name(), value as Boolean)
|
||||
else -> throw IllegalArgumentException("Unsupported type")
|
||||
}
|
||||
editor.apply()
|
||||
}
|
||||
}
|
@ -1,383 +0,0 @@
|
||||
package moe.fuqiuluo.shamrock.ui.app
|
||||
|
||||
import android.content.Context
|
||||
import moe.fuqiuluo.shamrock.ui.service.internal.broadcastToModule
|
||||
|
||||
object ShamrockConfig {
|
||||
fun getSSLKeyPath(ctx: Context): String {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getString("key_store", "")!!
|
||||
}
|
||||
|
||||
fun setSSLKeyPath(ctx: Context, path: String) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putString("key_store", path).apply()
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun getSSLPort(ctx: Context): Int {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getInt("ssl_port", 5701)
|
||||
}
|
||||
|
||||
fun setSSLPort(ctx: Context, port: Int) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putInt("ssl_port", port).apply()
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun getSSLAlias(ctx: Context): String {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getString("ssl_alias", "")!!
|
||||
}
|
||||
|
||||
fun setSSLAlias(ctx: Context, alias: String) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putString("ssl_alias", alias).apply()
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun getSSLPwd(ctx: Context): String {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getString("ssl_pwd", "")!!
|
||||
}
|
||||
|
||||
fun setSSLPwd(ctx: Context, alias: String) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putString("ssl_pwd", alias).apply()
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun getSSLPrivatePwd(ctx: Context): String {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getString("ssl_private_pwd", "")!!
|
||||
}
|
||||
|
||||
fun setSSLPrivatePwd(ctx: Context, alias: String) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putString("ssl_private_pwd", alias).apply()
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun getHttpAddr(ctx: Context): String {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getString("http_addr", "")!!
|
||||
}
|
||||
|
||||
fun setHttpAddr(ctx: Context, v: String) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putString("http_addr", v).apply()
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun isPro(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("pro_api", false)
|
||||
}
|
||||
|
||||
fun setPro(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("pro_api", v).apply()
|
||||
ctx.broadcastToModule {
|
||||
putExtra("type", "restart")
|
||||
putExtra("__cmd", "change_port")
|
||||
}
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun getToken(ctx: Context): String {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getString("token", null) ?: ""
|
||||
}
|
||||
|
||||
fun setToken(ctx: Context, v: String?) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putString("token", v).apply()
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun isWs(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("ws", false)
|
||||
}
|
||||
|
||||
fun setWs(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("ws", v).apply()
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun isWsClient(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("ws_client", false)
|
||||
}
|
||||
|
||||
fun setWsClient(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("ws_client", v).apply()
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun isTablet(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("tablet", false)
|
||||
}
|
||||
|
||||
fun setTablet(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("tablet", v).apply()
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun isUseCQCode(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("use_cqcode", false)
|
||||
}
|
||||
|
||||
fun setUseCQCode(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("use_cqcode", v).apply()
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun isWebhook(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("webhook", false)
|
||||
}
|
||||
|
||||
fun setWebhook(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("webhook", v).apply()
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun getWsAddr(ctx: Context): String {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getString("ws_addr", "")!!
|
||||
}
|
||||
|
||||
fun setWsAddr(ctx: Context, v: String) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putString("ws_addr", v).apply()
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun getHttpPort(ctx: Context): Int {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getInt("port", 5700)
|
||||
}
|
||||
|
||||
fun setHttpPort(ctx: Context, v: Int) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putInt("port", v).apply()
|
||||
ctx.broadcastToModule {
|
||||
putExtra("type", "port")
|
||||
putExtra("port", v)
|
||||
putExtra("__cmd", "change_port")
|
||||
}
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun getWsPort(ctx: Context): Int {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getInt("ws_port", 5800)
|
||||
}
|
||||
|
||||
fun setWsPort(ctx: Context, v: Int) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putInt("ws_port", v).apply()
|
||||
ctx.broadcastToModule {
|
||||
putExtra("type", "ws_port")
|
||||
putExtra("port", v)
|
||||
putExtra("__cmd", "change_port")
|
||||
}
|
||||
pushUpdate(ctx)
|
||||
}
|
||||
|
||||
fun is2B(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("2B", false)
|
||||
}
|
||||
|
||||
fun set2B(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("2B", v).apply()
|
||||
}
|
||||
|
||||
fun setAutoClean(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("auto_clear", v).apply()
|
||||
}
|
||||
|
||||
fun isAutoClean(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("auto_clear", false)
|
||||
}
|
||||
|
||||
fun isDebug(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("debug", false)
|
||||
}
|
||||
|
||||
fun setDebug(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("debug", v).apply()
|
||||
}
|
||||
|
||||
fun isAntiTrace(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("anti_qq_trace", true)
|
||||
}
|
||||
|
||||
fun isForbidUselessProcess(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("forbid_useless_process", false)
|
||||
}
|
||||
|
||||
fun setForbidUselessProcess(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("forbid_useless_process", v).apply()
|
||||
}
|
||||
|
||||
fun setAntiTrace(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("anti_qq_trace", v).apply()
|
||||
}
|
||||
|
||||
fun isInjectPacket(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("inject_packet", false)
|
||||
}
|
||||
|
||||
fun setInjectPacket(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("inject_packet", v).apply()
|
||||
}
|
||||
|
||||
fun enableAutoStart(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("enable_auto_start", false)
|
||||
}
|
||||
|
||||
fun disableAutoSyncSetting(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("disable_auto_sync_setting", false)
|
||||
}
|
||||
|
||||
fun enableAliveReply(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("alive_reply", false)
|
||||
}
|
||||
|
||||
fun allowShell(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("shell", false)
|
||||
}
|
||||
|
||||
fun setAutoStart(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("enable_auto_start", v).apply()
|
||||
}
|
||||
|
||||
fun setDisableAutoSyncSetting(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("disable_auto_sync_setting", v).apply()
|
||||
}
|
||||
|
||||
fun setAliveReply(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("alive_reply", v).apply()
|
||||
}
|
||||
|
||||
fun setShellStatus(ctx: Context, v: Boolean) {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
preferences.edit().putBoolean("shell", v).apply()
|
||||
}
|
||||
|
||||
fun enableSelfMsg(ctx: Context): Boolean {
|
||||
val preferences = ctx.getSharedPreferences("config", 0)
|
||||
return preferences.getBoolean("enable_self_msg", false)
|
||||
}
|
||||
|
||||
fun 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),
|
||||
)
|
||||
}
|
||||
|
||||
fun pushUpdate(ctx: Context) {
|
||||
ctx.broadcastToModule {
|
||||
getConfigMap(ctx).forEach { (key, value) ->
|
||||
if (value == null) {
|
||||
val v: String? = null
|
||||
this.putExtra(key, v)
|
||||
} else {
|
||||
when (value) {
|
||||
is Int -> this.putExtra(key, value)
|
||||
is Long -> this.putExtra(key, value)
|
||||
is Short -> this.putExtra(key, value)
|
||||
is Byte -> this.putExtra(key, value)
|
||||
is String -> this.putExtra(key, value)
|
||||
is ByteArray -> this.putExtra(key, value)
|
||||
is Boolean -> this.putExtra(key, value)
|
||||
is Float -> this.putExtra(key, value)
|
||||
is Double -> this.putExtra(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
putExtra("__cmd", "push_config")
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
@ -25,6 +24,7 @@ import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@ -45,10 +45,12 @@ import coil.compose.rememberAsyncImagePainter
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Size
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import moe.fuqiuluo.shamrock.R
|
||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
||||
import moe.fuqiuluo.shamrock.ui.app.Level
|
||||
import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig
|
||||
import moe.fuqiuluo.shamrock.app.config.ShamrockConfig
|
||||
import moe.fuqiuluo.shamrock.config.*
|
||||
import moe.fuqiuluo.shamrock.ui.theme.GlobalColor
|
||||
import moe.fuqiuluo.shamrock.ui.theme.LocalString
|
||||
import moe.fuqiuluo.shamrock.ui.theme.ThemeColor
|
||||
@ -72,110 +74,6 @@ fun DashboardFragment(
|
||||
InformationCard(ctx)
|
||||
APIInfoCard(ctx)
|
||||
FunctionCard(scope, ctx, LocalString.functionSetting)
|
||||
SSLCard(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SSLCard(ctx: Context) {
|
||||
ActionBox(
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
painter = painterResource(id = R.drawable.baseline_security_24),
|
||||
title = LocalString.sslSetting
|
||||
) {
|
||||
Column {
|
||||
Divider(
|
||||
modifier = Modifier,
|
||||
color = GlobalColor.Divider,
|
||||
thickness = 0.2.dp
|
||||
)
|
||||
|
||||
val sslPort = remember { mutableStateOf(ShamrockConfig.getSSLPort(ctx).toString()) }
|
||||
TextItem(
|
||||
title = "SSL端口",
|
||||
desc = "端口范围在0~65565,并确保可用。",
|
||||
text = sslPort,
|
||||
hint = "请输入端口号",
|
||||
error = "端口范围应在0~65565",
|
||||
checker = {
|
||||
it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }.getOrDefault(false)
|
||||
},
|
||||
confirm = {
|
||||
val newPort = sslPort.value.toInt()
|
||||
ShamrockConfig.setSSLPort(ctx, newPort)
|
||||
AppRuntime.log("设置SSL(HTTP)端口为$newPort,立即生效尝试中。")
|
||||
}
|
||||
)
|
||||
|
||||
val keyStore = remember { mutableStateOf(ShamrockConfig.getSSLKeyPath(ctx)) }
|
||||
TextItem(
|
||||
title = "SSL证书",
|
||||
desc = "BKS签名的证书。",
|
||||
text = keyStore,
|
||||
hint = "输入证书路径",
|
||||
error = "证书路径不合法或不存在",
|
||||
checker = {
|
||||
it.isNotBlank()
|
||||
},
|
||||
confirm = {
|
||||
val new = keyStore.value
|
||||
ShamrockConfig.setSSLKeyPath(ctx, new)
|
||||
AppRuntime.log("设置SSL证书为[$new]。")
|
||||
}
|
||||
)
|
||||
|
||||
val alias = remember { mutableStateOf(ShamrockConfig.getSSLAlias(ctx)) }
|
||||
TextItem(
|
||||
title = "SSL别名",
|
||||
desc = "BKS签名的别名,确保大小写区分正确。",
|
||||
text = alias,
|
||||
hint = "输入签名别名",
|
||||
error = "别名不合法",
|
||||
checker = {
|
||||
it.isNotBlank()
|
||||
},
|
||||
confirm = {
|
||||
val new = alias.value
|
||||
ShamrockConfig.setSSLAlias(ctx, new)
|
||||
AppRuntime.log("设置SSL别名为[$new]。")
|
||||
}
|
||||
)
|
||||
|
||||
val sslPwd = remember { mutableStateOf(ShamrockConfig.getSSLPwd(ctx)) }
|
||||
TextItem(
|
||||
title = "SSL密码",
|
||||
desc = "BKS签名的密码。",
|
||||
text = sslPwd,
|
||||
hint = "输入签名密码",
|
||||
error = "密码不合法",
|
||||
checker = {
|
||||
it.isNotBlank()
|
||||
},
|
||||
confirm = {
|
||||
val new = sslPwd.value
|
||||
ShamrockConfig.setSSLPwd(ctx, new)
|
||||
AppRuntime.log("设置SSL密码为[$new]。")
|
||||
}
|
||||
)
|
||||
|
||||
val sslPrivatePwd = remember { mutableStateOf(ShamrockConfig.getSSLPrivatePwd(ctx)) }
|
||||
TextItem(
|
||||
title = "SSL Private密码",
|
||||
desc = "BKS签名的Private密码。",
|
||||
text = sslPrivatePwd,
|
||||
hint = "输入Private密码",
|
||||
error = "密码不合法",
|
||||
checker = {
|
||||
it.isNotBlank()
|
||||
},
|
||||
confirm = {
|
||||
val new = sslPrivatePwd.value
|
||||
ShamrockConfig.setSSLPrivatePwd(ctx, new)
|
||||
AppRuntime.log("设置SSL Private密码为[$new]。")
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,93 +93,35 @@ private fun APIInfoCard(
|
||||
thickness = 0.2.dp
|
||||
)
|
||||
|
||||
val wsPort = remember { mutableStateOf(ShamrockConfig.getWsPort(ctx).toString()) }
|
||||
val port = remember { mutableStateOf(ShamrockConfig.getHttpPort(ctx).toString()) }
|
||||
val rpcPort = remember { mutableStateOf(ShamrockConfig[ctx, RPCPort].toString()) }
|
||||
TextItem(
|
||||
title = "主动HTTP端口",
|
||||
title = "RPC服务端口",
|
||||
desc = "端口范围在0~65565,并确保可用。",
|
||||
text = port,
|
||||
text = rpcPort,
|
||||
hint = "请输入端口号",
|
||||
error = "端口范围应在0~65565",
|
||||
checker = {
|
||||
it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }.getOrDefault(false) && wsPort.value != it
|
||||
it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }
|
||||
.getOrDefault(false) && rpcPort.value != it
|
||||
},
|
||||
confirm = {
|
||||
val newPort = port.value.toInt()
|
||||
ShamrockConfig.setHttpPort(ctx, newPort)
|
||||
val newPort = rpcPort.value.toInt()
|
||||
ShamrockConfig[ctx, RPCPort] = newPort
|
||||
AppRuntime.log("设置主动HTTP监听端口为$newPort,立即生效尝试中。")
|
||||
}
|
||||
)
|
||||
|
||||
val rpcAddress = remember { mutableStateOf(ShamrockConfig[ctx, RPCAddress]) }
|
||||
TextItem(
|
||||
title = "主动WebSocket端口",
|
||||
desc = "端口范围在0~65565,并确保可用。",
|
||||
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,
|
||||
title = "回调RPC地址",
|
||||
desc = "例如:kritor.support:8081",
|
||||
text = rpcAddress,
|
||||
hint = "请输入回调地址",
|
||||
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 },
|
||||
confirm = {
|
||||
ShamrockConfig.setToken(ctx, authToken.value)
|
||||
AppRuntime.log("设置鉴权Token为[${authToken.value}]。")
|
||||
ShamrockConfig[ctx, RPCAddress] = rpcAddress.value
|
||||
AppRuntime.log("设置回调RPC地址为[${rpcAddress.value}]。")
|
||||
}
|
||||
)
|
||||
|
||||
@ -314,59 +154,59 @@ private fun FunctionCard(
|
||||
Function(
|
||||
title = "强制平板模式",
|
||||
desc = "强制QQ使用平板模式,实现共存登录。",
|
||||
isSwitch = ShamrockConfig.isTablet(ctx)
|
||||
isSwitch = ShamrockConfig[ctx, ForceTablet]
|
||||
) {
|
||||
ShamrockConfig.setTablet(ctx, it)
|
||||
ShamrockConfig[ctx, ForceTablet] = it
|
||||
return@Function true
|
||||
}
|
||||
|
||||
Function(
|
||||
title = "HTTP回调",
|
||||
desc = "OneBot标准的HTTPAPI回调,Shamrock作为Client。",
|
||||
isSwitch = ShamrockConfig.isWebhook(ctx)
|
||||
title = "主动RPC",
|
||||
desc = "Kritor协议实现RPC",
|
||||
isSwitch = ShamrockConfig[ctx, ActiveRPC]
|
||||
) {
|
||||
ShamrockConfig.setWebhook(ctx, it)
|
||||
ShamrockConfig[ctx, ActiveRPC] = it
|
||||
return@Function true
|
||||
}
|
||||
|
||||
Function(
|
||||
title = "消息格式为CQ码",
|
||||
desc = "HTTPAPI回调的消息格式,关闭则为消息段。",
|
||||
isSwitch = ShamrockConfig.isUseCQCode(ctx)
|
||||
title = "被动RPC",
|
||||
desc = "Kritor协议实现RPC",
|
||||
isSwitch = ShamrockConfig[ctx, PassiveRPC]
|
||||
) {
|
||||
ShamrockConfig.setUseCQCode(ctx, it)
|
||||
ShamrockConfig[ctx, PassiveRPC] = it
|
||||
return@Function true
|
||||
}
|
||||
|
||||
Function(
|
||||
title = "主动WebSocket",
|
||||
desc = "OneBot标准WebSocket,Shamrock作为Server。",
|
||||
isSwitch = ShamrockConfig.isWs(ctx)
|
||||
run {
|
||||
val uploadResourceGroup = remember { mutableStateOf(ShamrockConfig[ctx, ResourceGroup]) }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.absolutePadding(left = 8.dp, right = 8.dp, top = 12.dp, bottom = 0.dp)
|
||||
) {
|
||||
ShamrockConfig.setWs(ctx, it)
|
||||
return@Function true
|
||||
Text(
|
||||
modifier = Modifier.padding(2.dp),
|
||||
text = "用来上传资源的群聊,错误的资源上传终点可能导致封禁,请自建一个群聊并填写在下方。",
|
||||
color = Color.Red,
|
||||
fontSize = 11.sp
|
||||
)
|
||||
}
|
||||
|
||||
Function(
|
||||
title = "被动WebSocket",
|
||||
desc = "OneBot标准WebSocket,Shamrock作为Client。",
|
||||
isSwitch = ShamrockConfig.isWsClient(ctx)
|
||||
) {
|
||||
ShamrockConfig.setWsClient(ctx, it)
|
||||
return@Function true
|
||||
TextItem(
|
||||
title = "接受资源群聊",
|
||||
desc = "用来上传资源的群聊,请自建一个群聊并填写在下方。",
|
||||
text = uploadResourceGroup,
|
||||
hint = "请输入群号",
|
||||
error = "群号不合法",
|
||||
checker = {
|
||||
it.isNotBlank() && it.toULongOrNull() != null
|
||||
},
|
||||
confirm = {
|
||||
val groupId = uploadResourceGroup.value
|
||||
ShamrockConfig[ctx, ResourceGroup] = groupId
|
||||
AppRuntime.log("设置接受资源群聊为[$groupId]。")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
Function(
|
||||
title = "专业级接口",
|
||||
desc = "如果你不知道你在做什么,请不要开启本功能。",
|
||||
descColor = Color.Red,
|
||||
isSwitch = ShamrockConfig.isPro(ctx)
|
||||
) {
|
||||
ShamrockConfig.setPro(ctx, it)
|
||||
AppRuntime.log("专业级API = $it", Level.WARN)
|
||||
return@Function true
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -445,9 +285,7 @@ private fun InfoItem(
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(onDoubleClick = {
|
||||
doubleClick?.invoke(content)
|
||||
}) {
|
||||
true
|
||||
}
|
||||
}) { true }
|
||||
,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
@ -23,7 +23,14 @@ import androidx.compose.ui.unit.sp
|
||||
import moe.fuqiuluo.shamrock.R
|
||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
||||
import moe.fuqiuluo.shamrock.ui.app.Level
|
||||
import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig
|
||||
import moe.fuqiuluo.shamrock.app.config.ShamrockConfig
|
||||
import moe.fuqiuluo.shamrock.config.AliveReply
|
||||
import moe.fuqiuluo.shamrock.config.AntiJvmTrace
|
||||
import moe.fuqiuluo.shamrock.config.B2Mode
|
||||
import moe.fuqiuluo.shamrock.config.DebugMode
|
||||
import moe.fuqiuluo.shamrock.config.EnableOldBDH
|
||||
import moe.fuqiuluo.shamrock.config.EnableSelfMessage
|
||||
import moe.fuqiuluo.shamrock.ui.service.handlers.InitHandler
|
||||
import moe.fuqiuluo.shamrock.ui.theme.GlobalColor
|
||||
import moe.fuqiuluo.shamrock.ui.theme.LocalString
|
||||
import moe.fuqiuluo.shamrock.ui.tools.NoticeTextDialog
|
||||
@ -68,9 +75,9 @@ fun LabFragment() {
|
||||
title = LocalString.b2Mode,
|
||||
desc = LocalString.b2ModeDesc,
|
||||
descColor = it,
|
||||
isSwitch = ShamrockConfig.is2B(ctx)
|
||||
isSwitch = ShamrockConfig[ctx, B2Mode]
|
||||
) {
|
||||
ShamrockConfig.set2B(ctx, it)
|
||||
ShamrockConfig[ctx, B2Mode] = it
|
||||
scope.toast(ctx, LocalString.restartToast)
|
||||
return@Function true
|
||||
}
|
||||
@ -79,10 +86,10 @@ fun LabFragment() {
|
||||
title = LocalString.showDebugLog,
|
||||
desc = LocalString.showDebugLogDesc,
|
||||
descColor = it,
|
||||
isSwitch = ShamrockConfig.isDebug(ctx)
|
||||
isSwitch = ShamrockConfig[ctx, DebugMode]
|
||||
) {
|
||||
ShamrockConfig.setDebug(ctx, it)
|
||||
ShamrockConfig.pushUpdate(ctx)
|
||||
ShamrockConfig[ctx, DebugMode] = it
|
||||
InitHandler.update(ctx)
|
||||
return@Function true
|
||||
}
|
||||
}
|
||||
@ -100,54 +107,13 @@ fun LabFragment() {
|
||||
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(
|
||||
title = "自回复测试",
|
||||
desc = "发送[ping],机器人发送一个具有调试信息的返回。",
|
||||
descColor = color,
|
||||
isSwitch = ShamrockConfig.enableAliveReply(ctx)
|
||||
isSwitch = ShamrockConfig[ctx, AliveReply]
|
||||
) {
|
||||
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)
|
||||
ShamrockConfig[ctx, AliveReply] = it
|
||||
return@Function true
|
||||
}
|
||||
|
||||
@ -194,25 +160,14 @@ fun LabFragment() {
|
||||
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(
|
||||
title = LocalString.antiTrace,
|
||||
desc = LocalString.antiTraceDesc,
|
||||
descColor = color,
|
||||
isSwitch = ShamrockConfig.isAntiTrace(ctx)
|
||||
isSwitch = ShamrockConfig[ctx, AntiJvmTrace]
|
||||
) {
|
||||
ShamrockConfig.setAntiTrace(ctx, it)
|
||||
ShamrockConfig.pushUpdate(ctx)
|
||||
ShamrockConfig[ctx, AntiJvmTrace] = it
|
||||
scope.toast(ctx, LocalString.restartToast)
|
||||
return@Function true
|
||||
}
|
||||
|
||||
@ -277,21 +232,10 @@ fun LabFragment() {
|
||||
title = "自发消息推送",
|
||||
desc = "推送Bot发送的消息,未做特殊处理请勿打开。",
|
||||
descColor = it,
|
||||
isSwitch = ShamrockConfig.enableSelfMsg(ctx)
|
||||
isSwitch = ShamrockConfig[ctx, EnableSelfMessage]
|
||||
) {
|
||||
ShamrockConfig.setEnableSelfMsg(ctx, it)
|
||||
ShamrockConfig.pushUpdate(ctx)
|
||||
return@Function true
|
||||
}
|
||||
|
||||
Function(
|
||||
title = "同步消息推送类型异换",
|
||||
desc = "推送来自同号异设备消息,将同步消息作为自发消息推送。",
|
||||
descColor = it,
|
||||
isSwitch = ShamrockConfig.enableSyncMsgAsSentMsg(ctx)
|
||||
) {
|
||||
ShamrockConfig.setEnableSyncMsgAsSentMsg(ctx, it)
|
||||
ShamrockConfig.pushUpdate(ctx)
|
||||
ShamrockConfig[ctx, EnableSelfMessage] = it
|
||||
InitHandler.update(ctx)
|
||||
return@Function true
|
||||
}
|
||||
|
||||
@ -299,10 +243,10 @@ fun LabFragment() {
|
||||
title = "启用旧版资源上传系统",
|
||||
desc = "如果NT内核无法上传资源,请打开本开关。",
|
||||
descColor = it,
|
||||
isSwitch = ShamrockConfig.enableOldBDH(ctx)
|
||||
isSwitch = ShamrockConfig[ctx, EnableOldBDH]
|
||||
) {
|
||||
ShamrockConfig.setEnableOldBDH(ctx, it)
|
||||
ShamrockConfig.pushUpdate(ctx)
|
||||
ShamrockConfig[ctx, EnableOldBDH] = it
|
||||
InitHandler.update(ctx)
|
||||
return@Function true
|
||||
}
|
||||
}
|
||||
|
@ -1,108 +0,0 @@
|
||||
@file:OptIn(DelicateCoroutinesApi::class)
|
||||
package moe.fuqiuluo.shamrock.ui.service
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import moe.fuqiuluo.shamrock.remote.structures.CommonResult
|
||||
import moe.fuqiuluo.shamrock.remote.structures.CurrentAccount
|
||||
import moe.fuqiuluo.shamrock.remote.structures.Status
|
||||
import moe.fuqiuluo.shamrock.tools.GlobalClient
|
||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime.AccountInfo
|
||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime.log
|
||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime.state
|
||||
import moe.fuqiuluo.shamrock.ui.app.Level
|
||||
import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig
|
||||
import moe.fuqiuluo.shamrock.ui.service.internal.broadcastToModule
|
||||
import java.net.ConnectException
|
||||
import java.util.Timer
|
||||
import kotlin.concurrent.timer
|
||||
|
||||
object DashboardInitializer {
|
||||
private var servicePort: Int = 0
|
||||
private lateinit var heartbeatTimer: Timer
|
||||
|
||||
operator fun invoke(context: Context, port: Int) {
|
||||
servicePort = port
|
||||
initHeartbeat(true, context)
|
||||
}
|
||||
|
||||
private fun initHeartbeat(reload: Boolean, context: Context) {
|
||||
if (::heartbeatTimer.isInitialized && !reload) {
|
||||
return
|
||||
}
|
||||
if (::heartbeatTimer.isInitialized) {
|
||||
heartbeatTimer.cancel()
|
||||
}
|
||||
heartbeatTimer = timer("heartbeat", false, 0, 1000L * 30) {
|
||||
checkService(context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkService(context: Context) {
|
||||
GlobalScope.launch {
|
||||
try {
|
||||
GlobalClient.get {
|
||||
url("http://127.0.0.1:$servicePort/get_account_info")
|
||||
val token = ShamrockConfig.getToken(context)
|
||||
if (token.isNotBlank()) {
|
||||
//header("Authorization", "Bearer $token")
|
||||
parameter("access_token", token)
|
||||
}
|
||||
}.let {
|
||||
if (it.status == HttpStatusCode.OK) {
|
||||
val result: CommonResult<CurrentAccount> = Json.decodeFromString(it.bodyAsText())
|
||||
state.isFined.value = result.retcode == 0
|
||||
if (result.retcode == Status.InternalHandlerError.code) {
|
||||
log("账号未登录。", Level.WARN)
|
||||
} else if (result.retcode != 0) {
|
||||
log("尝试从接口获取账号信息失败,未知错误。", Level.ERROR)
|
||||
} else {
|
||||
AccountInfo.let { account ->
|
||||
account.uin.value = result.data.uin.toString()
|
||||
account.nick.value = result.data.nick
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state.isFined.value = false
|
||||
log("尝试从接口获取账号信息失败,服务运行异常。", Level.ERROR)
|
||||
}
|
||||
}
|
||||
} catch (e: ConnectException) {
|
||||
state.isFined.value = false
|
||||
context.broadcastToModule {
|
||||
putExtra("__cmd", "checkAndStartService")
|
||||
}
|
||||
|
||||
if (ShamrockConfig.enableAutoStart(context)) {
|
||||
log("检测到Service死亡,正在尝试重新启动!")
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
val packageName = "com.tencent.mobileqq"
|
||||
val className = "com.tencent.mobileqq.activity.SplashActivity"
|
||||
|
||||
val intent = Intent()
|
||||
intent.component = ComponentName(packageName, className)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
intent.putExtra("from", "shamrock")
|
||||
startActivity(context, intent, Bundle.EMPTY)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
state.isFined.value = false
|
||||
log(e.stackTraceToString(), Level.ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package moe.fuqiuluo.shamrock.ui.service.handlers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
||||
import moe.fuqiuluo.shamrock.ui.service.DashboardInitializer
|
||||
|
||||
object FetchPortHandler: ModuleHandler() {
|
||||
override val cmd: String = "success"
|
||||
|
||||
override fun onReceive(callbackId: Int, values: ContentValues, context: Context) {
|
||||
AppRuntime.state.supportVoice.value = values.getAsBoolean("voice")
|
||||
DashboardInitializer(context, values.getAsInteger("port"))
|
||||
}
|
||||
}
|
@ -3,13 +3,37 @@ package moe.fuqiuluo.shamrock.ui.service.handlers
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
||||
import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig
|
||||
import moe.fuqiuluo.shamrock.app.config.ShamrockConfig
|
||||
import moe.fuqiuluo.shamrock.config.*
|
||||
|
||||
internal object InitHandler: ModuleHandler() {
|
||||
override val cmd: String = "init"
|
||||
|
||||
override fun onReceive(callbackId: Int, values: ContentValues, context: Context) {
|
||||
update(context)
|
||||
}
|
||||
|
||||
fun update(context: Context) {
|
||||
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,6 +29,7 @@ abstract class ModuleHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
putExtra("__cmd", cmd)
|
||||
putExtra("__hash", callbackId)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,49 @@
|
||||
package moe.fuqiuluo.shamrock.ui.service.handlers
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import moe.fuqiuluo.shamrock.tools.GlobalUi
|
||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
||||
import java.util.Timer
|
||||
import kotlin.concurrent.timer
|
||||
import kotlin.concurrent.timerTask
|
||||
|
||||
object SwitchStatus: ModuleHandler() {
|
||||
override val cmd: String
|
||||
get() = "switch_status"
|
||||
|
||||
private var lastActiveTime = 0L
|
||||
private var timer: Timer? = null
|
||||
|
||||
override fun onReceive(callbackId: Int, values: ContentValues, context: Context) {
|
||||
val voiceSwitch = values.getAsBoolean("voice")
|
||||
val nickname = values.getAsString("nickname")
|
||||
val account = values.getAsString("account")
|
||||
if (lastActiveTime == 0L) GlobalUi.post {
|
||||
Toast.makeText(context, "激活成功", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
AppRuntime.state.apply {
|
||||
isFined.value = true
|
||||
coreVersion.value = values.getAsString("core_version")
|
||||
coreName.value = "LSPosed"
|
||||
supportVoice.value = voiceSwitch
|
||||
}
|
||||
AppRuntime.AccountInfo.apply {
|
||||
uin.value = account
|
||||
nick.value = nickname
|
||||
}
|
||||
lastActiveTime = System.currentTimeMillis()
|
||||
startTimer()
|
||||
}
|
||||
|
||||
private fun startTimer() {
|
||||
timer?.cancel()
|
||||
timer = timer("SwitchStatus", true, 0, 5_000) {
|
||||
if (lastActiveTime != 0L && System.currentTimeMillis() - lastActiveTime > 30 * 1000) {
|
||||
AppRuntime.state.isFined.value = false
|
||||
lastActiveTime = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,9 +5,9 @@ import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import moe.fuqiuluo.shamrock.ui.service.ModuleTalker
|
||||
import moe.fuqiuluo.shamrock.ui.service.handlers.*
|
||||
import android.net.Uri
|
||||
|
||||
class MultifunctionalProvider: ContentProvider() {
|
||||
override fun insert(uri: Uri, content: ContentValues?): Uri {
|
||||
@ -28,8 +28,8 @@ class MultifunctionalProvider: ContentProvider() {
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
ModuleTalker.register(InitHandler)
|
||||
ModuleTalker.register(FetchPortHandler)
|
||||
ModuleTalker.register(LogHandler)
|
||||
ModuleTalker.register(SwitchStatus)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ class MultifunctionalProvider: ContentProvider() {
|
||||
|
||||
inline fun Context.broadcastToModule(intentBuilder: Intent.() -> Unit) {
|
||||
val intent = Intent()
|
||||
intent.action = "moe.fuqiuluo.xqbot.dynamic"
|
||||
intent.action = "moe.fuqiuluo.kritor.dynamic"
|
||||
intent.intentBuilder()
|
||||
sendBroadcast(intent)
|
||||
}
|
@ -1,23 +1,35 @@
|
||||
package moe.fuqiuluo.shamrock.ui.tools
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
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.MutableTransitionState
|
||||
import androidx.compose.animation.core.TweenSpec
|
||||
import androidx.compose.animation.core.tween
|
||||
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.background
|
||||
import androidx.compose.foundation.indication
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
@ -31,8 +43,10 @@ import androidx.compose.ui.layout.LastBaseline
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.Placeable
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -135,8 +149,7 @@ private fun TabBaselineLayout(
|
||||
text: @Composable (() -> Unit)?,
|
||||
icon: @Composable (() -> Unit)?
|
||||
) {
|
||||
Layout(
|
||||
{
|
||||
Layout({
|
||||
if (text != null) {
|
||||
Box(
|
||||
Modifier
|
||||
@ -147,8 +160,7 @@ private fun TabBaselineLayout(
|
||||
if (icon != null) {
|
||||
Box(Modifier.layoutId("icon")) { icon() }
|
||||
}
|
||||
}
|
||||
) { measurables, constraints ->
|
||||
}) { measurables, constraints ->
|
||||
val textPlaceable = text?.let {
|
||||
measurables.first { it.layoutId == "text" }.measure(
|
||||
// Measure with loose constraints for height as we don't want the text to take up more
|
||||
@ -247,21 +259,66 @@ fun ShamrockTab(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
text: @Composable (() -> Unit)? = null,
|
||||
icon: @Composable (() -> Unit)? = null,
|
||||
selectedContentColor: Color = GlobalColor.TabSelected,
|
||||
unselectedContentColor: Color = selectedContentColor,
|
||||
indication: Indication? = rememberRipple(bounded = true, color = selectedContentColor),
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
titleWithIcon: Pair<String, Int>,
|
||||
visibleState: MutableTransitionState<Boolean>
|
||||
) {
|
||||
val styledText: @Composable (() -> Unit)? = text?.let {
|
||||
@Composable {
|
||||
val style =
|
||||
MaterialTheme.typography.fromToken(PrimaryNavigationTabTokens.LabelTextFont)
|
||||
var text: @Composable (() -> Unit)? = null
|
||||
var icon: @Composable (() -> Unit)? = null
|
||||
|
||||
if (!selected) {
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(id = titleWithIcon.second),
|
||||
contentDescription = titleWithIcon.first,
|
||||
tint = Color.Unspecified,
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.width(24.dp)
|
||||
.padding(bottom = 5.dp)
|
||||
.indication(
|
||||
remember { MutableInteractionSource() },
|
||||
rememberRipple(color = Color.Transparent)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
text = {
|
||||
val style = MaterialTheme.typography
|
||||
.fromToken(PrimaryNavigationTabTokens.LabelTextFont)
|
||||
.copy(textAlign = TextAlign.Center)
|
||||
ProvideTextStyle(style, content = text)
|
||||
|
||||
ProvideTextStyle(style) {
|
||||
AnimatedVisibility(
|
||||
visibleState = visibleState,
|
||||
enter = remember {
|
||||
scaleIn(animationSpec = TweenSpec(150, easing = FastOutLinearInEasing))
|
||||
},
|
||||
exit = remember {
|
||||
scaleOut(animationSpec = TweenSpec(150, easing = FastOutSlowInEasing))
|
||||
},
|
||||
modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
text = titleWithIcon.first,
|
||||
color = GlobalColor.TabItem,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.padding(bottom = 5.dp)
|
||||
.indication(
|
||||
remember { MutableInteractionSource() },
|
||||
rememberRipple(color = Color.Transparent)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ShamrockTab(
|
||||
selected,
|
||||
onClick,
|
||||
@ -272,7 +329,10 @@ fun ShamrockTab(
|
||||
interactionSource,
|
||||
indication
|
||||
) {
|
||||
TabBaselineLayout(icon = icon, text = styledText)
|
||||
TabBaselineLayout(
|
||||
icon = icon,
|
||||
text = text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.9.21"
|
||||
kotlin("jvm") version "1.9.22"
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
@ -7,10 +7,6 @@ val DEPENDENCY_ANDROIDX = arrayOf(
|
||||
"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 kotlinx(name: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$name:$version"
|
||||
@ -19,8 +15,9 @@ fun ktor(target: String, name: String): String {
|
||||
return "io.ktor:ktor-$target-$name:${Versions.ktorVersion}"
|
||||
}
|
||||
|
||||
fun grpc(name: String, version: String) = "io.grpc:grpc-$name:$version"
|
||||
|
||||
object Versions {
|
||||
const val roomVersion = "2.5.0"
|
||||
|
||||
const val ktorVersion = "2.3.3"
|
||||
}
|
42
kritor/.gitignore
vendored
Normal file
42
kritor/.gitignore
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
76
kritor/build.gradle.kts
Normal file
76
kritor/build.gradle.kts
Normal file
@ -0,0 +1,76 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("com.google.protobuf") version "0.9.4"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "moe.whitechi73.kritor"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
protobuf(files("kritor/protos"))
|
||||
|
||||
implementation("com.google.protobuf:protobuf-java:4.26.0")
|
||||
|
||||
implementation(kotlinx("coroutines-core", "1.8.0"))
|
||||
|
||||
implementation(grpc("stub", "1.62.2"))
|
||||
implementation(grpc("protobuf", "1.62.2"))
|
||||
implementation(grpc("kotlin-stub", "1.4.1"))
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = "com.google.protobuf:protoc:4.26.0"
|
||||
}
|
||||
plugins {
|
||||
create("grpc") {
|
||||
artifact = "io.grpc:protoc-gen-grpc-java:1.62.2"
|
||||
}
|
||||
create("grpckt") {
|
||||
artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar"
|
||||
}
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().forEach {
|
||||
it.plugins {
|
||||
create("grpc")
|
||||
create("grpckt")
|
||||
}
|
||||
it.builtins {
|
||||
create("java")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
|
||||
}
|
0
kritor/consumer-rules.pro
Normal file
0
kritor/consumer-rules.pro
Normal file
1
kritor/kritor
Submodule
1
kritor/kritor
Submodule
Submodule kritor/kritor added at c49df3074c
21
kritor/proguard-rules.pro
vendored
Normal file
21
kritor/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
@ -15,7 +15,7 @@ dependencies {
|
||||
implementation("com.google.devtools.ksp:symbol-processing-api:1.9.21-1.0.15")
|
||||
implementation("com.squareup:kotlinpoet:1.14.2")
|
||||
|
||||
implementation(DEPENDENCY_PROTOBUF)
|
||||
//implementation(DEPENDENCY_PROTOBUF)
|
||||
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
||||
|
||||
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0")
|
||||
|
@ -0,0 +1,83 @@
|
||||
@file:Suppress("UNCHECKED_CAST")
|
||||
@file:OptIn(KspExperimental::class)
|
||||
|
||||
package moe.fuqiuluo.ksp.impl
|
||||
|
||||
import com.google.devtools.ksp.KspExperimental
|
||||
import com.google.devtools.ksp.getAnnotationsByType
|
||||
import com.google.devtools.ksp.processing.*
|
||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
|
||||
import com.squareup.kotlinpoet.FileSpec
|
||||
import com.squareup.kotlinpoet.FunSpec
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import kritor.service.Grpc
|
||||
|
||||
class GrpcProcessor(
|
||||
private val codeGenerator: CodeGenerator,
|
||||
private val logger: KSPLogger
|
||||
): SymbolProcessor {
|
||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||
val symbols = resolver.getSymbolsWithAnnotation(Grpc::class.qualifiedName!!)
|
||||
val actions = (symbols as Sequence<KSFunctionDeclaration>).toList()
|
||||
|
||||
if (actions.isEmpty()) return emptyList()
|
||||
|
||||
// 怎么返回nullable的结果
|
||||
val packageName = "kritor.handlers"
|
||||
val funcBuilder = FunSpec.builder("handleGrpc")
|
||||
.addModifiers(KModifier.SUSPEND)
|
||||
.addParameter("cmd", String::class)
|
||||
.addParameter("data", ByteArray::class)
|
||||
.returns(ByteArray::class)
|
||||
val fileSpec = FileSpec.scriptBuilder("AutoGrpcHandlers", packageName)
|
||||
|
||||
logger.warn("Found ${actions.size} grpc-actions")
|
||||
|
||||
//logger.error(resolver.getClassDeclarationByName("io.kritor.AuthReq").toString())
|
||||
//logger.error(resolver.getJavaClassByName("io.kritor.AuthReq").toString())
|
||||
//logger.error(resolver.getKotlinClassByName("io.kritor.AuthReq").toString())
|
||||
|
||||
actions.forEach { action ->
|
||||
val methodName = action.qualifiedName?.asString()!!
|
||||
val grpcMethod = action.getAnnotationsByType(Grpc::class).first()
|
||||
val service = grpcMethod.serviceName
|
||||
val funcName = grpcMethod.funcName
|
||||
funcBuilder.addStatement("if (cmd == \"${service}.${funcName}\") {\t")
|
||||
|
||||
val reqType = action.parameters[0].type.toString()
|
||||
val rspType = action.returnType.toString()
|
||||
funcBuilder.addStatement("val resp: $rspType = $methodName($reqType.parseFrom(data))")
|
||||
funcBuilder.addStatement("return resp.toByteArray()")
|
||||
|
||||
funcBuilder.addStatement("}")
|
||||
}
|
||||
funcBuilder.addStatement("return EMPTY_BYTE_ARRAY")
|
||||
fileSpec
|
||||
.addStatement("import io.kritor.authentication.*")
|
||||
.addStatement("import io.kritor.core.*")
|
||||
.addStatement("import io.kritor.customization.*")
|
||||
.addStatement("import io.kritor.developer.*")
|
||||
.addStatement("import io.kritor.file.*")
|
||||
.addStatement("import io.kritor.friend.*")
|
||||
.addStatement("import io.kritor.group.*")
|
||||
.addStatement("import io.kritor.guild.*")
|
||||
.addStatement("import io.kritor.message.*")
|
||||
.addStatement("import io.kritor.web.*")
|
||||
.addFunction(funcBuilder.build())
|
||||
.addImport("moe.fuqiuluo.symbols", "EMPTY_BYTE_ARRAY")
|
||||
runCatching {
|
||||
codeGenerator.createNewFile(
|
||||
dependencies = Dependencies(aggregating = false),
|
||||
packageName = packageName,
|
||||
fileName = fileSpec.name
|
||||
).use { outputStream ->
|
||||
outputStream.writer().use {
|
||||
fileSpec.build().writeTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return emptyList()
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
@file:OptIn(KspExperimental::class)
|
||||
@file:Suppress("LocalVariableName", "UNCHECKED_CAST")
|
||||
|
||||
package moe.fuqiuluo.ksp.impl
|
||||
|
||||
import com.google.devtools.ksp.KspExperimental
|
||||
import com.google.devtools.ksp.getAnnotationsByType
|
||||
import com.google.devtools.ksp.getClassDeclarationByName
|
||||
import com.google.devtools.ksp.getKotlinClassByName
|
||||
import com.google.devtools.ksp.processing.CodeGenerator
|
||||
import com.google.devtools.ksp.processing.Dependencies
|
||||
import com.google.devtools.ksp.processing.KSPLogger
|
||||
import com.google.devtools.ksp.processing.Resolver
|
||||
import com.google.devtools.ksp.processing.SymbolProcessor
|
||||
import com.google.devtools.ksp.symbol.ClassKind
|
||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||
import com.google.devtools.ksp.symbol.KSVisitorVoid
|
||||
import com.google.devtools.ksp.validate
|
||||
import com.squareup.kotlinpoet.FileSpec
|
||||
import com.squareup.kotlinpoet.FunSpec
|
||||
import moe.fuqiuluo.symbols.OneBotHandler
|
||||
|
||||
class OneBotHandlerProcessor(
|
||||
private val codeGenerator: CodeGenerator,
|
||||
private val logger: KSPLogger
|
||||
): SymbolProcessor {
|
||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||
val ActionManagerNode = resolver.getClassDeclarationByName("moe.fuqiuluo.shamrock.remote.action.ActionManager")
|
||||
?: resolver.getKotlinClassByName("moe.fuqiuluo.shamrock.remote.action.ActionManager")
|
||||
?: resolver.getClassDeclarationByName("ActionManager")
|
||||
val symbols = resolver.getSymbolsWithAnnotation(OneBotHandler::class.qualifiedName!!)
|
||||
val unableToProcess = symbols.filterNot { it.validate() }
|
||||
if (ActionManagerNode != null) {
|
||||
val oneBotHandlers = (symbols.filter {
|
||||
it is KSClassDeclaration && it.validate() && it.classKind == ClassKind.OBJECT
|
||||
} as Sequence<KSClassDeclaration>).toList()
|
||||
|
||||
if (oneBotHandlers.isNotEmpty()) {
|
||||
ActionManagerNode.accept(ActionManagerVisitor(oneBotHandlers), Unit)
|
||||
}
|
||||
}
|
||||
|
||||
return unableToProcess.toList()
|
||||
}
|
||||
|
||||
inner class ActionManagerVisitor(
|
||||
private val actionHandlers: List<KSClassDeclaration>
|
||||
): KSVisitorVoid() {
|
||||
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
|
||||
val packageName = classDeclaration.packageName.asString()
|
||||
|
||||
// generate kotlin `init { }`
|
||||
val fileSpec = FileSpec.builder(packageName, classDeclaration.qualifiedName?.asString() ?: run {
|
||||
throw IllegalStateException("ActionManagerVisitor: classDeclaration.qualifiedName is null")
|
||||
}).addFunction(FunSpec.builder("initManager").apply {
|
||||
actionHandlers.forEach { handler ->
|
||||
// fetch the params of the annotation
|
||||
val annotation = handler.getAnnotationsByType(OneBotHandler::class).first()
|
||||
val actionName = annotation.actionName
|
||||
val alias = annotation.alias
|
||||
alias.forEach { name ->
|
||||
addStatement("actionMap[\"$name\"] = ${handler.simpleName.asString()}")
|
||||
}
|
||||
addStatement("actionMap[\"$actionName\"] = ${handler.simpleName.asString()}")
|
||||
}
|
||||
}.build()).apply {
|
||||
addImport("moe.fuqiuluo.shamrock.remote.action.ActionManager", "actionMap")
|
||||
actionHandlers.forEach {
|
||||
addImport(it.packageName.asString(), it.simpleName.asString())
|
||||
}
|
||||
}.build()
|
||||
|
||||
codeGenerator.createNewFile(
|
||||
dependencies = Dependencies(aggregating = false),
|
||||
packageName = packageName,
|
||||
fileName = "Auto" + classDeclaration.simpleName.asString()
|
||||
).use { outputStream ->
|
||||
outputStream.writer().use {
|
||||
fileSpec.writeTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ class ProtobufProcessor(
|
||||
}.toList()
|
||||
|
||||
if (actions.isNotEmpty()) {
|
||||
actions.forEachIndexed { index, clz ->
|
||||
actions.forEachIndexed { _, clz ->
|
||||
if (clz.isInternal()) return@forEachIndexed
|
||||
if (clz.isPrivate()) return@forEachIndexed
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
@file:Suppress("UNCHECKED_CAST", "LocalVariableName", "PrivatePropertyName")
|
||||
@file:OptIn(KspExperimental::class)
|
||||
@file:OptIn(KspExperimental::class, KspExperimental::class)
|
||||
|
||||
package moe.fuqiuluo.ksp.impl
|
||||
|
||||
@ -27,10 +27,14 @@ class XposedHookProcessor(
|
||||
private val logger: KSPLogger
|
||||
): SymbolProcessor {
|
||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||
val symbols = resolver.getSymbolsWithAnnotation(XposedHook::class.qualifiedName!!)
|
||||
val symbols = resolver.getSymbolsWithAnnotation(
|
||||
annotationName = XposedHook::class.qualifiedName!!,
|
||||
inDepth = true
|
||||
)
|
||||
logger.warn("Found ${symbols.count()} classes annotated with XposedHook")
|
||||
val unableToProcess = symbols.filterNot { it.validate() }
|
||||
val actions = (symbols.filter {
|
||||
it is KSClassDeclaration && it.validate() && it.classKind == ClassKind.CLASS
|
||||
it is KSClassDeclaration && it.classKind == ClassKind.CLASS
|
||||
} as Sequence<KSClassDeclaration>).toList()
|
||||
|
||||
if (actions.isNotEmpty()) {
|
||||
@ -46,7 +50,7 @@ class XposedHookProcessor(
|
||||
}
|
||||
|
||||
val context = ClassName("android.content", "Context")
|
||||
val packageName = "moe.fuqiuluo.shamrock.xposed.hooks"
|
||||
val packageName = "moe.fuqiuluo.shamrock.xposed.actions"
|
||||
val fileSpec = FileSpec.builder(packageName, "AutoActionLoader").addFunction(FunSpec.builder("runFirstActions")
|
||||
.addParameter("ctx", context)
|
||||
.apply {
|
||||
@ -96,16 +100,6 @@ class XposedHookProcessor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.SymbolProcessorEnvironment
|
||||
import com.google.devtools.ksp.processing.SymbolProcessorProvider
|
||||
import moe.fuqiuluo.ksp.impl.OneBotHandlerProcessor
|
||||
import moe.fuqiuluo.ksp.impl.GrpcProcessor
|
||||
|
||||
@AutoService(SymbolProcessorProvider::class)
|
||||
class OneBotHandlerProcessorProvider: SymbolProcessorProvider {
|
||||
class GrpcProvider: SymbolProcessorProvider {
|
||||
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
|
||||
return OneBotHandlerProcessor(
|
||||
return GrpcProcessor(
|
||||
environment.codeGenerator,
|
||||
environment.logger
|
||||
)
|
@ -1,3 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
@ -35,7 +37,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(DEPENDENCY_PROTOBUF)
|
||||
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
||||
implementation(kotlinx("serialization-json", "1.6.2"))
|
||||
|
||||
@ -43,3 +44,7 @@ dependencies {
|
||||
|
||||
ksp(project(":processor"))
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
|
||||
}
|
@ -32,6 +32,11 @@ data class AdaptShareInfoReq(
|
||||
|
||||
@Serializable
|
||||
data class Template(
|
||||
@ProtoNumber(1) var templateId: UInt? = null,
|
||||
@ProtoNumber(1) var templateId: ULong? = null,
|
||||
@ProtoNumber(2) var templateData: ByteArray? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AdaptShareInfoResp(
|
||||
@ProtoNumber(2) var json: String? = null,
|
||||
): Protobuf<AdaptShareInfoResp>
|
@ -7,9 +7,9 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
||||
data class Ptt(
|
||||
@ProtoNumber(1) var fileType: UInt?=null,
|
||||
@ProtoNumber(2) var srcUin: ULong?=null,
|
||||
@ProtoNumber(3) var fileUuid: ByteArray?=null,
|
||||
@ProtoNumber(3) var fileUuid: String?=null,
|
||||
@ProtoNumber(4) var fileMd5: ByteArray?=null,
|
||||
@ProtoNumber(5) var fileName: ByteArray?=null,
|
||||
@ProtoNumber(5) var fileName: String?=null,
|
||||
@ProtoNumber(6) var fileSize: UInt?=null,
|
||||
@ProtoNumber(7) var reserve: ByteArray?=null,
|
||||
@ProtoNumber(8) var fileId: UInt?=null,
|
||||
@ -22,11 +22,19 @@ data class Ptt(
|
||||
@ProtoNumber(15) var magicPttIndex: UInt?=null,
|
||||
@ProtoNumber(16) var voiceSwitch: UInt?=null,
|
||||
@ProtoNumber(17) var pttUrl: ByteArray?=null,
|
||||
@ProtoNumber(18) var groupFileKey: ByteArray?=null,
|
||||
@ProtoNumber(18) var groupFileKey: String?=null,
|
||||
@ProtoNumber(19) var time: UInt?=null,
|
||||
@ProtoNumber(20) var downPara: ByteArray?=null,
|
||||
@ProtoNumber(29) var format: UInt?=null,
|
||||
@ProtoNumber(30) var pbReserve: ByteArray?=null,
|
||||
@ProtoNumber(30) var pbReserve: PbReserve?=null,
|
||||
@ProtoNumber(31) var rptPttUrls: List<String>? = null,
|
||||
@ProtoNumber(32) var downloadFlag: UInt?=null,
|
||||
){
|
||||
companion object{
|
||||
@Serializable
|
||||
data class PbReserve(
|
||||
@ProtoNumber(2) var magic: Int?=null,
|
||||
@ProtoNumber(7) var reserve: Int?=null,
|
||||
)
|
||||
}
|
||||
}
|
@ -9,9 +9,9 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
||||
@Serializable
|
||||
data class RichText(
|
||||
@ProtoNumber(1) val attr: Attr? = null,
|
||||
@ProtoNumber(2) val elements: List<Elem>? = null,
|
||||
@ProtoNumber(3) val not_online_file: NotOnlineFile? = null,
|
||||
@ProtoNumber(4) val ptt: Ptt? = null,
|
||||
@ProtoNumber(2) var elements: List<Elem>? = null,
|
||||
@ProtoNumber(3) var not_online_file: NotOnlineFile? = null,
|
||||
@ProtoNumber(4) var ptt: Ptt? = null,
|
||||
@ProtoNumber(5) val tmp_ptt: TmpPtt? = null,
|
||||
@ProtoNumber(6) val trans_211_tmp_msg: Trans211TmpMsg? = null,
|
||||
)
|
||||
|
@ -13,7 +13,7 @@ data class ButtonExtra(
|
||||
@Serializable
|
||||
data class Object1(
|
||||
@ProtoNumber(1) val rows: List<Row>? = null,
|
||||
@ProtoNumber(2) val appid: Int? = null,
|
||||
@ProtoNumber(2) val appid: ULong? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
@ -31,7 +31,7 @@ data class RecvLongMsgInfo(
|
||||
data class SendLongMsgInfo(
|
||||
@ProtoNumber(1) val type: Int? = null,
|
||||
@ProtoNumber(2) val uid: LongMsgUid? = null,
|
||||
@ProtoNumber(3) val groupUin: Int? = null,
|
||||
@ProtoNumber(3) val groupUin: ULong? = null,
|
||||
@ProtoNumber(4) val payload: ByteArray? = null,
|
||||
)
|
||||
|
||||
|
@ -1,55 +1 @@
|
||||
@file:OptIn(ExperimentalSerializationApi::class)
|
||||
package protobuf.message.multimedia
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import moe.fuqiuluo.symbols.Protobuf
|
||||
|
||||
@Serializable
|
||||
data class RichMediaForPicData(
|
||||
@ProtoNumber(1) val info: MediaInfo?,
|
||||
@ProtoNumber(2) val display: DisplayMediaInfo?,
|
||||
): Protobuf<RichMediaForPicData> {
|
||||
companion object {
|
||||
@Serializable
|
||||
data class MediaInfo(
|
||||
@ProtoNumber(1) val picture: Picture? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Picture(
|
||||
@ProtoNumber(1) val info: PictureInfo? = null,
|
||||
@ProtoNumber(2) val fileId: String? = null,
|
||||
@ProtoNumber(4) val time: ULong? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PictureInfo(
|
||||
@ProtoNumber(2) val md5Hex: String? = null,
|
||||
@ProtoNumber(3) val sha: String? = null,
|
||||
@ProtoNumber(4) val name: String? = null,
|
||||
@ProtoNumber(6) val width: Int? = null,
|
||||
@ProtoNumber(7) val height: Int? = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class DisplayMediaInfo(
|
||||
@ProtoNumber(1) val show: Show? = null,
|
||||
) {
|
||||
companion object {
|
||||
@Serializable
|
||||
data class Show(
|
||||
@ProtoNumber(2) val text: String? = null,
|
||||
@ProtoNumber(12) val download: Download? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Download(
|
||||
@ProtoNumber(30) val url: String? = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,8 +49,8 @@ data class UploadCompletedReq(
|
||||
@Serializable
|
||||
data class MsgInfo(
|
||||
@ProtoNumber(1) val msgInfoBody: List<MsgInfoBody>,
|
||||
@ProtoNumber(2) val extBizInfo: ExtBizInfo,
|
||||
)
|
||||
@ProtoNumber(2) val extBizInfo: ExtBizInfo?,
|
||||
): Protobuf<MsgInfo>
|
||||
|
||||
@Serializable
|
||||
data class MsgInfoBody(
|
||||
@ -106,7 +106,7 @@ data class UploadReq(
|
||||
@ProtoNumber(5) val compatQMsgSceneType: UInt? = null,
|
||||
@ProtoNumber(6) val extBizInfo: ExtBizInfo? = null,
|
||||
@ProtoNumber(7) val clientSeq: UInt? = null,
|
||||
@ProtoNumber(8) val noNeedCompatMsg: Boolean = false,
|
||||
@ProtoNumber(8) val noNeedCompatMsg: Boolean? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@ -114,7 +114,7 @@ data class ExtBizInfo(
|
||||
@ProtoNumber(1) val pic: PicExtBizInfo? = null,
|
||||
@ProtoNumber(2) val video: VideoExtBizInfo? = null,
|
||||
@ProtoNumber(3) val ptt: PttExtBizInfo? = null,
|
||||
@ProtoNumber(10) val busiType: UInt,
|
||||
@ProtoNumber(10) val busiType: UInt?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@ -132,15 +132,15 @@ data class PttExtBizInfo(
|
||||
|
||||
@Serializable
|
||||
data class VideoExtBizInfo(
|
||||
@ProtoNumber(1) val fromScene: UInt,
|
||||
@ProtoNumber(2) val toScene: UInt,
|
||||
@ProtoNumber(3) val bytesPbReserve: ByteArray,
|
||||
@ProtoNumber(1) val fromScene: UInt?,
|
||||
@ProtoNumber(2) val toScene: UInt?,
|
||||
@ProtoNumber(3) val bytesPbReserve: ByteArray?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PicExtBizInfo(
|
||||
@ProtoNumber(1) val bizType: UInt,
|
||||
@ProtoNumber(2) val textSummary: String,
|
||||
@ProtoNumber(1) val bizType: UInt?,
|
||||
@ProtoNumber(2) val textSummary: String?,
|
||||
@ProtoNumber(11) val bytesPbReserveC2c: ByteArray? = null,
|
||||
@ProtoNumber(12) val bytesPbReserveTroop: ByteArray? = null,
|
||||
@ProtoNumber(1001) val fromScene: UInt? = null,
|
||||
@ -156,15 +156,15 @@ data class UploadInfo(
|
||||
|
||||
@Serializable
|
||||
data class FileInfo(
|
||||
@ProtoNumber(1) val fileSize: ULong,
|
||||
@ProtoNumber(2) val md5: String,
|
||||
@ProtoNumber(3) val sha1: String,
|
||||
@ProtoNumber(4) val name: String,
|
||||
@ProtoNumber(5) val fileType: FileType,
|
||||
@ProtoNumber(6) val width: UInt,
|
||||
@ProtoNumber(7) val height: UInt,
|
||||
@ProtoNumber(8) val time: UInt,
|
||||
@ProtoNumber(9) val original: UInt,
|
||||
@ProtoNumber(1) val fileSize: ULong?,
|
||||
@ProtoNumber(2) val md5: String?,
|
||||
@ProtoNumber(3) val sha1: String?,
|
||||
@ProtoNumber(4) val name: String?,
|
||||
@ProtoNumber(5) val fileType: FileType?,
|
||||
@ProtoNumber(6) val width: UInt?,
|
||||
@ProtoNumber(7) val height: UInt?,
|
||||
@ProtoNumber(8) val time: UInt?,
|
||||
@ProtoNumber(9) val original: UInt?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@ -217,7 +217,7 @@ data class IndexNode(
|
||||
@ProtoNumber(3) val storeId: UInt, // 0为旧服务器 1为nt服务器
|
||||
@ProtoNumber(4) val uploadTime: ULong,
|
||||
@ProtoNumber(5) val ttl: ULong,
|
||||
@ProtoNumber(6) val subType: UInt,
|
||||
@ProtoNumber(6) val subType: UInt? = null,
|
||||
@ProtoNumber(7) val storeAppId: UInt? = null
|
||||
)
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
@file:OptIn(ExperimentalSerializationApi::class)
|
||||
package protobuf.oidb.cmd0x11c5
|
||||
|
||||
import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY
|
||||
import moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
@ -9,7 +9,7 @@ import moe.fuqiuluo.symbols.Protobuf
|
||||
|
||||
@Serializable
|
||||
data class NtV2RichMediaRsp(
|
||||
@ProtoNumber(1) val head: RspHead,
|
||||
@ProtoNumber(1) val head: RspHead?,
|
||||
@ProtoNumber(2) val upload: UploadRsp?,
|
||||
@ProtoNumber(3) val download: DownloadRsp?,
|
||||
@ProtoNumber(4) val downloadRkeyRsp: DownloadRkeyRsp?,
|
||||
@ -26,8 +26,8 @@ class DownloadSafeRsp
|
||||
|
||||
@Serializable
|
||||
data class UploadKeyRenewalRsp(
|
||||
@ProtoNumber(1) val ukey: String,
|
||||
@ProtoNumber(2) val ukeyTtlSec: ULong,
|
||||
@ProtoNumber(1) val ukey: String?,
|
||||
@ProtoNumber(2) val ukeyTtlSec: ULong?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@ -39,7 +39,7 @@ data class MsgInfoAuthRsp(
|
||||
|
||||
@Serializable
|
||||
data class UploadCompletedRsp(
|
||||
@ProtoNumber(1) val msgSeq: ULong
|
||||
@ProtoNumber(1) val msgSeq: ULong?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@ -47,13 +47,13 @@ class DeleteRsp
|
||||
|
||||
@Serializable
|
||||
data class DownloadRkeyRsp(
|
||||
@ProtoNumber(1) val rkeys: List<RKeyInfo>
|
||||
@ProtoNumber(1) val rkeys: List<RKeyInfo>?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RKeyInfo(
|
||||
@ProtoNumber(1) val rkey: String,
|
||||
@ProtoNumber(2) val rkeyTtlSec: ULong,
|
||||
@ProtoNumber(1) val rkey: String?,
|
||||
@ProtoNumber(2) val rkeyTtlSec: ULong?,
|
||||
@ProtoNumber(3) val storeId: UInt = 0u,
|
||||
@ProtoNumber(4) val rkeyCreateTime: UInt?,
|
||||
@ProtoNumber(4) val type: UInt?,
|
||||
@ -61,8 +61,8 @@ data class RKeyInfo(
|
||||
|
||||
@Serializable
|
||||
data class DownloadRsp(
|
||||
@ProtoNumber(1) val rkeyParam: String,
|
||||
@ProtoNumber(2) val rkeyTtlSec: ULong,
|
||||
@ProtoNumber(1) val rkeyParam: String?,
|
||||
@ProtoNumber(2) val rkeyTtlSec: ULong?,
|
||||
@ProtoNumber(3) val downloadInfo: DownloadInfo?,
|
||||
@ProtoNumber(4) val rkeyCreateTime: UInt?
|
||||
)
|
||||
@ -80,16 +80,16 @@ data class DownloadInfo(
|
||||
|
||||
@Serializable
|
||||
data class VideoExtInfo(
|
||||
@ProtoNumber(1) val videoCodecFormat: UInt,
|
||||
@ProtoNumber(1) val videoCodecFormat: UInt? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UploadRsp(
|
||||
@ProtoNumber(1) val ukey: String,
|
||||
@ProtoNumber(2) val ukeyTtlSec: ULong,
|
||||
@ProtoNumber(3) val ipv4: List<Ipv4>,
|
||||
@ProtoNumber(4) val ipv6: List<Ipv6>,
|
||||
@ProtoNumber(5) val msgSeq: ULong,
|
||||
@ProtoNumber(1) val ukey: String?,
|
||||
@ProtoNumber(2) val ukeyTtlSec: ULong?,
|
||||
@ProtoNumber(3) val ipv4: List<Ipv4>?,
|
||||
@ProtoNumber(4) val ipv6: List<Ipv6>?,
|
||||
@ProtoNumber(5) val msgSeq: ULong?,
|
||||
@ProtoNumber(6) val msgInfo: MsgInfo? = null,
|
||||
@ProtoNumber(7) val ext: List<RichmediaStorageTransInfo>? = null,
|
||||
@ProtoNumber(8) val compatQMsg: ByteArray? = null,
|
||||
@ -98,11 +98,11 @@ data class UploadRsp(
|
||||
|
||||
@Serializable
|
||||
data class SubFileInfo(
|
||||
@ProtoNumber(1) val subType: UInt,
|
||||
@ProtoNumber(2) val ukey: String,
|
||||
@ProtoNumber(3) val ukeyTTLSec: ULong,
|
||||
@ProtoNumber(4) val ipv4: List<Ipv4>,
|
||||
@ProtoNumber(5) val ipv6: List<Ipv6>,
|
||||
@ProtoNumber(1) val subType: UInt?,
|
||||
@ProtoNumber(2) val ukey: String?,
|
||||
@ProtoNumber(3) val ukeyTTLSec: ULong?,
|
||||
@ProtoNumber(4) val ipv4: List<Ipv4>?,
|
||||
@ProtoNumber(5) val ipv6: List<Ipv6>?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@ -132,8 +132,8 @@ data class Ipv6(
|
||||
|
||||
@Serializable
|
||||
data class RspHead(
|
||||
@ProtoNumber(1) val commonHead: CommonHead,
|
||||
@ProtoNumber(1) val commonHead: CommonHead?,
|
||||
@ProtoNumber(2) val retCode: UInt = 0u,
|
||||
@ProtoNumber(3) val msg: String
|
||||
@ProtoNumber(3) val msg: String?
|
||||
)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
package protobuf.oidb.cmd0x388
|
||||
|
||||
import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY
|
||||
import moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import moe.fuqiuluo.symbols.Protobuf
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
package protobuf.oidb.cmd0x388
|
||||
|
||||
import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY
|
||||
import moe.fuqiuluo.symbols.EMPTY_BYTE_ARRAY
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
|
@ -7,6 +7,8 @@ import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import moe.fuqiuluo.symbols.Protobuf
|
||||
|
||||
const val DEFAULT_DEVICE_INFO = "i=&imsi=&mac=02:00:00:00:00:00&m=Shamrock&o=114514&a=1919810&sd=0&c64=1&sc=1&p=8000*8000&aid=123456789012345678901234567890abcdef&f=Tencent&mm=5610&cf=1726&cc=8&qimei=&qimei36=&sharpP=1&n=nether_world&support_xsj_live=false&client_mod=concise&timezone=America/La_Paz&material_sdk_version=&vh265=&refreshrate=10086&hwlevel=9&suphdr=1&is_teenager_mod=8&liveH265=&bmst=5&AV1=0"
|
||||
|
||||
@Serializable
|
||||
data class QWebReq(
|
||||
@ProtoNumber(1) val seq: Int = 0,
|
||||
|
@ -6,9 +6,13 @@ import com.tencent.mobileqq.app.BusinessObserver;
|
||||
import com.tencent.mobileqq.app.MessageHandler;
|
||||
import com.tencent.qphone.base.remote.ToServiceMsg;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import mqq.app.AppRuntime;
|
||||
|
||||
public abstract class AppInterface extends AppRuntime {
|
||||
private final ConcurrentHashMap<String, BusinessHandler> allHandler = new ConcurrentHashMap<>();
|
||||
|
||||
public String getCurrentNickname() {
|
||||
return "";
|
||||
}
|
||||
|
@ -13,6 +13,10 @@ public abstract class BaseBusinessHandler extends OidbWrapper {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void addBusinessObserver(ToServiceMsg toServiceMsg, BusinessObserver businessObserver, boolean z) {
|
||||
|
||||
}
|
||||
|
||||
public final <T> T decodePacket(byte[] data, String name, T obj) {
|
||||
UniPacket uniPacket = new UniPacket(true);
|
||||
try {
|
||||
@ -24,6 +28,10 @@ public abstract class BaseBusinessHandler extends OidbWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean msgCmdFilter(String str) {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected abstract Set<String> getCommandList();
|
||||
|
||||
protected abstract Set<String> getPushCommandList();
|
||||
|
@ -8,6 +8,8 @@ public abstract class BusinessHandler extends BaseBusinessHandler {
|
||||
public BusinessHandler(AppInterface appInterface) {
|
||||
}
|
||||
|
||||
protected abstract Class<? extends BusinessObserver> observerClass();
|
||||
|
||||
@Override
|
||||
public Set<String> getCommandList() {
|
||||
return null;
|
||||
|
@ -0,0 +1,18 @@
|
||||
package com.tencent.mobileqq.msf.sdk;
|
||||
|
||||
import com.tencent.qphone.base.remote.FromServiceMsg;
|
||||
import com.tencent.qphone.base.remote.ToServiceMsg;
|
||||
|
||||
public class MsfMessagePair {
|
||||
public FromServiceMsg fromServiceMsg;
|
||||
public String sendProcess;
|
||||
public ToServiceMsg toServiceMsg;
|
||||
|
||||
public MsfMessagePair(String str, ToServiceMsg toServiceMsg, FromServiceMsg fromServiceMsg) {
|
||||
|
||||
}
|
||||
|
||||
public MsfMessagePair(ToServiceMsg toServiceMsg, FromServiceMsg fromServiceMsg) {
|
||||
|
||||
}
|
||||
}
|
@ -18,6 +18,11 @@ public class MsgService {
|
||||
public void addMsgListener(IKernelMsgListener listener) {
|
||||
}
|
||||
|
||||
public void removeMsgListener(@NotNull IKernelMsgListener iKernelMsgListener) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
public String getRichMediaFilePathForGuild(@NotNull RichMediaFilePathInfo richMediaFilePathInfo) {
|
||||
return null;
|
||||
}
|
||||
|
@ -66,6 +66,13 @@ public abstract class AppRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
public MobileQQ getApplication() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void startServlet(NewIntent newIntent) {
|
||||
}
|
||||
|
||||
public <T extends IRuntimeService> T getRuntimeService(Class<T> cls, String namespace) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
29
qqinterface/src/main/java/mqq/app/NewIntent.java
Normal file
29
qqinterface/src/main/java/mqq/app/NewIntent.java
Normal file
@ -0,0 +1,29 @@
|
||||
package mqq.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import com.tencent.mobileqq.app.BusinessObserver;
|
||||
|
||||
public class NewIntent extends Intent {
|
||||
public boolean runNow;
|
||||
|
||||
public NewIntent(Context context, Class<? extends Servlet> cls) {
|
||||
super(context, cls);
|
||||
}
|
||||
|
||||
public BusinessObserver getObserver() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isWithouLogin() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setObserver(BusinessObserver businessObserver) {
|
||||
|
||||
}
|
||||
|
||||
public void setWithouLogin(boolean z) {
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ buildscript {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools:r8:8.2.47")
|
||||
classpath("com.android.tools:r8:8.3.37")
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,8 +34,9 @@ rootProject.name = "Shamrock"
|
||||
include(
|
||||
":app",
|
||||
":xposed",
|
||||
":qqinterface"
|
||||
":qqinterface",
|
||||
":protobuf",
|
||||
":processor",
|
||||
":annotations",
|
||||
":kritor"
|
||||
)
|
||||
include(":protobuf")
|
||||
include(":processor")
|
||||
include(":annotations")
|
||||
|
@ -1,3 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
@ -61,6 +63,7 @@ dependencies {
|
||||
compileOnly("de.robv.android.xposed:api:82")
|
||||
compileOnly(project(":qqinterface"))
|
||||
|
||||
implementation(project(":kritor"))
|
||||
implementation(project(":protobuf"))
|
||||
implementation(project(":annotations"))
|
||||
ksp(project(":processor"))
|
||||
@ -70,27 +73,20 @@ dependencies {
|
||||
DEPENDENCY_ANDROIDX.forEach {
|
||||
implementation(it)
|
||||
}
|
||||
implementation(DEPENDENCY_JAVA_WEBSOCKET)
|
||||
implementation(DEPENDENCY_PROTOBUF)
|
||||
implementation(DEPENDENCY_JSON5K)
|
||||
|
||||
implementation(room("runtime"))
|
||||
kapt(room("compiler"))
|
||||
implementation(room("ktx"))
|
||||
|
||||
implementation(kotlinx("io-jvm", "0.1.16"))
|
||||
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
||||
|
||||
implementation(ktor("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", "content-negotiation"))
|
||||
implementation(ktor("client", "cio"))
|
||||
implementation(ktor("client", "okhttp"))
|
||||
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")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
@ -99,4 +95,8 @@ dependencies {
|
||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||
}
|
||||
|
||||
|
||||
tasks.withType<KotlinCompile>().all {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn")
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
// IByteData.aidl
|
||||
package moe.fuqiuluo.shamrock.xposed.ipc.bytedata;
|
||||
|
||||
import moe.fuqiuluo.shamrock.xposed.ipc.bytedata.IByteDataSign;
|
||||
|
||||
interface IByteData {
|
||||
IByteDataSign sign(String uin, String data, in byte[] salt);
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
// IByteDataSign.aidl
|
||||
package moe.fuqiuluo.shamrock.xposed.ipc.bytedata;
|
||||
|
||||
parcelable IByteDataSign;
|
@ -1,4 +0,0 @@
|
||||
// IQSign.aidl
|
||||
package moe.fuqiuluo.shamrock.xposed.ipc.qsign;
|
||||
|
||||
parcelable IQSign;
|
@ -1,14 +0,0 @@
|
||||
// IQSigner.aidl
|
||||
package moe.fuqiuluo.shamrock.xposed.ipc.qsign;
|
||||
|
||||
import moe.fuqiuluo.shamrock.xposed.ipc.qsign.IQSign;
|
||||
|
||||
interface IQSigner {
|
||||
IQSign sign(String cmd, int seq, String uin, in byte[] buffer);
|
||||
|
||||
byte[] energy(String module, in byte[] salt);
|
||||
|
||||
byte[] xwDebugId(String uin, String start, String end);
|
||||
|
||||
List<String> getCmdWhiteList();
|
||||
}
|
43
xposed/src/main/assets/config.properties
Normal file
43
xposed/src/main/assets/config.properties
Normal file
@ -0,0 +1,43 @@
|
||||
# Shamrock Config
|
||||
|
||||
# 资源上传群组
|
||||
resource_group=883536416
|
||||
|
||||
# 强制使用平板模式
|
||||
force_tablet=false
|
||||
|
||||
# 被动(反向)RPC开关
|
||||
passive_rpc=false
|
||||
# 被动(反向)RPC地址
|
||||
rpc_address=
|
||||
# 第一个被动RPC鉴权token
|
||||
rpc_address.ticket=
|
||||
# 如果有多个请使用
|
||||
# 我是第二个地址
|
||||
#rpc_address.1=
|
||||
# 第二个被动RPC鉴权token
|
||||
#rpc_address.1.ticket=
|
||||
|
||||
# 主动(正向)RPC开关
|
||||
active_rpc=false
|
||||
# 主动(正向)RPC端口
|
||||
rpc_port=5700
|
||||
# 主动RPC鉴权token
|
||||
active_ticket=
|
||||
# 多鉴权token支持
|
||||
# 第二个主动RPC鉴权token
|
||||
#active_ticket.1=
|
||||
|
||||
# 自回复开关
|
||||
#alive_reply=false
|
||||
# 自回复消息
|
||||
enable_self_message=false
|
||||
|
||||
# 旧BDH兼容开关
|
||||
enable_old_bdh=true
|
||||
|
||||
# 反JVM调用栈跟踪
|
||||
anti_jvm_trace=true
|
||||
|
||||
# 调试模式
|
||||
#debug=false
|
@ -173,7 +173,7 @@ NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) {
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_moe_fuqiuluo_shamrock_xposed_hooks_AntiDetection_antiNativeDetections(JNIEnv *env,
|
||||
Java_moe_fuqiuluo_shamrock_xposed_actions_AntiDetection_antiNativeDetections(JNIEnv *env,
|
||||
jobject thiz) {
|
||||
if (hook_function == nullptr) return false;
|
||||
hook_function((void*) __system_property_get, (void *)fake_system_property_get, (void **) &backup_system_property_get);
|
||||
|
64
xposed/src/main/java/kritor/auth/AuthInterceptor.kt
Normal file
64
xposed/src/main/java/kritor/auth/AuthInterceptor.kt
Normal file
@ -0,0 +1,64 @@
|
||||
package kritor.auth
|
||||
|
||||
import io.grpc.ForwardingServerCallListener
|
||||
import io.grpc.Metadata
|
||||
import io.grpc.ServerCall
|
||||
import io.grpc.ServerCallHandler
|
||||
import io.grpc.ServerInterceptor
|
||||
import moe.fuqiuluo.shamrock.config.ActiveTicket
|
||||
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||
|
||||
object AuthInterceptor: ServerInterceptor {
|
||||
/**
|
||||
* Intercept [ServerCall] dispatch by the `next` [ServerCallHandler]. General
|
||||
* semantics of [ServerCallHandler.startCall] apply and the returned
|
||||
* [io.grpc.ServerCall.Listener] must not be `null`.
|
||||
*
|
||||
*
|
||||
* If the implementation throws an exception, `call` will be closed with an error.
|
||||
* Implementations must not throw an exception if they started processing that may use `call` on another thread.
|
||||
*
|
||||
* @param call object to receive response messages
|
||||
* @param headers which can contain extra call metadata from [ClientCall.start],
|
||||
* e.g. authentication credentials.
|
||||
* @param next next processor in the interceptor chain
|
||||
* @return listener for processing incoming messages for `call`, never `null`.
|
||||
*/
|
||||
override fun <ReqT : Any?, RespT : Any?> interceptCall(
|
||||
call: ServerCall<ReqT, RespT>,
|
||||
headers: Metadata?,
|
||||
next: ServerCallHandler<ReqT, RespT>
|
||||
): ServerCall.Listener<ReqT> {
|
||||
val methodName = call.methodDescriptor.fullMethodName
|
||||
val ticket = getAllTicket()
|
||||
if (ticket.isNotEmpty() && !methodName.startsWith("Auth")) {
|
||||
val ticketHeader = headers?.get(Metadata.Key.of("ticket", Metadata.ASCII_STRING_MARSHALLER))
|
||||
if (ticketHeader == null || ticketHeader !in ticket) {
|
||||
call.close(io.grpc.Status.UNAUTHENTICATED.withDescription("Invalid ticket"), Metadata())
|
||||
return object: ServerCall.Listener<ReqT>() {}
|
||||
}
|
||||
}
|
||||
return object: ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(next.startCall(call, headers)) {
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllTicket(): List<String> {
|
||||
val result = arrayListOf<String>()
|
||||
val activeTicketName = ActiveTicket.name()
|
||||
var index = 0
|
||||
while (true) {
|
||||
val ticket = ShamrockConfig.getProperty(activeTicketName + if (index == 0) "" else ".$index", null)
|
||||
if (ticket.isNullOrEmpty()) {
|
||||
if (index == 0) {
|
||||
return result
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
result.add(ticket)
|
||||
}
|
||||
index++
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
140
xposed/src/main/java/kritor/client/KritorClient.kt
Normal file
140
xposed/src/main/java/kritor/client/KritorClient.kt
Normal file
@ -0,0 +1,140 @@
|
||||
@file:OptIn(DelicateCoroutinesApi::class)
|
||||
package kritor.client
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import io.grpc.ManagedChannel
|
||||
import io.grpc.ManagedChannelBuilder
|
||||
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 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()
|
||||
}
|
||||
channel = ManagedChannelBuilder
|
||||
.forAddress(host, port)
|
||||
.usePlaintext()
|
||||
.enableRetry() // 允许尝试
|
||||
.executor(Dispatchers.IO.asExecutor()) // 使用协程的调度器
|
||||
.build()
|
||||
}.onFailure {
|
||||
LogCenter.log("KritorClient start failed: ${it.stackTraceToString()}", Level.ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
fun listen(retryCnt: Int = -1) {
|
||||
if (::channelJob.isInitialized && channelJob.isActive) {
|
||||
channelJob.cancel()
|
||||
}
|
||||
channelJob = GlobalScope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val stub = ReverseServiceGrpcKt.ReverseServiceCoroutineStub(channel)
|
||||
registerEvent(EventType.EVENT_TYPE_MESSAGE)
|
||||
registerEvent(EventType.EVENT_TYPE_CORE_EVENT)
|
||||
registerEvent(EventType.EVENT_TYPE_REQUEST)
|
||||
registerEvent(EventType.EVENT_TYPE_NOTICE)
|
||||
stub.reverseStream(channelFlow {
|
||||
senderChannel.collect { send(it) }
|
||||
}).collect {
|
||||
onReceive(it)
|
||||
}
|
||||
}.onFailure {
|
||||
LogCenter.log("KritorClient listen failed, retry after 15s: ${it.stackTraceToString()}", Level.WARN)
|
||||
}
|
||||
delay(15.seconds)
|
||||
LogCenter.log("KritorClient listen retrying, retryCnt = $retryCnt", Level.WARN)
|
||||
if (retryCnt != 0) listen(retryCnt - 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun registerEvent(eventType: EventType) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
EventServiceGrpcKt.EventServiceCoroutineStub(channel).registerPassiveListener(channelFlow {
|
||||
when(eventType) {
|
||||
EventType.EVENT_TYPE_MESSAGE -> GlobalEventTransmitter.onMessageEvent {
|
||||
send(EventStructure.newBuilder().apply {
|
||||
this.type = EventType.EVENT_TYPE_MESSAGE
|
||||
this.message = it.second
|
||||
}.build())
|
||||
}
|
||||
EventType.EVENT_TYPE_CORE_EVENT -> {}
|
||||
EventType.EVENT_TYPE_NOTICE -> GlobalEventTransmitter.onNoticeEvent {
|
||||
send(EventStructure.newBuilder().apply {
|
||||
this.type = EventType.EVENT_TYPE_NOTICE
|
||||
this.notice = it
|
||||
}.build())
|
||||
}
|
||||
EventType.EVENT_TYPE_REQUEST -> GlobalEventTransmitter.onRequestEvent {
|
||||
send(EventStructure.newBuilder().apply {
|
||||
this.type = EventType.EVENT_TYPE_REQUEST
|
||||
this.request = it
|
||||
}.build())
|
||||
}
|
||||
EventType.UNRECOGNIZED -> {}
|
||||
}
|
||||
})
|
||||
}.onFailure {
|
||||
LogCenter.log("KritorClient registerEvent failed: ${it.stackTraceToString()}", Level.ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onReceive(request: Request) = GlobalScope.launch {
|
||||
//LogCenter.log("KritorClient onReceive: $request")
|
||||
runCatching {
|
||||
val rsp = handleGrpc(request.cmd, request.buf.toByteArray())
|
||||
senderChannel.emit(Response.newBuilder()
|
||||
.setCmd(request.cmd)
|
||||
.setCode(Response.ResponseCode.SUCCESS)
|
||||
.setMsg("success")
|
||||
.setSeq(request.seq)
|
||||
.setBuf(ByteString.copyFrom(rsp))
|
||||
.build())
|
||||
}.onFailure {
|
||||
senderChannel.emit(Response.newBuilder()
|
||||
.setCmd(request.cmd)
|
||||
.setCode(Response.ResponseCode.INTERNAL)
|
||||
.setMsg(it.stackTraceToString())
|
||||
.setSeq(request.seq)
|
||||
.setBuf(ByteString.EMPTY)
|
||||
.build())
|
||||
}
|
||||
}
|
||||
|
||||
fun isActive(): Boolean {
|
||||
return !channel.isShutdown
|
||||
}
|
||||
|
||||
fun close() {
|
||||
channel.shutdown()
|
||||
}
|
||||
}
|
6
xposed/src/main/java/kritor/handlers/GrpcHandlers.kt
Normal file
6
xposed/src/main/java/kritor/handlers/GrpcHandlers.kt
Normal file
@ -0,0 +1,6 @@
|
||||
package kritor.handlers
|
||||
|
||||
internal object GrpcHandlers {
|
||||
|
||||
|
||||
}
|
44
xposed/src/main/java/kritor/server/KritorServer.kt
Normal file
44
xposed/src/main/java/kritor/server/KritorServer.kt
Normal file
@ -0,0 +1,44 @@
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
package kritor.server
|
||||
|
||||
import io.grpc.Grpc
|
||||
import io.grpc.InsecureServerCredentials
|
||||
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 kotlin.coroutines.CoroutineContext
|
||||
|
||||
class KritorServer(
|
||||
private val port: Int
|
||||
): CoroutineScope {
|
||||
private val server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create())
|
||||
.executor(Dispatchers.IO.asExecutor())
|
||||
.intercept(AuthInterceptor)
|
||||
.addService(AuthenticationService)
|
||||
.addService(CoreService)
|
||||
.addService(FriendService)
|
||||
.addService(GroupService)
|
||||
.addService(GroupFileService)
|
||||
.addService(MessageService)
|
||||
.addService(EventService)
|
||||
.addService(WebService)
|
||||
.addService(DeveloperService)
|
||||
.addService(QsignService)
|
||||
.build()!!
|
||||
|
||||
fun start(block: Boolean = false) {
|
||||
LogCenter.log("KritorServer started at port $port.")
|
||||
server.start()
|
||||
|
||||
if (block) {
|
||||
server.awaitTermination()
|
||||
}
|
||||
}
|
||||
|
||||
override val coroutineContext: CoroutineContext =
|
||||
Dispatchers.IO.limitedParallelism(12)
|
||||
}
|
60
xposed/src/main/java/kritor/service/AuthenticationService.kt
Normal file
60
xposed/src/main/java/kritor/service/AuthenticationService.kt
Normal file
@ -0,0 +1,60 @@
|
||||
package kritor.service
|
||||
|
||||
import io.grpc.Status
|
||||
import io.grpc.StatusRuntimeException
|
||||
import io.kritor.authentication.*
|
||||
import io.kritor.authentication.AuthenticateResponse.AuthenticateResponseCode
|
||||
import kritor.auth.AuthInterceptor
|
||||
import moe.fuqiuluo.shamrock.config.ActiveTicket
|
||||
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||
import qq.service.QQInterfaces
|
||||
|
||||
internal object AuthenticationService: AuthenticationServiceGrpcKt.AuthenticationServiceCoroutineImplBase() {
|
||||
@Grpc("AuthenticationService", "Authenticate")
|
||||
override suspend fun authenticate(request: AuthenticateRequest): AuthenticateResponse {
|
||||
if (QQInterfaces.app.account != request.account) {
|
||||
return AuthenticateResponse.newBuilder().apply {
|
||||
code = AuthenticateResponseCode.NO_ACCOUNT
|
||||
msg = "No such account"
|
||||
}.build()
|
||||
}
|
||||
|
||||
val activeTicketName = ActiveTicket.name()
|
||||
var index = 0
|
||||
while (true) {
|
||||
val ticket = ShamrockConfig.getProperty(activeTicketName + if (index == 0) "" else ".$index", null)
|
||||
if (ticket.isNullOrEmpty()) {
|
||||
if (index == 0) {
|
||||
return AuthenticateResponse.newBuilder().apply {
|
||||
code = AuthenticateResponseCode.OK
|
||||
msg = "OK"
|
||||
}.build()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else if (ticket == request.ticket) {
|
||||
return AuthenticateResponse.newBuilder().apply {
|
||||
code = AuthenticateResponseCode.OK
|
||||
msg = "OK"
|
||||
}.build()
|
||||
}
|
||||
index++
|
||||
}
|
||||
|
||||
return AuthenticateResponse.newBuilder().apply {
|
||||
code = AuthenticateResponseCode.NO_TICKET
|
||||
msg = "Invalid ticket"
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("AuthenticationService", "GetAuthenticationState")
|
||||
override suspend fun getAuthenticationState(request: GetAuthenticationStateRequest): GetAuthenticationStateResponse {
|
||||
if (request.account != QQInterfaces.app.account) {
|
||||
throw StatusRuntimeException(Status.CANCELLED.withDescription("No such account"))
|
||||
}
|
||||
|
||||
return GetAuthenticationStateResponse.newBuilder().apply {
|
||||
isRequired = AuthInterceptor.getAllTicket().isNotEmpty()
|
||||
}.build()
|
||||
}
|
||||
}
|
101
xposed/src/main/java/kritor/service/CoreService.kt
Normal file
101
xposed/src/main/java/kritor/service/CoreService.kt
Normal file
@ -0,0 +1,101 @@
|
||||
package kritor.service
|
||||
|
||||
import android.util.Base64
|
||||
import com.tencent.mobileqq.app.QQAppInterface
|
||||
import io.grpc.Status
|
||||
import io.grpc.StatusRuntimeException
|
||||
import io.kritor.core.*
|
||||
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
|
||||
import moe.fuqiuluo.shamrock.utils.DownloadUtils
|
||||
import moe.fuqiuluo.shamrock.utils.FileUtils
|
||||
import moe.fuqiuluo.shamrock.utils.MD5
|
||||
import mqq.app.MobileQQ
|
||||
import qq.service.QQInterfaces.Companion.app
|
||||
import qq.service.contact.ContactHelper
|
||||
import java.io.File
|
||||
|
||||
internal object CoreService : CoreServiceGrpcKt.CoreServiceCoroutineImplBase() {
|
||||
@Grpc("CoreService", "GetVersion")
|
||||
override suspend fun getVersion(request: GetVersionRequest): GetVersionResponse {
|
||||
return GetVersionResponse.newBuilder().apply {
|
||||
this.version = ShamrockVersion
|
||||
this.appName = "Shamrock"
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("CoreService", "GetCurrentAccount")
|
||||
override suspend fun getCurrentAccount(request: GetCurrentAccountRequest): GetCurrentAccountResponse {
|
||||
return GetCurrentAccountResponse.newBuilder().apply {
|
||||
this.accountName = if (app is QQAppInterface) app.currentNickname else "unknown"
|
||||
this.accountUid = app.currentUid ?: ""
|
||||
this.accountUin = (app.currentUin ?: "0").toLong()
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("CoreService", "DownloadFile")
|
||||
override suspend fun downloadFile(request: DownloadFileRequest): DownloadFileResponse {
|
||||
val headerMap = mutableMapOf(
|
||||
"User-Agent" to "Shamrock"
|
||||
)
|
||||
if (request.hasHeaders()) {
|
||||
request.headers.split("[\r\n]").forEach {
|
||||
val pair = it.split("=")
|
||||
if (pair.size >= 2) {
|
||||
val (k, v) = pair
|
||||
headerMap[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tmp = FileUtils.getTmpFile("cache")
|
||||
if (request.hasBase64()) {
|
||||
val bytes = Base64.decode(request.base64, Base64.DEFAULT)
|
||||
tmp.writeBytes(bytes)
|
||||
} else if (request.hasUrl()) {
|
||||
if (!DownloadUtils.download(
|
||||
urlAdr = request.url,
|
||||
dest = tmp,
|
||||
headers = headerMap,
|
||||
threadCount = if (request.hasThreadCnt()) request.threadCnt else 3
|
||||
)
|
||||
) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("download failed"))
|
||||
}
|
||||
}
|
||||
tmp = if (!request.hasFileName()) FileUtils.renameByMd5(tmp)
|
||||
else tmp.parentFile!!.resolve(request.fileName).also {
|
||||
tmp.renameTo(it)
|
||||
}
|
||||
if (request.hasRootPath()) {
|
||||
tmp = File(request.rootPath).resolve(tmp.name).also {
|
||||
tmp.renameTo(it)
|
||||
}
|
||||
}
|
||||
|
||||
return DownloadFileResponse.newBuilder().apply {
|
||||
this.fileMd5 = MD5.genFileMd5Hex(tmp.absolutePath)
|
||||
this.fileAbsolutePath = tmp.absolutePath
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("CoreService", "SwitchAccount")
|
||||
override suspend fun switchAccount(request: SwitchAccountRequest): SwitchAccountResponse {
|
||||
val uin = when (request.accountCase!!) {
|
||||
SwitchAccountRequest.AccountCase.ACCOUNT_UID -> ContactHelper.getUinByUidAsync(request.accountUid)
|
||||
SwitchAccountRequest.AccountCase.ACCOUNT_UIN -> request.accountUin.toString()
|
||||
SwitchAccountRequest.AccountCase.ACCOUNT_NOT_SET -> throw StatusRuntimeException(
|
||||
Status.INVALID_ARGUMENT.withDescription(
|
||||
"account not found"
|
||||
)
|
||||
)
|
||||
}
|
||||
val account = MobileQQ.getMobileQQ().allAccounts.firstOrNull { it.uin == uin }
|
||||
?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("account not found"))
|
||||
runCatching {
|
||||
app.switchAccount(account, null)
|
||||
}.onFailure {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withCause(it).withDescription("failed to switch account"))
|
||||
}
|
||||
return SwitchAccountResponse.newBuilder().build()
|
||||
}
|
||||
}
|
42
xposed/src/main/java/kritor/service/DeveloperService.kt
Normal file
42
xposed/src/main/java/kritor/service/DeveloperService.kt
Normal file
@ -0,0 +1,42 @@
|
||||
package kritor.service
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import io.kritor.developer.*
|
||||
import moe.fuqiuluo.shamrock.utils.FileUtils
|
||||
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
|
||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||
import qq.service.QQInterfaces
|
||||
|
||||
internal object DeveloperService: DeveloperServiceGrpcKt.DeveloperServiceCoroutineImplBase() {
|
||||
@Grpc("DeveloperService", "ClearCache")
|
||||
override suspend fun clearCache(request: ClearCacheRequest): ClearCacheResponse {
|
||||
FileUtils.clearCache()
|
||||
MMKVFetcher.mmkvWithId("audio2silk")
|
||||
.clear()
|
||||
return ClearCacheResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("DeveloperService", "GetDeviceBattery")
|
||||
override suspend fun getDeviceBattery(request: GetDeviceBatteryRequest): GetDeviceBatteryResponse {
|
||||
return GetDeviceBatteryResponse.newBuilder().apply {
|
||||
PlatformUtils.getDeviceBattery().let {
|
||||
this.battery = it.battery
|
||||
this.scale = it.scale
|
||||
this.status = it.status
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("DeveloperService", "SendPacket")
|
||||
override suspend fun sendPacket(request: SendPacketRequest): SendPacketResponse {
|
||||
return SendPacketResponse.newBuilder().apply {
|
||||
val fromServiceMsg = QQInterfaces.sendBufferAW(request.command, request.isProtobuf, request.requestBuffer.toByteArray())
|
||||
if (fromServiceMsg?.wupBuffer == null) {
|
||||
this.isSuccess = false
|
||||
} else {
|
||||
this.isSuccess = true
|
||||
this.responseBuffer = ByteString.copyFrom(fromServiceMsg.wupBuffer)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
43
xposed/src/main/java/kritor/service/EventService.kt
Normal file
43
xposed/src/main/java/kritor/service/EventService.kt
Normal file
@ -0,0 +1,43 @@
|
||||
package kritor.service
|
||||
|
||||
import io.grpc.Status
|
||||
import io.grpc.StatusRuntimeException
|
||||
import io.kritor.event.EventServiceGrpcKt
|
||||
import io.kritor.event.EventStructure
|
||||
import io.kritor.event.EventType
|
||||
import io.kritor.event.RequestPushEvent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter
|
||||
|
||||
internal object EventService : EventServiceGrpcKt.EventServiceCoroutineImplBase() {
|
||||
override fun registerActiveListener(request: RequestPushEvent): Flow<EventStructure> {
|
||||
return channelFlow {
|
||||
when (request.type!!) {
|
||||
EventType.EVENT_TYPE_CORE_EVENT -> {}
|
||||
EventType.EVENT_TYPE_MESSAGE -> GlobalEventTransmitter.onMessageEvent {
|
||||
send(EventStructure.newBuilder().apply {
|
||||
this.type = EventType.EVENT_TYPE_MESSAGE
|
||||
this.message = it.second
|
||||
}.build())
|
||||
}
|
||||
|
||||
EventType.EVENT_TYPE_NOTICE -> GlobalEventTransmitter.onRequestEvent {
|
||||
send(EventStructure.newBuilder().apply {
|
||||
this.type = EventType.EVENT_TYPE_NOTICE
|
||||
this.request = it
|
||||
}.build())
|
||||
}
|
||||
|
||||
EventType.EVENT_TYPE_REQUEST -> GlobalEventTransmitter.onNoticeEvent {
|
||||
send(EventStructure.newBuilder().apply {
|
||||
this.type = EventType.EVENT_TYPE_NOTICE
|
||||
this.notice = it
|
||||
}.build())
|
||||
}
|
||||
|
||||
EventType.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
244
xposed/src/main/java/kritor/service/FriendService.kt
Normal file
244
xposed/src/main/java/kritor/service/FriendService.kt
Normal file
@ -0,0 +1,244 @@
|
||||
package kritor.service
|
||||
|
||||
import android.os.Bundle
|
||||
import com.tencent.mobileqq.profilecard.api.IProfileCardBlacklistApi
|
||||
import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst
|
||||
import com.tencent.mobileqq.profilecard.api.IProfileProtocolService
|
||||
import com.tencent.mobileqq.qroute.QRoute
|
||||
import io.grpc.Status
|
||||
import io.grpc.StatusRuntimeException
|
||||
import io.kritor.friend.*
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import qq.service.QQInterfaces
|
||||
import qq.service.contact.ContactHelper
|
||||
import qq.service.friend.FriendHelper
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
internal object FriendService : FriendServiceGrpcKt.FriendServiceCoroutineImplBase() {
|
||||
@Grpc("FriendService", "GetFriendList")
|
||||
override suspend fun getFriendList(request: GetFriendListRequest): GetFriendListResponse {
|
||||
val friendList = FriendHelper.getFriendList(if (request.hasRefresh()) request.refresh else false).onFailure {
|
||||
throw StatusRuntimeException(
|
||||
Status.INTERNAL
|
||||
.withDescription(it.stackTraceToString())
|
||||
)
|
||||
}.getOrThrow()
|
||||
|
||||
return GetFriendListResponse.newBuilder().apply {
|
||||
friendList.forEach {
|
||||
this.addFriendsInfo(FriendInfo.newBuilder().apply {
|
||||
uin = it.uin.toLong()
|
||||
uid = ContactHelper.getUidByUinAsync(uin)
|
||||
qid = ""
|
||||
nick = it.name ?: ""
|
||||
remark = it.remark ?: ""
|
||||
age = it.age
|
||||
level = 0
|
||||
gender = it.gender.toInt()
|
||||
groupId = it.groupid
|
||||
|
||||
ext = ExtInfo.newBuilder().build()
|
||||
})
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("FriendService", "GetFriendProfileCard")
|
||||
override suspend fun getFriendProfileCard(request: GetFriendProfileCardRequest): GetFriendProfileCardResponse {
|
||||
return GetFriendProfileCardResponse.newBuilder().apply {
|
||||
request.targetUinsList.forEach {
|
||||
ContactHelper.getProfileCard(it).getOrThrow().let { info ->
|
||||
addFriendsProfileCard(ProfileCard.newBuilder().apply {
|
||||
this.uin = info.uin.toLong()
|
||||
this.uid = ContactHelper.getUidByUinAsync(info.uin.toLong())
|
||||
this.nick = info.strNick
|
||||
this.remark = info.strReMark
|
||||
this.level = info.iQQLevel
|
||||
this.birthday = info.lBirthday
|
||||
this.loginDay = info.lLoginDays.toInt()
|
||||
this.voteCnt = info.lVoteCount.toInt()
|
||||
this.qid = info.qid ?: ""
|
||||
this.isSchoolVerified = info.schoolVerifiedFlag
|
||||
|
||||
this.ext = ExtInfo.newBuilder().apply {
|
||||
this.bigVip = info.bBigClubVipOpen == 1.toByte()
|
||||
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
|
||||
this.qqVip = info.bQQVipOpen == 1.toByte()
|
||||
this.superVip = info.bSuperQQOpen == 1.toByte()
|
||||
this.voted = info.bVoted == 1.toByte()
|
||||
}.build()
|
||||
}.build())
|
||||
}
|
||||
}
|
||||
request.targetUidsList.forEach {
|
||||
ContactHelper.getProfileCard(ContactHelper.getUinByUidAsync(it).toLong()).getOrThrow().let { info ->
|
||||
addFriendsProfileCard(ProfileCard.newBuilder().apply {
|
||||
this.uin = info.uin.toLong()
|
||||
this.uid = it
|
||||
this.nick = info.strNick
|
||||
this.remark = info.strReMark
|
||||
this.level = info.iQQLevel
|
||||
this.birthday = info.lBirthday
|
||||
this.loginDay = info.lLoginDays.toInt()
|
||||
this.voteCnt = info.lVoteCount.toInt()
|
||||
this.qid = info.qid ?: ""
|
||||
this.isSchoolVerified = info.schoolVerifiedFlag
|
||||
|
||||
this.ext = ExtInfo.newBuilder().apply {
|
||||
this.bigVip = info.bBigClubVipOpen == 1.toByte()
|
||||
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
|
||||
this.qqVip = info.bQQVipOpen == 1.toByte()
|
||||
this.superVip = info.bSuperQQOpen == 1.toByte()
|
||||
this.voted = info.bVoted == 1.toByte()
|
||||
}.build()
|
||||
}.build())
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("FriendService", "GetStrangerProfileCard")
|
||||
override suspend fun getStrangerProfileCard(request: GetStrangerProfileCardRequest): GetStrangerProfileCardResponse {
|
||||
return GetStrangerProfileCardResponse.newBuilder().apply {
|
||||
request.targetUinsList.forEach {
|
||||
ContactHelper.refreshAndGetProfileCard(it).getOrThrow().let { info ->
|
||||
addStrangersProfileCard(ProfileCard.newBuilder().apply {
|
||||
this.uin = info.uin.toLong()
|
||||
this.uid = ContactHelper.getUidByUinAsync(info.uin.toLong())
|
||||
this.nick = info.strNick
|
||||
this.level = info.iQQLevel
|
||||
this.birthday = info.lBirthday
|
||||
this.loginDay = info.lLoginDays.toInt()
|
||||
this.voteCnt = info.lVoteCount.toInt()
|
||||
this.qid = info.qid ?: ""
|
||||
this.isSchoolVerified = info.schoolVerifiedFlag
|
||||
|
||||
this.ext = ExtInfo.newBuilder().apply {
|
||||
this.bigVip = info.bBigClubVipOpen == 1.toByte()
|
||||
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
|
||||
this.qqVip = info.bQQVipOpen == 1.toByte()
|
||||
this.superVip = info.bSuperQQOpen == 1.toByte()
|
||||
this.voted = info.bVoted == 1.toByte()
|
||||
}.build()
|
||||
}.build())
|
||||
}
|
||||
}
|
||||
request.targetUidsList.forEach {
|
||||
ContactHelper.refreshAndGetProfileCard(ContactHelper.getUinByUidAsync(it).toLong()).getOrThrow()
|
||||
.let { info ->
|
||||
addStrangersProfileCard(ProfileCard.newBuilder().apply {
|
||||
this.uin = info.uin.toLong()
|
||||
this.uid = it
|
||||
this.nick = info.strNick
|
||||
this.level = info.iQQLevel
|
||||
this.birthday = info.lBirthday
|
||||
this.loginDay = info.lLoginDays.toInt()
|
||||
this.voteCnt = info.lVoteCount.toInt()
|
||||
this.qid = info.qid ?: ""
|
||||
this.isSchoolVerified = info.schoolVerifiedFlag
|
||||
|
||||
this.ext = ExtInfo.newBuilder().apply {
|
||||
this.bigVip = info.bBigClubVipOpen == 1.toByte()
|
||||
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
|
||||
this.qqVip = info.bQQVipOpen == 1.toByte()
|
||||
this.superVip = info.bSuperQQOpen == 1.toByte()
|
||||
this.voted = info.bVoted == 1.toByte()
|
||||
}.build()
|
||||
}.build())
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("FriendService", "SetProfileCard")
|
||||
override suspend fun setProfileCard(request: SetProfileCardRequest): SetProfileCardResponse {
|
||||
val bundle = Bundle()
|
||||
val service = QQInterfaces.app
|
||||
.getRuntimeService(IProfileProtocolService::class.java, "all")
|
||||
if (request.hasNickName()) {
|
||||
bundle.putString(IProfileProtocolConst.KEY_NICK, request.nickName)
|
||||
}
|
||||
if (request.hasCompany()) {
|
||||
bundle.putString(IProfileProtocolConst.KEY_COMPANY, request.company)
|
||||
}
|
||||
if (request.hasEmail()) {
|
||||
bundle.putString(IProfileProtocolConst.KEY_EMAIL, request.email)
|
||||
}
|
||||
if (request.hasCollege()) {
|
||||
bundle.putString(IProfileProtocolConst.KEY_COLLEGE, request.college)
|
||||
}
|
||||
if (request.hasPersonalNote()) {
|
||||
bundle.putString(IProfileProtocolConst.KEY_PERSONAL_NOTE, request.personalNote)
|
||||
}
|
||||
|
||||
if (request.hasBirthday()) {
|
||||
bundle.putInt(IProfileProtocolConst.KEY_BIRTHDAY, request.birthday)
|
||||
}
|
||||
if (request.hasAge()) {
|
||||
bundle.putInt(IProfileProtocolConst.KEY_AGE, request.age)
|
||||
}
|
||||
|
||||
service.setProfileDetail(bundle)
|
||||
return super.setProfileCard(request)
|
||||
}
|
||||
|
||||
@Grpc("FriendService", "IsBlackListUser")
|
||||
override suspend fun isBlackListUser(request: IsBlackListUserRequest): IsBlackListUserResponse {
|
||||
val uin = when (request.targetCase!!) {
|
||||
IsBlackListUserRequest.TargetCase.TARGET_UIN -> request.targetUin.toString()
|
||||
IsBlackListUserRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid)
|
||||
IsBlackListUserRequest.TargetCase.TARGET_NOT_SET -> throw StatusRuntimeException(
|
||||
Status.INVALID_ARGUMENT
|
||||
.withDescription("account not set")
|
||||
)
|
||||
}
|
||||
val blacklistApi = QRoute.api(IProfileCardBlacklistApi::class.java)
|
||||
val isBlack = withTimeoutOrNull(5000) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
blacklistApi.isBlackOrBlackedUin(uin) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
}
|
||||
} ?: false
|
||||
return IsBlackListUserResponse.newBuilder().setIsBlackListUser(isBlack).build()
|
||||
}
|
||||
|
||||
@Grpc("FriendService", "VoteUser")
|
||||
override suspend fun voteUser(request: VoteUserRequest): VoteUserResponse {
|
||||
ContactHelper.voteUser(
|
||||
when (request.targetCase!!) {
|
||||
VoteUserRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||
VoteUserRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
|
||||
VoteUserRequest.TargetCase.TARGET_NOT_SET -> throw StatusRuntimeException(
|
||||
Status.INVALID_ARGUMENT
|
||||
.withDescription("account not set")
|
||||
)
|
||||
}, request.voteCount
|
||||
).onFailure {
|
||||
throw StatusRuntimeException(
|
||||
Status.INTERNAL
|
||||
.withDescription(it.stackTraceToString())
|
||||
)
|
||||
}
|
||||
return VoteUserResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("FriendService", "GetUidByUin")
|
||||
override suspend fun getUidByUin(request: GetUidRequest): GetUidResponse {
|
||||
return GetUidResponse.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()
|
||||
}
|
||||
}
|
136
xposed/src/main/java/kritor/service/GroupFileService.kt
Normal file
136
xposed/src/main/java/kritor/service/GroupFileService.kt
Normal file
@ -0,0 +1,136 @@
|
||||
package kritor.service
|
||||
|
||||
import io.grpc.Status
|
||||
import io.grpc.StatusRuntimeException
|
||||
import io.kritor.file.*
|
||||
import moe.fuqiuluo.shamrock.tools.slice
|
||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
||||
import protobuf.auto.toByteArray
|
||||
import protobuf.oidb.cmd0x6d7.CreateFolderReq
|
||||
import protobuf.oidb.cmd0x6d7.DeleteFolderReq
|
||||
import protobuf.oidb.cmd0x6d7.Oidb0x6d7ReqBody
|
||||
import protobuf.oidb.cmd0x6d7.Oidb0x6d7RespBody
|
||||
import protobuf.oidb.cmd0x6d7.RenameFolderReq
|
||||
import qq.service.QQInterfaces
|
||||
import qq.service.file.GroupFileHelper
|
||||
import qq.service.file.GroupFileHelper.getGroupFileSystemInfo
|
||||
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
|
||||
import tencent.im.oidb.oidb_sso
|
||||
|
||||
internal object GroupFileService : GroupFileServiceGrpcKt.GroupFileServiceCoroutineImplBase() {
|
||||
@Grpc("GroupFileService", "CreateFolder")
|
||||
override suspend fun createFolder(request: CreateFolderRequest): CreateFolderResponse {
|
||||
val data = Oidb0x6d7ReqBody(
|
||||
createFolder = CreateFolderReq(
|
||||
groupCode = request.groupId.toULong(),
|
||||
appId = 3u,
|
||||
parentFolderId = "/",
|
||||
folderName = request.name
|
||||
)
|
||||
).toByteArray()
|
||||
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_0", 1751, 0, data)
|
||||
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||
if (fromServiceMsg.wupBuffer == null) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||
}
|
||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||
val rsp = oidbPkg.bytes_bodybuffer.get()
|
||||
.toByteArray()
|
||||
.decodeProtobuf<Oidb0x6d7RespBody>()
|
||||
if (rsp.createFolder?.retCode != 0) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to create folder: ${rsp.createFolder?.retCode}"))
|
||||
}
|
||||
return CreateFolderResponse.newBuilder().apply {
|
||||
this.id = rsp.createFolder?.folderInfo?.folderId ?: ""
|
||||
this.usedSpace = 0
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("GroupFileService", "DeleteFolder")
|
||||
override suspend fun deleteFolder(request: DeleteFolderRequest): DeleteFolderResponse {
|
||||
val fromServiceMsg = QQInterfaces.sendOidbAW(
|
||||
"OidbSvc.0x6d7_1", 1751, 1, Oidb0x6d7ReqBody(
|
||||
deleteFolder = DeleteFolderReq(
|
||||
groupCode = request.groupId.toULong(),
|
||||
appId = 3u,
|
||||
folderId = request.folderId
|
||||
)
|
||||
).toByteArray()
|
||||
) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||
if (fromServiceMsg.wupBuffer == null) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||
}
|
||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
|
||||
if (rsp.deleteFolder?.retCode != 0) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to delete folder: ${rsp.deleteFolder?.retCode}"))
|
||||
}
|
||||
return DeleteFolderResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupFileService", "DeleteFile")
|
||||
override suspend fun deleteFile(request: DeleteFileRequest): DeleteFileResponse {
|
||||
val oidb0x6d6ReqBody = oidb_0x6d6.ReqBody().apply {
|
||||
delete_file_req.set(oidb_0x6d6.DeleteFileReqBody().apply {
|
||||
uint64_group_code.set(request.groupId)
|
||||
uint32_app_id.set(3)
|
||||
uint32_bus_id.set(request.busId)
|
||||
str_parent_folder_id.set("/")
|
||||
str_file_id.set(request.fileId)
|
||||
})
|
||||
}
|
||||
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d6_3", 1750, 3, oidb0x6d6ReqBody.toByteArray())
|
||||
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||
if (fromServiceMsg.wupBuffer == null) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||
}
|
||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||
val rsp = oidb_0x6d6.RspBody().apply {
|
||||
mergeFrom(oidbPkg.bytes_bodybuffer.get().toByteArray())
|
||||
}
|
||||
if (rsp.delete_file_rsp.int32_ret_code.get() != 0) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to delete file: ${rsp.delete_file_rsp.int32_ret_code.get()}"))
|
||||
}
|
||||
return DeleteFileResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupFileService", "RenameFolder")
|
||||
override suspend fun renameFolder(request: RenameFolderRequest): RenameFolderResponse {
|
||||
val fromServiceMsg = QQInterfaces.sendOidbAW(
|
||||
"OidbSvc.0x6d7_3", 1751, 3, Oidb0x6d7ReqBody(
|
||||
renameFolder = RenameFolderReq(
|
||||
groupCode = request.groupId.toULong(),
|
||||
appId = 3u,
|
||||
folderId = request.folderId,
|
||||
folderName = request.name
|
||||
)
|
||||
).toByteArray()
|
||||
) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||
if (fromServiceMsg.wupBuffer == null) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||
}
|
||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
|
||||
if (rsp.renameFolder?.retCode != 0) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to rename folder: ${rsp.renameFolder?.retCode}"))
|
||||
}
|
||||
return RenameFolderResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupFileService", "GetFileSystemInfo")
|
||||
override suspend fun getFileSystemInfo(request: GetFileSystemInfoRequest): GetFileSystemInfoResponse {
|
||||
return getGroupFileSystemInfo(request.groupId)
|
||||
}
|
||||
|
||||
@Grpc("GroupFileService", "GetFileList")
|
||||
override suspend fun getFileList(request: GetFileListRequest): GetFileListResponse {
|
||||
return if (request.hasFolderId())
|
||||
GroupFileHelper.getGroupFiles(request.groupId, request.folderId)
|
||||
else
|
||||
GroupFileHelper.getGroupFiles(request.groupId)
|
||||
}
|
||||
}
|
401
xposed/src/main/java/kritor/service/GroupService.kt
Normal file
401
xposed/src/main/java/kritor/service/GroupService.kt
Normal file
@ -0,0 +1,401 @@
|
||||
package kritor.service
|
||||
|
||||
import io.grpc.Status
|
||||
import io.grpc.StatusRuntimeException
|
||||
import io.kritor.group.*
|
||||
import moe.fuqiuluo.shamrock.helper.TroopHonorHelper.decodeHonor
|
||||
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
||||
import qq.service.contact.ContactHelper
|
||||
import qq.service.group.GroupHelper
|
||||
|
||||
internal object GroupService : GroupServiceGrpcKt.GroupServiceCoroutineImplBase() {
|
||||
@Grpc("GroupService", "BanMember")
|
||||
override suspend fun banMember(request: BanMemberRequest): BanMemberResponse {
|
||||
if (!GroupHelper.isAdmin(request.groupId.toString())) {
|
||||
throw StatusRuntimeException(
|
||||
Status.PERMISSION_DENIED
|
||||
.withDescription("You are not admin of this group")
|
||||
)
|
||||
}
|
||||
|
||||
GroupHelper.banMember(
|
||||
request.groupId, when (request.targetCase!!) {
|
||||
BanMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||
BanMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
|
||||
else -> throw StatusRuntimeException(
|
||||
Status.INVALID_ARGUMENT
|
||||
.withDescription("target not set")
|
||||
)
|
||||
}, request.duration
|
||||
)
|
||||
|
||||
return BanMemberResponse.newBuilder().apply {
|
||||
groupId = request.groupId
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "PokeMember")
|
||||
override suspend fun pokeMember(request: PokeMemberRequest): PokeMemberResponse {
|
||||
GroupHelper.pokeMember(
|
||||
request.groupId, when (request.targetCase!!) {
|
||||
PokeMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||
PokeMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
|
||||
else -> throw StatusRuntimeException(
|
||||
Status.INVALID_ARGUMENT
|
||||
.withDescription("target not set")
|
||||
)
|
||||
}
|
||||
)
|
||||
return PokeMemberResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "KickMember")
|
||||
override suspend fun kickMember(request: KickMemberRequest): KickMemberResponse {
|
||||
if (!GroupHelper.isAdmin(request.groupId.toString())) {
|
||||
throw StatusRuntimeException(
|
||||
Status.PERMISSION_DENIED
|
||||
.withDescription("You are not admin of this group")
|
||||
)
|
||||
}
|
||||
GroupHelper.kickMember(
|
||||
request.groupId,
|
||||
request.rejectAddRequest,
|
||||
if (request.hasKickReason()) request.kickReason else "",
|
||||
when (request.targetCase!!) {
|
||||
KickMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||
KickMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
|
||||
else -> throw StatusRuntimeException(
|
||||
Status.INVALID_ARGUMENT
|
||||
.withDescription("target not set")
|
||||
)
|
||||
}
|
||||
)
|
||||
return KickMemberResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "LeaveGroup")
|
||||
override suspend fun leaveGroup(request: LeaveGroupRequest): LeaveGroupResponse {
|
||||
GroupHelper.resignTroop(request.groupId.toString())
|
||||
return LeaveGroupResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "ModifyMemberCard")
|
||||
override suspend fun modifyMemberCard(request: ModifyMemberCardRequest): ModifyMemberCardResponse {
|
||||
if (!GroupHelper.isAdmin(request.groupId.toString())) {
|
||||
throw StatusRuntimeException(
|
||||
Status.PERMISSION_DENIED
|
||||
.withDescription("You are not admin of this group")
|
||||
)
|
||||
}
|
||||
GroupHelper.modifyGroupMemberCard(
|
||||
request.groupId, when (request.targetCase!!) {
|
||||
ModifyMemberCardRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||
ModifyMemberCardRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid)
|
||||
.toLong()
|
||||
|
||||
else -> throw StatusRuntimeException(
|
||||
Status.INVALID_ARGUMENT
|
||||
.withDescription("target not set")
|
||||
)
|
||||
}, request.card
|
||||
)
|
||||
return ModifyMemberCardResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "ModifyGroupName")
|
||||
override suspend fun modifyGroupName(request: ModifyGroupNameRequest): ModifyGroupNameResponse {
|
||||
if (!GroupHelper.isAdmin(request.groupId.toString())) {
|
||||
throw StatusRuntimeException(
|
||||
Status.PERMISSION_DENIED
|
||||
.withDescription("You are not admin of this group")
|
||||
)
|
||||
}
|
||||
|
||||
GroupHelper.modifyTroopName(request.groupId.toString(), request.groupName)
|
||||
|
||||
return ModifyGroupNameResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "ModifyGroupRemark")
|
||||
override suspend fun modifyGroupRemark(request: ModifyGroupRemarkRequest): ModifyGroupRemarkResponse {
|
||||
GroupHelper.modifyGroupRemark(request.groupId, request.remark)
|
||||
|
||||
return ModifyGroupRemarkResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "SetGroupAdmin")
|
||||
override suspend fun setGroupAdmin(request: SetGroupAdminRequest): SetGroupAdminResponse {
|
||||
if (!GroupHelper.isOwner(request.groupId.toString())) {
|
||||
throw StatusRuntimeException(
|
||||
Status.PERMISSION_DENIED
|
||||
.withDescription("You are not admin of this group")
|
||||
)
|
||||
}
|
||||
|
||||
GroupHelper.setGroupAdmin(
|
||||
request.groupId, when (request.targetCase!!) {
|
||||
SetGroupAdminRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||
SetGroupAdminRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
|
||||
else -> throw StatusRuntimeException(
|
||||
Status.INVALID_ARGUMENT
|
||||
.withDescription("target not set")
|
||||
)
|
||||
}, request.isAdmin
|
||||
)
|
||||
|
||||
return SetGroupAdminResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "SetGroupUniqueTitle")
|
||||
override suspend fun setGroupUniqueTitle(request: SetGroupUniqueTitleRequest): SetGroupUniqueTitleResponse {
|
||||
if (!GroupHelper.isAdmin(request.groupId.toString())) {
|
||||
throw StatusRuntimeException(
|
||||
Status.PERMISSION_DENIED
|
||||
.withDescription("You are not admin of this group")
|
||||
)
|
||||
}
|
||||
|
||||
GroupHelper.setGroupUniqueTitle(
|
||||
request.groupId.toString(), when (request.targetCase!!) {
|
||||
SetGroupUniqueTitleRequest.TargetCase.TARGET_UIN -> request.targetUin
|
||||
SetGroupUniqueTitleRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid)
|
||||
.toLong()
|
||||
|
||||
else -> throw StatusRuntimeException(
|
||||
Status.INVALID_ARGUMENT
|
||||
.withDescription("target not set")
|
||||
)
|
||||
}.toString(), request.uniqueTitle
|
||||
)
|
||||
|
||||
return SetGroupUniqueTitleResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "SetGroupWholeBan")
|
||||
override suspend fun setGroupWholeBan(request: SetGroupWholeBanRequest): SetGroupWholeBanResponse {
|
||||
if (!GroupHelper.isAdmin(request.groupId.toString())) {
|
||||
throw StatusRuntimeException(
|
||||
Status.PERMISSION_DENIED
|
||||
.withDescription("You are not admin of this group")
|
||||
)
|
||||
}
|
||||
|
||||
GroupHelper.setGroupWholeBan(request.groupId, request.isBan)
|
||||
return SetGroupWholeBanResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "GetGroupInfo")
|
||||
override suspend fun getGroupInfo(request: GetGroupInfoRequest): GetGroupInfoResponse {
|
||||
val groupInfo = GroupHelper.getGroupInfo(request.groupId.toString(), true).onFailure {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group info").withCause(it))
|
||||
}.getOrThrow()
|
||||
return GetGroupInfoResponse.newBuilder().apply {
|
||||
this.groupInfo = GroupInfo.newBuilder().apply {
|
||||
groupId = groupInfo.troopcode.toLong()
|
||||
groupName =
|
||||
groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName }
|
||||
?: ""
|
||||
groupRemark = groupInfo.troopRemark ?: ""
|
||||
owner = groupInfo.troopowneruin?.toLong() ?: 0
|
||||
addAllAdmins(GroupHelper.getAdminList(groupId))
|
||||
maxMemberCount = groupInfo.wMemberMax
|
||||
memberCount = groupInfo.wMemberNum
|
||||
groupUin = groupInfo.troopuin?.toLong() ?: 0
|
||||
}.build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "GetGroupList")
|
||||
override suspend fun getGroupList(request: GetGroupListRequest): GetGroupListResponse {
|
||||
val groupList = GroupHelper.getGroupList(if (request.hasRefresh()) request.refresh else false).onFailure {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group list").withCause(it))
|
||||
}.getOrThrow()
|
||||
return GetGroupListResponse.newBuilder().apply {
|
||||
groupList.forEach { groupInfo ->
|
||||
this.addGroupsInfo(GroupInfo.newBuilder().apply {
|
||||
groupId = groupInfo.troopcode.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()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "GetGroupMemberInfo")
|
||||
override suspend fun getGroupMemberInfo(request: GetGroupMemberInfoRequest): GetGroupMemberInfoResponse {
|
||||
val memberInfo = GroupHelper.getTroopMemberInfoByUin(
|
||||
request.groupId.toString(), when (request.targetCase!!) {
|
||||
GetGroupMemberInfoRequest.TargetCase.TARGET_UID -> request.targetUin
|
||||
GetGroupMemberInfoRequest.TargetCase.TARGET_UIN -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
|
||||
else -> throw StatusRuntimeException(
|
||||
Status.INVALID_ARGUMENT
|
||||
.withDescription("target not set")
|
||||
)
|
||||
}.toString()
|
||||
).onFailure {
|
||||
throw StatusRuntimeException(
|
||||
Status.INTERNAL.withDescription("unable to get group member info").withCause(it)
|
||||
)
|
||||
}.getOrThrow()
|
||||
return GetGroupMemberInfoResponse.newBuilder().apply {
|
||||
groupMemberInfo = GroupMemberInfo.newBuilder().apply {
|
||||
uid =
|
||||
if (request.targetCase == GetGroupMemberInfoRequest.TargetCase.TARGET_UID) request.targetUid else ContactHelper.getUidByUinAsync(
|
||||
request.targetUin
|
||||
)
|
||||
uin = memberInfo.memberuin?.toLong() ?: 0
|
||||
nick = memberInfo.troopnick
|
||||
.ifNullOrEmpty { memberInfo.hwName }
|
||||
.ifNullOrEmpty { memberInfo.troopColorNick }
|
||||
.ifNullOrEmpty { memberInfo.friendnick } ?: ""
|
||||
age = memberInfo.age.toInt()
|
||||
uniqueTitle = memberInfo.mUniqueTitle ?: ""
|
||||
uniqueTitleExpireTime = memberInfo.mUniqueTitleExpire
|
||||
card = memberInfo.troopnick.ifNullOrEmpty { memberInfo.friendnick } ?: ""
|
||||
joinTime = memberInfo.join_time
|
||||
lastActiveTime = memberInfo.last_active_time
|
||||
level = memberInfo.level
|
||||
shutUpTimestamp = memberInfo.gagTimeStamp
|
||||
|
||||
distance = memberInfo.distance
|
||||
addAllHonors((memberInfo.honorList ?: "")
|
||||
.split("|")
|
||||
.filter { it.isNotBlank() }
|
||||
.map { it.toInt() })
|
||||
unfriendly = false
|
||||
cardChangeable = GroupHelper.isAdmin(request.groupId.toString())
|
||||
}.build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "GetGroupMemberList")
|
||||
override suspend fun getGroupMemberList(request: GetGroupMemberListRequest): GetGroupMemberListResponse {
|
||||
val memberList = GroupHelper.getGroupMemberList(
|
||||
request.groupId.toString(),
|
||||
if (request.hasRefresh()) request.refresh else false
|
||||
).onFailure {
|
||||
throw StatusRuntimeException(
|
||||
Status.INTERNAL.withDescription("unable to get group member list").withCause(it)
|
||||
)
|
||||
}.getOrThrow()
|
||||
return GetGroupMemberListResponse.newBuilder().apply {
|
||||
memberList.forEach { memberInfo ->
|
||||
this.addGroupMembersInfo(GroupMemberInfo.newBuilder().apply {
|
||||
uid = ContactHelper.getUidByUinAsync(memberInfo.memberuin?.toLong() ?: 0)
|
||||
uin = memberInfo.memberuin?.toLong() ?: 0
|
||||
nick = memberInfo.troopnick
|
||||
.ifNullOrEmpty { memberInfo.hwName }
|
||||
.ifNullOrEmpty { memberInfo.troopColorNick }
|
||||
.ifNullOrEmpty { memberInfo.friendnick } ?: ""
|
||||
age = memberInfo.age.toInt()
|
||||
uniqueTitle = memberInfo.mUniqueTitle ?: ""
|
||||
uniqueTitleExpireTime = memberInfo.mUniqueTitleExpire
|
||||
card = memberInfo.troopnick.ifNullOrEmpty { memberInfo.friendnick } ?: ""
|
||||
joinTime = memberInfo.join_time
|
||||
lastActiveTime = memberInfo.last_active_time
|
||||
level = memberInfo.level
|
||||
shutUpTimestamp = memberInfo.gagTimeStamp
|
||||
|
||||
distance = memberInfo.distance
|
||||
addAllHonors((memberInfo.honorList ?: "")
|
||||
.split("|")
|
||||
.filter { it.isNotBlank() }
|
||||
.map { it.toInt() })
|
||||
unfriendly = false
|
||||
cardChangeable = GroupHelper.isAdmin(request.groupId.toString())
|
||||
})
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "GetProhibitedUserList")
|
||||
override suspend fun getProhibitedUserList(request: GetProhibitedUserListRequest): GetProhibitedUserListResponse {
|
||||
val prohibitedList = GroupHelper.getProhibitedMemberList(request.groupId).onFailure {
|
||||
throw StatusRuntimeException(
|
||||
Status.INTERNAL.withDescription("unable to get prohibited user list").withCause(it)
|
||||
)
|
||||
}.getOrThrow()
|
||||
return GetProhibitedUserListResponse.newBuilder().apply {
|
||||
prohibitedList.forEach {
|
||||
this.addProhibitedUsersInfo(ProhibitedUserInfo.newBuilder().apply {
|
||||
uid = ContactHelper.getUidByUinAsync(it.memberUin)
|
||||
uin = it.memberUin
|
||||
prohibitedTime = it.shutuptimestap
|
||||
})
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "GetRemainCountAtAll")
|
||||
override suspend fun getRemainCountAtAll(request: GetRemainCountAtAllRequest): GetRemainCountAtAllResponse {
|
||||
val remainAtAllRsp = GroupHelper.getGroupRemainAtAllRemain(request.groupId).onFailure {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get remain count").withCause(it))
|
||||
}.getOrThrow()
|
||||
return GetRemainCountAtAllResponse.newBuilder().apply {
|
||||
accessAtAll = remainAtAllRsp.bool_can_at_all.get()
|
||||
remainCountForGroup = remainAtAllRsp.uint32_remain_at_all_count_for_group.get()
|
||||
remainCountForSelf = remainAtAllRsp.uint32_remain_at_all_count_for_uin.get()
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "GetNotJoinedGroupInfo")
|
||||
override suspend fun getNotJoinedGroupInfo(request: GetNotJoinedGroupInfoRequest): GetNotJoinedGroupInfoResponse {
|
||||
val groupInfo = GroupHelper.getNotJoinedGroupInfo(request.groupId).onFailure {
|
||||
throw StatusRuntimeException(
|
||||
Status.INTERNAL.withDescription("unable to get not joined group info").withCause(it)
|
||||
)
|
||||
}.getOrThrow()
|
||||
return GetNotJoinedGroupInfoResponse.newBuilder().apply {
|
||||
this.groupInfo = NotJoinedGroupInfo.newBuilder().apply {
|
||||
groupId = groupInfo.groupId
|
||||
groupName = groupInfo.groupName
|
||||
owner = groupInfo.owner
|
||||
maxMemberCount = groupInfo.maxMember
|
||||
memberCount = groupInfo.memberCount
|
||||
groupDesc = groupInfo.groupDesc
|
||||
createTime = groupInfo.createTime.toInt()
|
||||
groupFlag = groupInfo.groupFlag
|
||||
groupFlagExt = groupInfo.groupFlagExt
|
||||
}.build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("GroupService", "GetGroupHonor")
|
||||
override suspend fun getGroupHonor(request: GetGroupHonorRequest): GetGroupHonorResponse {
|
||||
return GetGroupHonorResponse.newBuilder().apply {
|
||||
GroupHelper.getGroupMemberList(request.groupId.toString(), true).onFailure {
|
||||
throw StatusRuntimeException(
|
||||
Status.INTERNAL.withDescription("unable to get group member list").withCause(it)
|
||||
)
|
||||
}.onSuccess { memberList ->
|
||||
memberList.forEach { member ->
|
||||
(member.honorList ?: "").split("|")
|
||||
.filter { it.isNotBlank() }
|
||||
.map { it.toInt() }.forEach {
|
||||
val honor = decodeHonor(member.memberuin.toLong(), it, member.mHonorRichFlag)
|
||||
if (honor != null) {
|
||||
addGroupHonorsInfo(GroupHonorInfo.newBuilder().apply {
|
||||
uid = ContactHelper.getUidByUinAsync(member.memberuin.toLong())
|
||||
uin = member.memberuin.toLong()
|
||||
nick = member.troopnick
|
||||
.ifNullOrEmpty { member.hwName }
|
||||
.ifNullOrEmpty { member.troopColorNick }
|
||||
.ifNullOrEmpty { member.friendnick } ?: ""
|
||||
honorName = honor.honorName
|
||||
avatar = honor.honorIconUrl
|
||||
id = honor.honorId
|
||||
description = honor.honorUrl
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
470
xposed/src/main/java/kritor/service/MessageService.kt
Normal file
470
xposed/src/main/java/kritor/service/MessageService.kt
Normal file
@ -0,0 +1,470 @@
|
||||
package kritor.service
|
||||
|
||||
import com.tencent.mobileqq.qroute.QRoute
|
||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
||||
import com.tencent.qqnt.msg.api.IMsgService
|
||||
import io.grpc.Status
|
||||
import io.grpc.StatusRuntimeException
|
||||
import io.kritor.common.*
|
||||
import io.kritor.message.*
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import protobuf.auto.toByteArray
|
||||
import protobuf.message.*
|
||||
import protobuf.message.element.GeneralFlags
|
||||
import protobuf.message.routing.C2C
|
||||
import protobuf.message.routing.Grp
|
||||
import qq.service.QQInterfaces
|
||||
import qq.service.contact.longPeer
|
||||
import qq.service.internals.NTServiceFetcher
|
||||
import qq.service.msg.*
|
||||
import qq.service.msg.ForwardMessageHelper
|
||||
import qq.service.msg.MessageHelper
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextUInt
|
||||
|
||||
internal object MessageService : MessageServiceGrpcKt.MessageServiceCoroutineImplBase() {
|
||||
@Grpc("MessageService", "SendMessage")
|
||||
override suspend fun sendMessage(request: SendMessageRequest): SendMessageResponse {
|
||||
val contact = request.contact.let {
|
||||
MessageHelper.generateContact(
|
||||
when (it.scene!!) {
|
||||
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
|
||||
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
|
||||
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
|
||||
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
|
||||
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
|
||||
}, it.peer, it.subPeer
|
||||
)
|
||||
}
|
||||
|
||||
val uniseq = MessageHelper.generateMsgId(contact.chatType)
|
||||
return SendMessageResponse.newBuilder().apply {
|
||||
this.messageId = MessageHelper.sendMessage(
|
||||
contact,
|
||||
NtMsgConvertor.convertToNtMsgs(contact, uniseq, request.elementsList),
|
||||
request.retryCount,
|
||||
uniseq
|
||||
).onFailure {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
|
||||
}.getOrThrow().toString()
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "SendMessageByResId")
|
||||
override suspend fun sendMessageByResId(request: SendMessageByResIdRequest): SendMessageByResIdResponse {
|
||||
val contact = request.contact
|
||||
val req = PbSendMsgReq(
|
||||
routingHead = when (request.contact.scene) {
|
||||
Scene.GROUP -> RoutingHead(grp = Grp(contact.longPeer().toUInt()))
|
||||
Scene.FRIEND -> RoutingHead(c2c = C2C(contact.longPeer().toUInt()))
|
||||
else -> RoutingHead(grp = Grp(contact.longPeer().toUInt()))
|
||||
},
|
||||
contentHead = ContentHead(1, 0, 0, 0),
|
||||
msgBody = MsgBody(
|
||||
richText = RichText(
|
||||
elements = arrayListOf(
|
||||
Elem(
|
||||
generalFlags = GeneralFlags(
|
||||
longTextFlag = 1u,
|
||||
longTextResid = request.resId
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
msgSeq = Random.nextUInt(),
|
||||
msgRand = Random.nextUInt(),
|
||||
msgVia = 0u
|
||||
)
|
||||
QQInterfaces.sendBuffer("MessageSvc.PbSendMsg", true, req.toByteArray())
|
||||
return SendMessageByResIdResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "SetMessageReaded")
|
||||
override suspend fun setMessageReaded(request: SetMessageReadRequest): SetMessageReadResponse {
|
||||
val contact = request.contact
|
||||
val kernelService = NTServiceFetcher.kernelService
|
||||
val sessionService = kernelService.wrapperSession
|
||||
val service = sessionService.msgService
|
||||
val chatType = when (contact.scene!!) {
|
||||
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
|
||||
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
|
||||
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
|
||||
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
|
||||
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
|
||||
}
|
||||
service.clearMsgRecords(Contact(chatType, contact.peer, contact.subPeer), null)
|
||||
return SetMessageReadResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "RecallMessage")
|
||||
override suspend fun recallMessage(request: RecallMessageRequest): RecallMessageResponse {
|
||||
val contact = request.contact.let {
|
||||
MessageHelper.generateContact(
|
||||
when (it.scene!!) {
|
||||
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
|
||||
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
|
||||
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
|
||||
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
|
||||
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
|
||||
}, it.peer, it.subPeer
|
||||
)
|
||||
}
|
||||
val kernelService = NTServiceFetcher.kernelService
|
||||
val sessionService = kernelService.wrapperSession
|
||||
val service = sessionService.msgService
|
||||
service.recallMsg(contact, arrayListOf(request.messageId.toLong())) { code, msg ->
|
||||
if (code != 0) {
|
||||
LogCenter.log("消息撤回失败: $code:$msg", Level.WARN)
|
||||
}
|
||||
}
|
||||
|
||||
return RecallMessageResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "GetMessage")
|
||||
override suspend fun getMessage(request: GetMessageRequest): GetMessageResponse {
|
||||
val contact = request.contact.let {
|
||||
MessageHelper.generateContact(
|
||||
when (it.scene!!) {
|
||||
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
|
||||
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
|
||||
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
|
||||
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
|
||||
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
|
||||
}, it.peer, it.subPeer
|
||||
)
|
||||
}
|
||||
val msg: MsgRecord = withTimeoutOrNull(5000) {
|
||||
val service = QRoute.api(IMsgService::class.java)
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
service.getMsgsByMsgId(contact, arrayListOf(request.messageId.toLong())) { code, _, msgRecords ->
|
||||
if (code == 0 && msgRecords.isNotEmpty()) {
|
||||
continuation.resume(msgRecords.first())
|
||||
} else {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
continuation.invokeOnCancellation {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
|
||||
|
||||
return GetMessageResponse.newBuilder().apply {
|
||||
this.message = PushMessageBody.newBuilder().apply {
|
||||
this.messageId = msg.msgId.toString()
|
||||
this.contact = request.contact
|
||||
this.sender = Sender.newBuilder().apply {
|
||||
this.uid = msg.senderUid ?: ""
|
||||
this.uin = msg.senderUin
|
||||
this.nick = msg.sendNickName ?: ""
|
||||
}.build()
|
||||
this.messageSeq = msg.msgSeq
|
||||
this.addAllElements(msg.elements.toKritorReqMessages(contact))
|
||||
}.build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "GetMessageBySeq")
|
||||
override suspend fun getMessageBySeq(request: GetMessageBySeqRequest): GetMessageBySeqResponse {
|
||||
val contact = request.contact.let {
|
||||
MessageHelper.generateContact(
|
||||
when (it.scene!!) {
|
||||
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
|
||||
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
|
||||
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
|
||||
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
|
||||
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
|
||||
}, it.peer, it.subPeer
|
||||
)
|
||||
}
|
||||
val msg: MsgRecord = withTimeoutOrNull(5000) {
|
||||
val service = QRoute.api(IMsgService::class.java)
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
service.getMsgsBySeqAndCount(contact, request.messageSeq, 1, true) { code, _, msgRecords ->
|
||||
if (code == 0 && msgRecords.isNotEmpty()) {
|
||||
continuation.resume(msgRecords.first())
|
||||
} else {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
continuation.invokeOnCancellation {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
|
||||
|
||||
return GetMessageBySeqResponse.newBuilder().apply {
|
||||
this.message = PushMessageBody.newBuilder().apply {
|
||||
this.messageId = msg.msgId.toString()
|
||||
this.contact = request.contact
|
||||
this.sender = Sender.newBuilder().apply {
|
||||
this.uin = msg.senderUin
|
||||
this.nick = msg.sendNickName ?: ""
|
||||
this.uid = msg.senderUid ?: ""
|
||||
}.build()
|
||||
this.messageSeq = msg.msgSeq
|
||||
this.addAllElements(msg.elements.toKritorReqMessages(contact))
|
||||
}.build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "GetHistoryMessage")
|
||||
override suspend fun getHistoryMessage(request: GetHistoryMessageRequest): GetHistoryMessageResponse {
|
||||
val contact = request.contact.let {
|
||||
MessageHelper.generateContact(
|
||||
when (it.scene!!) {
|
||||
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
|
||||
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
|
||||
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
|
||||
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
|
||||
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
|
||||
}, it.peer, it.subPeer
|
||||
)
|
||||
}
|
||||
val msgs: List<MsgRecord> = withTimeoutOrNull(5000) {
|
||||
val service = QRoute.api(IMsgService::class.java)
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
service.getMsgs(contact, request.startMessageId.toLong(), request.count, true) { code, _, msgRecords ->
|
||||
if (code == 0 && msgRecords.isNotEmpty()) {
|
||||
continuation.resume(msgRecords)
|
||||
} else {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
continuation.invokeOnCancellation {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Messages not found"))
|
||||
|
||||
return GetHistoryMessageResponse.newBuilder().apply {
|
||||
msgs.forEach {
|
||||
addMessages(PushMessageBody.newBuilder().apply {
|
||||
this.messageId = it.msgId.toString()
|
||||
this.contact = request.contact
|
||||
this.sender = Sender.newBuilder().apply {
|
||||
this.uin = it.senderUin
|
||||
this.nick = it.sendNickName ?: ""
|
||||
this.uid = it.senderUid ?: ""
|
||||
}.build()
|
||||
this.messageSeq = it.msgSeq
|
||||
this.addAllElements(it.elements.toKritorReqMessages(contact))
|
||||
})
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "UploadForwardMessage")
|
||||
override suspend fun uploadForwardMessage(request: UploadForwardMessageRequest): UploadForwardMessageResponse {
|
||||
val contact = request.contact.let {
|
||||
MessageHelper.generateContact(
|
||||
when (it.scene!!) {
|
||||
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
|
||||
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
|
||||
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
|
||||
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
|
||||
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
|
||||
}, it.peer, it.subPeer
|
||||
)
|
||||
}
|
||||
|
||||
val forwardMessage = ForwardMessageHelper.uploadMultiMsg(
|
||||
contact,
|
||||
request.messagesList
|
||||
).onFailure {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
|
||||
}.getOrThrow()
|
||||
|
||||
return UploadForwardMessageResponse.newBuilder().apply {
|
||||
this.resId = forwardMessage.resId
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "DownloadForwardMessage")
|
||||
override suspend fun downloadForwardMessage(request: DownloadForwardMessageRequest): DownloadForwardMessageResponse {
|
||||
return DownloadForwardMessageResponse.newBuilder().apply {
|
||||
this.addAllMessages(
|
||||
MessageHelper.getForwardMsg(request.resId).onFailure {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
|
||||
}.getOrThrow().map { detail ->
|
||||
PushMessageBody.newBuilder().apply {
|
||||
this.time = detail.time
|
||||
this.messageId = detail.qqMsgId.toString()
|
||||
this.messageSeq = detail.msgSeq
|
||||
this.contact = io.kritor.common.Contact.newBuilder().apply {
|
||||
this.scene = when (detail.msgType) {
|
||||
MsgConstant.KCHATTYPEC2C -> Scene.FRIEND
|
||||
MsgConstant.KCHATTYPEGROUP -> Scene.GROUP
|
||||
MsgConstant.KCHATTYPEGUILD -> Scene.GUILD
|
||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> Scene.STRANGER_FROM_GROUP
|
||||
MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN -> Scene.NEARBY
|
||||
else -> Scene.STRANGER
|
||||
}
|
||||
this.peer = detail.peerId.toString()
|
||||
}.build()
|
||||
this.sender = Sender.newBuilder().apply {
|
||||
this.uin = detail.sender.userId
|
||||
this.nick = detail.sender.nickName
|
||||
this.uid = detail.sender.uid
|
||||
}.build()
|
||||
detail.message?.elements?.toKritorResponseMessages(
|
||||
com.tencent.qqnt.kernel.nativeinterface.Contact(
|
||||
detail.msgType,
|
||||
detail.peerId.toString(),
|
||||
null
|
||||
)
|
||||
)?.let {
|
||||
this.addAllElements(it)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "DeleteEssenceMessage")
|
||||
override suspend fun deleteEssenceMessage(request: DeleteEssenceMessageRequest): DeleteEssenceMessageResponse {
|
||||
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString())
|
||||
val msg: MsgRecord = withTimeoutOrNull(5000) {
|
||||
val service = QRoute.api(IMsgService::class.java)
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
service.getMsgsByMsgId(contact, arrayListOf(request.messageId.toLong())) { code, _, msgRecords ->
|
||||
if (code == 0 && msgRecords.isNotEmpty()) {
|
||||
continuation.resume(msgRecords.first())
|
||||
} else {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
continuation.invokeOnCancellation {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
|
||||
if (MessageHelper.deleteEssenceMessage(request.groupId, msg.msgSeq, msg.msgRandom) == null)
|
||||
throw StatusRuntimeException(Status.NOT_FOUND.withDescription("delete essence message failed"))
|
||||
return DeleteEssenceMessageResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "GetEssenceMessageList")
|
||||
override suspend fun getEssenceMessageList(request: GetEssenceMessageListRequest): GetEssenceMessageListResponse {
|
||||
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString())
|
||||
return GetEssenceMessageListResponse.newBuilder().apply {
|
||||
MessageHelper.getEssenceMessageList(request.groupId, request.page, request.pageSize).onFailure {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
|
||||
}.getOrThrow().forEach {
|
||||
addMessages(EssenceMessageBody.newBuilder().apply {
|
||||
withTimeoutOrNull(5000) {
|
||||
val service = QRoute.api(IMsgService::class.java)
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
service.getMsgsBySeqAndCount(contact, it.messageSeq, 1, true) { code, _, msgRecords ->
|
||||
if (code == 0 && msgRecords.isNotEmpty()) {
|
||||
continuation.resume(msgRecords.first())
|
||||
} else {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
continuation.invokeOnCancellation {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
}?.let {
|
||||
this.messageId = it.msgId.toString()
|
||||
}
|
||||
this.messageSeq = it.messageSeq
|
||||
this.messageTime = it.senderTime.toInt()
|
||||
this.senderNick = it.senderNick
|
||||
this.senderUin = it.senderId
|
||||
this.operationTime = it.operatorTime.toInt()
|
||||
this.operatorNick = it.operatorNick
|
||||
this.operatorUin = it.operatorId
|
||||
this.jsonElements = it.messageContent.toString()
|
||||
})
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "SetEssenceMessage")
|
||||
override suspend fun setEssenceMessage(request: SetEssenceMessageRequest): SetEssenceMessageResponse {
|
||||
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString())
|
||||
val msg: MsgRecord = withTimeoutOrNull(5000) {
|
||||
val service = QRoute.api(IMsgService::class.java)
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
service.getMsgsByMsgId(contact, arrayListOf(request.messageId.toLong())) { code, _, msgRecords ->
|
||||
if (code == 0 && msgRecords.isNotEmpty()) {
|
||||
continuation.resume(msgRecords.first())
|
||||
} else {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
continuation.invokeOnCancellation {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
|
||||
if (MessageHelper.setEssenceMessage(request.groupId, msg.msgSeq, msg.msgRandom) == null) {
|
||||
throw StatusRuntimeException(Status.NOT_FOUND.withDescription("set essence message failed"))
|
||||
}
|
||||
return SetEssenceMessageResponse.newBuilder().build()
|
||||
}
|
||||
|
||||
@Grpc("MessageService", "ReactMessageWithEmoji")
|
||||
override suspend fun reactMessageWithEmoji(request: ReactMessageWithEmojiRequest): ReactMessageWithEmojiResponse {
|
||||
val contact = request.contact.let {
|
||||
MessageHelper.generateContact(
|
||||
when (it.scene!!) {
|
||||
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
|
||||
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
|
||||
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
|
||||
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
|
||||
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
||||
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
|
||||
}, it.peer, it.subPeer
|
||||
)
|
||||
}
|
||||
val msg: MsgRecord = withTimeoutOrNull(5000) {
|
||||
val service = QRoute.api(IMsgService::class.java)
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
service.getMsgsByMsgId(contact, arrayListOf(request.messageId.toLong())) { code, _, msgRecords ->
|
||||
if (code == 0 && msgRecords.isNotEmpty()) {
|
||||
continuation.resume(msgRecords.first())
|
||||
} else {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
continuation.invokeOnCancellation {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
|
||||
MessageHelper.setGroupMessageCommentFace(
|
||||
request.contact.longPeer(),
|
||||
msg.msgSeq.toULong(),
|
||||
request.faceId.toString(),
|
||||
request.isComment
|
||||
)
|
||||
return ReactMessageWithEmojiResponse.newBuilder().build()
|
||||
}
|
||||
}
|
26
xposed/src/main/java/kritor/service/QsignService.kt
Normal file
26
xposed/src/main/java/kritor/service/QsignService.kt
Normal file
@ -0,0 +1,26 @@
|
||||
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()
|
||||
}
|
||||
}
|
58
xposed/src/main/java/kritor/service/WebService.kt
Normal file
58
xposed/src/main/java/kritor/service/WebService.kt
Normal file
@ -0,0 +1,58 @@
|
||||
package kritor.service
|
||||
|
||||
import io.grpc.Status
|
||||
import io.grpc.StatusRuntimeException
|
||||
import io.kritor.web.*
|
||||
import qq.service.ticket.TicketHelper
|
||||
|
||||
internal object WebService: WebServiceGrpcKt.WebServiceCoroutineImplBase() {
|
||||
@Grpc("WebService", "GetCookies")
|
||||
override suspend fun getCookies(request: GetCookiesRequest): GetCookiesResponse {
|
||||
return GetCookiesResponse.newBuilder().apply {
|
||||
if (request.domain.isNullOrEmpty()) {
|
||||
this.cookie = TicketHelper.getCookie()
|
||||
} else {
|
||||
this.cookie = TicketHelper.getCookie(request.domain)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("WebService", "GetCredentials")
|
||||
override suspend fun getCredentials(request: GetCredentialsRequest): GetCredentialsResponse {
|
||||
return GetCredentialsResponse.newBuilder().apply {
|
||||
if (request.domain.isNullOrEmpty()) {
|
||||
val uin = TicketHelper.getUin()
|
||||
val skey = TicketHelper.getRealSkey(uin)
|
||||
val pskey = TicketHelper.getPSKey(uin)
|
||||
this.cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey;"
|
||||
this.bkn = TicketHelper.getCSRF(pskey)
|
||||
} else {
|
||||
val uin = TicketHelper.getUin()
|
||||
val skey = TicketHelper.getRealSkey(uin)
|
||||
val pskey = TicketHelper.getPSKey(uin, request.domain) ?: ""
|
||||
val pt4token = TicketHelper.getPt4Token(uin, request.domain) ?: ""
|
||||
this.cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey; pt4_token=$pt4token;"
|
||||
this.bkn = TicketHelper.getCSRF(pskey)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("WebService", "GetCSRFToken")
|
||||
override suspend fun getCSRFToken(request: GetCSRFTokenRequest): GetCSRFTokenResponse {
|
||||
return GetCSRFTokenResponse.newBuilder().apply {
|
||||
if (request.domain.isNullOrEmpty()) {
|
||||
this.bkn = TicketHelper.getCSRF()
|
||||
} else {
|
||||
this.bkn = TicketHelper.getCSRF(TicketHelper.getUin(), request.domain)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@Grpc("WebService", "GetHttpCookies")
|
||||
override suspend fun getHttpCookies(request: GetHttpCookiesRequest): GetHttpCookiesResponse {
|
||||
return GetHttpCookiesResponse.newBuilder().apply {
|
||||
this.cookie = TicketHelper.getHttpCookies(request.appid, request.daid, request.jumpUrl)
|
||||
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get http cookies"))
|
||||
}.build()
|
||||
}
|
||||
}
|
@ -1,201 +0,0 @@
|
||||
@file:OptIn(DelicateCoroutinesApi::class)
|
||||
|
||||
package moe.fuqiuluo.qqinterface.servlet
|
||||
|
||||
import android.os.Bundle
|
||||
import com.tencent.mobileqq.app.QQAppInterface
|
||||
import com.tencent.mobileqq.msf.core.MsfCore
|
||||
import com.tencent.mobileqq.pb.ByteStringMicro
|
||||
import com.tencent.qphone.base.remote.ToServiceMsg
|
||||
import io.ktor.utils.io.core.BytePacketBuilder
|
||||
import io.ktor.utils.io.core.readBytes
|
||||
import io.ktor.utils.io.core.writeFully
|
||||
import io.ktor.utils.io.core.writeInt
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.encodeToByteArray
|
||||
|
||||
import moe.fuqiuluo.shamrock.tools.slice
|
||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
|
||||
import moe.fuqiuluo.shamrock.xposed.helper.internal.DynamicReceiver
|
||||
import moe.fuqiuluo.shamrock.xposed.helper.internal.IPCRequest
|
||||
import protobuf.oidb.TrpcOidb
|
||||
import mqq.app.MobileQQ
|
||||
import protobuf.auto.toByteArray
|
||||
import tencent.im.oidb.oidb_sso
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
internal abstract class BaseSvc {
|
||||
companion object Default: CoroutineScope {
|
||||
val currentUin: String
|
||||
get() = app.currentAccountUin
|
||||
|
||||
val app: QQAppInterface
|
||||
get() = AppRuntimeFetcher.appRuntime as QQAppInterface
|
||||
|
||||
fun createToServiceMsg(cmd: String): ToServiceMsg {
|
||||
return ToServiceMsg("mobileqq.service", app.currentAccountUin, cmd)
|
||||
}
|
||||
|
||||
suspend fun sendOidbAW(cmd: String, cmdId: Int, serviceId: Int, data: ByteArray, trpc: Boolean = false, timeout: Long = 5000L): ByteArray? {
|
||||
val seq = MsfCore.getNextSeq()
|
||||
val buffer = withTimeoutOrNull(timeout) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
launch(Dispatchers.Default) {
|
||||
DynamicReceiver.register(IPCRequest(cmd, seq) {
|
||||
val buffer = it.getByteArrayExtra("buffer")!!
|
||||
continuation.resume(buffer)
|
||||
})
|
||||
}
|
||||
if (trpc) sendTrpcOidb(cmd, cmdId, serviceId, data, seq)
|
||||
else sendOidb(cmd, cmdId, serviceId, data, seq)
|
||||
}
|
||||
}.also {
|
||||
if (it == null)
|
||||
DynamicReceiver.unregister(seq)
|
||||
}?.copyOf()
|
||||
try {
|
||||
if (buffer != null && buffer.size >= 5 && buffer[4] == 120.toByte()) {
|
||||
val builder = BytePacketBuilder()
|
||||
val deBuffer = DeflateTools.uncompress(buffer.slice(4))
|
||||
builder.writeInt(deBuffer.size)
|
||||
builder.writeFully(deBuffer)
|
||||
return builder.build().readBytes()
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
return buffer
|
||||
}
|
||||
|
||||
suspend fun sendBufferAW(cmd: String, isPb: Boolean, data: ByteArray, timeout: Long = 5000L): ByteArray? {
|
||||
val seq = MsfCore.getNextSeq()
|
||||
val buffer = withTimeoutOrNull<ByteArray?>(timeout) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
launch(Dispatchers.Default) {
|
||||
DynamicReceiver.register(IPCRequest(cmd, seq) {
|
||||
val buffer = it.getByteArrayExtra("buffer")!!
|
||||
continuation.resume(buffer)
|
||||
})
|
||||
sendBuffer(cmd, isPb, data, seq)
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
if (it == null)
|
||||
DynamicReceiver.unregister(seq)
|
||||
}?.copyOf()
|
||||
try {
|
||||
if (buffer != null && buffer.size >= 5 && buffer[4] == 120.toByte()) {
|
||||
val builder = BytePacketBuilder()
|
||||
val deBuffer = DeflateTools.uncompress(buffer.slice(4))
|
||||
builder.writeInt(deBuffer.size)
|
||||
builder.writeFully(deBuffer)
|
||||
return builder.build().readBytes()
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
return buffer
|
||||
}
|
||||
|
||||
fun sendOidb(cmd: String, cmdId: Int, serviceId: Int, buffer: ByteArray, seq: Int = -1, trpc: Boolean = false) {
|
||||
if (trpc) {
|
||||
sendTrpcOidb(cmd, cmdId, serviceId, buffer, seq)
|
||||
return
|
||||
}
|
||||
val to = createToServiceMsg(cmd)
|
||||
val oidb = oidb_sso.OIDBSSOPkg()
|
||||
oidb.uint32_command.set(cmdId)
|
||||
oidb.uint32_service_type.set(serviceId)
|
||||
oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(buffer))
|
||||
oidb.str_client_version.set(PlatformUtils.getClientVersion(MobileQQ.getContext()))
|
||||
to.putWupBuffer(oidb.toByteArray())
|
||||
to.addAttribute("req_pb_protocol_flag", true)
|
||||
if (seq != -1) {
|
||||
to.addAttribute("shamrock_seq", seq)
|
||||
}
|
||||
app.sendToService(to)
|
||||
}
|
||||
|
||||
fun sendTrpcOidb(cmd: String, cmdId: Int, serviceId: Int, buffer: ByteArray, seq: Int = -1) {
|
||||
val to = createToServiceMsg(cmd)
|
||||
|
||||
val oidb = TrpcOidb(
|
||||
cmd = cmdId,
|
||||
service = serviceId,
|
||||
buffer = buffer,
|
||||
flag = 1
|
||||
)
|
||||
to.putWupBuffer(oidb.toByteArray())
|
||||
|
||||
to.addAttribute("req_pb_protocol_flag", true)
|
||||
if (seq != -1) {
|
||||
to.addAttribute("shamrock_seq", seq)
|
||||
}
|
||||
app.sendToService(to)
|
||||
}
|
||||
|
||||
fun sendBuffer(cmd: String, isPb: Boolean, buffer: ByteArray, seq: Int = MsfCore.getNextSeq()) {
|
||||
val toServiceMsg = ToServiceMsg("mobileqq.service", app.currentUin, cmd)
|
||||
toServiceMsg.putWupBuffer(buffer)
|
||||
toServiceMsg.addAttribute("req_pb_protocol_flag", isPb)
|
||||
toServiceMsg.addAttribute("shamrock_seq", seq)
|
||||
app.sendToService(toServiceMsg)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val coroutineContext: CoroutineContext by lazy {
|
||||
Dispatchers.IO.limitedParallelism(12)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun send(toServiceMsg: ToServiceMsg) {
|
||||
app.sendToService(toServiceMsg)
|
||||
}
|
||||
|
||||
protected suspend fun sendAW(toServiceMsg: ToServiceMsg, timeout: Long = 5000L): ByteArray? {
|
||||
val seq = MsfCore.getNextSeq()
|
||||
val buffer = withTimeoutOrNull<ByteArray?>(timeout) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
launch(Dispatchers.Default) {
|
||||
DynamicReceiver.register(IPCRequest(toServiceMsg.serviceCmd, seq) {
|
||||
val buffer = it.getByteArrayExtra("buffer")!!
|
||||
continuation.resume(buffer)
|
||||
})
|
||||
toServiceMsg.addAttribute("shamrock_seq", seq)
|
||||
send(toServiceMsg)
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
if (it == null) DynamicReceiver.unregister(seq)
|
||||
}?.copyOf()
|
||||
try {
|
||||
if (buffer != null && buffer.size >= 5 && buffer[4] == 120.toByte()) {
|
||||
val builder = BytePacketBuilder()
|
||||
val deBuffer = DeflateTools.uncompress(buffer.slice(4))
|
||||
builder.writeInt(deBuffer.size)
|
||||
builder.writeFully(deBuffer)
|
||||
return builder.build().readBytes()
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
return buffer
|
||||
}
|
||||
|
||||
protected fun sendExtra(cmd: String, builder: (Bundle) -> Unit) {
|
||||
val toServiceMsg = createToServiceMsg(cmd)
|
||||
builder(toServiceMsg.extraData)
|
||||
app.sendToService(toServiceMsg)
|
||||
}
|
||||
|
||||
protected fun sendPb(cmd: String, buffer: ByteArray, seq: Int) {
|
||||
val toServiceMsg = createToServiceMsg(cmd)
|
||||
toServiceMsg.putWupBuffer(buffer)
|
||||
toServiceMsg.addAttribute("req_pb_protocol_flag", true)
|
||||
toServiceMsg.addAttribute("shamrock_seq", seq)
|
||||
app.sendToService(toServiceMsg)
|
||||
}
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet
|
||||
|
||||
import VIP.GetCustomOnlineStatusReq
|
||||
import VIP.GetCustomOnlineStatusRsp
|
||||
import com.qq.jce.wup.UniPacket
|
||||
import com.tencent.mobileqq.data.Card
|
||||
import com.tencent.mobileqq.profilecard.api.IProfileDataService
|
||||
import com.tencent.mobileqq.profilecard.api.IProfileProtocolService
|
||||
import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType.Application.Json
|
||||
import io.ktor.http.contentType
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import moe.fuqiuluo.shamrock.tools.GlobalClient
|
||||
import moe.fuqiuluo.shamrock.tools.json
|
||||
import moe.fuqiuluo.shamrock.tools.slice
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import mqq.app.Packet
|
||||
import tencent.im.oidb.cmd0x11b2.oidb_0x11b2
|
||||
import tencent.im.oidb.oidb_sso
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
internal object CardSvc: BaseSvc() {
|
||||
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")
|
||||
Packet.decodePacket(resp, "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 buffer = sendOidbAW("OidbSvcTrpcTcp.0x11ca_0", 4790, 0, reqBody.toByteArray())
|
||||
?: error("unable to fetch contact ark_json_text")
|
||||
val body = oidb_sso.OIDBSSOPkg()
|
||||
body.mergeFrom(buffer.slice(4))
|
||||
val rsp = oidb_0x11b2.BusinessCardV3Rsp()
|
||||
rsp.mergeFrom(body.bytes_bodybuffer.get().toByteArray())
|
||||
return rsp.signed_ark_msg.get()
|
||||
}
|
||||
|
||||
suspend fun getProfileCard(uin: Long): Result<Card> {
|
||||
return getProfileCardFromCache(uin).onFailure {
|
||||
return refreshAndGetProfileCard(uin)
|
||||
}
|
||||
}
|
||||
|
||||
fun getProfileCardFromCache(uin: Long): Result<Card> {
|
||||
val profileDataService = app
|
||||
.getRuntimeService(IProfileDataService::class.java, "all")
|
||||
val card = profileDataService.getProfileCard(uin.toString(), true)
|
||||
return if (card == null || card.strNick.isNullOrEmpty()) {
|
||||
Result.failure(Exception("unable to fetch profile card"))
|
||||
} else {
|
||||
Result.success(card)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun refreshAndGetProfileCard(uin: Long): Result<Card> {
|
||||
val dataService = app
|
||||
.getRuntimeService(IProfileDataService::class.java, "all")
|
||||
val card = refreshCardLock.withLock {
|
||||
suspendCancellableCoroutine {
|
||||
app.addObserver(object: ProfileCardObserver() {
|
||||
override fun onGetProfileCard(success: Boolean, obj: Any) {
|
||||
app.removeObserver(this)
|
||||
if (!success || obj !is Card) {
|
||||
it.resume(null)
|
||||
} else {
|
||||
dataService.saveProfileCard(obj)
|
||||
it.resume(obj)
|
||||
}
|
||||
}
|
||||
})
|
||||
app.getRuntimeService(IProfileProtocolService::class.java, "all")
|
||||
.requestProfileCard(app.currentUin, uin.toString(), 12, 0L, 0.toByte(), 0L, 0L, null, "", 0L, 10004, null, 0.toByte())
|
||||
}
|
||||
}
|
||||
return if (card == null || card.strNick.isNullOrEmpty()) {
|
||||
Result.failure(Exception("unable to fetch profile card"))
|
||||
} else {
|
||||
Result.success(card)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet
|
||||
|
||||
import kotlinx.serialization.encodeToByteArray
|
||||
import protobuf.auto.toByteArray
|
||||
|
||||
import protobuf.oidb.cmd0x9082.Oidb0x9082
|
||||
|
||||
internal object ChatSvc: BaseSvc() {
|
||||
fun setGroupMessageCommentFace(peer: Long, msgSeq: ULong, faceIndex: String, isSet: Boolean) {
|
||||
val serviceId = if (isSet) 1 else 2
|
||||
sendOidb("OidbSvcTrpcTcp.0x9082_$serviceId", 36994, serviceId, Oidb0x9082(
|
||||
peer = peer.toULong(),
|
||||
msgSeq = msgSeq,
|
||||
faceIndex = faceIndex,
|
||||
flag = 1u,
|
||||
u1 = 0u,
|
||||
u2 = 0u
|
||||
).toByteArray())
|
||||
}
|
||||
}
|
@ -1,248 +0,0 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet
|
||||
|
||||
import com.tencent.mobileqq.pb.ByteStringMicro
|
||||
import moe.fuqiuluo.qqinterface.servlet.structures.*
|
||||
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
|
||||
import moe.fuqiuluo.shamrock.tools.slice
|
||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||
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
|
||||
|
||||
internal object FileSvc: BaseSvc() {
|
||||
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 resultBuffer = sendOidbAW("OidbSvc.0x6d7_0", 1751, 0, data)
|
||||
?: return Result.failure(Exception("unable to fetch result"))
|
||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||
oidbPkg.mergeFrom(resultBuffer.slice(4))
|
||||
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 buffer = sendOidbAW("OidbSvc.0x6d7_1", 1751, 1, Oidb0x6d7ReqBody(
|
||||
deleteFolder = DeleteFolderReq(
|
||||
groupCode = groupId.toULong(),
|
||||
appId = 3u,
|
||||
folderId = folderUid
|
||||
)
|
||||
).toByteArray()) ?: return false
|
||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||
oidbPkg.mergeFrom(buffer.slice(4))
|
||||
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 buffer = sendOidbAW("OidbSvc.0x6d7_2", 1751, 2, Oidb0x6d7ReqBody(
|
||||
moveFolder = MoveFolderReq(
|
||||
groupCode = groupId.toULong(),
|
||||
appId = 3u,
|
||||
folderId = folderUid,
|
||||
parentFolderId = "/"
|
||||
)
|
||||
).toByteArray()) ?: return false
|
||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||
oidbPkg.mergeFrom(buffer.slice(4))
|
||||
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 buffer = sendOidbAW("OidbSvc.0x6d7_3", 1751, 3, Oidb0x6d7ReqBody(
|
||||
renameFolder = RenameFolderReq(
|
||||
groupCode = groupId.toULong(),
|
||||
appId = 3u,
|
||||
folderId = folderUid,
|
||||
folderName = name
|
||||
)
|
||||
).toByteArray()) ?: return false
|
||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||
oidbPkg.mergeFrom(buffer.slice(4))
|
||||
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 = oidb_sso.OIDBSSOPkg()
|
||||
oidbPkg.mergeFrom(result.slice(4))
|
||||
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(oidb_sso.OIDBSSOPkg()
|
||||
.mergeFrom(rspGetFileCntBuffer.slice(4))
|
||||
.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(oidb_sso.OIDBSSOPkg()
|
||||
.mergeFrom(rspGetFileSpaceBuffer.slice(4))
|
||||
.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_000L)
|
||||
|
||||
return kotlin.runCatching {
|
||||
val files = arrayListOf<FileInfo>()
|
||||
val dirs = arrayListOf<FolderInfo>()
|
||||
if (rspGetFileListBuffer != null) {
|
||||
val oidb = oidb_sso.OIDBSSOPkg().mergeFrom(rspGetFileListBuffer.slice(4).let {
|
||||
if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it
|
||||
})
|
||||
|
||||
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.toHexString()}", Level.ERROR)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,360 +0,0 @@
|
||||
@file:OptIn(ExperimentalSerializationApi::class)
|
||||
|
||||
package moe.fuqiuluo.qqinterface.servlet
|
||||
|
||||
import com.tencent.mobileqq.qqguildsdk.api.IGPSService
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GProGuildRole
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GProRoleCreateInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GProRoleMemberList
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GProRolePermission
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
|
||||
import moe.fuqiuluo.qqinterface.servlet.structures.GProChannelInfo
|
||||
import moe.fuqiuluo.qqinterface.servlet.structures.GetGuildMemberListNextToken
|
||||
import moe.fuqiuluo.qqinterface.servlet.structures.GuildInfo
|
||||
import moe.fuqiuluo.qqinterface.servlet.structures.GuildStatus
|
||||
import moe.fuqiuluo.qqinterface.servlet.structures.SlowModeInfo
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
|
||||
import moe.fuqiuluo.shamrock.tools.slice
|
||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
|
||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
||||
import protobuf.auto.toByteArray
|
||||
import protobuf.guild.GetGuildFeedsReq
|
||||
import protobuf.guild.GetGuildFeedsRsp
|
||||
import protobuf.oidb.cmd0xf88.GProFilter
|
||||
import protobuf.oidb.cmd0xf88.GProUserInfo
|
||||
import protobuf.oidb.cmd0xf88.Oidb0xf88Req
|
||||
import protobuf.oidb.cmd0xf88.Oidb0xf88Rsp
|
||||
import protobuf.oidb.cmx0xf57.Oidb0xf57Filter
|
||||
import protobuf.oidb.cmx0xf57.Oidb0xf57GuildInfo
|
||||
import protobuf.oidb.cmx0xf57.Oidb0xf57MetaInfo
|
||||
import protobuf.oidb.cmx0xf57.Oidb0xf57Req
|
||||
import protobuf.oidb.cmx0xf57.Oidb0xf57Rsp
|
||||
import protobuf.oidb.cmx0xf57.Oidb0xf57U1
|
||||
import protobuf.oidb.cmx0xf57.Oidb0xf57U2
|
||||
import protobuf.qweb.QWebExtInfo
|
||||
import protobuf.qweb.QWebReq
|
||||
import protobuf.qweb.QWebRsp
|
||||
import tencent.im.oidb.oidb_sso
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
internal object GProSvc: BaseSvc() {
|
||||
fun getSelfTinyId(): ULong {
|
||||
val service = app.getRuntimeService(IGPSService::class.java, "all")
|
||||
return service.selfTinyId.toULong()
|
||||
}
|
||||
|
||||
suspend fun getGuildInfo(guildId: ULong): Result<Oidb0xf57MetaInfo> {
|
||||
val respBuffer = sendOidbAW("OidbSvcTrpcTcp.0xf57_9", 0xf57, 9, Oidb0xf57Req(
|
||||
filter = Oidb0xf57Filter(
|
||||
u1 = Oidb0xf57U1(1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u),
|
||||
u2 = Oidb0xf57U2(1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u)
|
||||
),
|
||||
guildInfo = Oidb0xf57GuildInfo(guildId = guildId)
|
||||
).toByteArray())
|
||||
val body = oidb_sso.OIDBSSOPkg()
|
||||
if (respBuffer == null) {
|
||||
return Result.failure(Exception("unable to send packet"))
|
||||
}
|
||||
body.mergeFrom(respBuffer.slice(4))
|
||||
return runCatching {
|
||||
body.bytes_bodybuffer.get()
|
||||
.toByteArray()
|
||||
.decodeProtobuf<Oidb0xf57Rsp>().metaInfo
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getGuildFeeds(guildId: ULong, channelId: ULong, startIndex: Int): Result<GetGuildFeedsRsp> {
|
||||
val buffer = sendBufferAW("QChannelSvr.trpc.qchannel.commreader.ComReader.GetGuildFeeds", true, QWebReq(
|
||||
seq = 10,
|
||||
qua = PlatformUtils.getQUA(),
|
||||
deviceInfo = "i=&imsi=&mac=02:00:00:00:00:00&m=Shamrock&o=114514&a=1919810&sd=0&c64=1&sc=1&p=8000*8000&aid=123456789012345678901234567890abcdef&f=Tencent&mm=5610&cf=1726&cc=8&qimei=&qimei36=&sharpP=1&n=nether_world&support_xsj_live=false&client_mod=concise&timezone=America/La_Paz&material_sdk_version=&vh265=&refreshrate=10086&hwlevel=9&suphdr=1&is_teenager_mod=8&liveH265=&bmst=5&AV1=0",
|
||||
buffer = GetGuildFeedsReq(
|
||||
count = 12,
|
||||
from = startIndex,
|
||||
feedAttchInfo = EMPTY_BYTE_ARRAY,
|
||||
guildId = guildId,
|
||||
getType = 1,
|
||||
u7 = 0,
|
||||
u8 = 1,
|
||||
u9 = EMPTY_BYTE_ARRAY
|
||||
).toByteArray(),
|
||||
traceId = app.account + "_0_0",
|
||||
extinfo = listOf(
|
||||
QWebExtInfo("fc-appid", "96"),
|
||||
QWebExtInfo("environment_id", "shamrock"),
|
||||
QWebExtInfo("tiny_id", getSelfTinyId().toString()),
|
||||
)
|
||||
).toByteArray()) ?: return Result.failure(Exception("unable to send packet"))
|
||||
val webRsp = buffer.slice(4).decodeProtobuf<QWebRsp>()
|
||||
if(webRsp.buffer == null) return Result.failure(Exception("server error"))
|
||||
val wupBuffer = webRsp.buffer!!
|
||||
val feeds = wupBuffer.decodeProtobuf<GetGuildFeedsRsp>()
|
||||
return Result.success(feeds)
|
||||
}
|
||||
|
||||
fun getChannelList(guildId: ULong, refresh: Boolean = false): Result<ArrayList<GProChannelInfo>> {
|
||||
if (refresh) {
|
||||
refreshGuildInfo(guildId)
|
||||
}
|
||||
val result = arrayListOf<GProChannelInfo>()
|
||||
app.getRuntimeService(IGPSService::class.java, "all").getChannelList(guildId.toString()).forEach {
|
||||
result.add(GProChannelInfo(
|
||||
ownerGuildId = guildId,
|
||||
guildId = it.guildId,
|
||||
channelId = it.channelUin.toLong(),
|
||||
channelUin = it.channelUin.toLong(),
|
||||
channelName = it.channelName ?: "",
|
||||
channelType = it.type,
|
||||
createTime = it.createTime,
|
||||
creatorTinyId = it.creatorId.toLong(),
|
||||
talkPermission = it.talkPermission,
|
||||
visibleType = it.visibleType,
|
||||
currentSlowMode = it.slowModeKey,
|
||||
slowModes = it.gProSlowModeInfoList.map {
|
||||
SlowModeInfo(it.slowModeKey, it.slowModeText, it.speakFrequency, it.slowModeCircle)
|
||||
},
|
||||
appIconUrl = it.iconUrl,
|
||||
jumpType = it.appChannelJumpType,
|
||||
jumpSwitch = it.jumpSwitch,
|
||||
jumpUrl = it.appChannelJumpUrl,
|
||||
categoryId = it.categoryId,
|
||||
myTalkPermission = it.myTalkPermissionType,
|
||||
maxMemberCount = it.channelMemberMax
|
||||
))
|
||||
}
|
||||
return Result.success(result)
|
||||
}
|
||||
|
||||
fun refreshGuildInfo(guildId: ULong) {
|
||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
||||
kernelGProService.refreshGuildInfo(guildId.toLong(), true, 1)
|
||||
}
|
||||
|
||||
suspend fun getGuildMemberList(
|
||||
guildId: ULong,
|
||||
startIndex: Long = 0,
|
||||
roleIndex: Long = 1,
|
||||
count: Int = 50,
|
||||
fetchAll: Boolean = false,
|
||||
result: ArrayList<GProRoleMemberList> = arrayListOf()
|
||||
): Result<Pair<GetGuildMemberListNextToken, ArrayList<GProRoleMemberList>>> {
|
||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
||||
|
||||
val fetchGuildMemberListResult: Pair<GetGuildMemberListNextToken, ArrayList<GProRoleMemberList>> = (withTimeoutOrNull(5000) {
|
||||
suspendCancellableCoroutine {
|
||||
kernelGProService.fetchMemberListWithRole(guildId.toLong(), 0, startIndex, roleIndex, count, 0) { code, reason, finish, nextIndex, nextRoleIdIndex, _, seq, roleList ->
|
||||
if (code == 0) {
|
||||
it.resume(GetGuildMemberListNextToken(nextIndex, nextRoleIdIndex, seq, finish) to roleList)
|
||||
} else {
|
||||
LogCenter.log("fetchMemberListWithRole failed: $code($reason)", Level.WARN)
|
||||
it.resume(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}) ?: return Result.failure(Exception("unable to fetch guild member list"))
|
||||
|
||||
val nextToken = fetchGuildMemberListResult.first
|
||||
val roleList = fetchGuildMemberListResult.second
|
||||
result.addAll(roleList)
|
||||
return if (fetchAll) {
|
||||
if (!fetchGuildMemberListResult.first.finish) {
|
||||
getGuildMemberList(guildId, nextToken.startIndex, nextToken.roleIndex, count, true, result)
|
||||
} else {
|
||||
Result.success(nextToken.copy(finish = true) to result)
|
||||
}
|
||||
} else {
|
||||
Result.success(nextToken to result)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getSelfGuildInfo(): Result<GProUserInfo> {
|
||||
val selfTinyId = getSelfTinyId()
|
||||
return getUserGuildInfo(0u, selfTinyId)
|
||||
}
|
||||
|
||||
suspend fun getUserGuildInfo(
|
||||
guildId: ULong,
|
||||
memberTinyId: ULong
|
||||
): Result<GProUserInfo> {
|
||||
val respBuffer = sendOidbAW("OidbSvcTrpcTcp.0xf88_1", 0xf88, 1, Oidb0xf88Req(
|
||||
filter = GProFilter(1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u),
|
||||
memberId = 0uL,
|
||||
tinyId = memberTinyId,
|
||||
guildId = guildId
|
||||
).toByteArray())
|
||||
val body = oidb_sso.OIDBSSOPkg()
|
||||
if (respBuffer == null) {
|
||||
return Result.failure(Exception("unable to send packet"))
|
||||
}
|
||||
body.mergeFrom(respBuffer.slice(4))
|
||||
return runCatching {
|
||||
body.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0xf88Rsp>().userInfo!!
|
||||
}
|
||||
}
|
||||
|
||||
private fun getGuildListByOldApi(result: ArrayList<GuildInfo>) {
|
||||
app.getRuntimeService(IGPSService::class.java, "all").guildList?.forEach {
|
||||
result.add(GuildInfo(
|
||||
guildId = it.guildID.toLong(),
|
||||
guildName = it.guildName ?: "",
|
||||
guildDisplayId = it.guildNumber ?: "",
|
||||
profile = it.profile ?: "",
|
||||
status = GuildStatus(
|
||||
isEnable = !it.isFrozen && !it.isBanned,
|
||||
isBanned = it.isBanned,
|
||||
isFrozen = it.isFrozen
|
||||
),
|
||||
ownerId = 0,
|
||||
shutUpTime = it.shutUpExpireTime,
|
||||
allowSearch = it.allowSearch
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getGuildListByNt(result: ArrayList<GuildInfo>) {
|
||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
||||
kernelGProService.guildListFromCache.forEach {
|
||||
if (it.result != 0) return@forEach
|
||||
val guildInfo = it.guildInfo
|
||||
result.add(GuildInfo(
|
||||
guildId = it.guildId,
|
||||
guildName = guildInfo.guildName ?: "",
|
||||
guildDisplayId = guildInfo.guildNumber ?: "",
|
||||
profile = guildInfo.profile ?: "",
|
||||
status = GuildStatus(
|
||||
isEnable = guildInfo.guildStatus?.isEnable == 1,
|
||||
isBanned = guildInfo.guildStatus?.isBanned == 1,
|
||||
isFrozen = guildInfo.guildStatus?.isFrozen == 1
|
||||
),
|
||||
ownerId = guildInfo.ownerTinyid,
|
||||
shutUpTime = guildInfo.shutupExpireTime,
|
||||
allowSearch = guildInfo.allowSearch == 1
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchGuildMemberRoles(guildId: ULong, tinyId: ULong, refresh: Boolean = false): Result<ArrayList<GProGuildRole>> {
|
||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
||||
if (refresh) {
|
||||
kernelGProService.refreshGuildUserProfileInfo(guildId.toLong(), tinyId.toLong(), 1)
|
||||
}
|
||||
val result: ArrayList<GProGuildRole> = withTimeoutOrNull(5000) {
|
||||
suspendCancellableCoroutine {
|
||||
kernelGProService.fetchMemberRoles(guildId.toLong(), 0, tinyId.toLong(), 2) { code, reason, roles ->
|
||||
it.resume(roles)
|
||||
}
|
||||
}
|
||||
} ?: return Result.failure(Exception("unable to fetch guild member roles"))
|
||||
return Result.success(result)
|
||||
}
|
||||
|
||||
fun getGuildList(refresh: Boolean = false, forceOldApi: Boolean): ArrayList<GuildInfo> {
|
||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
||||
if (refresh) {
|
||||
kernelGProService.refreshGuildList(true)
|
||||
kernelGProService.guildListFromCache.forEach {
|
||||
refreshGuildInfo(it.guildId.toULong())
|
||||
}
|
||||
}
|
||||
val result = arrayListOf<GuildInfo>()
|
||||
if (PlatformUtils.getQQVersionCode() < PlatformUtils.QQ_9_0_8_VER || forceOldApi) {
|
||||
getGuildListByOldApi(result)
|
||||
} else {
|
||||
runCatching {
|
||||
getGuildListByNt(result)
|
||||
}.onFailure {
|
||||
LogCenter.log("GetGuildListByNt failed: ${it.stackTraceToString()}", Level.ERROR)
|
||||
getGuildListByOldApi(result) // 防止QQ更新API导致无法获取
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun getGuildRoles(guildId: ULong): Result<List<GProGuildRole>> {
|
||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
||||
val roles: List<GProGuildRole> = withTimeoutOrNull(5000) {
|
||||
suspendCancellableCoroutine {
|
||||
kernelGProService.fetchRoleListWithPermission(guildId.toLong(), 1) { code, _, roles, _, _, _ ->
|
||||
if (code != 0) it.resume(null) else it.resume(roles)
|
||||
}
|
||||
}
|
||||
} ?: return Result.failure(Exception("unable to fetch guild roles"))
|
||||
return Result.success(roles)
|
||||
}
|
||||
|
||||
fun deleteGuildRole(guildId: ULong, roleId: ULong) {
|
||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
||||
kernelGProService.deleteRole(guildId.toLong(), roleId.toLong()) { code, msg, result ->
|
||||
if (code != 0) {
|
||||
LogCenter.log("deleteGuildRole failed: $code($msg) => $result", Level.WARN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setMemberRole(guildId: ULong, tinyId: ULong, roleId: ULong, isSet: Boolean) {
|
||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
||||
val addList = arrayListOf<Long>()
|
||||
val rmList = arrayListOf<Long>()
|
||||
(if (isSet) addList else rmList).add(roleId.toLong())
|
||||
kernelGProService.setMemberRoles(guildId.toLong(), 0, 0, tinyId.toLong(), addList, rmList) { code, msg, result ->
|
||||
if (code != 0) {
|
||||
LogCenter.log("setMemberRole failed: $code($msg) => $result", Level.WARN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getGuildRolePermission(guildId: ULong, roleId: ULong): Result<GProGuildRole> {
|
||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
||||
val role:GProGuildRole = withTimeoutOrNull(5000) {
|
||||
suspendCancellableCoroutine {
|
||||
kernelGProService.fetchRoleWithPermission(guildId.toLong(), roleId.toLong(), 1) { code, msg, role, _, _, _ ->
|
||||
if (code != 0) {
|
||||
LogCenter.log("getGuildRolePermission failed: $code($msg)", Level.WARN)
|
||||
it.resume(null)
|
||||
} else it.resume(role)
|
||||
}
|
||||
}
|
||||
} ?: return Result.failure(Exception("unable to fetch guild role permission"))
|
||||
return Result.success(role)
|
||||
}
|
||||
|
||||
suspend fun updateGuildRole(guildId: ULong, roleId: ULong, name: String, color: Long): Result<Unit> {
|
||||
val oldInfo = getGuildRolePermission(guildId, roleId).onFailure {
|
||||
return Result.failure(it)
|
||||
}.getOrThrow()
|
||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
||||
val info = GProRoleCreateInfo(
|
||||
name, color, oldInfo.bHoist, oldInfo.rolePermissions
|
||||
)
|
||||
kernelGProService.setRoleInfo(guildId.toLong(), roleId.toLong(), info) { code, msg, result ->
|
||||
if (code != 0) {
|
||||
LogCenter.log("updateGuildRole failed: $code($msg) => $result", Level.WARN)
|
||||
}
|
||||
}
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
suspend fun createGuildRole(guildId: ULong, name: String, color: Long, initialUsers: ArrayList<Long>): Result<GProGuildRole> {
|
||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
||||
val permission = GProRolePermission(false, arrayListOf())
|
||||
val info = GProRoleCreateInfo(name, color, false, permission)
|
||||
val role: GProGuildRole = withTimeoutOrNull(5000) {
|
||||
suspendCancellableCoroutine {
|
||||
kernelGProService.createRole(guildId.toLong(), info, initialUsers) { code, msg, result, role ->
|
||||
if (code != 0) {
|
||||
LogCenter.log("createGuildRole failed: $code($msg) => $result", Level.WARN)
|
||||
it.resume(null)
|
||||
} else it.resume(role)
|
||||
}
|
||||
}
|
||||
} ?: return Result.failure(Exception("unable to create guild role"))
|
||||
return Result.success(role)
|
||||
}
|
||||
}
|
@ -1,340 +0,0 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet
|
||||
|
||||
import com.tencent.mobileqq.qroute.QRoute
|
||||
import com.tencent.mobileqq.troop.api.ITroopMemberNameService
|
||||
import com.tencent.qqnt.kernel.api.IKernelService
|
||||
import com.tencent.qqnt.kernel.nativeinterface.*
|
||||
import com.tencent.qqnt.msg.api.IMsgService
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.toListMap
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
|
||||
import moe.fuqiuluo.shamrock.helper.ContactHelper
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
||||
import moe.fuqiuluo.shamrock.remote.service.data.MessageDetail
|
||||
import moe.fuqiuluo.shamrock.remote.service.data.MessageSender
|
||||
import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult
|
||||
import moe.fuqiuluo.shamrock.tools.*
|
||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
|
||||
import moe.fuqiuluo.shamrock.xposed.helper.msgService
|
||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
||||
import protobuf.auto.toByteArray
|
||||
import protobuf.message.PushMsgBody
|
||||
import protobuf.message.longmsg.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
internal object MsgSvc : BaseSvc() {
|
||||
private suspend fun prepareTempChatFromGroup(
|
||||
groupId: String,
|
||||
peerId: String
|
||||
): Result<Unit> {
|
||||
LogCenter.log("主动临时消息,创建临时会话。", Level.INFO)
|
||||
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
|
||||
?: return Result.failure(Exception("获取消息服务失败"))
|
||||
msgService.prepareTempChat(
|
||||
TempChatPrepareInfo(
|
||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP,
|
||||
ContactHelper.getUidByUinAsync(peerId = peerId.toLong()),
|
||||
app.getRuntimeService(ITroopMemberNameService::class.java, "all")
|
||||
.getTroopMemberNameRemarkFirst(groupId, peerId),
|
||||
groupId,
|
||||
EMPTY_BYTE_ARRAY,
|
||||
app.currentUid,
|
||||
"",
|
||||
TempChatGameSession()
|
||||
)
|
||||
) { code, reason ->
|
||||
if (code != 0) {
|
||||
LogCenter.log("临时会话创建失败: $code, $reason", Level.ERROR)
|
||||
}
|
||||
}
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
suspend fun getTempChatInfo(chatType: Int, uid: String): Result<TempChatInfo> {
|
||||
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
|
||||
?: return Result.failure(Exception("获取消息服务失败"))
|
||||
val info: TempChatInfo = withTimeoutOrNull(5000) {
|
||||
suspendCancellableCoroutine {
|
||||
msgService.getTempChatInfo(chatType, uid) { code, msg, tempChatInfo ->
|
||||
if (code == 0) {
|
||||
it.resume(tempChatInfo)
|
||||
} else {
|
||||
LogCenter.log("获取临时会话信息失败: $code:$msg", Level.ERROR)
|
||||
it.resume(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: return Result.failure(Exception("获取临时会话信息失败"))
|
||||
return Result.success(info)
|
||||
}
|
||||
|
||||
/**
|
||||
* 正常获取
|
||||
*/
|
||||
suspend fun getMsg(hash: Int): Result<MsgRecord> {
|
||||
val mapping = MessageHelper.getMsgMappingByHash(hash)
|
||||
?: return Result.failure(Exception("没有对应消息映射,消息获取失败"))
|
||||
|
||||
val peerId = mapping.peerId
|
||||
val contact = MessageHelper.generateContact(mapping.chatType, peerId, mapping.subPeerId)
|
||||
|
||||
val msg = withTimeoutOrNull(5000) {
|
||||
val service = QRoute.api(IMsgService::class.java)
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
service.getMsgsByMsgId(contact, arrayListOf(mapping.qqMsgId)) { code, _, msgRecords ->
|
||||
if (code == 0 && msgRecords.isNotEmpty()) {
|
||||
continuation.resume(msgRecords.first())
|
||||
} else {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
continuation.invokeOnCancellation {
|
||||
continuation.resume(null)
|
||||
} // 貌似不会被取消,写了也没什么鸟用啊?
|
||||
}
|
||||
}
|
||||
|
||||
return if (msg != null) {
|
||||
Result.success(msg)
|
||||
} else {
|
||||
Result.failure(Exception("获取消息失败"))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMsgByQMsgId(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
qqMsgId: Long,
|
||||
subPeerId: String = ""
|
||||
): Result<MsgRecord> {
|
||||
val contact = MessageHelper.generateContact(chatType, peerId, subPeerId)
|
||||
val service = QRoute.api(IMsgService::class.java)
|
||||
|
||||
val msg = withTimeoutOrNull(5000) {
|
||||
suspendCoroutine { continuation ->
|
||||
service.getMsgsByMsgId(contact, arrayListOf(qqMsgId)) { code, _, msgRecords ->
|
||||
if (code == 0 && msgRecords.isNotEmpty()) {
|
||||
continuation.resume(msgRecords.first())
|
||||
} else {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (msg != null) {
|
||||
Result.success(msg)
|
||||
} else {
|
||||
Result.failure(Exception("获取消息失败"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 什么鸟屎都获取不到
|
||||
*/
|
||||
suspend fun getMsgBySeq(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
seq: Long
|
||||
): Result<MsgRecord> {
|
||||
val contact = MessageHelper.generateContact(chatType, peerId)
|
||||
val msg = withTimeoutOrNull(1000) {
|
||||
val service = QRoute.api(IMsgService::class.java)
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
service.getMsgsBySeqs(contact, arrayListOf(seq)) { code, _, msgRecords ->
|
||||
continuation.resume(msgRecords?.firstOrNull())
|
||||
}
|
||||
continuation.invokeOnCancellation {
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (msg != null) {
|
||||
Result.success(msg)
|
||||
} else {
|
||||
Result.failure(Exception("获取消息失败"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回消息 同步 HTTP API
|
||||
*/
|
||||
suspend fun recallMsg(msgHash: Int): Pair<Int, String> {
|
||||
val kernelService = NTServiceFetcher.kernelService
|
||||
val sessionService = kernelService.wrapperSession
|
||||
val msgService = sessionService.msgService
|
||||
|
||||
val mapping = MessageHelper.getMsgMappingByHash(msgHash)
|
||||
?: return -1 to "无法找到消息映射"
|
||||
|
||||
val contact = MessageHelper.generateContact(mapping.chatType, mapping.peerId, mapping.subPeerId)
|
||||
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
msgService.recallMsg(contact, arrayListOf(mapping.qqMsgId)) { code, why ->
|
||||
continuation.resume(code to why)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*
|
||||
* Aio 腾讯内部命名 All In One
|
||||
*/
|
||||
suspend fun sendToAio(
|
||||
chatType: Int,
|
||||
peedId: String,
|
||||
message: JsonArray,
|
||||
fromId: String = peedId,
|
||||
retryCnt: Int
|
||||
): Result<SendMsgResult> {
|
||||
// 主动临时消息
|
||||
when (chatType) {
|
||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
|
||||
prepareTempChatFromGroup(fromId, peedId).onFailure {
|
||||
LogCenter.log("主动临时消息,创建临时会话失败。", Level.ERROR)
|
||||
return Result.failure(Exception("主动临时消息,创建临时会话失败。"))
|
||||
}
|
||||
}
|
||||
}
|
||||
val result =
|
||||
MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0))
|
||||
if (result.isFailure) {
|
||||
LogCenter.log("sendToAio: " + result.exceptionOrNull()?.stackTraceToString(), Level.ERROR)
|
||||
return result
|
||||
}
|
||||
val sendResult = result.getOrThrow()
|
||||
return if (sendResult.isTimeout) {
|
||||
// 发送失败,可能网络问题出现红色感叹号,重试
|
||||
// 例如 rich media transfer failed
|
||||
delay(100)
|
||||
MessageHelper.resendMsg(chatType, peedId, fromId, sendResult.qqMsgId, retryCnt, sendResult.msgHashId)
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun uploadMultiMsg(
|
||||
uid: String,
|
||||
groupUin: String?,
|
||||
messages: List<PushMsgBody>,
|
||||
): Result<String> {
|
||||
val payload = LongMsgPayload(
|
||||
action = listOf(
|
||||
LongMsgAction(
|
||||
command = "MultiMsg",
|
||||
data = LongMsgContent(
|
||||
body = messages
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
LogCenter.log(payload.toByteArray().toHexString(), Level.DEBUG)
|
||||
|
||||
val req = LongMsgReq(
|
||||
sendInfo = SendLongMsgInfo(
|
||||
type = if (groupUin == null) 1 else 3,
|
||||
uid = LongMsgUid(groupUin ?: uid),
|
||||
groupUin = groupUin?.toInt(),
|
||||
payload = DeflateTools.gzip(payload.toByteArray())
|
||||
),
|
||||
setting = LongMsgSettings(
|
||||
field1 = 4,
|
||||
field2 = 2,
|
||||
field3 = 9,
|
||||
field4 = 0
|
||||
)
|
||||
)
|
||||
val buffer = sendBufferAW(
|
||||
"trpc.group.long_msg_interface.MsgService.SsoSendLongMsg",
|
||||
true,
|
||||
req.toByteArray()
|
||||
) ?: return Result.failure(Exception("unable to upload multi message"))
|
||||
val rsp = buffer.slice(4).decodeProtobuf<LongMsgRsp>()
|
||||
return rsp.sendResult?.resId?.let { Result.success(it) }
|
||||
?: Result.failure(Exception("unable to upload multi message"))
|
||||
}
|
||||
|
||||
suspend fun getMultiMsg(resId: String): Result<List<MessageDetail>> {
|
||||
val req = LongMsgReq(
|
||||
recvInfo = RecvLongMsgInfo(
|
||||
uid = LongMsgUid(TicketSvc.getUid()),
|
||||
resId = resId,
|
||||
u1 = 3
|
||||
),
|
||||
setting = LongMsgSettings(
|
||||
field1 = 2,
|
||||
field2 = 2,
|
||||
field3 = 9,
|
||||
field4 = 0
|
||||
)
|
||||
)
|
||||
val buffer = sendBufferAW(
|
||||
"trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg",
|
||||
true,
|
||||
req.toByteArray()
|
||||
) ?: return Result.failure(Exception("unable to get multi message"))
|
||||
val rsp = buffer.slice(4).decodeProtobuf<LongMsgRsp>()
|
||||
val zippedPayload = DeflateTools.ungzip(
|
||||
rsp.recvResult?.payload ?: return Result.failure(Exception("unable to get multi message"))
|
||||
)
|
||||
LogCenter.log(zippedPayload.toHexString(), Level.DEBUG)
|
||||
val payload = zippedPayload.decodeProtobuf<LongMsgPayload>()
|
||||
payload.action?.forEach {
|
||||
if (it.command == "MultiMsg") {
|
||||
return Result.success(it.data?.body?.map { msg ->
|
||||
val chatType =
|
||||
if (msg.contentHead!!.msgType == 82) MsgConstant.KCHATTYPEGROUP else MsgConstant.KCHATTYPEC2C
|
||||
MessageDetail(
|
||||
time = msg.contentHead?.msgTime?.toInt() ?: 0,
|
||||
msgType = MessageHelper.obtainDetailTypeByMsgType(chatType),
|
||||
msgId = 0, // MessageHelper.generateMsgIdHash(chatType, msg.content!!.msgViaRandom), msgViaRandom 为空
|
||||
msgSeq = msg.contentHead!!.msgSeq ?: 0,
|
||||
sender = MessageSender(
|
||||
msg.msgHead?.peer ?: 0,
|
||||
msg.msgHead?.responseGrp?.memberCard?.ifEmpty { msg.msgHead?.forward?.friendName }
|
||||
?: msg.msgHead?.forward?.friendName ?: "",
|
||||
"unknown",
|
||||
0,
|
||||
msg.msgHead?.peerUid ?: "",
|
||||
msg.msgHead?.peerUid ?: ""
|
||||
),
|
||||
message = msg.body?.richText?.elements?.toSegments(chatType, msg.msgHead?.peer.toString(), "0")
|
||||
?.toListMap() ?: emptyList(),
|
||||
peerId = msg.msgHead?.peer ?: 0,
|
||||
groupId = if (chatType == MsgConstant.KCHATTYPEGROUP) msg.msgHead?.responseGrp?.groupCode?.toLong()
|
||||
?: 0 else 0,
|
||||
targetId = if (chatType != MsgConstant.KCHATTYPEGROUP) msg.msgHead?.peer ?: 0 else 0
|
||||
)
|
||||
}
|
||||
?: return Result.failure(Exception("Msg is empty")))
|
||||
}
|
||||
}
|
||||
return Result.failure(Exception("Can't find msg"))
|
||||
}
|
||||
|
||||
class MessageCallback(
|
||||
private val peerId: String,
|
||||
var msgHash: Int
|
||||
) : IOperateCallback {
|
||||
override fun onResult(code: Int, reason: String?) {
|
||||
if (code != 0 && msgHash != 0) {
|
||||
MessageHelper.removeMsgByHashCode(msgHash)
|
||||
}
|
||||
when (code) {
|
||||
110 -> LogCenter.log("消息发送: $peerId, 无该联系人无法发送。")
|
||||
120 -> LogCenter.log("消息发送: $peerId, 禁言状态无法发送。")
|
||||
5 -> LogCenter.log("消息发送: $peerId, 当前不支持该消息类型。")
|
||||
else -> LogCenter.log("消息发送: $peerId, code: $code $reason")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||
import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgService
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import io.ktor.utils.io.core.BytePacketBuilder
|
||||
import io.ktor.utils.io.core.readBytes
|
||||
import io.ktor.utils.io.core.writeFully
|
||||
import io.ktor.utils.io.core.writeInt
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.MessageTempHandler
|
||||
|
||||
import moe.fuqiuluo.shamrock.remote.action.handlers.GetHistoryMsg
|
||||
import moe.fuqiuluo.shamrock.remote.service.listener.AioListener
|
||||
import moe.fuqiuluo.shamrock.tools.broadcast
|
||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||
import protobuf.message.element.LightAppElem
|
||||
import protobuf.message.PushMsgBody
|
||||
import protobuf.message.ContentHead
|
||||
import protobuf.message.Elem
|
||||
import protobuf.message.RichText
|
||||
import protobuf.message.ResponseHead
|
||||
import protobuf.message.MsgBody
|
||||
import protobuf.push.MessagePush
|
||||
import mqq.app.MobileQQ
|
||||
import protobuf.auto.toByteArray
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
internal object PacketSvc: BaseSvc() {
|
||||
/**
|
||||
* 伪造收到Json卡片消息
|
||||
*/
|
||||
suspend fun fakeSelfRecvJsonMsg(msgService: IKernelMsgService, content: String): Long {
|
||||
return fakeReceiveSelfMsg(msgService) {
|
||||
listOf(
|
||||
Elem(
|
||||
lightApp = LightAppElem((byteArrayOf(1) + DeflateTools.compress(content.toByteArray())))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fakeReceiveSelfMsg(msgService: IKernelMsgService, builder: () -> List<Elem>): Long {
|
||||
val latestMsg = withTimeoutOrNull(3000) {
|
||||
suspendCancellableCoroutine {
|
||||
msgService.getMsgs(Contact(MsgConstant.KCHATTYPEC2C, app.currentUid, ""), 0L, 1, true) { code, why, msgs ->
|
||||
it.resume(GetHistoryMsg.GetMsgResult(code, why, msgs))
|
||||
}
|
||||
}
|
||||
}?.data?.firstOrNull()
|
||||
val msgSeq = (latestMsg?.msgSeq ?: 0) + 1
|
||||
|
||||
val msgPush = MessagePush(
|
||||
msgBody = PushMsgBody(
|
||||
msgHead = ResponseHead(
|
||||
peer = app.longAccountUin,
|
||||
peerUid = app.currentUid,
|
||||
flag = 1001,
|
||||
receiver = app.longAccountUin,
|
||||
receiverUid = app.currentUid
|
||||
),
|
||||
contentHead = ContentHead(
|
||||
msgType = 166,
|
||||
msgSubType = 11,
|
||||
msgSeq = msgSeq,
|
||||
msgViaRandom = msgSeq,
|
||||
msgTime = System.currentTimeMillis() / 1000,
|
||||
u2 = 1,
|
||||
sequence = msgSeq,
|
||||
msgRandom = msgService.getMsgUniqueId(System.currentTimeMillis()),
|
||||
u4 = msgSeq - 2,
|
||||
u5 = msgSeq
|
||||
),
|
||||
body = MsgBody(RichText(
|
||||
elements = builder()
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
fakeReceive("trpc.msg.olpush.OlPushService.MsgPush", 10000, msgPush.toByteArray())
|
||||
return withTimeoutOrNull(5000L) {
|
||||
suspendCancellableCoroutine {
|
||||
MessageTempHandler.registerTemporaryMsgListener(msgSeq) {
|
||||
it.resume(this.msgId)
|
||||
}
|
||||
it.invokeOnCancellation {
|
||||
MessageTempHandler.unregisterTemporaryMsgListener(msgSeq)
|
||||
}
|
||||
}
|
||||
} ?: -1L
|
||||
}
|
||||
|
||||
/**
|
||||
* 伪造QQ收到某个包
|
||||
*/
|
||||
private fun fakeReceive(cmd: String, seq: Int, buffer: ByteArray) {
|
||||
MobileQQ.getContext().broadcast("msf") {
|
||||
putExtra("__cmd", "fake_packet")
|
||||
putExtra("package_cmd", cmd)
|
||||
putExtra("package_uin", app.currentUin)
|
||||
putExtra("package_seq", seq)
|
||||
val wupBuffer = BytePacketBuilder().apply {
|
||||
writeInt(buffer.size + 4)
|
||||
writeFully(buffer)
|
||||
}.build()
|
||||
putExtra("package_buffer", wupBuffer.readBytes())
|
||||
wupBuffer.release()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,402 +0,0 @@
|
||||
@file:OptIn(ExperimentalSerializationApi::class)
|
||||
|
||||
package moe.fuqiuluo.qqinterface.servlet
|
||||
|
||||
import com.tencent.mobileqq.app.QQAppInterface
|
||||
import com.tencent.mobileqq.transfile.HttpNetReq
|
||||
import com.tencent.mobileqq.transfile.INetEngineListener
|
||||
import com.tencent.mobileqq.transfile.NetReq
|
||||
import com.tencent.mobileqq.transfile.NetResp
|
||||
import com.tencent.mobileqq.transfile.ServerAddr
|
||||
import com.tencent.mobileqq.transfile.api.IHttpEngineService
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.io.core.BytePacketBuilder
|
||||
import kotlinx.io.core.readBytes
|
||||
import kotlinx.io.core.writeFully
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.encodeToByteArray
|
||||
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
|
||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||
import moe.fuqiuluo.shamrock.utils.MD5
|
||||
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
|
||||
import protobuf.fav.WeiyunAddRichMediaReq
|
||||
import protobuf.fav.WeiyunAuthor
|
||||
import protobuf.fav.WeiyunCollectCommInfo
|
||||
import protobuf.fav.WeiyunComm
|
||||
import protobuf.fav.WeiyunCommonReq
|
||||
import protobuf.fav.WeiyunFastUploadResourceReq
|
||||
import protobuf.fav.WeiyunGetFavContentReq
|
||||
import protobuf.fav.WeiyunGetFavListReq
|
||||
import protobuf.fav.WeiyunMsgHead
|
||||
import protobuf.fav.WeiyunPicInfo
|
||||
import protobuf.fav.WeiyunRichMediaContent
|
||||
import protobuf.fav.WeiyunRichMediaSummary
|
||||
import mqq.manager.TicketManager
|
||||
import oicq.wlogin_sdk.request.Ticket
|
||||
import oicq.wlogin_sdk.request.WtTicketPromise
|
||||
import oicq.wlogin_sdk.tools.ErrMsg
|
||||
import protobuf.auto.toByteArray
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
|
||||
/**
|
||||
* QQ收藏相关接口
|
||||
*/
|
||||
internal object QFavSvc: BaseSvc() {
|
||||
private val SERVER_LIST_COLLECTOR = listOf(ServerAddr().also {
|
||||
it.isIpv6 = false
|
||||
it.mIp = "collector.weiyun.com"
|
||||
it.port = 80
|
||||
})
|
||||
private val SERVER_LIST_PICUP = listOf(ServerAddr().also {
|
||||
it.isIpv6 = false
|
||||
it.mIp = "pic.pieceup.qq.com"
|
||||
it.port = 80
|
||||
})
|
||||
private const val VERSION = 12820
|
||||
private const val APPID = 30244
|
||||
private const val SUB_APPID = 538116905
|
||||
private const val MAJOR_VERSION = 8
|
||||
private const val MINOR_VERSION = 9
|
||||
private var seq = 1
|
||||
|
||||
suspend fun getItemList(
|
||||
category: Int,
|
||||
startPos: Int,
|
||||
pageSize: Int,
|
||||
): Result<NetResp> {
|
||||
val baseReq = WeiyunCommonReq(
|
||||
getFavListReq = WeiyunGetFavListReq(
|
||||
type = 0u,
|
||||
bid = 0u,
|
||||
category = category.toUInt(),
|
||||
startTime = 0u,
|
||||
orderType = 0u,
|
||||
startPos = startPos.toUInt(),
|
||||
pageSize = pageSize.toUInt(),
|
||||
syncPolicy = 0u,
|
||||
reqSource = 0u
|
||||
)
|
||||
)
|
||||
return sendWeiyunReq(20000, baseReq)
|
||||
}
|
||||
|
||||
suspend fun getItemContent(
|
||||
id: String
|
||||
): Result<NetResp> {
|
||||
return sendWeiyunReq(20001, WeiyunCommonReq(
|
||||
getFavContentReq = WeiyunGetFavContentReq(
|
||||
cidList = arrayListOf(id)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun addImageMsg(
|
||||
uin: Long,
|
||||
name: String,
|
||||
groupId: Long = 0,
|
||||
groupName: String = "",
|
||||
picUrl: String,
|
||||
pid: String,
|
||||
width: Int, height: Int,
|
||||
size: Long,
|
||||
md5: String,
|
||||
): Result<NetResp> {
|
||||
val md5Bytes = md5.hex2ByteArray()
|
||||
return sendWeiyunReq(20009, WeiyunCommonReq(
|
||||
addRichMediaReq = WeiyunAddRichMediaReq(
|
||||
commInfo = WeiyunCollectCommInfo(
|
||||
bid = 1u,
|
||||
category = 1u,
|
||||
author = WeiyunAuthor(
|
||||
type = if (groupId == 0L) 1u else 2u,
|
||||
numId = uin.toULong(),
|
||||
strId = name,
|
||||
groupId = groupId.toULong(),
|
||||
groupName = groupName
|
||||
),
|
||||
createTime = System.currentTimeMillis().toULong() - 2000u,
|
||||
seq = System.currentTimeMillis().toULong() - 1000u,
|
||||
bizDataList = arrayListOf("""{"recordAudioOnly":false,"audioOnly":false,"fileOnly":false}""".toByteArray()),
|
||||
originalAppId = 0u,
|
||||
customGroupId = 0u
|
||||
),
|
||||
summary = WeiyunRichMediaSummary(
|
||||
title = "",
|
||||
brief = "[图片]",
|
||||
picList = arrayListOf(
|
||||
WeiyunPicInfo(
|
||||
uri = picUrl,
|
||||
md5 = md5Bytes,
|
||||
sha1 = md5.toByteArray(),
|
||||
name = "",
|
||||
note = "",
|
||||
width = width.toUInt(),
|
||||
height = height.toUInt(),
|
||||
size = size.toULong(),
|
||||
type = 0u,
|
||||
picId = pid
|
||||
)
|
||||
),
|
||||
contentType = 1u
|
||||
),
|
||||
richMediaContent = listOf(
|
||||
WeiyunRichMediaContent(
|
||||
rawData = """<img src="$picUrl" />""".toByteArray(),
|
||||
picList = listOf(
|
||||
WeiyunPicInfo(
|
||||
uri = picUrl,
|
||||
md5 = md5Bytes,
|
||||
sha1 = md5.toByteArray(),
|
||||
name = "",
|
||||
note = "",
|
||||
width = width.toUInt(),
|
||||
height = height.toUInt(),
|
||||
size = size.toULong(),
|
||||
type = 0u,
|
||||
picId = pid
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun applyUpImageMsg(
|
||||
uin: Long,
|
||||
name: String,
|
||||
groupId: Long = 0,
|
||||
groupName: String = "",
|
||||
width: Int, height: Int,
|
||||
image: File
|
||||
): Result<NetResp> {
|
||||
if (!image.exists()) {
|
||||
return Result.failure(IllegalArgumentException("image file not exists"))
|
||||
}
|
||||
val md5 = MD5.genFileMd5(image.absolutePath)
|
||||
return sendWeiyunReq(20010, WeiyunCommonReq(
|
||||
fastUploadResourceReq = WeiyunFastUploadResourceReq(
|
||||
picInfoList = listOf(
|
||||
WeiyunPicInfo(
|
||||
md5 = md5,
|
||||
name = md5.toHexString(),
|
||||
width = width.toUInt(),
|
||||
height = height.toUInt(),
|
||||
size = image.length().toULong(),
|
||||
type = 1u,
|
||||
picId = "/storage/emulated/0/DCIM/temp.jpeg",
|
||||
owner = WeiyunAuthor(
|
||||
type = if (groupId == 0L) 1u else 2u,
|
||||
numId = uin.toULong(),
|
||||
strId = name,
|
||||
groupId = groupId.toULong(),
|
||||
groupName = groupName
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun addRichMediaMsg(
|
||||
uin: Long,
|
||||
name: String,
|
||||
groupId: Long = 0,
|
||||
groupName: String = "",
|
||||
time: Long = System.currentTimeMillis(),
|
||||
content: String
|
||||
): Result<NetResp> {
|
||||
return sendWeiyunReq(20009, WeiyunCommonReq(
|
||||
addRichMediaReq = WeiyunAddRichMediaReq(
|
||||
commInfo = WeiyunCollectCommInfo(
|
||||
bid = 1u,
|
||||
category = 1u,
|
||||
author = WeiyunAuthor(
|
||||
type = if (groupId == 0L) 1u else 2u,
|
||||
numId = uin.toULong(),
|
||||
strId = name,
|
||||
groupId = groupId.toULong(),
|
||||
groupName = groupName
|
||||
),
|
||||
createTime = time.toULong() - 2000u,
|
||||
seq = time.toULong() - 1000u,
|
||||
originalAppId = 0u,
|
||||
customGroupId = 0u
|
||||
),
|
||||
summary = WeiyunRichMediaSummary(
|
||||
brief = content,
|
||||
contentType = 1u
|
||||
),
|
||||
richMediaContent = listOf(
|
||||
WeiyunRichMediaContent(
|
||||
rawData = content.textToHtml().toByteArray(),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.textToHtml(): String {
|
||||
return replace("\n", "<div><br/></div>")
|
||||
}
|
||||
|
||||
suspend fun sendPicUpBlock(
|
||||
fileSize: Long,
|
||||
offset: Long,
|
||||
block: ByteArray,
|
||||
blockSize: Long,
|
||||
sha: ByteArray,
|
||||
pid: String,
|
||||
outputStream: ByteArrayOutputStream = ByteArrayOutputStream(),
|
||||
): Result<NetResp> {
|
||||
return suspendCancellableCoroutine {
|
||||
val httpNetReq = HttpNetReq()
|
||||
httpNetReq.userData = null
|
||||
httpNetReq.mCallback = object: INetEngineListener {
|
||||
override fun onResp(netResp: NetResp) {
|
||||
if (netResp.mHttpCode != 200 && netResp.mResult != 0 && netResp.mErrDesc.isNullOrEmpty()) {
|
||||
netResp.mErrDesc = netResp.mRespProperties["User-ErrMsg"]
|
||||
}
|
||||
netResp.mRespData = outputStream.toByteArray().copyOf()
|
||||
it.resume(Result.success(netResp))
|
||||
}
|
||||
|
||||
override fun onUpdateProgeress(netReq: NetReq, curr: Long, final: Long) {}
|
||||
}
|
||||
val vi = (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getA2(app.currentAccountUin)
|
||||
//LogCenter.log(pSKey)
|
||||
httpNetReq.mHttpMethod = HttpNetReq.HTTP_POST
|
||||
httpNetReq.mSendData = BytePacketBuilder().apply {
|
||||
writeInt(-1412589450)
|
||||
writeInt(10000)
|
||||
writeInt(0)
|
||||
writeInt(sha.size + 16 + blockSize.toInt())
|
||||
writeShort(0)
|
||||
writeShort(sha.size.toShort())
|
||||
writeFully(sha)
|
||||
writeInt(fileSize.toInt())
|
||||
writeInt(offset.toInt())
|
||||
writeInt(blockSize.toInt())
|
||||
writeFully(block)
|
||||
}.build().readBytes()
|
||||
httpNetReq.mOutStream = outputStream
|
||||
httpNetReq.mStartDownOffset = 0L
|
||||
httpNetReq.mReqProperties["Shamrock"] = "true"
|
||||
httpNetReq.mReqProperties["Cookie"] = String.format("uin=%s;vt=%d;vi=%s;pid=%s;appid=%d", app.currentAccountUin, 8, vi, pid, APPID)
|
||||
httpNetReq.mReqProperties["host"] = "pic.pieceup.qq.com"
|
||||
httpNetReq.mReqProperties["Range"] = "bytes=0-"
|
||||
httpNetReq.mReqProperties["Content-Length"] = httpNetReq.mSendData.size.toString()
|
||||
httpNetReq.mReqProperties["Accept-Encoding"] = "gzip"
|
||||
httpNetReq.mReqProperties["Content-Encoding"] = "gzip"
|
||||
httpNetReq.mPrioty = 1
|
||||
httpNetReq.mReqUrl = "https://pic.pieceup.qq.com/"
|
||||
httpNetReq.mServerList = SERVER_LIST_PICUP
|
||||
val service = AppRuntimeFetcher.appRuntime
|
||||
.getRuntimeService(IHttpEngineService::class.java, "qqfav")
|
||||
service.sendReq(httpNetReq)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendWeiyunReq(
|
||||
cmd: Int,
|
||||
req: WeiyunCommonReq,
|
||||
outputStream: ByteArrayOutputStream = ByteArrayOutputStream(),
|
||||
): Result<NetResp> {
|
||||
return suspendCancellableCoroutine {
|
||||
val httpNetReq = HttpNetReq()
|
||||
httpNetReq.userData = null
|
||||
httpNetReq.mCallback = object: INetEngineListener {
|
||||
override fun onResp(netResp: NetResp) {
|
||||
if (netResp.mHttpCode != 200 && netResp.mResult != 0 && netResp.mErrDesc.isNullOrEmpty()) {
|
||||
netResp.mErrDesc = netResp.mRespProperties["User-ErrMsg"]
|
||||
}
|
||||
netResp.mRespData = outputStream.toByteArray().copyOf()
|
||||
it.resume(Result.success(netResp))
|
||||
}
|
||||
|
||||
override fun onUpdateProgeress(netReq: NetReq, curr: Long, final: Long) {}
|
||||
}
|
||||
val pSKey = getWeiYunPSKey()
|
||||
httpNetReq.mHttpMethod = HttpNetReq.HTTP_POST
|
||||
httpNetReq.mSendData = DeflateTools.gzip(packData(packHead(cmd, pSKey), WeiyunComm(
|
||||
req = req
|
||||
).toByteArray()))
|
||||
httpNetReq.mOutStream = outputStream
|
||||
httpNetReq.mStartDownOffset = 0L
|
||||
httpNetReq.mReqProperties["Shamrock"] = "true"
|
||||
httpNetReq.mReqProperties["Cookie"] = String.format("uin=%s;vt=%d;vi=%s;appid=%d", app.currentAccountUin, 27, pSKey, APPID)
|
||||
httpNetReq.mReqProperties["host"] = "collector.weiyun.com"
|
||||
httpNetReq.mReqProperties["Range"] = "bytes=0-"
|
||||
httpNetReq.mReqProperties["Content-Length"] = httpNetReq.mSendData.size.toString()
|
||||
httpNetReq.mReqProperties["Accept-Encoding"] = "gzip"
|
||||
httpNetReq.mReqProperties["Content-Encoding"] = "gzip"
|
||||
httpNetReq.mPrioty = 1
|
||||
httpNetReq.mReqUrl = "https://collector.weiyun.com/collector.fcg"
|
||||
httpNetReq.mServerList = SERVER_LIST_COLLECTOR
|
||||
val service = AppRuntimeFetcher.appRuntime
|
||||
.getRuntimeService(IHttpEngineService::class.java, "qqfav")
|
||||
service.sendReq(httpNetReq)
|
||||
}
|
||||
}
|
||||
|
||||
private fun packHead(cmd: Int, pskey: String): ByteArray {
|
||||
return WeiyunMsgHead(
|
||||
uin = app.longAccountUin.toULong(),
|
||||
seq = seq++.toUInt(),
|
||||
type = 1u,
|
||||
cmd = cmd.toUInt(),
|
||||
appId = APPID.toUInt(),
|
||||
version = VERSION.toUInt(),
|
||||
netType = 3u,
|
||||
keyType = 27u,
|
||||
key = pskey.toByteArray(),
|
||||
majorVersion = MAJOR_VERSION.toUInt(),
|
||||
minorVersion = MINOR_VERSION.toUInt(),
|
||||
).toByteArray()
|
||||
}
|
||||
|
||||
private fun packData(head: ByteArray, body: ByteArray): ByteArray {
|
||||
val len = 16 + head.size + body.size
|
||||
val buf = ByteBuffer.allocate(len)
|
||||
buf.putInt(SUB_APPID)
|
||||
buf.putShort(1)
|
||||
buf.putInt(len)
|
||||
buf.putInt(body.size)
|
||||
buf.putShort(0)
|
||||
buf.put(head)
|
||||
buf.put(body)
|
||||
return buf.array()
|
||||
}
|
||||
|
||||
private fun getWeiYunPSKey(): String {
|
||||
val pskey = (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager)
|
||||
.getPskey(app.currentAccountUin, 16L, arrayOf("weiyun.com"), WeiYunPSKeyPromise)
|
||||
return if (pskey != null) pskey.getPSkey("weiyun.com") else ""
|
||||
}
|
||||
|
||||
private object WeiYunPSKeyPromise: WtTicketPromise {
|
||||
override fun Done(ticket: Ticket) {
|
||||
LogCenter.log("Fav: getPskeyPromise: done", Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun Failed(errMsg: ErrMsg) {
|
||||
LogCenter.log("Fav: getPskeyPromise: failed, $errMsg", Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun Timeout(errMsg: ErrMsg) {
|
||||
LogCenter.log("Fav: getPskeyPromise: timeout, $errMsg", Level.DEBUG)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet
|
||||
|
||||
import QQService.SvcDevLoginInfo
|
||||
import QQService.SvcReqGetDevLoginInfo
|
||||
import QQService.SvcRspGetDevLoginInfo
|
||||
import com.qq.jce.wup.UniPacket
|
||||
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
|
||||
import mqq.app.MobileQQ
|
||||
import mqq.app.Packet
|
||||
import oicq.wlogin_sdk.tools.util
|
||||
|
||||
internal object QSafeSvc: BaseSvc() {
|
||||
|
||||
suspend fun getOnlineClients(): ArrayList<SvcDevLoginInfo>? {
|
||||
val req = SvcReqGetDevLoginInfo()
|
||||
req.vecGuid = util.getGuidFromFile(MobileQQ.getContext())
|
||||
req.strAppName = MobileQQ.getMobileQQ().qqProcessName.split(":")[0]
|
||||
req.iLoginType = 1
|
||||
req.iRequireMax = 20
|
||||
req.iGetDevListType = 6
|
||||
|
||||
val uniPacket = UniPacket()
|
||||
uniPacket.servantName = "StatSvc"
|
||||
uniPacket.funcName = "SvcReqGetDevLoginInfo"
|
||||
uniPacket.put("SvcReqGetDevLoginInfo", req)
|
||||
val resp = sendBufferAW("StatSvc.GetDevLoginInfo", false, uniPacket.encode())
|
||||
?: return null
|
||||
|
||||
return Packet.decodePacket(resp, "SvcRspGetDevLoginInfo", SvcRspGetDevLoginInfo()).vecCurrentLoginDevInfo
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet
|
||||
|
||||
internal object VisitorSvc: BaseSvc() {
|
||||
const val FROM_C2C_AIO = 2
|
||||
const val FROM_CONDITION_SEARCH = 9
|
||||
const val FROM_CONTACTS_TAB = 5
|
||||
const val FROM_FACE_2_FACE_ADD_FRIEND = 11
|
||||
const val FROM_MAYKNOW_FRIEND = 3
|
||||
const val FROM_QCIRCLE = 4
|
||||
const val FROM_QQ_TROOP = 1
|
||||
const val FROM_QZONE = 7
|
||||
const val FROM_SCAN = 6
|
||||
const val FROM_SEARCH = 8
|
||||
const val FROM_SETTING_ME = 12
|
||||
const val FROM_SHARE_CARD = 10
|
||||
|
||||
const val IS_BLACK_LIST = "is_blacklist_user_profile"
|
||||
const val PROFILE_CARD_IS_BLACK = 2
|
||||
const val PROFILE_CARD_IS_BLACKED = 1
|
||||
const val PROFILE_CARD_NOT_BLACK = 3
|
||||
|
||||
const val SUB_FROM_C2C_AIO = 21
|
||||
const val SUB_FROM_C2C_INTERACTIVE_LOGO = 25
|
||||
const val SUB_FROM_C2C_LEFT_SLIDE = 23
|
||||
const val SUB_FROM_C2C_OTHER = 24
|
||||
const val SUB_FROM_C2C_SETTING = 22
|
||||
const val SUB_FROM_C2C_TOFU = 26
|
||||
const val SUB_FROM_CONDITION_SEARCH_OTHER = 99
|
||||
const val SUB_FROM_CONDITION_SEARCH_RESULT = 91
|
||||
const val SUB_FROM_CONTACTS_FRIEND_TAB = 51
|
||||
const val SUB_FROM_CONTACTS_TAB = 55
|
||||
const val SUB_FROM_FACE_2_FACE_ADD_FRIEND_RESULT_AVATAR = 111
|
||||
const val SUB_FROM_FACE_2_FACE_OTHER = 119
|
||||
const val SUB_FROM_FRIEND_APPLY = 56
|
||||
const val SUB_FROM_FRIEND_NOTIFY_MORE = 57
|
||||
const val SUB_FROM_FRIEND_NOTIFY_TAB = 54
|
||||
const val SUB_FROM_GROUPING_TAB = 52
|
||||
const val SUB_FROM_MAYKNOW_FRIEND_CONTACT_TAB = 31
|
||||
const val SUB_FROM_MAYKNOW_FRIEND_CONTACT_TAB_MORE = 37
|
||||
const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE = 34
|
||||
const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE_MORE = 39
|
||||
const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE_SEARCH = 36
|
||||
const val SUB_FROM_MAYKNOW_FRIEND_NEW_FRIEND_PAGE = 32
|
||||
const val SUB_FROM_MAYKNOW_FRIEND_OTHER = 35
|
||||
const val SUB_FROM_MAYKNOW_FRIEND_SEARCH = 33
|
||||
const val SUB_FROM_MAYKNOW_FRIEND_SEARCH_MORE = 38
|
||||
const val SUB_FROM_PHONE_LIST_TAB = 53
|
||||
const val SUB_FROM_QCIRCLE_OTHER = 42
|
||||
const val SUB_FROM_QCIRCLE_PROFILE = 41
|
||||
const val SUB_FROM_QQ_TROOP_ACTIVE_MEMBER = 15
|
||||
const val SUB_FROM_QQ_TROOP_ADMIN = 16
|
||||
const val SUB_FROM_QQ_TROOP_AIO = 11
|
||||
const val SUB_FROM_QQ_TROOP_MEMBER = 12
|
||||
const val SUB_FROM_QQ_TROOP_OTHER = 14
|
||||
const val SUB_FROM_QQ_TROOP_SETTING_MEMBER_LIST = 17
|
||||
const val SUB_FROM_QQ_TROOP_TEMP_SESSION = 13
|
||||
const val SUB_FROM_QRCODE_SCAN_DRAWER = 64
|
||||
const val SUB_FROM_QRCODE_SCAN_NEW = 61
|
||||
const val SUB_FROM_QRCODE_SCAN_OLD = 62
|
||||
const val SUB_FROM_QRCODE_SCAN_OTHER = 69
|
||||
const val SUB_FROM_QRCODE_SCAN_PROFILE = 63
|
||||
const val SUB_FROM_QZONE_HOME = 71
|
||||
const val SUB_FROM_QZONE_OTHER = 79
|
||||
const val SUB_FROM_SEARCH_CONTACT_TAB_MORE_FIND_PROFILE = 83
|
||||
const val SUB_FROM_SEARCH_FIND_PROFILE_TAB = 82
|
||||
const val SUB_FROM_SEARCH_MESSAGE_TAB_MORE_FIND_PROFILE = 84
|
||||
const val SUB_FROM_SEARCH_NEW_FRIEND_MORE_FIND_PROFILE = 85
|
||||
const val SUB_FROM_SEARCH_OTHER = 89
|
||||
const val SUB_FROM_SEARCH_TAB = 81
|
||||
const val SUB_FROM_SETTING_ME_AVATAR = 121
|
||||
const val SUB_FROM_SETTING_ME_OTHER = 129
|
||||
const val SUB_FROM_SHARE_CARD_C2C = 101
|
||||
const val SUB_FROM_SHARE_CARD_OTHER = 109
|
||||
const val SUB_FROM_SHARE_CARD_TROOP = 102
|
||||
const val SUB_FROM_TYPE_DEFAULT = 0
|
||||
|
||||
suspend fun vote(target: Long, count: Int): Result<Unit> {
|
||||
if(count !in 1 .. 20) {
|
||||
return Result.failure(IllegalArgumentException("vote count must be in 1 .. 20"))
|
||||
}
|
||||
val card = CardSvc.getProfileCard(target).onFailure {
|
||||
return Result.failure(RuntimeException("unable to fetch contact info"))
|
||||
}.getOrThrow()
|
||||
sendExtra("VisitorSvc.ReqFavorite") {
|
||||
it.putLong(moe.fuqiuluo.shamrock.remote.service.data.profile.ProfileProtocolConst.PARAM_SELF_UIN, currentUin.toLong())
|
||||
it.putLong(moe.fuqiuluo.shamrock.remote.service.data.profile.ProfileProtocolConst.PARAM_TARGET_UIN, target)
|
||||
it.putByteArray("vCookies", card.vCookies)
|
||||
it.putBoolean("nearby_people", true)
|
||||
it.putInt("favoriteSource", FROM_CONTACTS_TAB)
|
||||
it.putInt("iCount", count)
|
||||
it.putInt("from", FROM_CONTACTS_TAB)
|
||||
}
|
||||
return Result.success(Unit)
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet.ark
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
|
||||
import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77
|
||||
|
||||
internal object ArkMsgSvc: BaseSvc() {
|
||||
fun tryShareMusic(
|
||||
chatType: Int,
|
||||
peerId: Long,
|
||||
msgId: Long,
|
||||
arkAppInfo: ArkAppInfo,
|
||||
title: String,
|
||||
singer: String,
|
||||
jumpUrl: String,
|
||||
previewUrl: String,
|
||||
musicUrl: String,
|
||||
) {
|
||||
val req = oidb_cmd0xb77.ReqBody()
|
||||
req.appid.set(arkAppInfo.appId)
|
||||
req.app_type.set(1)
|
||||
req.msg_style.set(4)
|
||||
req.client_info.set(oidb_cmd0xb77.ClientInfo().also {
|
||||
it.platform.set(1)
|
||||
it.sdk_version.set(arkAppInfo.version)
|
||||
it.android_package_name.set(arkAppInfo.packageName)
|
||||
it.android_signature.set(arkAppInfo.signature)
|
||||
})
|
||||
req.ext_info.set(oidb_cmd0xb77.ExtInfo().also {
|
||||
it.msg_seq.set(msgId)
|
||||
})
|
||||
req.recv_uin.set(peerId)
|
||||
req.rich_msg_body.set(oidb_cmd0xb77.RichMsgBody().also {
|
||||
it.title.set(title)
|
||||
it.summary.set(singer)
|
||||
it.url.set(jumpUrl)
|
||||
it.picture_url.set(previewUrl)
|
||||
it.music_url.set(musicUrl)
|
||||
})
|
||||
when (chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> req.send_type.set(1)
|
||||
MsgConstant.KCHATTYPEC2C -> req.send_type.set(0)
|
||||
else -> error("不支持该聊天类型发送音乐分享")
|
||||
}
|
||||
sendOidb("OidbSvc.0xb77_9", 0xb77, 9, req.toByteArray())
|
||||
}
|
||||
|
||||
/*
|
||||
suspend fun tryShareJsonMessage(
|
||||
jsonString: String,
|
||||
arkAppInfo: ArkAppInfo = ArkAppInfo.DanMaKu,
|
||||
): Result<String> {
|
||||
val msgSeq = MessageHelper.generateMsgId(MsgConstant.KCHATTYPEC2C).qqMsgId
|
||||
val req = oidb_cmd0xb77.ReqBody()
|
||||
req.appid.set(arkAppInfo.appId)
|
||||
req.app_type.set(1)
|
||||
req.msg_style.set(10)
|
||||
req.client_info.set(oidb_cmd0xb77.ClientInfo().also {
|
||||
it.platform.set(1)
|
||||
it.sdk_version.set(arkAppInfo.version)
|
||||
it.android_package_name.set(arkAppInfo.packageName)
|
||||
it.android_signature.set(arkAppInfo.signature)
|
||||
})
|
||||
req.ext_info.set(oidb_cmd0xb77.ExtInfo().also {
|
||||
it.tag_name.set(ByteStringMicro.copyFromUtf8("shamrock"))
|
||||
it.msg_seq.set(msgSeq)
|
||||
})
|
||||
req.send_type.set(0)
|
||||
req.recv_uin.set(TicketSvc.getLongUin())
|
||||
req.mini_app_msg_body.set(oidb_cmd0xb77.MiniAppMsgBody().also {
|
||||
it.mini_app_appid.set(arkAppInfo.miniAppId)
|
||||
it.mini_app_path.set("pages")
|
||||
it.web_page_url.set("https://im.qq.com/index/")
|
||||
it.title.set("title")
|
||||
it.desc.set("desc")
|
||||
it.json_str.set(jsonString)
|
||||
})
|
||||
sendOidb("OidbSvc.0xb77_9", 0xb77, 9, req.toByteArray())
|
||||
val signedJson: String = withTimeoutOrNull(5.seconds) {
|
||||
suspendCancellableCoroutine {
|
||||
AioListener.registerTemporaryMsgListener(msgSeq) {
|
||||
it.resume(elements.first {
|
||||
it.elementType == MsgConstant.KELEMTYPEARKSTRUCT
|
||||
}.arkElement.bytesData)
|
||||
}
|
||||
it.invokeOnCancellation {
|
||||
AioListener.unregisterTemporaryMsgListener(msgSeq)
|
||||
}
|
||||
}
|
||||
} ?: return Result.failure(Exception("unable to sign json"))
|
||||
return Result.success(signedJson)
|
||||
}*/
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet.ark
|
||||
|
||||
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
|
||||
|
||||
internal object LightAppSvc: BaseSvc() {
|
||||
suspend fun adaptShare() {
|
||||
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet.msg
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.converter.ElemConverter
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.converter.NtMsgElementConverter
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
||||
import protobuf.message.Elem
|
||||
|
||||
@JvmName("elemListToSegments")
|
||||
internal suspend fun List<Elem>.toSegments(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String
|
||||
): List<MessageSegment> {
|
||||
val messageData = arrayListOf<MessageSegment>()
|
||||
this.forEach { msg ->
|
||||
kotlin.runCatching {
|
||||
val elementType = if (msg.text != null) {
|
||||
1
|
||||
} else if (msg.face != null) {
|
||||
2
|
||||
} else if (msg.notOnlineImage != null) {
|
||||
4
|
||||
} else if (msg.customFace != null) {
|
||||
8
|
||||
} else if (msg.generalFlags != null) {
|
||||
37
|
||||
} else if (msg.srcMsg != null) {
|
||||
45
|
||||
} else if (msg.lightApp != null) {
|
||||
51
|
||||
} else if (msg.commonElem != null) {
|
||||
53
|
||||
} else
|
||||
throw UnsupportedOperationException("不支持的消息element类型:$msg")
|
||||
val converter = ElemConverter[elementType]
|
||||
converter?.invoke(chatType, peerId, subPeer, msg)
|
||||
?: throw UnsupportedOperationException("不支持的消息element类型:$elementType")
|
||||
}.onSuccess {
|
||||
messageData.add(it)
|
||||
}.onFailure {
|
||||
if (it is UnknownError) {
|
||||
// 不处理的消息类型,抛出unknown error
|
||||
} else {
|
||||
LogCenter.log("消息element转换错误:$it", Level.WARN)
|
||||
}
|
||||
}
|
||||
}
|
||||
return messageData
|
||||
}
|
||||
|
||||
@JvmName("msgElementListToSegments")
|
||||
internal suspend fun List<MsgElement>.toSegments(chatType: Int, peerId: String, subPeer: String): List<MessageSegment> {
|
||||
val messageData = arrayListOf<MessageSegment>()
|
||||
this.forEach { msg ->
|
||||
kotlin.runCatching {
|
||||
val converter = NtMsgElementConverter[msg.elementType]
|
||||
converter?.invoke(chatType, peerId, subPeer, msg)
|
||||
?: throw UnsupportedOperationException("不支持的消息element类型:${msg.elementType}")
|
||||
}.onSuccess {
|
||||
messageData.add(it)
|
||||
}.onFailure {
|
||||
if (it is UnknownError) {
|
||||
// 不处理的消息类型,抛出unknown error
|
||||
} else {
|
||||
LogCenter.log("消息element转换错误:$it, elementType: ${msg.elementType}", Level.WARN)
|
||||
}
|
||||
}
|
||||
}
|
||||
return messageData
|
||||
}
|
||||
|
||||
internal suspend fun List<MsgElement>.toCQCode(chatType: Int, peerId: String, subPeer: String): String {
|
||||
if (this.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
return MessageHelper.nativeEncodeCQCode(this.toSegments(chatType, peerId, subPeer).map {
|
||||
val params = hashMapOf<String, String>()
|
||||
params["_type"] = it.type
|
||||
it.data.forEach { (key, value) ->
|
||||
params[key] = value.toString()
|
||||
}
|
||||
params
|
||||
})
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet.msg
|
||||
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import moe.fuqiuluo.shamrock.tools.json
|
||||
|
||||
|
||||
internal data class MessageSegment(
|
||||
val type: String,
|
||||
val data: Map<String, Any?> = emptyMap()
|
||||
) {
|
||||
fun toJson(): JsonObject {
|
||||
return mapOf(
|
||||
"type" to type.json,
|
||||
"data" to data.json
|
||||
).json
|
||||
}
|
||||
}
|
||||
|
||||
internal fun List<MessageSegment>.toJson(): JsonArray {
|
||||
return this.map {
|
||||
it.toJson()
|
||||
}.json
|
||||
}
|
||||
|
||||
internal fun List<MessageSegment>.toListMap(): List<Map<String, JsonElement>> {
|
||||
return this.map {
|
||||
mapOf(
|
||||
"type" to it.type.json,
|
||||
"data" to it.data.json
|
||||
).json
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet.msg
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import java.util.Collections
|
||||
|
||||
internal object MessageTempHandler {
|
||||
// 通过MSG SEQ临时监听器
|
||||
private val tempMessageListenerMap = Collections.synchronizedMap(HashMap<Long, suspend MsgRecord.() -> Unit>())
|
||||
|
||||
fun registerTemporaryMsgListener(
|
||||
msgSeq: Long,
|
||||
listener: suspend MsgRecord.() -> Unit
|
||||
) {
|
||||
LogCenter.log({ "注册临时消息监听器: $msgSeq" }, Level.DEBUG)
|
||||
tempMessageListenerMap[msgSeq] = listener
|
||||
}
|
||||
|
||||
fun unregisterTemporaryMsgListener(msgSeq: Long) {
|
||||
tempMessageListenerMap.remove(msgSeq)
|
||||
}
|
||||
|
||||
suspend fun notify(record: MsgRecord): Boolean {
|
||||
tempMessageListenerMap.firstNotNullOfOrNull {
|
||||
if (it.key == record.msgSeq) it else null
|
||||
}?.let {
|
||||
it.value(record)
|
||||
tempMessageListenerMap.remove(it.key)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
@ -1,622 +0,0 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet.msg.converter
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import io.ktor.util.*
|
||||
import kotlinx.io.core.ByteReadPacket
|
||||
import kotlinx.io.core.discardExact
|
||||
import kotlinx.io.core.readUInt
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment
|
||||
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
|
||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.helper.db.ImageDB
|
||||
import moe.fuqiuluo.shamrock.helper.db.ImageMapping
|
||||
import moe.fuqiuluo.shamrock.helper.db.MessageDB
|
||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||||
import moe.fuqiuluo.shamrock.tools.asString
|
||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
||||
import protobuf.message.Elem
|
||||
import protobuf.message.element.commelem.ButtonExtra
|
||||
import protobuf.message.element.commelem.MarkdownExtra
|
||||
import protobuf.message.element.commelem.QFaceExtra
|
||||
|
||||
|
||||
internal typealias IElemConverter = suspend (Int, String, String, Elem) -> MessageSegment
|
||||
|
||||
internal object ElemConverter {
|
||||
private val convertMap = mapOf(
|
||||
1 to ElemConverter::convertTextElem,
|
||||
2 to ElemConverter::convertFaceElem,
|
||||
4 to ElemConverter::convertNotOnlineImageElem,
|
||||
8 to ElemConverter::convertCustomFaceElem,
|
||||
// MsgConstant.KELEMTYPEPTT to ElemConverter::convertVoiceElem,
|
||||
// MsgConstant.KELEMTYPEVIDEO to ElemConverter::convertVideoElem,
|
||||
// MsgConstant.KELEMTYPEMARKETFACE to ElemConverter::convertMarketFaceElem,
|
||||
37 to ElemConverter::convertGeneralFlagsElem,
|
||||
45 to ElemConverter::convertReplyElem,
|
||||
51 to ElemConverter::convertStructJsonElem,
|
||||
53 to ElemConverter::convertCommonElem,
|
||||
// MsgConstant.KELEMTYPEGRAYTIP to ElemConverter::convertGrayTipsElem,
|
||||
// MsgConstant.KELEMTYPEFILE to ElemConverter::convertFileElem,
|
||||
// //MsgConstant.KELEMTYPEMULTIFORWARD to ElemConverter::convertXmlMultiMsgElem,
|
||||
// //MsgConstant.KELEMTYPESTRUCTLONGMSG to ElemConverter::convertXmlLongMsgElem,
|
||||
// MsgConstant.KELEMTYPEFACEBUBBLE to ElemConverter::convertBubbleFaceElem,
|
||||
)
|
||||
|
||||
operator fun get(type: Int): IElemConverter? = convertMap[type]
|
||||
|
||||
/**
|
||||
* 文本 / 艾特 消息转换消息段
|
||||
*/
|
||||
private suspend fun convertTextElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: Elem
|
||||
): MessageSegment {
|
||||
val text = element.text!!
|
||||
if (text.attr6Buf != null) {
|
||||
val at = ByteReadPacket(text.attr6Buf!!)
|
||||
at.discardExact(7)
|
||||
val uin = at.readUInt()
|
||||
return MessageSegment(
|
||||
type = "at",
|
||||
data = mapOf(
|
||||
"qq" to uin
|
||||
)
|
||||
)
|
||||
} else {
|
||||
return MessageSegment(
|
||||
type = "text",
|
||||
data = mapOf(
|
||||
"text" to text.str!!
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 小表情 / 戳一戳 消息转换消息段
|
||||
*/
|
||||
private suspend fun convertFaceElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: Elem
|
||||
): MessageSegment {
|
||||
val face = element.face!!
|
||||
return MessageSegment(
|
||||
type = "face",
|
||||
data = mapOf(
|
||||
"id" to face.index!!
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片消息转换消息段
|
||||
*/
|
||||
private suspend fun convertCustomFaceElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: Elem
|
||||
): MessageSegment {
|
||||
val customFace = element.customFace!!
|
||||
|
||||
val md5 = customFace.md5.toHexString()
|
||||
|
||||
ImageDB.getInstance().imageMappingDao().insert(
|
||||
ImageMapping(md5.uppercase(), chatType, customFace.size!!.toLong())
|
||||
)
|
||||
|
||||
val origUrl = customFace.origUrl!!
|
||||
|
||||
return MessageSegment(
|
||||
type = "image",
|
||||
data = mapOf(
|
||||
"file" to md5,
|
||||
"url" to when (chatType) {
|
||||
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
|
||||
origUrl,
|
||||
md5
|
||||
)
|
||||
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5)
|
||||
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
||||
},
|
||||
"type" to if (customFace.origin == true) "original" else "show"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun convertNotOnlineImageElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: Elem
|
||||
): MessageSegment {
|
||||
val notOnlineImage = element.notOnlineImage!!
|
||||
|
||||
val md5 = notOnlineImage.picMd5.toHexString()
|
||||
|
||||
ImageDB.getInstance().imageMappingDao().insert(
|
||||
ImageMapping(md5.uppercase(), chatType, notOnlineImage.fileLen!!.toLong())
|
||||
)
|
||||
|
||||
val origUrl = notOnlineImage.origUrl!!
|
||||
|
||||
return MessageSegment(
|
||||
type = "image",
|
||||
data = mapOf(
|
||||
"file" to md5,
|
||||
"url" to when (chatType) {
|
||||
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
|
||||
origUrl,
|
||||
md5
|
||||
)
|
||||
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5)
|
||||
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
||||
},
|
||||
"type" to if (notOnlineImage.original == true) "original" else "show"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun convertGeneralFlagsElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: Elem
|
||||
): MessageSegment {
|
||||
val generalFlags = element.generalFlags!!
|
||||
if (generalFlags.longTextFlag == 1u) {
|
||||
return MessageSegment(
|
||||
type = "general_flags",
|
||||
data = mapOf(
|
||||
"res_id" to generalFlags.longTextResid
|
||||
)
|
||||
)
|
||||
}
|
||||
throw UnknownError("no segment")
|
||||
}
|
||||
|
||||
//
|
||||
// /**
|
||||
// * 语音消息转换消息段
|
||||
// */
|
||||
// private suspend fun convertVoiceElem(
|
||||
// chatType: Int,
|
||||
// peerId: String,
|
||||
// subPeer: String,
|
||||
// element: Elem
|
||||
// ): MessageSegment {
|
||||
// val record = element.pttElement
|
||||
//
|
||||
// val md5 = if (record.fileName.startsWith("silk"))
|
||||
// record.fileName.substring(5)
|
||||
// else record.md5HexStr
|
||||
//
|
||||
// return MessageSegment(
|
||||
// type = "record",
|
||||
// data = mapOf(
|
||||
// "file" to md5,
|
||||
// "url" to when (chatType) {
|
||||
// MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPttDownUrl(
|
||||
// "0",
|
||||
// record.md5HexStr,
|
||||
// record.fileUuid
|
||||
// )
|
||||
//
|
||||
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid)
|
||||
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
|
||||
// "0",
|
||||
// record.md5HexStr,
|
||||
// record.fileUuid
|
||||
// )
|
||||
//
|
||||
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
||||
// }
|
||||
// ).also {
|
||||
// if (record.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE) {
|
||||
// it["magic"] = "1"
|
||||
// }
|
||||
// if ((it["url"] as String).isBlank()) {
|
||||
// it.remove("url")
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 视频消息转换消息段
|
||||
// */
|
||||
// private suspend fun convertVideoElem(
|
||||
// chatType: Int,
|
||||
// peerId: String,
|
||||
// subPeer: String,
|
||||
// element: Elem
|
||||
// ): MessageSegment {
|
||||
// val video = element.videoElement
|
||||
// val md5 = if (video.fileName.contains("/")) {
|
||||
// video.videoMd5.takeIf {
|
||||
// !it.isNullOrEmpty()
|
||||
// }?.hex2ByteArray() ?: video.fileName.split("/").let {
|
||||
// it[it.size - 2].hex2ByteArray()
|
||||
// }
|
||||
// } else video.fileName.split(".")[0].hex2ByteArray()
|
||||
//
|
||||
// //LogCenter.log({ "receive video msg: $video" }, Level.DEBUG)
|
||||
//
|
||||
// return MessageSegment(
|
||||
// type = "video",
|
||||
// data = mapOf(
|
||||
// "file" to video.fileName,
|
||||
// "url" to when (chatType) {
|
||||
// MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
||||
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
|
||||
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
||||
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
||||
// }
|
||||
// ).also {
|
||||
// if ((it["url"] as String).isBlank())
|
||||
// it.remove("url")
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 商城大表情消息转换消息段
|
||||
// */
|
||||
// private suspend fun convertMarketFaceElem(
|
||||
// chatType: Int,
|
||||
// peerId: String,
|
||||
// subPeer: String,
|
||||
// element: Elem
|
||||
// ): MessageSegment {
|
||||
// val face = element.marketFaceElement
|
||||
// return when (face.emojiId.lowercase()) {
|
||||
// "4823d3adb15df08014ce5d6796b76ee1" -> MessageSegment("dice")
|
||||
// "83c8a293ae65ca140f348120a77448ee" -> MessageSegment("rps")
|
||||
// else -> MessageSegment(
|
||||
// type = "mface",
|
||||
// data = mapOf(
|
||||
// "id" to face.emojiId
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
/**
|
||||
* 回复消息转消息段
|
||||
*/
|
||||
private suspend fun convertReplyElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: Elem
|
||||
): MessageSegment {
|
||||
val srcMsg = element.srcMsg!!
|
||||
val msgId = srcMsg.pbReserve?.msgRand?.toLong() ?: 0
|
||||
val msgHash = if (msgId != 0L) {
|
||||
MessageHelper.generateMsgIdHash(chatType, msgId)
|
||||
} else {
|
||||
val msgSeq = srcMsg.origSeqs?.first()?.toInt() ?: 0
|
||||
MessageDB.getInstance().messageMappingDao()
|
||||
.queryByMsgSeq(chatType, peerId, msgSeq)?.msgHashId
|
||||
?: kotlin.run {
|
||||
LogCenter.log("消息映射关系未找到: Message($msgSeq)", Level.WARN)
|
||||
MessageHelper.generateMsgIdHash(chatType, msgId)
|
||||
}
|
||||
}
|
||||
|
||||
return MessageSegment(
|
||||
type = "reply",
|
||||
data = mapOf(
|
||||
"id" to msgHash
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON消息转消息段
|
||||
*/
|
||||
private suspend fun convertStructJsonElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: Elem
|
||||
): MessageSegment {
|
||||
val data = element.lightApp!!.data!!
|
||||
val jsonStr =
|
||||
(if (data[0].toInt() == 1) DeflateTools.uncompress(data.sliceArray(1 until data.size)) else data.sliceArray(
|
||||
1 until data.size
|
||||
)).toString()
|
||||
val json = jsonStr.asJsonObject
|
||||
return when (json["app"].asString) {
|
||||
"com.tencent.multimsg" -> {
|
||||
val info = json["meta"].asJsonObject["detail"].asJsonObject
|
||||
MessageSegment(
|
||||
type = "forward",
|
||||
data = mapOf(
|
||||
"id" to info["resid"].asString
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"com.tencent.troopsharecard" -> {
|
||||
val info = json["meta"].asJsonObject["contact"].asJsonObject
|
||||
MessageSegment(
|
||||
type = "contact",
|
||||
data = mapOf(
|
||||
"type" to "group",
|
||||
"id" to info["jumpUrl"].asString.split("group_code=")[1]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"com.tencent.contact.lua" -> {
|
||||
val info = json["meta"].asJsonObject["contact"].asJsonObject
|
||||
MessageSegment(
|
||||
type = "contact",
|
||||
data = mapOf(
|
||||
"type" to "private",
|
||||
"id" to info["jumpUrl"].asString.split("uin=")[1]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"com.tencent.map" -> {
|
||||
val info = json["meta"].asJsonObject["Location.Search"].asJsonObject
|
||||
MessageSegment(
|
||||
type = "location",
|
||||
data = mapOf(
|
||||
"lat" to info["lat"].asString,
|
||||
"lon" to info["lng"].asString,
|
||||
"content" to info["address"].asString,
|
||||
"title" to info["name"].asString
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> MessageSegment(
|
||||
type = "json",
|
||||
data = mapOf(
|
||||
"data" to jsonStr
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun convertCommonElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: Elem
|
||||
): MessageSegment {
|
||||
val commonElem = element.commonElem!!
|
||||
return when (commonElem.serviceType) {
|
||||
|
||||
37 -> {
|
||||
val qFaceExtra = commonElem.elem!!.decodeProtobuf<QFaceExtra>()
|
||||
when (qFaceExtra.faceId) {
|
||||
358 -> MessageSegment(
|
||||
type = "dice",
|
||||
data = mapOf(
|
||||
"result" to qFaceExtra.result!!
|
||||
)
|
||||
)
|
||||
|
||||
359 -> MessageSegment(
|
||||
type = "rps",
|
||||
data = mapOf(
|
||||
"result" to qFaceExtra.result!!
|
||||
)
|
||||
)
|
||||
|
||||
else -> MessageSegment(
|
||||
type = "face",
|
||||
data = mapOf(
|
||||
"id" to qFaceExtra.faceId!!,
|
||||
"big" to true,
|
||||
"result" to qFaceExtra.result!! // (1布 2剪 3锤) (骰子123456)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
45 -> {
|
||||
val markdownExtra = commonElem.elem!!.decodeProtobuf<MarkdownExtra>()
|
||||
MessageSegment(
|
||||
type = "markdown",
|
||||
data = mapOf(
|
||||
"content" to markdownExtra.content!!
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
46 -> {
|
||||
val buttonExtra = commonElem.elem!!.decodeProtobuf<ButtonExtra>()
|
||||
MessageSegment(
|
||||
type = "button",
|
||||
data = buttonExtra.field1!!.let {
|
||||
mapOf(
|
||||
"rows" to it.rows!!.map { row ->
|
||||
mapOf(
|
||||
"buttons" to row.buttons!!.map { button ->
|
||||
val renderData = button.renderData
|
||||
val action = button.action
|
||||
val permission = action?.permission
|
||||
mapOf(
|
||||
"id" to button.id,
|
||||
"render_data" to mapOf(
|
||||
"label" to (renderData?.label ?: ""),
|
||||
"visited_label" to (renderData?.visitedLabel ?: ""),
|
||||
"style" to (renderData?.style ?: 0)
|
||||
),
|
||||
"action" to mapOf(
|
||||
"type" to (action?.type ?: 0),
|
||||
"permission" to mapOf(
|
||||
"type" to (permission?.type ?: 0),
|
||||
"specify_role_ids" to permission?.specifyRoleIds,
|
||||
"specify_user_ids" to permission?.specifyUserIds
|
||||
),
|
||||
"unsupport_tips" to (action?.unsupportTips ?: ""),
|
||||
"data" to (action?.data ?: ""),
|
||||
"reply" to action?.reply,
|
||||
"enter" to action?.enter,
|
||||
)
|
||||
)
|
||||
})
|
||||
},
|
||||
"appid" to it.appid
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
else -> MessageSegment(
|
||||
type = "common",
|
||||
data = mapOf(
|
||||
"data" to commonElem.elem!!.encodeBase64()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// /**
|
||||
// * 灰色提示条消息过滤
|
||||
// */
|
||||
// private suspend fun convertGrayTipsElem(
|
||||
// chatType: Int,
|
||||
// peerId: String,
|
||||
// subPeer: String,
|
||||
// element: Elem
|
||||
// ): MessageSegment {
|
||||
// val tip = element.grayTipElement
|
||||
// when (tip.subElementType) {
|
||||
// MsgConstant.GRAYTIPELEMENTSUBTYPEJSON -> {
|
||||
// val notify = tip.jsonGrayTipElement
|
||||
// when (notify.busiId) {
|
||||
// /* 新人入群 */ 17L, /* 群戳一戳 */1061L,
|
||||
// /* 群撤回 */1014L, /* 群设精消息 */2401L,
|
||||
// /* 群头衔 */2407L -> {
|
||||
// }
|
||||
//
|
||||
// else -> LogCenter.log("不支持的灰条类型(JSON): ${notify.busiId}", Level.WARN)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> {
|
||||
// val notify = tip.xmlElement
|
||||
// when (notify.busiId) {
|
||||
// /* 群戳一戳 */1061L, /* 群打卡 */1068L -> {}
|
||||
// else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// else -> LogCenter.log("不支持的提示类型: ${tip.subElementType}", Level.WARN)
|
||||
// }
|
||||
// // 提示类消息,这里提供的是一个xml,不具备解析通用性
|
||||
// // 在这里不推送
|
||||
// throw UnknownError()
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 文件消息转换消息段
|
||||
// */
|
||||
// private suspend fun convertFileElem(
|
||||
// chatType: Int,
|
||||
// peerId: String,
|
||||
// subPeer: String,
|
||||
// element: Elem
|
||||
// ): MessageSegment {
|
||||
// val fileMsg = element.fileElement
|
||||
// val fileName = fileMsg.fileName
|
||||
// val fileSize = fileMsg.fileSize
|
||||
// val expireTime = fileMsg.expireTime ?: 0
|
||||
// val fileId = fileMsg.fileUuid
|
||||
// val bizId = fileMsg.fileBizId ?: 0
|
||||
// val fileSubId = fileMsg.fileSubId ?: ""
|
||||
// val url = when (chatType) {
|
||||
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
|
||||
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(peerId, subPeer, fileId, bizId)
|
||||
// else -> RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId)
|
||||
// }
|
||||
//
|
||||
// return MessageSegment(
|
||||
// type = "file",
|
||||
// data = mapOf(
|
||||
// "name" to fileName,
|
||||
// "size" to fileSize,
|
||||
// "expire" to expireTime,
|
||||
// "id" to fileId,
|
||||
// "url" to url,
|
||||
// "biz" to bizId,
|
||||
// "sub" to fileSubId
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 老板QQ的合并转发信息
|
||||
// */
|
||||
// private suspend fun convertXmlMultiMsgElem(
|
||||
// chatType: Int,
|
||||
// peerId: String,
|
||||
// subPeer: String,
|
||||
// element: Elem
|
||||
// ): MessageSegment {
|
||||
// val multiMsg = element.multiForwardElem
|
||||
// return MessageSegment(
|
||||
// type = "forward",
|
||||
// data = mapOf(
|
||||
// "id" to multiMsg.resId
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// private suspend fun convertXmlLongMsgElem(
|
||||
// chatType: Int,
|
||||
// peerId: String,
|
||||
// subPeer: String,
|
||||
// element: Elem
|
||||
// ): MessageSegment {
|
||||
// val longMsg = element.structLongElem
|
||||
// return MessageSegment(
|
||||
// type = "forward",
|
||||
// data = mapOf(
|
||||
// "id" to longMsg.resId
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
//
|
||||
//
|
||||
// private suspend fun convertBubbleFaceElem(
|
||||
// chatType: Int,
|
||||
// peerId: String,
|
||||
// subPeer: String,
|
||||
// element: Elem
|
||||
// ): MessageSegment {
|
||||
// val bubbleElement = element.faceBubbleElement
|
||||
// return MessageSegment(
|
||||
// type = "bubble_face",
|
||||
// data = mapOf(
|
||||
// "id" to bubbleElement.yellowFaceInfo.index,
|
||||
// "count" to (bubbleElement.faceCount ?: 1),
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
|
||||
|
||||
}
|
@ -1,595 +0,0 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet.msg.converter
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonArray
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment
|
||||
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
|
||||
import moe.fuqiuluo.shamrock.helper.ContactHelper
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
||||
import moe.fuqiuluo.shamrock.helper.db.ImageDB
|
||||
import moe.fuqiuluo.shamrock.helper.db.ImageMapping
|
||||
import moe.fuqiuluo.shamrock.helper.db.MessageDB
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||||
import moe.fuqiuluo.shamrock.tools.asString
|
||||
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
|
||||
|
||||
internal typealias IMsgElementConverter = suspend (Int, String, String, MsgElement) -> MessageSegment
|
||||
|
||||
internal object NtMsgElementConverter {
|
||||
private val convertMap = hashMapOf(
|
||||
MsgConstant.KELEMTYPETEXT to NtMsgElementConverter::convertTextElem,
|
||||
MsgConstant.KELEMTYPEFACE to NtMsgElementConverter::convertFaceElem,
|
||||
MsgConstant.KELEMTYPEPIC to NtMsgElementConverter::convertImageElem,
|
||||
MsgConstant.KELEMTYPEPTT to NtMsgElementConverter::convertVoiceElem,
|
||||
MsgConstant.KELEMTYPEVIDEO to NtMsgElementConverter::convertVideoElem,
|
||||
MsgConstant.KELEMTYPEMARKETFACE to NtMsgElementConverter::convertMarketFaceElem,
|
||||
MsgConstant.KELEMTYPEARKSTRUCT to NtMsgElementConverter::convertStructJsonElem,
|
||||
MsgConstant.KELEMTYPEREPLY to NtMsgElementConverter::convertReplyElem,
|
||||
MsgConstant.KELEMTYPEGRAYTIP to NtMsgElementConverter::convertGrayTipsElem,
|
||||
MsgConstant.KELEMTYPEFILE to NtMsgElementConverter::convertFileElem,
|
||||
MsgConstant.KELEMTYPEMARKDOWN to NtMsgElementConverter::convertMarkdownElem,
|
||||
//MsgConstant.KELEMTYPEMULTIFORWARD to MsgElementConverter::convertXmlMultiMsgElem,
|
||||
//MsgConstant.KELEMTYPESTRUCTLONGMSG to MsgElementConverter::convertXmlLongMsgElem,
|
||||
MsgConstant.KELEMTYPEFACEBUBBLE to NtMsgElementConverter::convertBubbleFaceElem,
|
||||
MsgConstant.KELEMTYPEINLINEKEYBOARD to NtMsgElementConverter::convertInlineKeyboardElem
|
||||
)
|
||||
|
||||
operator fun get(type: Int): IMsgElementConverter? = convertMap[type]
|
||||
|
||||
/**
|
||||
* 文本 / 艾特 消息转换消息段
|
||||
*/
|
||||
private suspend fun convertTextElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val text = element.textElement
|
||||
return if (text.atType != MsgConstant.ATTYPEUNKNOWN) {
|
||||
MessageSegment(
|
||||
type = "at",
|
||||
data = hashMapOf(
|
||||
"qq" to ContactHelper.getUinByUidAsync(text.atNtUid),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
MessageSegment(
|
||||
type = "text",
|
||||
data = hashMapOf(
|
||||
"text" to text.content
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 小表情 / 戳一戳 消息转换消息段
|
||||
*/
|
||||
private suspend fun convertFaceElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val face = element.faceElement
|
||||
|
||||
if (face.faceType == 5) {
|
||||
return MessageSegment(
|
||||
type = "poke",
|
||||
data = hashMapOf(
|
||||
"type" to face.pokeType,
|
||||
"id" to face.vaspokeId,
|
||||
"strength" to face.pokeStrength
|
||||
)
|
||||
)
|
||||
}
|
||||
when (face.faceIndex) {
|
||||
114 -> {
|
||||
return MessageSegment(
|
||||
type = "basketball",
|
||||
data = hashMapOf(
|
||||
"id" to face.resultId.ifEmpty { "0" }.toInt(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
358 -> {
|
||||
if (face.sourceType == 1) return MessageSegment("new_dice")
|
||||
return MessageSegment(
|
||||
type = "new_dice",
|
||||
data = hashMapOf(
|
||||
"id" to face.resultId.ifEmpty { "0" }.toInt()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
359 -> {
|
||||
if (face.resultId.isEmpty()) return MessageSegment("new_rps")
|
||||
return MessageSegment(
|
||||
type = "new_rps",
|
||||
data = hashMapOf(
|
||||
"id" to face.resultId.ifEmpty { "0" }.toInt()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
394 -> {
|
||||
//LogCenter.log(face.toString())
|
||||
return MessageSegment(
|
||||
type = "face",
|
||||
data = hashMapOf(
|
||||
"id" to face.faceIndex,
|
||||
"big" to (face.faceType == 3),
|
||||
"result" to (face.resultId ?: "1")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> return MessageSegment(
|
||||
type = "face",
|
||||
data = hashMapOf(
|
||||
"id" to face.faceIndex,
|
||||
"big" to (face.faceType == 3)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片消息转换消息段
|
||||
*/
|
||||
private suspend fun convertImageElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val image = element.picElement
|
||||
val md5 = (image.md5HexStr ?: image.fileName
|
||||
.replace("{", "")
|
||||
.replace("}", "")
|
||||
.replace("-", "").split(".")[0])
|
||||
.uppercase()
|
||||
|
||||
ImageDB.getInstance().imageMappingDao().insert(
|
||||
ImageMapping(md5, chatType, image.fileSize)
|
||||
)
|
||||
|
||||
//LogCenter.log(image.toString())
|
||||
|
||||
val originalUrl = image.originImageUrl ?: ""
|
||||
LogCenter.log({ "receive image: $image" }, Level.DEBUG)
|
||||
|
||||
return MessageSegment(
|
||||
type = "image",
|
||||
data = hashMapOf(
|
||||
"file" to md5,
|
||||
"url" to when (chatType) {
|
||||
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
|
||||
originalUrl = originalUrl,
|
||||
md5 = md5,
|
||||
fileId = image.fileUuid,
|
||||
width = image.picWidth.toUInt(),
|
||||
height = image.picHeight.toUInt(),
|
||||
sha = "",
|
||||
fileSize = image.fileSize.toULong(),
|
||||
peer = peerId
|
||||
)
|
||||
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(
|
||||
originalUrl = originalUrl,
|
||||
md5 = md5,
|
||||
fileId = image.fileUuid,
|
||||
width = image.picWidth.toUInt(),
|
||||
height = image.picHeight.toUInt(),
|
||||
sha = "",
|
||||
fileSize = image.fileSize.toULong(),
|
||||
peer = peerId,
|
||||
)
|
||||
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(
|
||||
originalUrl = originalUrl,
|
||||
md5 = md5,
|
||||
fileId = image.fileUuid,
|
||||
width = image.picWidth.toUInt(),
|
||||
height = image.picHeight.toUInt(),
|
||||
sha = "",
|
||||
fileSize = image.fileSize.toULong(),
|
||||
peer = peerId,
|
||||
subPeer = subPeer
|
||||
)
|
||||
|
||||
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
||||
},
|
||||
"subType" to image.picSubType,
|
||||
"type" to if (image.isFlashPic == true) "flash" else if (image.original) "original" else "show"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 语音消息转换消息段
|
||||
*/
|
||||
private suspend fun convertVoiceElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val record = element.pttElement
|
||||
|
||||
val md5 = if (record.fileName.startsWith("silk"))
|
||||
record.fileName.substring(5)
|
||||
else record.md5HexStr
|
||||
|
||||
return MessageSegment(
|
||||
type = "record",
|
||||
data = hashMapOf(
|
||||
"file" to md5,
|
||||
"url" to when (chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPttDownUrl(
|
||||
"0",
|
||||
record.md5HexStr,
|
||||
record.fileUuid
|
||||
)
|
||||
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
|
||||
"0",
|
||||
record.md5HexStr,
|
||||
record.fileUuid
|
||||
)
|
||||
|
||||
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
||||
}
|
||||
).also {
|
||||
if (record.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE) {
|
||||
it["magic"] = "1"
|
||||
}
|
||||
if ((it["url"] as String).isBlank()) {
|
||||
it.remove("url")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频消息转换消息段
|
||||
*/
|
||||
private suspend fun convertVideoElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val video = element.videoElement
|
||||
val md5 = if (video.fileName.contains("/")) {
|
||||
video.videoMd5.takeIf {
|
||||
!it.isNullOrEmpty()
|
||||
}?.hex2ByteArray() ?: video.fileName.split("/").let {
|
||||
it[it.size - 2].hex2ByteArray()
|
||||
}
|
||||
} else video.fileName.split(".")[0].hex2ByteArray()
|
||||
|
||||
//LogCenter.log({ "receive video msg: $video" }, Level.DEBUG)
|
||||
|
||||
return MessageSegment(
|
||||
type = "video",
|
||||
data = hashMapOf(
|
||||
"file" to video.fileName,
|
||||
"url" to when (chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
||||
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
||||
}
|
||||
).also {
|
||||
if ((it["url"] as String).isBlank())
|
||||
it.remove("url")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 商城大表情消息转换消息段
|
||||
*/
|
||||
private suspend fun convertMarketFaceElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val face = element.marketFaceElement
|
||||
return when (face.emojiId.lowercase()) {
|
||||
"4823d3adb15df08014ce5d6796b76ee1" -> MessageSegment("dice")
|
||||
"83c8a293ae65ca140f348120a77448ee" -> MessageSegment("rps")
|
||||
else -> MessageSegment(
|
||||
type = "mface",
|
||||
data = hashMapOf(
|
||||
"id" to face.emojiId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON消息转消息段
|
||||
*/
|
||||
private suspend fun convertStructJsonElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val data = element.arkElement.bytesData.asJsonObject
|
||||
return when (data["app"].asString) {
|
||||
"com.tencent.multimsg" -> {
|
||||
val info = data["meta"].asJsonObject["detail"].asJsonObject
|
||||
MessageSegment(
|
||||
type = "forward",
|
||||
data = mapOf(
|
||||
"id" to info["resid"].asString
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"com.tencent.troopsharecard" -> {
|
||||
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
||||
MessageSegment(
|
||||
type = "contact",
|
||||
data = hashMapOf(
|
||||
"type" to "group",
|
||||
"id" to info["jumpUrl"].asString.split("group_code=")[1]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"com.tencent.contact.lua" -> {
|
||||
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
||||
MessageSegment(
|
||||
type = "contact",
|
||||
data = hashMapOf(
|
||||
"type" to "private",
|
||||
"id" to info["jumpUrl"].asString.split("uin=")[1]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"com.tencent.map" -> {
|
||||
val info = data["meta"].asJsonObject["Location.Search"].asJsonObject
|
||||
MessageSegment(
|
||||
type = "location",
|
||||
data = hashMapOf(
|
||||
"lat" to info["lat"].asString,
|
||||
"lon" to info["lng"].asString,
|
||||
"content" to info["address"].asString,
|
||||
"title" to info["name"].asString
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> MessageSegment(
|
||||
type = "json",
|
||||
data = mapOf(
|
||||
"data" to element.arkElement.bytesData.asJsonObject.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回复消息转消息段
|
||||
*/
|
||||
private suspend fun convertReplyElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val reply = element.replyElement
|
||||
val msgId = reply.replayMsgId
|
||||
val msgHash = if (msgId != 0L) {
|
||||
MessageHelper.generateMsgIdHash(chatType, msgId)
|
||||
} else {
|
||||
MessageDB.getInstance().messageMappingDao()
|
||||
.queryByMsgSeq(chatType, peerId, reply.replayMsgSeq?.toInt() ?: 0)?.msgHashId
|
||||
?: kotlin.run {
|
||||
LogCenter.log("消息映射关系未找到: Message($reply)", Level.WARN)
|
||||
MessageHelper.generateMsgIdHash(chatType, reply.sourceMsgIdInRecords)
|
||||
}
|
||||
}
|
||||
|
||||
return MessageSegment(
|
||||
type = "reply",
|
||||
data = mapOf(
|
||||
"id" to msgHash
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 灰色提示条消息过滤
|
||||
*/
|
||||
private suspend fun convertGrayTipsElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val tip = element.grayTipElement
|
||||
when (tip.subElementType) {
|
||||
MsgConstant.GRAYTIPELEMENTSUBTYPEJSON -> {
|
||||
val notify = tip.jsonGrayTipElement
|
||||
when (notify.busiId) {
|
||||
/* 新人入群 */ 17L, /* 群戳一戳 */1061L,
|
||||
/* 群撤回 */1014L, /* 群设精消息 */2401L,
|
||||
/* 群头衔 */2407L -> {
|
||||
}
|
||||
|
||||
else -> LogCenter.log("不支持的灰条类型(JSON): ${notify.busiId}", Level.WARN)
|
||||
}
|
||||
}
|
||||
|
||||
MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> {
|
||||
val notify = tip.xmlElement
|
||||
when (notify.busiId) {
|
||||
/* 群戳一戳 */1061L, /* 群打卡 */1068L -> {}
|
||||
else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN)
|
||||
}
|
||||
}
|
||||
|
||||
else -> LogCenter.log("不支持的提示类型: ${tip.subElementType}", Level.WARN)
|
||||
}
|
||||
// 提示类消息,这里提供的是一个xml,不具备解析通用性
|
||||
// 在这里不推送
|
||||
throw UnknownError()
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件消息转换消息段
|
||||
*/
|
||||
private suspend fun convertFileElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val fileMsg = element.fileElement
|
||||
val fileName = fileMsg.fileName
|
||||
val fileSize = fileMsg.fileSize
|
||||
val expireTime = fileMsg.expireTime ?: 0
|
||||
val fileId = fileMsg.fileUuid
|
||||
val bizId = fileMsg.fileBizId ?: 0
|
||||
val fileSubId = fileMsg.fileSubId ?: ""
|
||||
val url = when (chatType) {
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(peerId, subPeer, fileId, bizId)
|
||||
else -> RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId)
|
||||
}
|
||||
|
||||
return MessageSegment(
|
||||
type = "file",
|
||||
data = mapOf(
|
||||
"name" to fileName,
|
||||
"size" to fileSize,
|
||||
"expire" to expireTime,
|
||||
"id" to fileId,
|
||||
"url" to url,
|
||||
"biz" to bizId,
|
||||
"sub" to fileSubId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 老板QQ的合并转发信息
|
||||
*/
|
||||
private suspend fun convertXmlMultiMsgElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val multiMsg = element.multiForwardMsgElement
|
||||
return MessageSegment(
|
||||
type = "forward",
|
||||
data = mapOf(
|
||||
"id" to multiMsg.resId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun convertXmlLongMsgElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val longMsg = element.structLongMsgElement
|
||||
return MessageSegment(
|
||||
type = "forward",
|
||||
data = mapOf(
|
||||
"id" to longMsg.resId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun convertMarkdownElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val markdown = element.markdownElement
|
||||
return MessageSegment(
|
||||
type = "markdown",
|
||||
data = mapOf(
|
||||
"content" to markdown.content
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun convertBubbleFaceElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val bubbleElement = element.faceBubbleElement
|
||||
return MessageSegment(
|
||||
type = "bubble_face",
|
||||
data = mapOf(
|
||||
"id" to bubbleElement.yellowFaceInfo.index,
|
||||
"count" to (bubbleElement.faceCount ?: 1),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun convertInlineKeyboardElem(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
subPeer: String,
|
||||
element: MsgElement
|
||||
): MessageSegment {
|
||||
val keyboard = element.inlineKeyboardElement
|
||||
return MessageSegment(
|
||||
type = "button",
|
||||
data = mapOf(
|
||||
"rows" to keyboard.rows.map { row ->
|
||||
mapOf("buttons" to row.buttons.map { button ->
|
||||
mapOf(
|
||||
"id" to button.id,
|
||||
"render_data" to mapOf(
|
||||
"label" to (button?.label ?: ""),
|
||||
"visited_label" to (button?.visitedLabel ?: ""),
|
||||
"style" to (button?.style ?: 0)
|
||||
|
||||
),
|
||||
"action" to mapOf(
|
||||
"type" to (button?.type ?: 0),
|
||||
"permission" to mapOf(
|
||||
"type" to (button?.permissionType ?: 0),
|
||||
"specify_role_ids" to button?.specifyRoleIds,
|
||||
"specify_user_ids" to button?.specifyTinyids
|
||||
),
|
||||
"unsupport_tips" to (button?.unsupportTips ?: ""),
|
||||
"data" to (button?.data ?: ""),
|
||||
"reply" to button?.isReply,
|
||||
"enter" to button?.enter
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
},
|
||||
"appid" to keyboard.botAppid
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -1,576 +0,0 @@
|
||||
package moe.fuqiuluo.qqinterface.servlet.msg.maker
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import moe.fuqiuluo.qqinterface.servlet.CardSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.toJson
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
|
||||
import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc
|
||||
import moe.fuqiuluo.shamrock.helper.*
|
||||
import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToMessageElements
|
||||
import moe.fuqiuluo.shamrock.tools.*
|
||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
||||
import moe.fuqiuluo.shamrock.utils.FileUtils
|
||||
import protobuf.auto.toByteArray
|
||||
import protobuf.message.Elem
|
||||
import protobuf.message.element.*
|
||||
import protobuf.message.element.commelem.*
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextULong
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
internal typealias IElemMaker = suspend (Int, Long, String, JsonObject) -> Result<Elem>
|
||||
|
||||
internal object ElemMaker {
|
||||
private val makerArray = hashMapOf(
|
||||
"text" to ElemMaker::createTextElem,
|
||||
"at" to ElemMaker::createAtElem,
|
||||
"face" to ElemMaker::createFaceElem,
|
||||
"pic" to ElemMaker::createImageElem,
|
||||
"image" to ElemMaker::createImageElem,
|
||||
// "voice" to ElemMaker::createRecordElem,
|
||||
// "record" to ElemMaker::createRecordElem,
|
||||
// "video" to ElemMaker::createVideoElem,
|
||||
"markdown" to ElemMaker::createMarkdownElem,
|
||||
"button" to ElemMaker::createButtonElem,
|
||||
"dice" to ElemMaker::createNewDiceElem,
|
||||
"rps" to ElemMaker::createNewRpsElem,
|
||||
"poke" to ElemMaker::createPokeElem,
|
||||
// "anonymous" to ElemMaker::createAnonymousElem,
|
||||
// "share" to ElemMaker::createShareElem,
|
||||
// "contact" to ElemMaker::createContactElem,
|
||||
// "location" to ElemMaker::createLocationElem,
|
||||
// "music" to ElemMaker::createMusicElem,
|
||||
"reply" to ElemMaker::createReplyElem,
|
||||
// "touch" to ElemMaker::createTouchElem,
|
||||
"weather" to ElemMaker::createWeatherElem,
|
||||
"json" to ElemMaker::createJsonElem,
|
||||
//"forward" to MessageMaker::createForwardElem,
|
||||
//"multi_msg" to MessageMaker::createLongMsgStruct,
|
||||
//"bubble_face" to ElemMaker::createBubbleFaceElem,
|
||||
)
|
||||
|
||||
operator fun get(type: String): IElemMaker? = makerArray[type]
|
||||
|
||||
private suspend fun createTextElem(
|
||||
chatType: Int,
|
||||
msgId: Long,
|
||||
peerId: String,
|
||||
data: JsonObject
|
||||
): Result<Elem> {
|
||||
data.checkAndThrow("text")
|
||||
val elem = Elem(
|
||||
text = TextMsg(data["text"].asString)
|
||||
)
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun createAtElem(
|
||||
chatType: Int,
|
||||
msgId: Long,
|
||||
peerId: String,
|
||||
data: JsonObject
|
||||
): Result<Elem> {
|
||||
return when (chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> {
|
||||
data.checkAndThrow("qq")
|
||||
|
||||
val qq: Long
|
||||
val type: Int
|
||||
val display = when (val qqStr = data["qq"].asString) {
|
||||
"0", "all" -> {
|
||||
qq = 0
|
||||
type = 1
|
||||
"@全体成员"
|
||||
}
|
||||
|
||||
"online" -> {
|
||||
qq = 0
|
||||
type = 64
|
||||
"@在线成员"
|
||||
}
|
||||
|
||||
else -> {
|
||||
qq = qqStr.toLong()
|
||||
type = 0
|
||||
"@" + (data["name"].asStringOrNull ?: GroupSvc.getTroopMemberInfoByUinV2(
|
||||
peerId.toLong(),
|
||||
qq,
|
||||
true
|
||||
)
|
||||
.let {
|
||||
val info = it.getOrNull()
|
||||
if (info == null)
|
||||
LogCenter.log("无法获取群成员信息: $qqStr", Level.ERROR)
|
||||
info?.troopnick
|
||||
.ifNullOrEmpty(info?.friendnick)
|
||||
.ifNullOrEmpty(qqStr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
val attr6: ByteBuffer = ByteBuffer.allocate(6)
|
||||
attr6.put(byteArrayOf(0, 1, 0, 0, 0))
|
||||
attr6.putChar(display.length.toChar())
|
||||
attr6.putChar(type.toChar())
|
||||
attr6.putBuf32Long(qq)
|
||||
attr6.put(byteArrayOf(0, 0))
|
||||
val elem = Elem(
|
||||
text = TextMsg(str = display, attr6Buf = attr6.array())
|
||||
)
|
||||
Result.success(elem)
|
||||
}
|
||||
|
||||
MsgConstant.KCHATTYPEC2C -> {
|
||||
data.checkAndThrow("qq")
|
||||
|
||||
val qq = data["qq"].asLong
|
||||
val display =
|
||||
"@" + (data["name"].asStringOrNull ?: CardSvc.getProfileCard(qq)
|
||||
.onSuccess {
|
||||
it.strNick.ifNullOrEmpty(qq.toString())
|
||||
}.onFailure {
|
||||
LogCenter.log("无法获取QQ信息: $qq", Level.WARN)
|
||||
})
|
||||
|
||||
val elem = Elem(
|
||||
text = TextMsg(str = display)
|
||||
)
|
||||
Result.success(elem)
|
||||
}
|
||||
|
||||
else -> Result.failure(ActionMsgException)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createFaceElem(
|
||||
chatType: Int,
|
||||
msgId: Long,
|
||||
peerId: String,
|
||||
data: JsonObject
|
||||
): Result<Elem> {
|
||||
data.checkAndThrow("id")
|
||||
val faceId = data["id"].asInt
|
||||
val elem = if (data["big"].asBooleanOrNull == true) {
|
||||
Elem(
|
||||
commonElem = CommonElem(
|
||||
serviceType = 37,
|
||||
elem = QFaceExtra(
|
||||
packId = "1",
|
||||
stickerId = "1",
|
||||
faceId = faceId,
|
||||
field4 = 1,
|
||||
field5 = 1,
|
||||
result = "",
|
||||
faceText = "", //todo 表情名字
|
||||
field9 = 1
|
||||
).toByteArray(),
|
||||
businessType = 1
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Elem(
|
||||
face = FaceMsg(
|
||||
index = faceId
|
||||
)
|
||||
)
|
||||
}
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun createImageElem(
|
||||
chatType: Int,
|
||||
msgId: Long,
|
||||
peerId: String,
|
||||
data: JsonObject
|
||||
): Result<Elem> {
|
||||
val isOriginal = data["original"].asBooleanOrNull ?: true
|
||||
val isFlash = data["flash"].asBooleanOrNull ?: false
|
||||
val filePath = data["file"].asStringOrNull
|
||||
val url = data["url"].asStringOrNull
|
||||
var file: File? = null
|
||||
if (filePath != null) {
|
||||
val md5 = filePath
|
||||
.replace(regex = "[{}\\-]".toRegex(), replacement = "")
|
||||
.split(".")[0].lowercase()
|
||||
file = if (md5.length == 32) {
|
||||
FileUtils.getFileByMd5(md5)
|
||||
} else {
|
||||
FileUtils.parseAndSave(filePath)
|
||||
}
|
||||
}
|
||||
if ((file == null || !file.exists()) && url != null) {
|
||||
file = FileUtils.parseAndSave(url)
|
||||
}
|
||||
if (file?.exists() == false) {
|
||||
throw LogicException("Image(${file.name}) file is not exists, please check your filename.")
|
||||
}
|
||||
requireNotNull(file)
|
||||
|
||||
val options = BitmapFactory.Options()
|
||||
options.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeFile(file.absolutePath, options)
|
||||
val exifInterface = ExifInterface(file.absolutePath)
|
||||
val orientation = exifInterface.getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION,
|
||||
ExifInterface.ORIENTATION_UNDEFINED
|
||||
)
|
||||
val picWidth: Int
|
||||
val picHeight: Int
|
||||
if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) {
|
||||
picWidth = options.outWidth
|
||||
picHeight = options.outHeight
|
||||
} else {
|
||||
picWidth = options.outHeight
|
||||
picHeight = options.outWidth
|
||||
}
|
||||
|
||||
val uploadRet = NtV2RichMediaSvc.tryUploadResourceByNt(
|
||||
chatType = chatType,
|
||||
elementType = MsgConstant.KELEMTYPEPIC,
|
||||
resources = arrayListOf(file),
|
||||
timeout = 30.seconds
|
||||
).getOrThrow().first()
|
||||
LogCenter.log(uploadRet.toString(), Level.DEBUG)
|
||||
|
||||
val elem = when (chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> Elem(
|
||||
customFace = CustomFace(
|
||||
filePath = uploadRet.fileName,
|
||||
fileId = uploadRet.uuid.toUInt(),
|
||||
serverIp = 0u,
|
||||
serverPort = 0u,
|
||||
fileType = FileUtils.getPicType(file).toUInt(),
|
||||
useful = 1u,
|
||||
md5 = uploadRet.md5.hex2ByteArray(),
|
||||
bizType = data["subType"].asIntOrNull?.toUInt(),
|
||||
imageType = FileUtils.getPicType(file).toUInt(),
|
||||
width = picWidth.toUInt(),
|
||||
height = picHeight.toUInt(),
|
||||
size = uploadRet.fileSize.toUInt(),
|
||||
origin = isOriginal,
|
||||
thumbWidth = 0u,
|
||||
thumbHeight = 0u,
|
||||
pbReserve = CustomFace.Companion.PbReserve(
|
||||
field1 = 0,
|
||||
field3 = 0,
|
||||
field4 = 0,
|
||||
field10 = 0,
|
||||
field21 = CustomFace.Companion.Object1(
|
||||
field1 = 0,
|
||||
field2 = "",
|
||||
field3 = 0,
|
||||
field4 = 0,
|
||||
field5 = 0,
|
||||
md5Str = uploadRet.md5
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
MsgConstant.KCHATTYPEC2C -> Elem(
|
||||
notOnlineImage = NotOnlineImage(
|
||||
filePath = uploadRet.fileName,
|
||||
fileLen = uploadRet.fileSize.toUInt(),
|
||||
downloadPath = uploadRet.uuid,
|
||||
imgType = FileUtils.getPicType(file).toUInt(),
|
||||
picMd5 = uploadRet.md5.hex2ByteArray(),
|
||||
picHeight = picWidth.toUInt(),
|
||||
picWidth = picHeight.toUInt(),
|
||||
resId = uploadRet.uuid,
|
||||
original = isOriginal, // true
|
||||
pbReserve = NotOnlineImage.Companion.PbReserve(
|
||||
field1 = 0,
|
||||
field3 = 0,
|
||||
field4 = 0,
|
||||
field10 = 0,
|
||||
field20 = NotOnlineImage.Companion.Object1(
|
||||
field1 = 0,
|
||||
field2 = "",
|
||||
field3 = 0,
|
||||
field4 = 0,
|
||||
field5 = 0,
|
||||
field7 = "",
|
||||
),
|
||||
md5Str = uploadRet.md5
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
else -> throw LogicException("Not supported chatType($chatType) for PictureMsg")
|
||||
}
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun createReplyElem(
|
||||
chatType: Int,
|
||||
msgId: Long,
|
||||
peerId: String,
|
||||
data: JsonObject
|
||||
): Result<Elem> {
|
||||
data.checkAndThrow("id")
|
||||
val msgHash = data["id"].asInt
|
||||
val mapping = MessageHelper.getMsgMappingByHash(msgHash)
|
||||
?: return Result.failure(Exception("不存在该消息映射,无法回复消息"))
|
||||
|
||||
if (mapping.qqMsgId == 0L) {
|
||||
// 貌似获取失败了,555
|
||||
LogCenter.log("无法获取被回复消息", Level.ERROR)
|
||||
return Result.failure(Exception("无法获取被回复消息"))
|
||||
}
|
||||
|
||||
val elem = if (data.containsKey("text")) {
|
||||
data.checkAndThrow("qq", "time", "seq")
|
||||
Elem(
|
||||
srcMsg = SourceMsg(
|
||||
origSeqs = listOf(data["seq"].asInt),
|
||||
senderUin = data["qq"].asString.toULong(),
|
||||
time = data["time"].asString.toULong(),
|
||||
flag = 1u,
|
||||
elems = listOf(
|
||||
Elem(
|
||||
text = TextMsg(
|
||||
data["text"].asString
|
||||
)
|
||||
)
|
||||
),
|
||||
type = 0u,
|
||||
pbReserve = SourceMsg.Companion.PbReserve(
|
||||
msgRand = Random.nextInt().toULong(),
|
||||
field8 = Random.nextInt(0, 10000)
|
||||
),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val msg =
|
||||
MsgSvc.getMsgByQMsgId(chatType, mapping.peerId, mapping.qqMsgId).getOrNull() ?: return Result.failure(
|
||||
Exception("无法获取被回复消息")
|
||||
)
|
||||
Elem(
|
||||
srcMsg = SourceMsg(
|
||||
origSeqs = listOf(msg.msgSeq.toInt()),
|
||||
senderUin = msg.senderUin.toULong(),
|
||||
time = msg.msgTime.toULong(),
|
||||
flag = 1u,
|
||||
elems = messageArrayToMessageElements(
|
||||
msg.chatType,
|
||||
msg.msgId,
|
||||
msg.peerUin.toString(),
|
||||
msg.elements.toSegments(
|
||||
msg.chatType,
|
||||
if (msg.chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(),
|
||||
msg.channelId ?: msg.peerUin.toString()
|
||||
).toJson()
|
||||
).second,
|
||||
type = 0u,
|
||||
pbReserve = SourceMsg.Companion.PbReserve(
|
||||
msgRand = Random.nextULong(),
|
||||
senderUid = msg.senderUid,
|
||||
receiverUid = TicketSvc.getUid(),
|
||||
field8 = Random.nextInt(0, 10000)
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun createJsonElem(
|
||||
chatType: Int,
|
||||
msgId: Long,
|
||||
peerId: String,
|
||||
data: JsonObject
|
||||
): Result<Elem> {
|
||||
data.checkAndThrow("data")
|
||||
|
||||
val elem = Elem(
|
||||
lightApp = LightAppElem(
|
||||
data = DeflateTools.compress(data.toString().toByteArray())
|
||||
)
|
||||
)
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun createWeatherElem(
|
||||
chatType: Int,
|
||||
msgId: Long,
|
||||
peerId: String,
|
||||
data: JsonObject
|
||||
): Result<Elem> {
|
||||
var code = data["code"].asIntOrNull
|
||||
|
||||
if (code == null) {
|
||||
data.checkAndThrow("city")
|
||||
val city = data["city"].asString
|
||||
code = WeatherSvc.searchCity(city).onFailure {
|
||||
LogCenter.log("无法获取城市天气: $city", Level.ERROR)
|
||||
}.getOrNull()?.firstOrNull()?.adcode
|
||||
}
|
||||
|
||||
if (code != null) {
|
||||
WeatherSvc.fetchWeatherCard(code).onSuccess {
|
||||
// OidbSvc.0xdc2_34
|
||||
// 00 00 00 DF 08 C2 1B 10 22 22 C4 01 0A B7 01 08 A2 E0 F2 2F 10 01 18 00 2A 02 08 01 58 FB 91 F6 AE 02 62 A1 01 08 01 52 08 E5 8C 97 E4 BA AC 20 20 5A 19 2D 33 C2 B0 2F 33 C2 B0 0A E7 A9 BA E6 B0 94 E8 B4 A8 E9 87 8F 3A E8 89 AF 62 11 5B E5 88 86 E4 BA AB 5D 20 E5 8C 97 E4 BA AC 20 20 6A 25 68 74 74 70 73 3A 2F 2F 77 65 61 74 68 65 72 2E 6D 70 2E 71 71 2E 63 6F 6D 2F 3F 73 74 3D 30 26 5F 77 76 3D 31 72 3E 68 74 74 70 73 3A 2F 2F 69 6D 67 63 61 63 68 65 2E 71 71 2E 63 6F 6D 2F 61 63 2F 71 71 77 65 61 74 68 65 72 2F 69 6D 61 67 65 2F 73 68 61 72 65 5F 69 63 6F 6E 2F 66 69 6E 65 2E 70 6E 67 12 08 08 01 10 FB 91 F6 AE 02 32 0D 61 6E 64 72 6F 69 64 20 39 2E 30 2E 38
|
||||
return createJsonElem(
|
||||
chatType, msgId, peerId, it["weekStore"]
|
||||
.asJsonObject["share"].asJsonObject
|
||||
)
|
||||
}.onFailure {
|
||||
LogCenter.log("无法发送天气分享", Level.ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.failure(ActionMsgException)
|
||||
}
|
||||
|
||||
private suspend fun createPokeElem(
|
||||
chatType: Int,
|
||||
msgId: Long,
|
||||
peerId: String,
|
||||
data: JsonObject
|
||||
): Result<Elem> {
|
||||
data.checkAndThrow("type", "id")
|
||||
val elem = Elem(
|
||||
commonElem = CommonElem(
|
||||
serviceType = 2,
|
||||
elem = PokeExtra(
|
||||
type = data["type"].asInt,
|
||||
field7 = 0,
|
||||
field8 = 0
|
||||
).toByteArray(),
|
||||
businessType = data["id"].asInt
|
||||
)
|
||||
)
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun createNewDiceElem(
|
||||
chatType: Int,
|
||||
msgId: Long,
|
||||
peerId: String,
|
||||
data: JsonObject
|
||||
): Result<Elem> {
|
||||
val elem = Elem(
|
||||
commonElem = CommonElem(
|
||||
serviceType = 37,
|
||||
elem = QFaceExtra(
|
||||
packId = "1",
|
||||
stickerId = "33",
|
||||
faceId = 358,
|
||||
field4 = 1,
|
||||
field5 = 2,
|
||||
result = "",
|
||||
faceText = "/骰子",
|
||||
field9 = 1
|
||||
).toByteArray(),
|
||||
businessType = 2
|
||||
)
|
||||
)
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun createNewRpsElem(
|
||||
chatType: Int,
|
||||
msgId: Long,
|
||||
peerId: String,
|
||||
data: JsonObject
|
||||
): Result<Elem> {
|
||||
val elem = Elem(
|
||||
commonElem = CommonElem(
|
||||
serviceType = 37,
|
||||
elem = QFaceExtra(
|
||||
packId = "1",
|
||||
stickerId = "34",
|
||||
faceId = 359,
|
||||
field4 = 1,
|
||||
field5 = 2,
|
||||
result = "",
|
||||
faceText = "/包剪锤",
|
||||
field9 = 1
|
||||
).toByteArray(),
|
||||
businessType = 1
|
||||
)
|
||||
)
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun createMarkdownElem(
|
||||
chatType: Int,
|
||||
msgId: Long,
|
||||
peerId: String,
|
||||
data: JsonObject
|
||||
): Result<Elem> {
|
||||
data.checkAndThrow("content")
|
||||
val elem = Elem(
|
||||
commonElem = CommonElem(
|
||||
serviceType = 45,
|
||||
elem = MarkdownExtra(data["content"].asString).toByteArray(),
|
||||
businessType = 1
|
||||
)
|
||||
)
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private suspend fun createButtonElem(
|
||||
chatType: Int,
|
||||
msgId: Long,
|
||||
peerId: String,
|
||||
data: JsonObject
|
||||
): Result<Elem> {
|
||||
data.checkAndThrow("buttons")
|
||||
val elem = Elem(
|
||||
commonElem = CommonElem(
|
||||
serviceType = 46,
|
||||
elem = ButtonExtra(
|
||||
field1 = Object1(
|
||||
rows = data["buttons"].asJsonArray.map { row ->
|
||||
Row(buttons = row.asJsonArray.map {
|
||||
val button = it.asJsonObject
|
||||
val renderData = button["render_data"].asJsonObject
|
||||
val action = button["action"].asJsonObject
|
||||
val permission = action["permission"].asJsonObject
|
||||
Button(
|
||||
id = button["id"].asStringOrNull,
|
||||
renderData = RenderData(
|
||||
label = renderData["label"].asString,
|
||||
visitedLabel = renderData["visited_label"].asString,
|
||||
style = renderData["style"].asInt
|
||||
),
|
||||
action = Action(
|
||||
type = action["type"].asInt,
|
||||
permission = Permission(
|
||||
type = permission["type"].asInt,
|
||||
specifyRoleIds = permission["specify_role_ids"].asJsonArrayOrNull?.map { id -> id.asString },
|
||||
specifyUserIds = permission["specify_user_ids"].asJsonArrayOrNull?.map { id -> id.asString }
|
||||
),
|
||||
unsupportTips = action["unsupport_tips"].asString,
|
||||
data = action["data"].asString,
|
||||
reply = action["reply"].asBooleanOrNull,
|
||||
enter = action["enter"].asBooleanOrNull
|
||||
)
|
||||
)
|
||||
})
|
||||
},
|
||||
appid = data["appid"].asIntOrNull
|
||||
)
|
||||
).toByteArray(),
|
||||
businessType = 1
|
||||
)
|
||||
)
|
||||
return Result.success(elem)
|
||||
}
|
||||
|
||||
private fun JsonObject.checkAndThrow(vararg key: String) {
|
||||
key.forEach {
|
||||
if (!containsKey(it)) throw ParamsException(it)
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user