58 Commits

Author SHA1 Message Date
40ffa2b71b Update bug.md 2024-07-21 21:40:04 +08:00
cca2cbbbd7 Version Restriction Notice 2024-07-19 03:35:45 +08:00
40d2911135 修复资源rkey获取异常 2024-07-16 17:47:39 +08:00
823d9911a0 fix #338 2024-07-16 17:11:48 +08:00
4adbc12a0b fix /get_group_root_files 2024-07-08 09:53:15 +08:00
114fbfdd23 fix /get_group_root_files 2024-07-08 09:44:26 +08:00
0a563d60a1 fix crash in QQ9.0.71 2024-07-08 09:17:56 +08:00
5fb1d0aeb9 fix ping-pong 2024-07-08 09:11:49 +08:00
012ecaa85d fix #335 2024-07-08 08:55:49 +08:00
7ea127a279 update shamrock 2024-07-08 08:15:05 +08:00
02f83a3c48 fix: remove debug log 2024-07-07 15:26:30 +08:00
4016c05882 fix: 9.0.71 compatible issues (part of) 2024-07-07 15:04:30 +08:00
59d762eecf fix https://github.com/KarinJS/kritor/issues/10 2024-04-11 12:37:54 +08:00
e891bc8512 fix build 2024-04-11 01:17:13 +08:00
494c70f2f8 update kritor 2024-04-11 00:58:36 +08:00
7baf459b2a Shamrock: fix scene and group code 2024-04-10 21:09:10 +08:00
36a09ca088 update kritor 2024-04-08 20:00:44 +08:00
926c4659f6 Shamrock: add kritor metadata 2024-04-07 16:51:21 +08:00
cb7c68f36c fix: build error 2024-04-07 16:27:35 +08:00
72af39208c update kritor 2024-04-07 16:08:33 +08:00
042f4bd330 fix build err 2024-04-04 19:44:56 +08:00
9aef71b09f fix build err 2024-04-04 18:56:48 +08:00
9cbe755520 fix missing elem-type for kritor 2024-04-04 18:51:58 +08:00
df02f9f872 fix #316 2024-03-28 19:37:10 +08:00
5cbb695a66 fix crash 2024-03-28 19:27:11 +08:00
c014e85faa update kritor 2024-03-27 16:21:49 +08:00
4a396b0935 Update kritor 2024-03-25 01:12:59 +08:00
d59fcf9f6a update kritor 2024-03-25 01:10:33 +08:00
cdc664f44a fix build error 2024-03-24 05:33:57 +08:00
ad313f384c fix kritor 2024-03-24 05:19:50 +08:00
b6a510ce05 Update .gitmodules 2024-03-21 19:35:16 +08:00
bed5947909 update kritor 2024-03-21 19:15:58 +08:00
fb6578d243 chore: try to fix ci 2024-03-21 17:50:39 +08:00
d33cace7aa Shamrock: forward messages resources upload 2024-03-21 17:39:51 +08:00
659d4e5da4 commit Readme.md 2024-03-21 16:21:54 +08:00
ac2aee8c0e 関連プロジェクトのヒントを追加する 2024-03-21 16:17:25 +08:00
0faada7b5a Merge pull request #310 from whitechi73/kritor
kritorをmasterブランチに設定する
2024-03-21 16:15:12 +08:00
680317da13 kritorをmasterブランチに設定する
kritorをmasterブランチに設定する
2024-03-21 16:13:45 +08:00
d66358a1f3 Shamrock: 提供开发者服务支持
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-18 21:02:05 +08:00
ee5fcc3403 Shamrock: 精华消息支持
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-18 11:49:38 +08:00
6c9b282c6a Shamrock: 实现消息服务
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-16 19:39:45 +08:00
3a07116093 Shamrock: 实现事件推送
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-15 01:37:28 +08:00
a95d8d85e8 Shamrock: 完成消息推送
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-14 20:33:30 +08:00
7bacea3288 Shamrock: イベントプッシュの実装
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-12 21:02:36 +08:00
ca47f9dbdf Shamrock: グループファイルサービスの実装
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-12 18:46:05 +08:00
cb4268edef Shamrock: 实现Kritor核心服务
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-11 12:48:11 +08:00
c16f9d543c Shamrock: 实现联系人服务
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-11 12:30:42 +08:00
a528030cbb Shamrock: 实现鉴权操作
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 16:05:20 +08:00
bbdfb7c04e Shamrock: 实现鉴权
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 16:04:52 +08:00
1afc0ac6a6 Shamrock: 修复服务器TLS异常
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 07:57:23 +08:00
638bf72392 Shamrock: 修复配置文件下发
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 07:44:02 +08:00
07364c8298 Shamrock: 修复kritor构建错误
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 02:26:53 +08:00
ee6e13a5bb Shamrock: 修正频道proto文件错误
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 01:58:18 +08:00
c3934778c7 Shamrock: 修正频道proto文件错误
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 01:56:02 +08:00
13a49dd70b Shamrock: 添加对kritor的依赖
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 01:00:20 +08:00
5637db43be Shamrock: 重构收包起,减少拷贝
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-10 00:33:26 +08:00
69cdbad643 Shamrock: rm CIO
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-08 01:19:26 +08:00
a06708bf95 Shamrock: build for kritor
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-08 01:11:24 +08:00
393 changed files with 9312 additions and 22954 deletions

View File

@ -21,6 +21,7 @@ labels: bug
## 系统信息
- Shamrock 版本:
- QQ 版本:
- Android 版本:
- LSPosed 框架版本:
- 设备的制造商和型号:

View File

@ -3,7 +3,7 @@ name: Build Shamrock APK
on:
workflow_dispatch:
push:
branches: [ v1.0.9 ]
branches: [ master ]
paths-ignore:
- '**.md'
- '**.txt'
@ -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
View File

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

View File

@ -16,19 +16,22 @@
## 简介
☘ 基于 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/)
>
> 社区地址:[discord](https://discord.gg/MKR2wz863h)
## 兼容|迁移|替代 说明
仅支持QQ9.0.70以上的版本,低版本问题将不再修复与处理。
- 一键移植:本项目基于 go-cqhttp 的文档进行开发实现。
- 平行部署:可多平台部署,未来将会支持 Docker 部署的教程
- 平行部署:可多平台部署。
## 相关项目

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ android {
minSdk = 27
targetSdk = 34
versionCode = getVersionCode()
versionName = "1.0.9" + ".r${getGitCommitCount()}." + getVersionName()
versionName = "1.1.1" + ".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"))

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
#include "jni.h"
#include "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, "&", "&amp;");
replace_string(tmpValue, "[", "&#91;");
replace_string(tmpValue, "]", "&#93;");
replace_string(tmpValue, ",", "&#44;");
result.append(tmpValue);
env->ReleaseStringUTFChars(text, textString);
} else {
result.append("[CQ:");
result.append(typeString);
while (env->CallBooleanMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "hasNext", "()Z"))) {
jobject entry = env->CallObjectMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "next", "()Ljava/lang/Object;"));
auto key = (jstring) env->CallObjectMethod(entry, getKeyMethod);
auto value = (jstring) env->CallObjectMethod(entry, getValueMethod);
auto keyString = env->GetStringUTFChars(key, nullptr);
auto valueString = env->GetStringUTFChars(value, nullptr);
if (strcmp(keyString, "_type") != 0) {
std::string tmpValue = valueString;
replace_string(tmpValue, "&", "&amp;");
replace_string(tmpValue, "[", "&#91;");
replace_string(tmpValue, "]", "&#93;");
replace_string(tmpValue, ",", "&#44;");
result.append(",").append(keyString).append("=").append(tmpValue);
}
env->ReleaseStringUTFChars(key, keyString);
env->ReleaseStringUTFChars(value, valueString);
env->DeleteLocalRef(entry);
env->DeleteLocalRef(key);
env->DeleteLocalRef(value);
}
result.append("]");
}
env->ReleaseStringUTFChars(type, typeString);
}
env->DeleteLocalRef(List);
env->DeleteLocalRef(Map);
env->DeleteLocalRef(setClass);
env->DeleteLocalRef(entryClass);
return env->NewStringUTF(result.c_str());
}
extern "C"
JNIEXPORT jlong JNICALL
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_insertChatTypeToMsgId(JNIEnv *env, jobject thiz,

View File

@ -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库成功~");
}

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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,50 +154,32 @@ 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由Shamrock放出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由客户端提供反向的rpc服务",
isSwitch = ShamrockConfig[ctx, PassiveRPC]
) {
ShamrockConfig.setUseCQCode(ctx, it)
return@Function true
}
Function(
title = "主动WebSocket",
desc = "OneBot标准WebSocketShamrock作为Server。",
isSwitch = ShamrockConfig.isWs(ctx)
) {
ShamrockConfig.setWs(ctx, it)
return@Function true
}
Function(
title = "被动WebSocket",
desc = "OneBot标准WebSocketShamrock作为Client。",
isSwitch = ShamrockConfig.isWsClient(ctx)
) {
ShamrockConfig.setWsClient(ctx, it)
ShamrockConfig[ctx, PassiveRPC] = it
return@Function true
}
run {
val uploadResourceGroup = remember { mutableStateOf(ShamrockConfig.getUploadResourceGroup(ctx)) }
val uploadResourceGroup = remember { mutableStateOf(ShamrockConfig[ctx, ResourceGroup]) }
Column(
modifier = Modifier
.absolutePadding(left = 8.dp, right = 8.dp, top = 12.dp, bottom = 0.dp)
@ -380,23 +202,11 @@ private fun FunctionCard(
},
confirm = {
val groupId = uploadResourceGroup.value
ShamrockConfig.setUploadResourceGroup(ctx, groupId)
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
}*/
}
}
}

View File

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

View File

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

View File

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

View File

@ -3,13 +3,37 @@ package moe.fuqiuluo.shamrock.ui.service.handlers
import android.content.ContentValues
import android.content.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]
}
}

View File

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

View File

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

View File

@ -5,9 +5,9 @@ import android.content.ContentValues
import android.content.Context
import android.content.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)
}

View File

@ -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,20 +149,18 @@ private fun TabBaselineLayout(
text: @Composable (() -> Unit)?,
icon: @Composable (() -> Unit)?
) {
Layout(
{
if (text != null) {
Box(
Modifier
.layoutId("text")
.padding(horizontal = HorizontalTextPadding)
) { text() }
}
if (icon != null) {
Box(Modifier.layoutId("icon")) { icon() }
}
Layout({
if (text != null) {
Box(
Modifier
.layoutId("text")
.padding(horizontal = HorizontalTextPadding)
) { text() }
}
) { measurables, constraints ->
if (icon != null) {
Box(Modifier.layoutId("icon")) { icon() }
}
}) { 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)
.copy(textAlign = TextAlign.Center)
ProvideTextStyle(style, content = text)
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) {
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
)
}
}

View File

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

View File

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

View File

@ -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
View File

@ -0,0 +1,42 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

76
kritor/build.gradle.kts Normal file
View File

@ -0,0 +1,76 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.protobuf") version "0.9.4"
}
android {
namespace = "moe.whitechi73.kritor"
compileSdk = 34
defaultConfig {
minSdk = 24
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
protobuf(files("kritor/protos"))
implementation("com.google.protobuf:protobuf-java:4.26.0")
implementation(kotlinx("coroutines-core", "1.8.0"))
implementation(grpc("stub", "1.62.2"))
implementation(grpc("protobuf", "1.62.2"))
implementation(grpc("kotlin-stub", "1.4.1"))
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.26.0"
}
plugins {
create("grpc") {
artifact = "io.grpc:protoc-gen-grpc-java:1.62.2"
}
create("grpckt") {
artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar"
}
}
generateProtoTasks {
all().forEach {
it.plugins {
create("grpc")
create("grpckt")
}
it.builtins {
create("java")
}
}
}
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
}

View File

1
kritor/kritor Submodule

Submodule kritor/kritor added at 3dec747a8e

21
kritor/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -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")

View File

@ -0,0 +1,83 @@
@file:Suppress("UNCHECKED_CAST")
@file:OptIn(KspExperimental::class)
package moe.fuqiuluo.ksp.impl
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getAnnotationsByType
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import kritor.service.Grpc
class GrpcProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
): SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation(Grpc::class.qualifiedName!!)
val actions = (symbols as Sequence<KSFunctionDeclaration>).toList()
if (actions.isEmpty()) return emptyList()
// 怎么返回nullable的结果
val packageName = "kritor.handlers"
val funcBuilder = FunSpec.builder("handleGrpc")
.addModifiers(KModifier.SUSPEND)
.addParameter("cmd", String::class)
.addParameter("data", ByteArray::class)
.returns(ByteArray::class)
val fileSpec = FileSpec.scriptBuilder("AutoGrpcHandlers", packageName)
logger.warn("Found ${actions.size} grpc-actions")
//logger.error(resolver.getClassDeclarationByName("io.kritor.AuthReq").toString())
//logger.error(resolver.getJavaClassByName("io.kritor.AuthReq").toString())
//logger.error(resolver.getKotlinClassByName("io.kritor.AuthReq").toString())
actions.forEach { action ->
val methodName = action.qualifiedName?.asString()!!
val grpcMethod = action.getAnnotationsByType(Grpc::class).first()
val service = grpcMethod.serviceName
val funcName = grpcMethod.funcName
funcBuilder.addStatement("if (cmd == \"${service}.${funcName}\") {\t")
val reqType = action.parameters[0].type.toString()
val rspType = action.returnType.toString()
funcBuilder.addStatement("val resp: $rspType = $methodName($reqType.parseFrom(data))")
funcBuilder.addStatement("return resp.toByteArray()")
funcBuilder.addStatement("}")
}
funcBuilder.addStatement("return EMPTY_BYTE_ARRAY")
fileSpec
.addStatement("import io.kritor.authentication.*")
.addStatement("import io.kritor.core.*")
.addStatement("import io.kritor.customization.*")
.addStatement("import io.kritor.developer.*")
.addStatement("import io.kritor.file.*")
.addStatement("import io.kritor.friend.*")
.addStatement("import io.kritor.group.*")
.addStatement("import io.kritor.guild.*")
.addStatement("import io.kritor.message.*")
.addStatement("import io.kritor.web.*")
.addFunction(funcBuilder.build())
.addImport("moe.fuqiuluo.symbols", "EMPTY_BYTE_ARRAY")
runCatching {
codeGenerator.createNewFile(
dependencies = Dependencies(aggregating = false),
packageName = packageName,
fileName = fileSpec.name
).use { outputStream ->
outputStream.writer().use {
fileSpec.build().writeTo(it)
}
}
}
return emptyList()
}
}

View File

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

View File

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

View File

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

View File

@ -4,12 +4,12 @@ import com.google.auto.service.AutoService
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.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
)

View File

@ -37,7 +37,6 @@ android {
}
dependencies {
implementation(DEPENDENCY_PROTOBUF)
implementation(kotlinx("serialization-protobuf", "1.6.2"))
implementation(kotlinx("serialization-json", "1.6.2"))
@ -47,5 +46,5 @@ dependencies {
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
}

View File

@ -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

View File

@ -8,7 +8,9 @@ import moe.fuqiuluo.symbols.Protobuf
data class TrpcOidb(
@ProtoNumber(1) val cmd: Int = Int.MIN_VALUE,
@ProtoNumber(2) val service: Int = Int.MIN_VALUE,
@ProtoNumber(4) val buffer: ByteArray,
@ProtoNumber(3) val result: UInt? = null,
@ProtoNumber(4) val buffer: ByteArray? = null,
@ProtoNumber(5) val msg: String? = null,
//@ProtoNumber(11) val traceParams: Map<String, String> = mapOf(),
@ProtoNumber(12) val flag: Int = Int.MIN_VALUE,
): Protobuf<TrpcOidb>

View File

@ -94,7 +94,8 @@ data class DeleteReq(
@Serializable
data class DownloadRkeyReq(
@ProtoNumber(1) val types: List<Int>
@ProtoNumber(1) val types: List<Int>,
@ProtoNumber(2) val downloadType: Int
)
@Serializable

View File

@ -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?,
@ -52,11 +52,11 @@ data class DownloadRkeyRsp(
@Serializable
data class RKeyInfo(
@ProtoNumber(1) val rkey: String?,
@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?,
@ProtoNumber(4) val type: UInt,
)
@Serializable

View File

@ -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

View File

@ -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

View File

@ -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 "";
}

View File

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

View File

@ -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;

View File

@ -1,15 +0,0 @@
package com.tencent.mobileqq.l0.b;
public class a {
public void b () {
}
public void c (c cVar) {
}
public void onUpdate(int progress, boolean z, Object obj) {
}
}

View File

@ -1,23 +0,0 @@
package com.tencent.mobileqq.l0.b;
import android.graphics.Point;
import java.util.ArrayList;
public class b {
// text
public String a;
// confidence
public int b;
// coordinates
public ArrayList<Point> c;
/* renamed from: d */
public int d;
/* renamed from: e */
public boolean e;
}

View File

@ -1,39 +0,0 @@
package com.tencent.mobileqq.l0.b;
import java.util.ArrayList;
import java.util.HashMap;
public class c {
// image
public String a;
// width
public int b;
// height
public int c;
// lang
public String d;
// url
public String e;
// results
public ArrayList<b> f;
public ArrayList<String> g;
public HashMap<String, String> h;
public int i;
public int j;
public int k;
public String l;
public int m;
}

View File

@ -1,7 +1,7 @@
package com.tencent.mobileqq.msf.core;
public class MsfCore {
public synchronized int getNextSeq() {
public static synchronized int getNextSeq() {
return 0;
}
}

View File

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

View File

@ -1,22 +0,0 @@
package com.tencent.mobileqq.msf.service;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import androidx.annotation.Nullable;
import com.tencent.mobileqq.msf.core.MsfCore;
public class MsfService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
public static MsfCore getCore() {
return null;
}
}

View File

@ -1,38 +0,0 @@
package com.tencent.mobileqq.ocr.a;
public class a {
public String a;
// 1
public int b;
// file location
public String c;
// null
public String d;
// 0
public long e;
// md5
public String f;
// null
public String g;
// false
public boolean h;
// 0
public int i;
// 0
public int j;
// 0
public long k;
// null
public String l;
}

View File

@ -1,10 +0,0 @@
package com.tencent.mobileqq.ocr.api;
import mqq.app.api.IRuntimeService;
public interface IPicOcrService extends IRuntimeService {
void putOcrResult(String str, com.tencent.mobileqq.l0.b.c cVar);
void requestOcr(com.tencent.mobileqq.ocr.a.a aVar, com.tencent.mobileqq.l0.b.a callback);
}

View File

@ -1,4 +0,0 @@
package com.tencent.mobileqq.ocr.api.impl;
public class OcrServiceImpl {
}

View File

@ -1,4 +0,0 @@
package com.tencent.mobileqq.ocr.api.impl;
public class PicOcrServiceImpl {
}

View File

@ -1,8 +1,8 @@
package com.tencent.qqnt.aio.api;
import com.tencent.mobileqq.qroute.QRouteApi;
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback;
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback;
public interface IAIOFileTransfer extends QRouteApi {
void sendLocalFile(Contact contact, String path, IOperateCallback cb);

View File

@ -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;
}

View File

@ -0,0 +1,4 @@
package com.tencent.qqnt.kernel.nativeinterface;
public class GProGuildTopFeedMsg {
}

View File

@ -1,7 +1,5 @@
package com.tencent.qqnt.kernel.nativeinterface;
import com.tencent.qqnt.kernelpublic.nativeinterface.MemberRole;
import java.io.Serializable;
public final class GrayTipGroupMember implements Serializable {

View File

@ -1,7 +1,5 @@
package com.tencent.qqnt.kernel.nativeinterface;
import com.tencent.qqnt.kernelpublic.nativeinterface.MemberRole;
import java.util.ArrayList;

View File

@ -1,7 +1,5 @@
package com.tencent.qqnt.kernel.nativeinterface;
import com.tencent.qqnt.kernelpublic.nativeinterface.MemberRole;
public final class GroupSimpleInfo implements IKernelModel {
String avatarUrl;
int discussToGroupMaxMsgSeq;

View File

@ -1,8 +1,9 @@
package com.tencent.qqnt.kernel.nativeinterface;
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
import java.util.ArrayList;
import java.util.HashMap;
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
public interface IKernelMsgListener {
void onAddSendMsg(MsgRecord msgRecord);
@ -17,7 +18,7 @@ public interface IKernelMsgListener {
void onCustomWithdrawConfigUpdate(CustomWithdrawConfig customWithdrawConfig);
void onDraftUpdate(Contact contact, ArrayList<MsgElement> arrayList, long j2);
void onDraftUpdate(Contact contact, ArrayList<MsgElement> arrayList, long j);
void onEmojiDownloadComplete(EmojiNotifyInfo emojiNotifyInfo);
@ -31,7 +32,7 @@ public interface IKernelMsgListener {
void onFirstViewGroupGuildMapping(ArrayList<FirstViewGroupGuildInfo> arrayList);
void onGrabPasswordRedBag(int i2, String str, int i3, RecvdOrder recvdOrder, MsgRecord msgRecord);
void onGrabPasswordRedBag(int i, String str, int i2, RecvdOrder recvdOrder, MsgRecord msgRecord);
void onGroupFileInfoAdd(GroupItem groupItem);
@ -49,6 +50,8 @@ public interface IKernelMsgListener {
void onGuildNotificationAbstractUpdate(GuildNotificationAbstractInfo guildNotificationAbstractInfo);
void onGuildTopFeedUpdate(GProGuildTopFeedMsg gProGuildTopFeedMsg);
void onHitCsRelatedEmojiResult(DownloadRelateEmojiResultInfo downloadRelateEmojiResultInfo);
void onHitEmojiKeywordResult(HitRelatedEmojiWordsResult hitRelatedEmojiWordsResult);
@ -63,7 +66,7 @@ public interface IKernelMsgListener {
void onLineDev(ArrayList<DevInfo> arrayList);
void onLogLevelChanged(long j2);
void onLogLevelChanged(long j);
void onMsgAbstractUpdate(ArrayList<MsgAbstract> arrayList);
@ -77,14 +80,16 @@ public interface IKernelMsgListener {
void onMsgInfoListUpdate(ArrayList<MsgRecord> arrayList);
void onMsgQRCodeStatusChanged(int i2);
void onMsgQRCodeStatusChanged(int i);
void onMsgRecall(int i2, String str, long j2);
void onMsgRecall(int i, String str, long j);
void onMsgSecurityNotify(MsgRecord msgRecord);
void onMsgSettingUpdate(MsgSetting msgSetting);
void onMsgWithRichLinkInfoUpdate(ArrayList<MsgRecord> arrayList);
void onNtFirstViewMsgSyncEnd();
void onNtMsgSyncEnd();
@ -93,11 +98,11 @@ public interface IKernelMsgListener {
void onReadFeedEventUpdate(FirstViewDirectMsgNotifyInfo firstViewDirectMsgNotifyInfo);
void onRecvGroupGuildFlag(int i2);
void onRecvGroupGuildFlag(int i);
void onRecvMsg(ArrayList<MsgRecord> arrayList);
void onRecvMsgSvrRspTransInfo(long j2, Contact contact, int i2, int i3, String str, byte[] bArr);
void onRecvMsgSvrRspTransInfo(long j, Contact contact, int i, int i2, String str, byte[] bArr);
void onRecvOnlineFileMsg(ArrayList<MsgRecord> arrayList);
@ -105,7 +110,9 @@ public interface IKernelMsgListener {
void onRecvSysMsg(ArrayList<Byte> arrayList);
void onRecvUDCFlag(int i2);
void onRecvUDCFlag(int i);
void onRedTouchChanged();
void onRichMediaDownloadComplete(FileTransNotifyInfo fileTransNotifyInfo);
@ -115,9 +122,9 @@ public interface IKernelMsgListener {
void onSearchGroupFileInfoUpdate(SearchGroupFileResult searchGroupFileResult);
void onSendMsgError(long j2, Contact contact, int i2, String str);
void onSendMsgError(long j, Contact contact, int i, String str);
void onSysMsgNotification(int i2, long j2, long j3, boolean z, ArrayList<Byte> arrayList);
void onSysMsgNotification(int i, long j, long j2, boolean z, ArrayList<Byte> arrayList);
void onTempChatInfoUpdate(TempChatInfo tempChatInfo);
@ -129,9 +136,11 @@ public interface IKernelMsgListener {
void onUserOnlineStatusChanged(boolean z);
void onUserSecQualityChanged(QueryUserSecQualityRsp queryUserSecQualityRsp);
void onUserTabStatusChanged(ArrayList<TabStatusInfo> arrayList);
void onlineStatusBigIconDownloadPush(int i2, long j2, String str);
void onlineStatusBigIconDownloadPush(int i, long j, String str);
void onlineStatusSmallIconDownloadPush(int i2, long j2, String str);
void onlineStatusSmallIconDownloadPush(int i, long j, String str);
}

View File

@ -0,0 +1,9 @@
package com.tencent.qqnt.kernel.nativeinterface;
public enum MemberRole {
UNSPECIFIED,
STRANGER,
MEMBER,
ADMIN,
OWNER
}

View File

@ -0,0 +1,4 @@
package com.tencent.qqnt.kernel.nativeinterface;
public class QueryUserSecQualityRsp {
}

View File

@ -10,7 +10,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import kotlin.Pair;
import kotlinx.coroutines.flow.Flow;
public interface IMsgService extends QRouteApi {

View File

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

View File

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

View File

@ -26,7 +26,7 @@ buildscript {
}
}
dependencies {
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")

View File

@ -60,9 +60,10 @@ kotlin {
}
dependencies {
compileOnly ("de.robv.android.xposed:api:82")
compileOnly (project(":qqinterface"))
compileOnly("de.robv.android.xposed:api:82")
compileOnly(project(":qqinterface"))
implementation(project(":kritor"))
implementation(project(":protobuf"))
implementation(project(":annotations"))
ksp(project(":processor"))
@ -72,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")
@ -101,6 +95,8 @@ dependencies {
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
tasks.withType<KotlinCompile>().all {
kotlinOptions {
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn")
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -173,7 +173,7 @@ NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) {
extern "C"
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);

View File

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

View File

@ -0,0 +1,164 @@
@file:OptIn(DelicateCoroutinesApi::class)
package kritor.client
import com.google.protobuf.ByteString
import io.grpc.CallOptions
import io.grpc.Channel
import io.grpc.ClientCall
import io.grpc.ClientInterceptor
import io.grpc.ForwardingClientCall
import io.grpc.Metadata
import io.grpc.ManagedChannel
import io.grpc.ManagedChannelBuilder
import io.grpc.MethodDescriptor
import io.kritor.common.Request
import io.kritor.common.Response
import io.kritor.event.EventServiceGrpcKt
import io.kritor.event.EventStructure
import io.kritor.event.EventType
import io.kritor.reverse.ReverseServiceGrpcKt
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch
import kritor.handlers.handleGrpc
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
import qq.service.ticket.TicketHelper
import kotlin.time.Duration.Companion.seconds
internal class KritorClient(
val host: String,
val port: Int
) {
private lateinit var channel: ManagedChannel
private lateinit var channelJob: Job
private val senderChannel = MutableSharedFlow<Response>()
fun start() {
runCatching {
if (::channel.isInitialized && isActive()){
channel.shutdown()
}
val interceptor = object : ClientInterceptor {
override fun <ReqT, RespT> interceptCall(method: MethodDescriptor<ReqT, RespT>, callOptions: CallOptions, next: Channel): ClientCall<ReqT, RespT> {
return object : ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
override fun start(responseListener: Listener<RespT>, headers: Metadata) {
headers.merge(Metadata().apply {
put(Metadata.Key.of("kritor-self-uin", Metadata.ASCII_STRING_MARSHALLER), TicketHelper.getUin())
put(Metadata.Key.of("kritor-self-uid", Metadata.ASCII_STRING_MARSHALLER), TicketHelper.getUid())
put(Metadata.Key.of("kritor-self-version", Metadata.ASCII_STRING_MARSHALLER), "OpenShamrock-$ShamrockVersion")
})
super.start(responseListener, headers)
}
}
}
}
channel = ManagedChannelBuilder
.forAddress(host, port)
.usePlaintext()
.enableRetry() // 允许尝试
.executor(Dispatchers.IO.asExecutor()) // 使用协程的调度器
.intercept(interceptor)
.build()
}.onFailure {
LogCenter.log("KritorClient start failed: ${it.stackTraceToString()}", Level.ERROR)
}
}
fun listen(retryCnt: Int = -1) {
if (::channelJob.isInitialized && channelJob.isActive) {
channelJob.cancel()
}
channelJob = GlobalScope.launch(Dispatchers.IO) {
runCatching {
val stub = ReverseServiceGrpcKt.ReverseServiceCoroutineStub(channel)
registerEvent(EventType.EVENT_TYPE_MESSAGE)
registerEvent(EventType.EVENT_TYPE_CORE_EVENT)
registerEvent(EventType.EVENT_TYPE_REQUEST)
registerEvent(EventType.EVENT_TYPE_NOTICE)
stub.reverseStream(channelFlow {
senderChannel.collect { send(it) }
}).collect {
onReceive(it)
}
}.onFailure {
LogCenter.log("KritorClient listen failed, retry after 15s: ${it.stackTraceToString()}", Level.WARN)
}
delay(15.seconds)
LogCenter.log("KritorClient listen retrying, retryCnt = $retryCnt", Level.WARN)
if (retryCnt != 0) listen(retryCnt - 1)
}
}
fun registerEvent(eventType: EventType) {
GlobalScope.launch(Dispatchers.IO) {
runCatching {
EventServiceGrpcKt.EventServiceCoroutineStub(channel).registerPassiveListener(channelFlow {
when(eventType) {
EventType.EVENT_TYPE_MESSAGE -> GlobalEventTransmitter.onMessageEvent {
send(EventStructure.newBuilder().apply {
this.type = EventType.EVENT_TYPE_MESSAGE
this.message = it.second
}.build())
}
EventType.EVENT_TYPE_CORE_EVENT -> {}
EventType.EVENT_TYPE_NOTICE -> GlobalEventTransmitter.onNoticeEvent {
send(EventStructure.newBuilder().apply {
this.type = EventType.EVENT_TYPE_NOTICE
this.notice = it
}.build())
}
EventType.EVENT_TYPE_REQUEST -> GlobalEventTransmitter.onRequestEvent {
send(EventStructure.newBuilder().apply {
this.type = EventType.EVENT_TYPE_REQUEST
this.request = it
}.build())
}
EventType.UNRECOGNIZED -> {}
}
})
}.onFailure {
LogCenter.log("KritorClient registerEvent failed: ${it.stackTraceToString()}", Level.ERROR)
}
}
}
private suspend fun onReceive(request: Request) = GlobalScope.launch {
//LogCenter.log("KritorClient onReceive: $request")
runCatching {
val rsp = handleGrpc(request.cmd, request.buf.toByteArray())
senderChannel.emit(Response.newBuilder()
.setCmd(request.cmd)
.setCode(Response.ResponseCode.SUCCESS)
.setMsg("success")
.setSeq(request.seq)
.setBuf(ByteString.copyFrom(rsp))
.build())
}.onFailure {
senderChannel.emit(Response.newBuilder()
.setCmd(request.cmd)
.setCode(Response.ResponseCode.INTERNAL)
.setMsg(it.stackTraceToString())
.setSeq(request.seq)
.setBuf(ByteString.EMPTY)
.build())
}
}
fun isActive(): Boolean {
return !channel.isShutdown
}
fun close() {
channel.shutdown()
}
}

View File

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

View File

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

View File

@ -0,0 +1,60 @@
package kritor.service
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.authentication.*
import io.kritor.authentication.AuthenticateResponse.AuthenticateResponseCode
import kritor.auth.AuthInterceptor
import moe.fuqiuluo.shamrock.config.ActiveTicket
import moe.fuqiuluo.shamrock.config.ShamrockConfig
import qq.service.QQInterfaces
internal object AuthenticationService: AuthenticationServiceGrpcKt.AuthenticationServiceCoroutineImplBase() {
@Grpc("AuthenticationService", "Authenticate")
override suspend fun authenticate(request: AuthenticateRequest): AuthenticateResponse {
if (QQInterfaces.app.account != request.account) {
return AuthenticateResponse.newBuilder().apply {
code = AuthenticateResponseCode.NO_ACCOUNT
msg = "No such account"
}.build()
}
val activeTicketName = ActiveTicket.name()
var index = 0
while (true) {
val ticket = ShamrockConfig.getProperty(activeTicketName + if (index == 0) "" else ".$index", null)
if (ticket.isNullOrEmpty()) {
if (index == 0) {
return AuthenticateResponse.newBuilder().apply {
code = AuthenticateResponseCode.OK
msg = "OK"
}.build()
} else {
break
}
} else if (ticket == request.ticket) {
return AuthenticateResponse.newBuilder().apply {
code = AuthenticateResponseCode.OK
msg = "OK"
}.build()
}
index++
}
return AuthenticateResponse.newBuilder().apply {
code = AuthenticateResponseCode.NO_TICKET
msg = "Invalid ticket"
}.build()
}
@Grpc("AuthenticationService", "GetAuthenticationState")
override suspend fun getAuthenticationState(request: GetAuthenticationStateRequest): GetAuthenticationStateResponse {
if (request.account != QQInterfaces.app.account) {
throw StatusRuntimeException(Status.CANCELLED.withDescription("No such account"))
}
return GetAuthenticationStateResponse.newBuilder().apply {
isRequired = AuthInterceptor.getAllTicket().isNotEmpty()
}.build()
}
}

View File

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

View File

@ -0,0 +1,67 @@
package kritor.service
import com.google.protobuf.ByteString
import io.kritor.developer.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
import moe.fuqiuluo.shamrock.utils.PlatformUtils
import qq.service.QQInterfaces
import java.io.File
internal object DeveloperService: DeveloperServiceGrpcKt.DeveloperServiceCoroutineImplBase() {
@Grpc("DeveloperService", "Shell")
override suspend fun shell(request: ShellRequest): ShellResponse {
if (request.commandList.isEmpty()) return ShellResponse.newBuilder().setIsSuccess(false).build()
val runtime = Runtime.getRuntime()
val result = withTimeoutOrNull(5000L) {
withContext(Dispatchers.IO) {
runtime.exec(request.commandList.toTypedArray(), null, File(request.directory)).apply { waitFor() }
}
}
return ShellResponse.newBuilder().apply {
if (result == null) {
isSuccess = false
} else {
isSuccess = true
result.inputStream.use {
data = it.readBytes().toString(Charsets.UTF_8)
}
}
}.build()
}
@Grpc("DeveloperService", "ClearCache")
override suspend fun clearCache(request: ClearCacheRequest): ClearCacheResponse {
FileUtils.clearCache()
MMKVFetcher.mmkvWithId("audio2silk")
.clear()
return ClearCacheResponse.newBuilder().build()
}
@Grpc("DeveloperService", "GetDeviceBattery")
override suspend fun getDeviceBattery(request: GetDeviceBatteryRequest): GetDeviceBatteryResponse {
return GetDeviceBatteryResponse.newBuilder().apply {
PlatformUtils.getDeviceBattery().let {
this.battery = it.battery
this.scale = it.scale
this.status = it.status
}
}.build()
}
@Grpc("DeveloperService", "SendPacket")
override suspend fun sendPacket(request: SendPacketRequest): SendPacketResponse {
return SendPacketResponse.newBuilder().apply {
val fromServiceMsg = QQInterfaces.sendBufferAW(request.command, request.isProtobuf, request.requestBuffer.toByteArray())
if (fromServiceMsg?.wupBuffer == null) {
this.isSuccess = false
} else {
this.isSuccess = true
this.responseBuffer = ByteString.copyFrom(fromServiceMsg.wupBuffer)
}
}.build()
}
}

View File

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

View File

@ -0,0 +1,244 @@
package kritor.service
import android.os.Bundle
import com.tencent.mobileqq.profilecard.api.IProfileCardBlacklistApi
import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst
import com.tencent.mobileqq.profilecard.api.IProfileProtocolService
import com.tencent.mobileqq.qroute.QRoute
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.friend.*
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import qq.service.QQInterfaces
import qq.service.contact.ContactHelper
import qq.service.friend.FriendHelper
import kotlin.coroutines.resume
internal object FriendService : FriendServiceGrpcKt.FriendServiceCoroutineImplBase() {
@Grpc("FriendService", "GetFriendList")
override suspend fun getFriendList(request: GetFriendListRequest): GetFriendListResponse {
val friendList = FriendHelper.getFriendList(if (request.hasRefresh()) request.refresh else false).onFailure {
throw StatusRuntimeException(
Status.INTERNAL
.withDescription(it.stackTraceToString())
)
}.getOrThrow()
return GetFriendListResponse.newBuilder().apply {
friendList.forEach {
this.addFriendsInfo(FriendInfo.newBuilder().apply {
uin = it.uin.toLong()
uid = ContactHelper.getUidByUinAsync(uin)
qid = ""
nick = it.name ?: ""
remark = it.remark ?: ""
age = it.age
level = 0
gender = it.gender.toInt()
groupId = it.groupid
ext = ExtInfo.newBuilder().build()
})
}
}.build()
}
@Grpc("FriendService", "GetFriendProfileCard")
override suspend fun getFriendProfileCard(request: GetFriendProfileCardRequest): GetFriendProfileCardResponse {
return GetFriendProfileCardResponse.newBuilder().apply {
request.targetUinsList.forEach {
ContactHelper.getProfileCard(it).getOrThrow().let { info ->
addFriendsProfileCard(ProfileCard.newBuilder().apply {
this.uin = info.uin.toLong()
this.uid = ContactHelper.getUidByUinAsync(info.uin.toLong())
this.nick = info.strNick
this.remark = info.strReMark
this.level = info.iQQLevel
this.birthday = info.lBirthday
this.loginDay = info.lLoginDays.toInt()
this.voteCnt = info.lVoteCount.toInt()
this.qid = info.qid ?: ""
this.isSchoolVerified = info.schoolVerifiedFlag
this.ext = ExtInfo.newBuilder().apply {
this.bigVip = info.bBigClubVipOpen == 1.toByte()
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
this.qqVip = info.bQQVipOpen == 1.toByte()
this.superVip = info.bSuperQQOpen == 1.toByte()
this.voted = info.bVoted == 1.toByte()
}.build()
}.build())
}
}
request.targetUidsList.forEach {
ContactHelper.getProfileCard(ContactHelper.getUinByUidAsync(it).toLong()).getOrThrow().let { info ->
addFriendsProfileCard(ProfileCard.newBuilder().apply {
this.uin = info.uin.toLong()
this.uid = it
this.nick = info.strNick
this.remark = info.strReMark
this.level = info.iQQLevel
this.birthday = info.lBirthday
this.loginDay = info.lLoginDays.toInt()
this.voteCnt = info.lVoteCount.toInt()
this.qid = info.qid ?: ""
this.isSchoolVerified = info.schoolVerifiedFlag
this.ext = ExtInfo.newBuilder().apply {
this.bigVip = info.bBigClubVipOpen == 1.toByte()
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
this.qqVip = info.bQQVipOpen == 1.toByte()
this.superVip = info.bSuperQQOpen == 1.toByte()
this.voted = info.bVoted == 1.toByte()
}.build()
}.build())
}
}
}.build()
}
@Grpc("FriendService", "GetStrangerProfileCard")
override suspend fun getStrangerProfileCard(request: GetStrangerProfileCardRequest): GetStrangerProfileCardResponse {
return GetStrangerProfileCardResponse.newBuilder().apply {
request.targetUinsList.forEach {
ContactHelper.refreshAndGetProfileCard(it).getOrThrow().let { info ->
addStrangersProfileCard(ProfileCard.newBuilder().apply {
this.uin = info.uin.toLong()
this.uid = ContactHelper.getUidByUinAsync(info.uin.toLong())
this.nick = info.strNick
this.level = info.iQQLevel
this.birthday = info.lBirthday
this.loginDay = info.lLoginDays.toInt()
this.voteCnt = info.lVoteCount.toInt()
this.qid = info.qid ?: ""
this.isSchoolVerified = info.schoolVerifiedFlag
this.ext = ExtInfo.newBuilder().apply {
this.bigVip = info.bBigClubVipOpen == 1.toByte()
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
this.qqVip = info.bQQVipOpen == 1.toByte()
this.superVip = info.bSuperQQOpen == 1.toByte()
this.voted = info.bVoted == 1.toByte()
}.build()
}.build())
}
}
request.targetUidsList.forEach {
ContactHelper.refreshAndGetProfileCard(ContactHelper.getUinByUidAsync(it).toLong()).getOrThrow()
.let { info ->
addStrangersProfileCard(ProfileCard.newBuilder().apply {
this.uin = info.uin.toLong()
this.uid = it
this.nick = info.strNick
this.level = info.iQQLevel
this.birthday = info.lBirthday
this.loginDay = info.lLoginDays.toInt()
this.voteCnt = info.lVoteCount.toInt()
this.qid = info.qid ?: ""
this.isSchoolVerified = info.schoolVerifiedFlag
this.ext = ExtInfo.newBuilder().apply {
this.bigVip = info.bBigClubVipOpen == 1.toByte()
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
this.qqVip = info.bQQVipOpen == 1.toByte()
this.superVip = info.bSuperQQOpen == 1.toByte()
this.voted = info.bVoted == 1.toByte()
}.build()
}.build())
}
}
}.build()
}
@Grpc("FriendService", "SetProfileCard")
override suspend fun setProfileCard(request: SetProfileCardRequest): SetProfileCardResponse {
val bundle = Bundle()
val service = QQInterfaces.app
.getRuntimeService(IProfileProtocolService::class.java, "all")
if (request.hasNick()) {
bundle.putString(IProfileProtocolConst.KEY_NICK, request.nick)
}
if (request.hasCompany()) {
bundle.putString(IProfileProtocolConst.KEY_COMPANY, request.company)
}
if (request.hasEmail()) {
bundle.putString(IProfileProtocolConst.KEY_EMAIL, request.email)
}
if (request.hasCollege()) {
bundle.putString(IProfileProtocolConst.KEY_COLLEGE, request.college)
}
if (request.hasPersonalNote()) {
bundle.putString(IProfileProtocolConst.KEY_PERSONAL_NOTE, request.personalNote)
}
if (request.hasBirthday()) {
bundle.putInt(IProfileProtocolConst.KEY_BIRTHDAY, request.birthday)
}
if (request.hasAge()) {
bundle.putInt(IProfileProtocolConst.KEY_AGE, request.age)
}
service.setProfileDetail(bundle)
return super.setProfileCard(request)
}
@Grpc("FriendService", "IsBlackListUser")
override suspend fun isBlackListUser(request: IsBlackListUserRequest): IsBlackListUserResponse {
val uin = when (request.targetCase!!) {
IsBlackListUserRequest.TargetCase.TARGET_UIN -> request.targetUin.toString()
IsBlackListUserRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid)
IsBlackListUserRequest.TargetCase.TARGET_NOT_SET -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("account not set")
)
}
val blacklistApi = QRoute.api(IProfileCardBlacklistApi::class.java)
val isBlack = withTimeoutOrNull(5000) {
suspendCancellableCoroutine { continuation ->
blacklistApi.isBlackOrBlackedUin(uin) {
continuation.resume(it)
}
}
} ?: false
return IsBlackListUserResponse.newBuilder().setIsBlackListUser(isBlack).build()
}
@Grpc("FriendService", "VoteUser")
override suspend fun voteUser(request: VoteUserRequest): VoteUserResponse {
ContactHelper.voteUser(
when (request.targetCase!!) {
VoteUserRequest.TargetCase.TARGET_UIN -> request.targetUin
VoteUserRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
VoteUserRequest.TargetCase.TARGET_NOT_SET -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("account not set")
)
}, request.voteCount
).onFailure {
throw StatusRuntimeException(
Status.INTERNAL
.withDescription(it.stackTraceToString())
)
}
return VoteUserResponse.newBuilder().build()
}
@Grpc("FriendService", "GetUidByUin")
override suspend fun getUidByUin(request: GetUidByUinRequest): GetUidByUinResponse {
return GetUidByUinResponse.newBuilder().apply {
request.targetUinsList.forEach {
putUidMap(it, ContactHelper.getUidByUinAsync(it))
}
}.build()
}
@Grpc("FriendService", "GetUinByUid")
override suspend fun getUinByUid(request: GetUinByUidRequest): GetUinByUidResponse {
return GetUinByUidResponse.newBuilder().apply {
request.targetUidsList.forEach {
putUinMap(it, ContactHelper.getUinByUidAsync(it).toLong())
}
}.build()
}
}

View File

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

View File

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

View File

@ -0,0 +1,475 @@
package kritor.service
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import com.tencent.qqnt.msg.api.IMsgService
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.common.*
import io.kritor.message.*
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import protobuf.auto.toByteArray
import protobuf.message.*
import protobuf.message.element.GeneralFlags
import protobuf.message.routing.C2C
import protobuf.message.routing.Grp
import qq.service.QQInterfaces
import qq.service.contact.longPeer
import qq.service.internals.NTServiceFetcher
import qq.service.msg.*
import qq.service.msg.ForwardMessageHelper
import qq.service.msg.MessageHelper
import kotlin.coroutines.resume
import kotlin.random.Random
import kotlin.random.nextUInt
internal object MessageService : MessageServiceGrpcKt.MessageServiceCoroutineImplBase() {
@Grpc("MessageService", "SendMessage")
override suspend fun sendMessage(request: SendMessageRequest): SendMessageResponse {
val contact = request.contact.let {
MessageHelper.generateContact(
when (it.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}, it.peer, it.subPeer
)
}
val uniseq = MessageHelper.generateMsgId(contact.chatType)
return SendMessageResponse.newBuilder().apply {
this.messageId = MessageHelper.sendMessage(
contact,
NtMsgConvertor.convertToNtMsgs(contact, uniseq, request.elementsList),
request.retryCount,
uniseq
).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
}.getOrThrow().toString()
}.build()
}
@Grpc("MessageService", "SendMessageByResId")
override suspend fun sendMessageByResId(request: SendMessageByResIdRequest): SendMessageByResIdResponse {
val contact = request.contact
val req = PbSendMsgReq(
routingHead = when (request.contact.scene) {
Scene.GROUP -> RoutingHead(grp = Grp(contact.longPeer().toUInt()))
Scene.FRIEND -> RoutingHead(c2c = C2C(contact.longPeer().toUInt()))
else -> RoutingHead(grp = Grp(contact.longPeer().toUInt()))
},
contentHead = ContentHead(1, 0, 0, 0),
msgBody = MsgBody(
richText = RichText(
elements = arrayListOf(
Elem(
generalFlags = GeneralFlags(
longTextFlag = 1u,
longTextResid = request.resId
)
)
)
)
),
msgSeq = Random.nextUInt(),
msgRand = Random.nextUInt(),
msgVia = 0u
)
QQInterfaces.sendBuffer("MessageSvc.PbSendMsg", true, req.toByteArray())
return SendMessageByResIdResponse.newBuilder().build()
}
@Grpc("MessageService", "SetMessageReaded")
override suspend fun setMessageReaded(request: SetMessageReadRequest): SetMessageReadResponse {
val contact = request.contact
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val service = sessionService.msgService
val chatType = when (contact.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}
service.clearMsgRecords(
Contact(
chatType,
contact.peer,
contact.subPeer
), null)
return SetMessageReadResponse.newBuilder().build()
}
@Grpc("MessageService", "RecallMessage")
override suspend fun recallMessage(request: RecallMessageRequest): RecallMessageResponse {
val contact = request.contact.let {
MessageHelper.generateContact(
when (it.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}, it.peer, it.subPeer
)
}
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val service = sessionService.msgService
service.recallMsg(contact, arrayListOf(request.messageId.toLong())) { code, msg ->
if (code != 0) {
LogCenter.log("消息撤回失败: $code:$msg", Level.WARN)
}
}
return RecallMessageResponse.newBuilder().build()
}
@Grpc("MessageService", "GetMessage")
override suspend fun getMessage(request: GetMessageRequest): GetMessageResponse {
val contact = request.contact.let {
MessageHelper.generateContact(
when (it.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}, it.peer, it.subPeer
)
}
val msg: MsgRecord = withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
suspendCancellableCoroutine { continuation ->
service.getMsgsByMsgId(contact, arrayListOf(request.messageId.toLong())) { code, _, msgRecords ->
if (code == 0 && msgRecords.isNotEmpty()) {
continuation.resume(msgRecords.first())
} else {
continuation.resume(null)
}
}
continuation.invokeOnCancellation {
continuation.resume(null)
}
}
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
return GetMessageResponse.newBuilder().apply {
this.message = PushMessageBody.newBuilder().apply {
this.messageId = msg.msgId.toString()
this.contact = request.contact
this.sender = Sender.newBuilder().apply {
this.uid = msg.senderUid ?: ""
this.uin = msg.senderUin
this.nick = msg.sendNickName ?: ""
}.build()
this.messageSeq = msg.msgSeq
this.addAllElements(msg.elements.toKritorReqMessages(contact))
}.build()
}.build()
}
@Grpc("MessageService", "GetMessageBySeq")
override suspend fun getMessageBySeq(request: GetMessageBySeqRequest): GetMessageBySeqResponse {
val contact = request.contact.let {
MessageHelper.generateContact(
when (it.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}, it.peer, it.subPeer
)
}
val msg: MsgRecord = withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
suspendCancellableCoroutine { continuation ->
service.getMsgsBySeqAndCount(contact, request.messageSeq, 1, true) { code, _, msgRecords ->
if (code == 0 && msgRecords.isNotEmpty()) {
continuation.resume(msgRecords.first())
} else {
continuation.resume(null)
}
}
continuation.invokeOnCancellation {
continuation.resume(null)
}
}
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
return GetMessageBySeqResponse.newBuilder().apply {
this.message = PushMessageBody.newBuilder().apply {
this.messageId = msg.msgId.toString()
this.contact = request.contact
this.sender = Sender.newBuilder().apply {
this.uin = msg.senderUin
this.nick = msg.sendNickName ?: ""
this.uid = msg.senderUid ?: ""
}.build()
this.messageSeq = msg.msgSeq
this.addAllElements(msg.elements.toKritorReqMessages(contact))
}.build()
}.build()
}
@Grpc("MessageService", "GetHistoryMessage")
override suspend fun getHistoryMessage(request: GetHistoryMessageRequest): GetHistoryMessageResponse {
val contact = request.contact.let {
MessageHelper.generateContact(
when (it.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}, it.peer, it.subPeer
)
}
val msgs: List<MsgRecord> = withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
suspendCancellableCoroutine { continuation ->
service.getMsgs(contact, request.startMessageId.toLong(), request.count, true) { code, _, msgRecords ->
if (code == 0 && msgRecords.isNotEmpty()) {
continuation.resume(msgRecords)
} else {
continuation.resume(null)
}
}
continuation.invokeOnCancellation {
continuation.resume(null)
}
}
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Messages not found"))
return GetHistoryMessageResponse.newBuilder().apply {
msgs.forEach {
addMessages(PushMessageBody.newBuilder().apply {
this.messageId = it.msgId.toString()
this.contact = request.contact
this.sender = Sender.newBuilder().apply {
this.uin = it.senderUin
this.nick = it.sendNickName ?: ""
this.uid = it.senderUid ?: ""
}.build()
this.messageSeq = it.msgSeq
this.addAllElements(it.elements.toKritorReqMessages(contact))
})
}
}.build()
}
@Grpc("MessageService", "UploadForwardMessage")
override suspend fun uploadForwardMessage(request: UploadForwardMessageRequest): UploadForwardMessageResponse {
val contact = request.contact.let {
MessageHelper.generateContact(
when (it.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}, it.peer, it.subPeer
)
}
val forwardMessage = ForwardMessageHelper.uploadMultiMsg(
contact,
request.messagesList
).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
}.getOrThrow()
return UploadForwardMessageResponse.newBuilder().apply {
this.resId = forwardMessage.resId
}.build()
}
@Grpc("MessageService", "DownloadForwardMessage")
override suspend fun downloadForwardMessage(request: DownloadForwardMessageRequest): DownloadForwardMessageResponse {
return DownloadForwardMessageResponse.newBuilder().apply {
this.addAllMessages(
MessageHelper.getForwardMsg(request.resId).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
}.getOrThrow().map { detail ->
PushMessageBody.newBuilder().apply {
this.time = detail.time.toLong()
this.messageId = detail.qqMsgId.toString()
this.messageSeq = detail.msgSeq
this.contact = io.kritor.common.Contact.newBuilder().apply {
this.scene = when (detail.msgType) {
MsgConstant.KCHATTYPEC2C -> Scene.FRIEND
MsgConstant.KCHATTYPEGROUP -> Scene.GROUP
MsgConstant.KCHATTYPEGUILD -> Scene.GUILD
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> Scene.STRANGER_FROM_GROUP
MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN -> Scene.NEARBY
else -> Scene.STRANGER
}
this.peer = detail.peerId.toString()
}.build()
this.sender = Sender.newBuilder().apply {
this.uin = detail.sender.userId
this.nick = detail.sender.nickName
this.uid = detail.sender.uid
}.build()
detail.message?.elements?.toKritorResponseMessages(
Contact(
detail.msgType,
detail.peerId.toString(),
null
)
)?.let {
this.addAllElements(it)
}
}.build()
}
)
}.build()
}
@Grpc("MessageService", "DeleteEssenceMessage")
override suspend fun deleteEssenceMessage(request: DeleteEssenceMessageRequest): DeleteEssenceMessageResponse {
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString())
val msg: MsgRecord = withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
suspendCancellableCoroutine { continuation ->
service.getMsgsByMsgId(contact, arrayListOf(request.messageId.toLong())) { code, _, msgRecords ->
if (code == 0 && msgRecords.isNotEmpty()) {
continuation.resume(msgRecords.first())
} else {
continuation.resume(null)
}
}
continuation.invokeOnCancellation {
continuation.resume(null)
}
}
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
if (MessageHelper.deleteEssenceMessage(request.groupId, msg.msgSeq, msg.msgRandom) == null)
throw StatusRuntimeException(Status.NOT_FOUND.withDescription("delete essence message failed"))
return DeleteEssenceMessageResponse.newBuilder().build()
}
@Grpc("MessageService", "GetEssenceMessageList")
override suspend fun getEssenceMessageList(request: GetEssenceMessageListRequest): GetEssenceMessageListResponse {
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString())
return GetEssenceMessageListResponse.newBuilder().apply {
MessageHelper.getEssenceMessageList(request.groupId, request.page, request.pageSize).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
}.getOrThrow().forEach {
addMessages(EssenceMessageBody.newBuilder().apply {
withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
suspendCancellableCoroutine { continuation ->
service.getMsgsBySeqAndCount(contact, it.messageSeq, 1, true) { code, _, msgRecords ->
if (code == 0 && msgRecords.isNotEmpty()) {
continuation.resume(msgRecords.first())
} else {
continuation.resume(null)
}
}
continuation.invokeOnCancellation {
continuation.resume(null)
}
}
}?.let {
this.messageId = it.msgId.toString()
}
this.messageSeq = it.messageSeq
this.messageTime = it.senderTime
this.senderNick = it.senderNick
this.senderUin = it.senderId
this.operationTime = it.operatorTime
this.operatorNick = it.operatorNick
this.operatorUin = it.operatorId
this.jsonElements = it.messageContent.toString()
})
}
}.build()
}
@Grpc("MessageService", "SetEssenceMessage")
override suspend fun setEssenceMessage(request: SetEssenceMessageRequest): SetEssenceMessageResponse {
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString())
val msg: MsgRecord = withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
suspendCancellableCoroutine { continuation ->
service.getMsgsByMsgId(contact, arrayListOf(request.messageId.toLong())) { code, _, msgRecords ->
if (code == 0 && msgRecords.isNotEmpty()) {
continuation.resume(msgRecords.first())
} else {
continuation.resume(null)
}
}
continuation.invokeOnCancellation {
continuation.resume(null)
}
}
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
if (MessageHelper.setEssenceMessage(request.groupId, msg.msgSeq, msg.msgRandom) == null) {
throw StatusRuntimeException(Status.NOT_FOUND.withDescription("set essence message failed"))
}
return SetEssenceMessageResponse.newBuilder().build()
}
@Grpc("MessageService", "ReactMessageWithEmoji")
override suspend fun reactMessageWithEmoji(request: ReactMessageWithEmojiRequest): ReactMessageWithEmojiResponse {
val contact = request.contact.let {
MessageHelper.generateContact(
when (it.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}, it.peer, it.subPeer
)
}
val msg: MsgRecord = withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
suspendCancellableCoroutine { continuation ->
service.getMsgsByMsgId(contact, arrayListOf(request.messageId.toLong())) { code, _, msgRecords ->
if (code == 0 && msgRecords.isNotEmpty()) {
continuation.resume(msgRecords.first())
} else {
continuation.resume(null)
}
}
continuation.invokeOnCancellation {
continuation.resume(null)
}
}
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
MessageHelper.setGroupMessageCommentFace(
request.contact.longPeer(),
msg.msgSeq.toULong(),
request.faceId.toString(),
request.isSet
)
return ReactMessageWithEmojiResponse.newBuilder().build()
}
}

View File

@ -0,0 +1,33 @@
package kritor.service
import com.google.protobuf.ByteString
import com.tencent.mobileqq.fe.FEKit
import com.tencent.mobileqq.qsec.qsecdandelionsdk.Dandelion
import io.kritor.developer.*
internal object QsignService: QsignServiceGrpcKt.QsignServiceCoroutineImplBase() {
@Grpc("QsignService", "Sign")
override suspend fun sign(request: SignRequest): SignResponse {
return SignResponse.newBuilder().apply {
val result = FEKit.getInstance().getSign(request.command, request.buffer.toByteArray(), request.seq, request.uin)
this.secSig = ByteString.copyFrom(result.sign)
this.secDeviceToken = ByteString.copyFrom(result.token)
this.secExtra = ByteString.copyFrom(result.extra)
}.build()
}
@Grpc("QsignService", "Energy")
override suspend fun energy(request: EnergyRequest): EnergyResponse {
return EnergyResponse.newBuilder().apply {
this.result = ByteString.copyFrom(Dandelion.getInstance().fly(request.data, request.salt.toByteArray()))
}.build()
}
@Grpc("QsignService", "GetCmdWhitelist")
override suspend fun getCmdWhitelist(request: GetCmdWhitelistRequest): GetCmdWhitelistResponse {
return GetCmdWhitelistResponse.newBuilder().apply {
addAllCommands(FEKit.getInstance().cmdWhiteList)
}.build()
}
}

View File

@ -0,0 +1,58 @@
package kritor.service
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.web.*
import qq.service.ticket.TicketHelper
internal object WebService: WebServiceGrpcKt.WebServiceCoroutineImplBase() {
@Grpc("WebService", "GetCookies")
override suspend fun getCookies(request: GetCookiesRequest): GetCookiesResponse {
return GetCookiesResponse.newBuilder().apply {
if (request.domain.isNullOrEmpty()) {
this.cookie = TicketHelper.getCookie()
} else {
this.cookie = TicketHelper.getCookie(request.domain)
}
}.build()
}
@Grpc("WebService", "GetCredentials")
override suspend fun getCredentials(request: GetCredentialsRequest): GetCredentialsResponse {
return GetCredentialsResponse.newBuilder().apply {
if (request.domain.isNullOrEmpty()) {
val uin = TicketHelper.getUin()
val skey = TicketHelper.getRealSkey(uin)
val pskey = TicketHelper.getPSKey(uin)
this.cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey;"
this.bkn = TicketHelper.getCSRF(pskey)
} else {
val uin = TicketHelper.getUin()
val skey = TicketHelper.getRealSkey(uin)
val pskey = TicketHelper.getPSKey(uin, request.domain) ?: ""
val pt4token = TicketHelper.getPt4Token(uin, request.domain) ?: ""
this.cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey; pt4_token=$pt4token;"
this.bkn = TicketHelper.getCSRF(pskey)
}
}.build()
}
@Grpc("WebService", "GetCSRFToken")
override suspend fun getCSRFToken(request: GetCSRFTokenRequest): GetCSRFTokenResponse {
return GetCSRFTokenResponse.newBuilder().apply {
if (request.domain.isNullOrEmpty()) {
this.bkn = TicketHelper.getCSRF()
} else {
this.bkn = TicketHelper.getCSRF(TicketHelper.getUin(), request.domain)
}
}.build()
}
@Grpc("WebService", "GetHttpCookies")
override suspend fun getHttpCookies(request: GetHttpCookiesRequest): GetHttpCookiesResponse {
return GetHttpCookiesResponse.newBuilder().apply {
this.cookie = TicketHelper.getHttpCookies(request.appid, request.daid, request.jumpUrl)
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get http cookies"))
}.build()
}
}

View File

@ -1,203 +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.msf.service.MsfService
import com.tencent.mobileqq.pb.ByteStringMicro
import com.tencent.qphone.base.remote.ToServiceMsg
import com.tencent.qqnt.kernel.api.IKernelService
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 = MsfService.getCore().nextSeq
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 = MsfService.getCore().nextSeq
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 = MsfService.getCore().nextSeq) {
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 = MsfService.getCore().nextSeq
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)
}
}

View File

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

View File

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

View File

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

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