diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..430402e8
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "kritor"]
+ path = kritor
+ url = https://github.com/KarinJS/kritor
diff --git a/README.md b/README.md
index 7d10185e..1a69a17a 100644
--- a/README.md
+++ b/README.md
@@ -28,24 +28,8 @@
## 兼容|迁移|替代 说明
- 一键移植:本项目基于 go-cqhttp 的文档进行开发实现。
-- 平行部署:可多平台部署,未来将会支持 Docker 部署的教程。
-
-## 相关项目
-
-
+- 平行部署:可多平台部署,未来将会支持 Docker 部署的教程。
+- 替代方案:[Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core)
## 权限声明
diff --git a/annotations/build.gradle.kts b/annotations/build.gradle.kts
index cebd7f61..dbb3902c 100644
--- a/annotations/build.gradle.kts
+++ b/annotations/build.gradle.kts
@@ -9,6 +9,6 @@ java {
}
dependencies {
- implementation(DEPENDENCY_PROTOBUF)
+ //implementation(DEPENDENCY_PROTOBUF)
implementation(kotlinx("serialization-protobuf", "1.6.2"))
}
\ No newline at end of file
diff --git a/annotations/src/main/java/kritor/service/Grpc.kt b/annotations/src/main/java/kritor/service/Grpc.kt
new file mode 100644
index 00000000..2d6fe33d
--- /dev/null
+++ b/annotations/src/main/java/kritor/service/Grpc.kt
@@ -0,0 +1,11 @@
+package kritor.service
+
+import kotlin.reflect.KClass
+
+@Retention(AnnotationRetention.SOURCE)
+@Target(AnnotationTarget.FUNCTION)
+annotation class Grpc(
+ val serviceName: String,
+ val funcName: String,
+
+)
diff --git a/annotations/src/main/java/moe/fuqiuluo/symbols/Protobuf.kt b/annotations/src/main/java/moe/fuqiuluo/symbols/Protobuf.kt
index a9e148ec..43ec4e3d 100644
--- a/annotations/src/main/java/moe/fuqiuluo/symbols/Protobuf.kt
+++ b/annotations/src/main/java/moe/fuqiuluo/symbols/Protobuf.kt
@@ -5,6 +5,8 @@ import kotlinx.serialization.protobuf.ProtoBuf
import kotlin.reflect.KClass
+val EMPTY_BYTE_ARRAY = ByteArray(0)
+
interface Protobuf>
inline fun > ByteArray.decodeProtobuf(to: KClass? = null): T {
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 24c36669..f2031351 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -17,7 +17,7 @@ android {
minSdk = 27
targetSdk = 34
versionCode = getVersionCode()
- versionName = "1.0.9" + ".r${getGitCommitCount()}." + getVersionName()
+ versionName = "1.1.0" + ".r${getGitCommitCount()}." + getVersionName()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -201,14 +201,8 @@ dependencies {
implementation("io.coil-kt:coil-compose:2.4.0")
implementation(kotlinx("io-jvm", "0.1.16"))
- implementation(ktor("server", "core"))
- implementation(ktor("server", "host-common"))
- implementation(ktor("server", "status-pages"))
- implementation(ktor("server", "netty"))
- implementation(ktor("server", "content-negotiation"))
implementation(ktor("client", "core"))
- implementation(ktor("client", "content-negotiation"))
- implementation(ktor("client", "cio"))
+ implementation(ktor("client", "okhttp"))
implementation(ktor("serialization", "kotlinx-json"))
implementation(project(":xposed"))
diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt
index a69e8ff2..ea53a892 100644
--- a/app/src/main/cpp/CMakeLists.txt
+++ b/app/src/main/cpp/CMakeLists.txt
@@ -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)
diff --git a/app/src/main/cpp/cqcode.cpp b/app/src/main/cpp/cqcode.cpp
deleted file mode 100644
index 60a5682a..00000000
--- a/app/src/main/cpp/cqcode.cpp
+++ /dev/null
@@ -1,138 +0,0 @@
-#include
-#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>& dest) {
- std::string cache;
- bool is_start = false;
- std::string key_tmp;
- std::unordered_map 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 kv;
- replace_string(cache, "[", "[");
- replace_string(cache, "]", "]");
- replace_string(cache, "&", "&");
- kv.emplace("_type", "text");
- kv.emplace("text", cache);
- dest.push_back(kv);
- cache.clear();
- }
- std::string_view cq_flag(&code[i],4);
- if(cq_flag == "[CQ:"){
- is_start = true;
- i += 3;
- }else{
- cache += c;
- }
- }
- }
- else if (c == "=") {
- if (is_start) {
- if (cache.empty()) {
- throw illegal_code();
- } else {
- if (key_tmp.empty()) {
- key_tmp.append(cache);
- cache.clear();
- } else {
- cache += c;
- }
- }
- } else {
- cache += c;
- }
- }
- else if (c == ",") {
- if (is_start) {
- if (kv.count("_type") == 0 && !cache.empty()) {
- kv.emplace("_type", cache);
- cache.clear();
- } else {
- if (!key_tmp.empty()) {
- replace_string(cache, "[", "[");
- replace_string(cache, "]", "]");
- replace_string(cache, ",", ",");
- replace_string(cache, "&", "&");
- kv.emplace(key_tmp, cache);
- cache.clear();
- key_tmp.clear();
- }
- }
- } else {
- cache += c;
- }
- }
- else if (c == "]") {
- if (is_start) {
- if (!cache.empty()) {
- if (!key_tmp.empty()) {
- replace_string(cache, "[", "[");
- replace_string(cache, "]", "]");
- replace_string(cache, ",", ",");
- replace_string(cache, "&", "&");
- kv.emplace(key_tmp, cache);
- } else {
- kv.emplace("_type", cache);
- }
- dest.push_back(kv);
- kv.clear();
- key_tmp.clear();
- cache.clear();
- is_start = false;
- }
- } else {
- cache += c;
- }
- }
- else {
- cache += c;
- i += (utf8_char_len - 1);
- }
- }
- if (!cache.empty()) {
- std::unordered_map kv;
- replace_string(cache, "[", "[");
- replace_string(cache, "]", "]");
- replace_string(cache, "&", "&");
- kv.emplace("_type", "text");
- kv.emplace("text", cache);
- dest.push_back(kv);
- }
-}
diff --git a/app/src/main/cpp/group_honor.cpp b/app/src/main/cpp/group_honor.cpp
deleted file mode 100644
index ebae11fa..00000000
--- a/app/src/main/cpp/group_honor.cpp
+++ /dev/null
@@ -1,87 +0,0 @@
-#include "jni.h"
-#include
-#include
-#include
-
-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_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, "",
- "(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;
-}
diff --git a/app/src/main/cpp/interface/cqcode.h b/app/src/main/cpp/interface/cqcode.h
deleted file mode 100644
index aed24f94..00000000
--- a/app/src/main/cpp/interface/cqcode.h
+++ /dev/null
@@ -1,20 +0,0 @@
-#ifndef UNTITLED_CQCODE_H
-#define UNTITLED_CQCODE_H
-
-#include
-#include
-#include
-#include
-
-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>& dest);
-
-void encode_cqcode(const std::vector>& segment, std::string& dest);
-
-#endif //UNTITLED_CQCODE_H
diff --git a/app/src/main/cpp/message.cpp b/app/src/main/cpp/message.cpp
index 97fec4ae..09284d4d 100644
--- a/app/src/main/cpp/message.cpp
+++ b/app/src/main/cpp/message.cpp
@@ -1,5 +1,4 @@
#include "jni.h"
-#include "cqcode.h"
#include
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, "", "()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> dest;
- try {
- decode_cqcode(cppCode, dest);
- } catch (illegal_code& code) {
- return arrayList;
- }
-
- jclass HashMap = env->FindClass("java/util/HashMap");
- jmethodID NewHashMap = env->GetMethodID(HashMap, "", "()V");
- jclass String = env->FindClass("java/lang/String");
- jmethodID NewString = env->GetMethodID(String, "", "([BLjava/lang/String;)V");
- jstring charset = env->NewStringUTF("UTF-8");
- jmethodID put = env->GetMethodID(HashMap, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
- for (auto& map : dest) {
- jobject hashMap = env->NewObject(HashMap, NewHashMap);
- for (const auto& pair : map) {
- jbyteArray keyArray = env->NewByteArray((int) pair.first.size());
- jbyteArray valueArray = env->NewByteArray((int) pair.second.size());
- env->SetByteArrayRegion(keyArray, 0, (int) pair.first.size(), (jbyte*)pair.first.c_str());
- env->SetByteArrayRegion(valueArray, 0, (int) pair.second.size(), (jbyte*)pair.second.c_str());
- auto key = (jstring) env->NewObject(String, NewString, keyArray, charset);
- auto value = (jstring) env->NewObject(String, NewString, valueArray, charset);
- env->CallObjectMethod(hashMap, put, key, value);
- }
- env->CallBooleanMethod(arrayList, ArrayListAdd, hashMap);
- }
-
- env->DeleteLocalRef(ArrayList);
- env->DeleteLocalRef(HashMap);
- env->DeleteLocalRef(String);
- env->DeleteLocalRef(charset);
- env->ReleaseStringUTFChars(code, cCode);
-
- return arrayList;
-}
-
-extern "C"
-JNIEXPORT jstring JNICALL
-Java_moe_fuqiuluo_shamrock_helper_MessageHelper_nativeEncodeCQCode(JNIEnv *env, jobject thiz,
- jobject segment_list) {
- jclass List = env->FindClass("java/util/List");
- jmethodID ListSize = env->GetMethodID(List, "size", "()I");
- jmethodID ListGet = env->GetMethodID(List, "get", "(I)Ljava/lang/Object;");
- jclass Map = env->FindClass("java/util/Map");
- jmethodID MapGet = env->GetMethodID(Map, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
- jmethodID entrySetMethod = env->GetMethodID(Map, "entrySet", "()Ljava/util/Set;");
- jclass setClass = env->FindClass("java/util/Set");
- jmethodID iteratorMethod = env->GetMethodID(setClass, "iterator", "()Ljava/util/Iterator;");
- jclass entryClass = env->FindClass("java/util/Map$Entry");
- jmethodID getKeyMethod = env->GetMethodID(entryClass, "getKey", "()Ljava/lang/Object;");
- jmethodID getValueMethod = env->GetMethodID(entryClass, "getValue", "()Ljava/lang/Object;");
-
- std::string result;
- jint size = env->CallIntMethod(segment_list, ListSize);
- for (int i = 0; i < size; i++ ) {
- jobject segment = env->CallObjectMethod(segment_list, ListGet, i);
- jobject entrySet = env->CallObjectMethod(segment, entrySetMethod);
- jobject iterator = env->CallObjectMethod(entrySet, iteratorMethod);
- auto type = (jstring) env->CallObjectMethod(segment, MapGet, env->NewStringUTF("_type"));
- auto typeString = env->GetStringUTFChars(type, nullptr);
- if (strcmp(typeString, "text") == 0) {
- auto text = (jstring) env->CallObjectMethod(segment, MapGet, env->NewStringUTF("text"));
- auto textString = env->GetStringUTFChars(text, nullptr);
- std::string tmpValue = textString;
- replace_string(tmpValue, "&", "&");
- replace_string(tmpValue, "[", "[");
- replace_string(tmpValue, "]", "]");
- replace_string(tmpValue, ",", ",");
- result.append(tmpValue);
- env->ReleaseStringUTFChars(text, textString);
- } else {
- result.append("[CQ:");
- result.append(typeString);
- while (env->CallBooleanMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "hasNext", "()Z"))) {
- jobject entry = env->CallObjectMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "next", "()Ljava/lang/Object;"));
- auto key = (jstring) env->CallObjectMethod(entry, getKeyMethod);
- auto value = (jstring) env->CallObjectMethod(entry, getValueMethod);
- auto keyString = env->GetStringUTFChars(key, nullptr);
- auto valueString = env->GetStringUTFChars(value, nullptr);
- if (strcmp(keyString, "_type") != 0) {
- std::string tmpValue = valueString;
- replace_string(tmpValue, "&", "&");
- replace_string(tmpValue, "[", "[");
- replace_string(tmpValue, "]", "]");
- replace_string(tmpValue, ",", ",");
- result.append(",").append(keyString).append("=").append(tmpValue);
- }
- env->ReleaseStringUTFChars(key, keyString);
- env->ReleaseStringUTFChars(value, valueString);
- env->DeleteLocalRef(entry);
- env->DeleteLocalRef(key);
- env->DeleteLocalRef(value);
- }
- result.append("]");
- }
- env->ReleaseStringUTFChars(type, typeString);
- }
-
- env->DeleteLocalRef(List);
- env->DeleteLocalRef(Map);
- env->DeleteLocalRef(setClass);
- env->DeleteLocalRef(entryClass);
- return env->NewStringUTF(result.c_str());
-}
-
-
extern "C"
JNIEXPORT jlong JNICALL
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_insertChatTypeToMsgId(JNIEnv *env, jobject thiz,
diff --git a/app/src/main/cpp/shamrock.cpp b/app/src/main/cpp/shamrock.cpp
index 9e90b5c2..ce6a499d 100644
--- a/app/src/main/cpp/shamrock.cpp
+++ b/app/src/main/cpp/shamrock.cpp
@@ -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库成功~");
}
diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/MainActivity.kt b/app/src/main/java/moe/fuqiuluo/shamrock/MainActivity.kt
index 5b0b1551..52f4e809 100644
--- a/app/src/main/java/moe/fuqiuluo/shamrock/MainActivity.kt
+++ b/app/src/main/java/moe/fuqiuluo/shamrock/MainActivity.kt
@@ -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
diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/app/config/ConfigKey.kt b/app/src/main/java/moe/fuqiuluo/shamrock/app/config/ConfigKey.kt
new file mode 100644
index 00000000..40ad8b05
--- /dev/null
+++ b/app/src/main/java/moe/fuqiuluo/shamrock/app/config/ConfigKey.kt
@@ -0,0 +1,2 @@
+package moe.fuqiuluo.shamrock.app.config
+
diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/app/config/ShamrockConfig.kt b/app/src/main/java/moe/fuqiuluo/shamrock/app/config/ShamrockConfig.kt
new file mode 100644
index 00000000..21c2a2dd
--- /dev/null
+++ b/app/src/main/java/moe/fuqiuluo/shamrock/app/config/ShamrockConfig.kt
@@ -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 get(ctx: Context, key: ConfigKey): 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 set(ctx: Context, key: ConfigKey, 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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/app/ShamrockConfig.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/app/ShamrockConfig.kt
deleted file mode 100644
index f12999b7..00000000
--- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/app/ShamrockConfig.kt
+++ /dev/null
@@ -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 {
- 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")
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt
index 9d195c2a..2b3bfa20 100644
--- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt
+++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/DashboardFragment.kt
@@ -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",
+ isSwitch = ShamrockConfig[ctx, ActiveRPC]
) {
- ShamrockConfig.setWebhook(ctx, it)
+ ShamrockConfig[ctx, ActiveRPC] = it
return@Function true
}
Function(
- title = "消息格式为CQ码",
- desc = "HTTPAPI回调的消息格式,关闭则为消息段。",
- isSwitch = ShamrockConfig.isUseCQCode(ctx)
+ title = "被动RPC",
+ desc = "Kritor协议实现RPC",
+ isSwitch = ShamrockConfig[ctx, PassiveRPC]
) {
- ShamrockConfig.setUseCQCode(ctx, it)
- return@Function true
- }
-
- Function(
- title = "主动WebSocket",
- desc = "OneBot标准WebSocket,Shamrock作为Server。",
- isSwitch = ShamrockConfig.isWs(ctx)
- ) {
- ShamrockConfig.setWs(ctx, it)
- return@Function true
- }
-
- Function(
- title = "被动WebSocket",
- desc = "OneBot标准WebSocket,Shamrock作为Client。",
- isSwitch = ShamrockConfig.isWsClient(ctx)
- ) {
- ShamrockConfig.setWsClient(ctx, it)
+ 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
- }*/
}
}
}
diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt
index 3b59952f..3df5d164 100644
--- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt
+++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt
@@ -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)
- ) {
- ShamrockConfig.setEnableSelfMsg(ctx, it)
- ShamrockConfig.pushUpdate(ctx)
- return@Function true
- }
-
- Function(
- title = "同步消息推送类型异换",
- desc = "推送来自同号异设备消息,将同步消息作为自发消息推送。",
- descColor = it,
- isSwitch = ShamrockConfig.enableSyncMsgAsSentMsg(ctx)
+ isSwitch = ShamrockConfig[ctx, EnableSelfMessage]
) {
- 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
}
}
diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/DashboardInitializer.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/DashboardInitializer.kt
deleted file mode 100644
index ef906d65..00000000
--- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/DashboardInitializer.kt
+++ /dev/null
@@ -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 = 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)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/FetchPortHandler.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/FetchPortHandler.kt
deleted file mode 100644
index 5b8f9a1d..00000000
--- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/FetchPortHandler.kt
+++ /dev/null
@@ -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"))
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/InitHandler.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/InitHandler.kt
index 1147cb2c..349d5552 100644
--- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/InitHandler.kt
+++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/InitHandler.kt
@@ -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()
+
+ 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 ConfigKey.update(context: Context, map: HashMap) {
+ map[name()] = ShamrockConfig[context, this]
}
}
\ No newline at end of file
diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/ModuleHandler.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/ModuleHandler.kt
index 547b0741..c13b6552 100644
--- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/ModuleHandler.kt
+++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/ModuleHandler.kt
@@ -29,6 +29,7 @@ abstract class ModuleHandler {
}
}
}
+ putExtra("__cmd", cmd)
putExtra("__hash", callbackId)
}
}
diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/SwitchStatus.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/SwitchStatus.kt
new file mode 100644
index 00000000..52b24f88
--- /dev/null
+++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/handlers/SwitchStatus.kt
@@ -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
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/internal/MultifunctionalProvider.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/internal/MultifunctionalProvider.kt
index bb1aba19..4ccc1e82 100644
--- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/internal/MultifunctionalProvider.kt
+++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/service/internal/MultifunctionalProvider.kt
@@ -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)
}
\ No newline at end of file
diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/tools/tabs.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/tools/tabs.kt
index ec54545a..effdf1d0 100644
--- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/tools/tabs.kt
+++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/tools/tabs.kt
@@ -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,
+ visibleState: MutableTransitionState
) {
- 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
+ )
}
}
diff --git a/build.gradle.kts b/build.gradle.kts
index bd278cb6..6561f5bc 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -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
}
diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt
index 13e0d4cc..b2897283 100644
--- a/buildSrc/src/main/kotlin/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/Dependencies.kt
@@ -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"
diff --git a/processor/src/main/java/moe/fuqiuluo/ksp/impl/OneBotHandlerProcessor.kt b/processor/src/main/java/moe/fuqiuluo/ksp/impl/OneBotHandlerProcessor.kt
deleted file mode 100644
index ee95e5f4..00000000
--- a/processor/src/main/java/moe/fuqiuluo/ksp/impl/OneBotHandlerProcessor.kt
+++ /dev/null
@@ -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 {
- 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).toList()
-
- if (oneBotHandlers.isNotEmpty()) {
- ActionManagerNode.accept(ActionManagerVisitor(oneBotHandlers), Unit)
- }
- }
-
- return unableToProcess.toList()
- }
-
- inner class ActionManagerVisitor(
- private val actionHandlers: List
- ): 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)
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/processor/src/main/java/moe/fuqiuluo/ksp/providers/OneBotHandlerProcessorProvider.kt b/processor/src/main/java/moe/fuqiuluo/ksp/providers/OneBotHandlerProcessorProvider.kt
deleted file mode 100644
index 102d5466..00000000
--- a/processor/src/main/java/moe/fuqiuluo/ksp/providers/OneBotHandlerProcessorProvider.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package moe.fuqiuluo.ksp.providers
-
-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
-
-@AutoService(SymbolProcessorProvider::class)
-class OneBotHandlerProcessorProvider: SymbolProcessorProvider {
- override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
- return OneBotHandlerProcessor(
- environment.codeGenerator,
- environment.logger
- )
- }
-}
\ No newline at end of file
diff --git a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/IByteData.aidl b/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/IByteData.aidl
deleted file mode 100644
index 7f1d4c26..00000000
--- a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/IByteData.aidl
+++ /dev/null
@@ -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);
-}
\ No newline at end of file
diff --git a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/IByteDataSign.aidl b/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/IByteDataSign.aidl
deleted file mode 100644
index b31eec06..00000000
--- a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/bytedata/IByteDataSign.aidl
+++ /dev/null
@@ -1,4 +0,0 @@
-// IByteDataSign.aidl
-package moe.fuqiuluo.shamrock.xposed.ipc.bytedata;
-
-parcelable IByteDataSign;
\ No newline at end of file
diff --git a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/qsign/IQSign.aidl b/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/qsign/IQSign.aidl
deleted file mode 100644
index d4083741..00000000
--- a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/qsign/IQSign.aidl
+++ /dev/null
@@ -1,4 +0,0 @@
-// IQSign.aidl
-package moe.fuqiuluo.shamrock.xposed.ipc.qsign;
-
-parcelable IQSign;
\ No newline at end of file
diff --git a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/qsign/IQSigner.aidl b/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/qsign/IQSigner.aidl
deleted file mode 100644
index 6007e73c..00000000
--- a/xposed/src/main/aidl/moe/fuqiuluo/shamrock/xposed/ipc/qsign/IQSigner.aidl
+++ /dev/null
@@ -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 getCmdWhiteList();
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/BaseSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/BaseSvc.kt
deleted file mode 100644
index d66c70d1..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/BaseSvc.kt
+++ /dev/null
@@ -1,201 +0,0 @@
-@file:OptIn(DelicateCoroutinesApi::class)
-
-package moe.fuqiuluo.qqinterface.servlet
-
-import android.os.Bundle
-import com.tencent.mobileqq.app.QQAppInterface
-import com.tencent.mobileqq.msf.core.MsfCore
-import com.tencent.mobileqq.pb.ByteStringMicro
-import com.tencent.qphone.base.remote.ToServiceMsg
-import io.ktor.utils.io.core.BytePacketBuilder
-import io.ktor.utils.io.core.readBytes
-import io.ktor.utils.io.core.writeFully
-import io.ktor.utils.io.core.writeInt
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.DelicateCoroutinesApi
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.withTimeoutOrNull
-import kotlinx.serialization.encodeToByteArray
-
-import moe.fuqiuluo.shamrock.tools.slice
-import moe.fuqiuluo.shamrock.utils.DeflateTools
-import moe.fuqiuluo.shamrock.utils.PlatformUtils
-import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
-import moe.fuqiuluo.shamrock.xposed.helper.internal.DynamicReceiver
-import moe.fuqiuluo.shamrock.xposed.helper.internal.IPCRequest
-import protobuf.oidb.TrpcOidb
-import mqq.app.MobileQQ
-import protobuf.auto.toByteArray
-import tencent.im.oidb.oidb_sso
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.resume
-
-internal abstract class BaseSvc {
- companion object Default: CoroutineScope {
- val currentUin: String
- get() = app.currentAccountUin
-
- val app: QQAppInterface
- get() = AppRuntimeFetcher.appRuntime as QQAppInterface
-
- fun createToServiceMsg(cmd: String): ToServiceMsg {
- return ToServiceMsg("mobileqq.service", app.currentAccountUin, cmd)
- }
-
- suspend fun sendOidbAW(cmd: String, cmdId: Int, serviceId: Int, data: ByteArray, trpc: Boolean = false, timeout: Long = 5000L): ByteArray? {
- val seq = MsfCore.getNextSeq()
- val buffer = withTimeoutOrNull(timeout) {
- suspendCancellableCoroutine { continuation ->
- launch(Dispatchers.Default) {
- DynamicReceiver.register(IPCRequest(cmd, seq) {
- val buffer = it.getByteArrayExtra("buffer")!!
- continuation.resume(buffer)
- })
- }
- if (trpc) sendTrpcOidb(cmd, cmdId, serviceId, data, seq)
- else sendOidb(cmd, cmdId, serviceId, data, seq)
- }
- }.also {
- if (it == null)
- DynamicReceiver.unregister(seq)
- }?.copyOf()
- try {
- if (buffer != null && buffer.size >= 5 && buffer[4] == 120.toByte()) {
- val builder = BytePacketBuilder()
- val deBuffer = DeflateTools.uncompress(buffer.slice(4))
- builder.writeInt(deBuffer.size)
- builder.writeFully(deBuffer)
- return builder.build().readBytes()
- }
- } catch (_: Exception) { }
- return buffer
- }
-
- suspend fun sendBufferAW(cmd: String, isPb: Boolean, data: ByteArray, timeout: Long = 5000L): ByteArray? {
- val seq = MsfCore.getNextSeq()
- val buffer = withTimeoutOrNull(timeout) {
- suspendCancellableCoroutine { continuation ->
- launch(Dispatchers.Default) {
- DynamicReceiver.register(IPCRequest(cmd, seq) {
- val buffer = it.getByteArrayExtra("buffer")!!
- continuation.resume(buffer)
- })
- sendBuffer(cmd, isPb, data, seq)
- }
- }
- }.also {
- if (it == null)
- DynamicReceiver.unregister(seq)
- }?.copyOf()
- try {
- if (buffer != null && buffer.size >= 5 && buffer[4] == 120.toByte()) {
- val builder = BytePacketBuilder()
- val deBuffer = DeflateTools.uncompress(buffer.slice(4))
- builder.writeInt(deBuffer.size)
- builder.writeFully(deBuffer)
- return builder.build().readBytes()
- }
- } catch (_: Exception) { }
- return buffer
- }
-
- fun sendOidb(cmd: String, cmdId: Int, serviceId: Int, buffer: ByteArray, seq: Int = -1, trpc: Boolean = false) {
- if (trpc) {
- sendTrpcOidb(cmd, cmdId, serviceId, buffer, seq)
- return
- }
- val to = createToServiceMsg(cmd)
- val oidb = oidb_sso.OIDBSSOPkg()
- oidb.uint32_command.set(cmdId)
- oidb.uint32_service_type.set(serviceId)
- oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(buffer))
- oidb.str_client_version.set(PlatformUtils.getClientVersion(MobileQQ.getContext()))
- to.putWupBuffer(oidb.toByteArray())
- to.addAttribute("req_pb_protocol_flag", true)
- if (seq != -1) {
- to.addAttribute("shamrock_seq", seq)
- }
- app.sendToService(to)
- }
-
- fun sendTrpcOidb(cmd: String, cmdId: Int, serviceId: Int, buffer: ByteArray, seq: Int = -1) {
- val to = createToServiceMsg(cmd)
-
- val oidb = TrpcOidb(
- cmd = cmdId,
- service = serviceId,
- buffer = buffer,
- flag = 1
- )
- to.putWupBuffer(oidb.toByteArray())
-
- to.addAttribute("req_pb_protocol_flag", true)
- if (seq != -1) {
- to.addAttribute("shamrock_seq", seq)
- }
- app.sendToService(to)
- }
-
- fun sendBuffer(cmd: String, isPb: Boolean, buffer: ByteArray, seq: Int = MsfCore.getNextSeq()) {
- val toServiceMsg = ToServiceMsg("mobileqq.service", app.currentUin, cmd)
- toServiceMsg.putWupBuffer(buffer)
- toServiceMsg.addAttribute("req_pb_protocol_flag", isPb)
- toServiceMsg.addAttribute("shamrock_seq", seq)
- app.sendToService(toServiceMsg)
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- override val coroutineContext: CoroutineContext by lazy {
- Dispatchers.IO.limitedParallelism(12)
- }
- }
-
- protected fun send(toServiceMsg: ToServiceMsg) {
- app.sendToService(toServiceMsg)
- }
-
- protected suspend fun sendAW(toServiceMsg: ToServiceMsg, timeout: Long = 5000L): ByteArray? {
- val seq = MsfCore.getNextSeq()
- val buffer = withTimeoutOrNull(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)
- }
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/CardSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/CardSvc.kt
deleted file mode 100644
index a113b746..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/CardSvc.kt
+++ /dev/null
@@ -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 {
- return getProfileCardFromCache(uin).onFailure {
- return refreshAndGetProfileCard(uin)
- }
- }
-
- fun getProfileCardFromCache(uin: Long): Result {
- 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 {
- 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)
- }
- }
-
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ChatSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ChatSvc.kt
deleted file mode 100644
index b839ef90..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ChatSvc.kt
+++ /dev/null
@@ -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())
- }
-}
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FileSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FileSvc.kt
deleted file mode 100644
index dfebd803..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FileSvc.kt
+++ /dev/null
@@ -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 {
- 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()
- 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()
- 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()
- 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()
- 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 {
- 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 {
- 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()
- val dirs = arrayListOf()
- 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)
- }
- }
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FriendSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FriendSvc.kt
deleted file mode 100644
index 953239b8..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/FriendSvc.kt
+++ /dev/null
@@ -1,121 +0,0 @@
-@file:OptIn(DelicateCoroutinesApi::class)
-@file:Suppress("IllegalIdentifier")
-package moe.fuqiuluo.qqinterface.servlet
-
-import com.tencent.common.app.AppInterface
-import com.tencent.mobileqq.data.Friends
-import com.tencent.mobileqq.friend.api.IFriendDataService
-import com.tencent.mobileqq.friend.api.IFriendHandlerService
-import com.tencent.mobileqq.qroute.QRoute
-import com.tencent.mobileqq.relation.api.IAddFriendTempApi
-import kotlinx.coroutines.DelicateCoroutinesApi
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
-import moe.fuqiuluo.shamrock.tools.slice
-import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
-import mqq.app.AppRuntime
-import tencent.mobileim.structmsg.structmsg
-import kotlin.coroutines.resume
-
-internal object FriendSvc: BaseSvc() {
-
- suspend fun getFriendList(refresh: Boolean): Result> {
- val runtime = AppRuntimeFetcher.appRuntime
- val service = runtime.getRuntimeService(IFriendDataService::class.java, "all")
- if(refresh || !service.isInitFinished) {
- if(!requestFriendList(runtime, service)) {
- return Result.failure(Exception("获取好友列表失败"))
- }
- }
- return Result.success(service.allFriends)
- }
-
- // ProfileService.Pb.ReqSystemMsgAction.Friend
- fun requestFriendRequest(msgSeq: Long, uin: Long, remark: String = "", approve: Boolean? = true, notSee: Boolean? = false) {
- val app = AppRuntimeFetcher.appRuntime
- if (app !is AppInterface)
- throw RuntimeException("AppRuntime cannot cast to AppInterface")
- val service = QRoute.api(IAddFriendTempApi::class.java)
- val action = structmsg.SystemMsgActionInfo()
- action.type.set(if (approve != false) 2 else 3)
- action.group_id.set(0)
- action.remark.set(remark)
- val snInfo = structmsg.AddFrdSNInfo()
- snInfo.uint32_not_see_dynamic.set(if (notSee != false) 1 else 0)
- snInfo.uint32_set_sn.set(0)
- action.addFrdSNInfo.set(snInfo)
- service.sendFriendSystemMsgAction(1, msgSeq, uin, 1, 2004, 11, 0, action, 0,
- structmsg.StructMsg(), false,
- app
- )
- }
-
- suspend fun requestFriendSystemMsgNew(msgNum: Int, latestFriendSeq: Long = 0, latestGroupSeq: Long = 0, retryCnt: Int = 3): List? {
- if (retryCnt < 0) {
- return ArrayList()
- }
- val req = structmsg.ReqSystemMsgNew()
- req.msg_num.set(msgNum)
- req.latest_friend_seq.set(latestFriendSeq)
- req.latest_group_seq.set(latestGroupSeq)
- req.version.set(1000)
- req.checktype.set(2)
- val flag = structmsg.FlagInfo()
-// flag.GrpMsg_Kick_Admin.set(1)
-// flag.GrpMsg_HiddenGrp.set(1)
-// flag.GrpMsg_WordingDown.set(1)
- flag.FrdMsg_GetBusiCard.set(1)
-// flag.GrpMsg_GetOfficialAccount.set(1)
-// flag.GrpMsg_GetPayInGroup.set(1)
- flag.FrdMsg_Discuss2ManyChat.set(1)
-// flag.GrpMsg_NotAllowJoinGrp_InviteNotFrd.set(1)
- flag.FrdMsg_NeedWaitingMsg.set(1)
- flag.FrdMsg_uint32_need_all_unread_msg.set(1)
-// flag.GrpMsg_NeedAutoAdminWording.set(1)
-// flag.GrpMsg_get_transfer_group_msg_flag.set(1)
-// flag.GrpMsg_get_quit_pay_group_msg_flag.set(1)
-// flag.GrpMsg_support_invite_auto_join.set(1)
-// flag.GrpMsg_mask_invite_auto_join.set(1)
-// flag.GrpMsg_GetDisbandedByAdmin.set(1)
- flag.GrpMsg_GetC2cInviteJoinGroup.set(1)
- req.flag.set(flag)
- req.is_get_frd_ribbon.set(false)
- req.is_get_grp_ribbon.set(false)
- req.friend_msg_type_flag.set(1)
- req.uint32_req_msg_type.set(1)
- req.uint32_need_uid.set(1)
- val respBuffer = sendBufferAW("ProfileService.Pb.ReqSystemMsgNew.Friend", true, req.toByteArray())
- return if (respBuffer == null) {
- ArrayList()
- } else {
- try {
- val msg = structmsg.RspSystemMsgNew()
- msg.mergeFrom(respBuffer.slice(4))
- return msg.friendmsgs.get()
- } catch (err: Throwable) {
- requestFriendSystemMsgNew(msgNum, latestFriendSeq, latestGroupSeq, retryCnt - 1)
- }
-
- }
- }
-
-
- private suspend fun requestFriendList(runtime: AppRuntime, dataService: IFriendDataService): Boolean {
- val service = runtime.getRuntimeService(IFriendHandlerService::class.java, "all")
- service.requestFriendList(true, 0)
- return suspendCancellableCoroutine { continuation ->
- val waiter = GlobalScope.launch {
- while (!dataService.isInitFinished) {
- delay(200)
- }
- continuation.resume(true)
- }
- continuation.invokeOnCancellation {
- waiter.cancel()
- continuation.resume(false)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GProSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GProSvc.kt
deleted file mode 100644
index daea9dc6..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GProSvc.kt
+++ /dev/null
@@ -1,361 +0,0 @@
-@file:OptIn(ExperimentalSerializationApi::class)
-
-package moe.fuqiuluo.qqinterface.servlet
-
-import com.tencent.mobileqq.qqguildsdk.api.IGPSService
-import com.tencent.qqnt.kernel.nativeinterface.GProGuildRole
-import com.tencent.qqnt.kernel.nativeinterface.GProRoleCreateInfo
-import com.tencent.qqnt.kernel.nativeinterface.GProRoleMemberList
-import com.tencent.qqnt.kernel.nativeinterface.GProRolePermission
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.withTimeoutOrNull
-import kotlinx.serialization.ExperimentalSerializationApi
-
-import moe.fuqiuluo.qqinterface.servlet.structures.GProChannelInfo
-import moe.fuqiuluo.qqinterface.servlet.structures.GetGuildMemberListNextToken
-import moe.fuqiuluo.qqinterface.servlet.structures.GuildInfo
-import moe.fuqiuluo.qqinterface.servlet.structures.GuildStatus
-import moe.fuqiuluo.qqinterface.servlet.structures.SlowModeInfo
-import moe.fuqiuluo.shamrock.helper.Level
-import moe.fuqiuluo.shamrock.helper.LogCenter
-import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
-import moe.fuqiuluo.shamrock.tools.slice
-import moe.fuqiuluo.shamrock.utils.PlatformUtils
-import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
-import moe.fuqiuluo.symbols.decodeProtobuf
-import protobuf.auto.toByteArray
-import protobuf.guild.GetGuildFeedsReq
-import protobuf.guild.GetGuildFeedsRsp
-import protobuf.oidb.cmd0xf88.GProFilter
-import protobuf.oidb.cmd0xf88.GProUserInfo
-import protobuf.oidb.cmd0xf88.Oidb0xf88Req
-import protobuf.oidb.cmd0xf88.Oidb0xf88Rsp
-import protobuf.oidb.cmx0xf57.Oidb0xf57Filter
-import protobuf.oidb.cmx0xf57.Oidb0xf57GuildInfo
-import protobuf.oidb.cmx0xf57.Oidb0xf57MetaInfo
-import protobuf.oidb.cmx0xf57.Oidb0xf57Req
-import protobuf.oidb.cmx0xf57.Oidb0xf57Rsp
-import protobuf.oidb.cmx0xf57.Oidb0xf57U1
-import protobuf.oidb.cmx0xf57.Oidb0xf57U2
-import protobuf.qweb.DEFAULT_DEVICE_INFO
-import protobuf.qweb.QWebExtInfo
-import protobuf.qweb.QWebReq
-import protobuf.qweb.QWebRsp
-import tencent.im.oidb.oidb_sso
-import kotlin.coroutines.resume
-
-internal object GProSvc: BaseSvc() {
- fun getSelfTinyId(): ULong {
- val service = app.getRuntimeService(IGPSService::class.java, "all")
- return service.selfTinyId.toULong()
- }
-
- suspend fun getGuildInfo(guildId: ULong): Result {
- val respBuffer = sendOidbAW("OidbSvcTrpcTcp.0xf57_9", 0xf57, 9, Oidb0xf57Req(
- filter = Oidb0xf57Filter(
- u1 = Oidb0xf57U1(1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u),
- u2 = Oidb0xf57U2(1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u)
- ),
- guildInfo = Oidb0xf57GuildInfo(guildId = guildId)
- ).toByteArray())
- val body = oidb_sso.OIDBSSOPkg()
- if (respBuffer == null) {
- return Result.failure(Exception("unable to send packet"))
- }
- body.mergeFrom(respBuffer.slice(4))
- return runCatching {
- body.bytes_bodybuffer.get()
- .toByteArray()
- .decodeProtobuf().metaInfo
- }
- }
-
- suspend fun getGuildFeeds(guildId: ULong, channelId: ULong, startIndex: Int): Result {
- val buffer = sendBufferAW("QChannelSvr.trpc.qchannel.commreader.ComReader.GetGuildFeeds", true, QWebReq(
- seq = 10,
- qua = PlatformUtils.getQUA(),
- deviceInfo = DEFAULT_DEVICE_INFO,
- buffer = GetGuildFeedsReq(
- count = 12,
- from = startIndex,
- feedAttchInfo = EMPTY_BYTE_ARRAY,
- guildId = guildId,
- getType = 1,
- u7 = 0,
- u8 = 1,
- u9 = EMPTY_BYTE_ARRAY
- ).toByteArray(),
- traceId = app.account + "_0_0",
- extinfo = listOf(
- QWebExtInfo("fc-appid", "96"),
- QWebExtInfo("environment_id", "shamrock"),
- QWebExtInfo("tiny_id", getSelfTinyId().toString()),
- )
- ).toByteArray()) ?: return Result.failure(Exception("unable to send packet"))
- val webRsp = buffer.slice(4).decodeProtobuf()
- if(webRsp.buffer == null) return Result.failure(Exception("server error"))
- val wupBuffer = webRsp.buffer!!
- val feeds = wupBuffer.decodeProtobuf()
- return Result.success(feeds)
- }
-
- fun getChannelList(guildId: ULong, refresh: Boolean = false): Result> {
- if (refresh) {
- refreshGuildInfo(guildId)
- }
- val result = arrayListOf()
- app.getRuntimeService(IGPSService::class.java, "all").getChannelList(guildId.toString()).forEach {
- result.add(GProChannelInfo(
- ownerGuildId = guildId,
- guildId = it.guildId,
- channelId = it.channelUin.toLong(),
- channelUin = it.channelUin.toLong(),
- channelName = it.channelName ?: "",
- channelType = it.type,
- createTime = it.createTime,
- creatorTinyId = it.creatorId.toLong(),
- talkPermission = it.talkPermission,
- visibleType = it.visibleType,
- currentSlowMode = it.slowModeKey,
- slowModes = it.gProSlowModeInfoList.map {
- SlowModeInfo(it.slowModeKey, it.slowModeText, it.speakFrequency, it.slowModeCircle)
- },
- appIconUrl = it.iconUrl,
- jumpType = it.appChannelJumpType,
- jumpSwitch = it.jumpSwitch,
- jumpUrl = it.appChannelJumpUrl,
- categoryId = it.categoryId,
- myTalkPermission = it.myTalkPermissionType,
- maxMemberCount = it.channelMemberMax
- ))
- }
- return Result.success(result)
- }
-
- fun refreshGuildInfo(guildId: ULong) {
- val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
- kernelGProService.refreshGuildInfo(guildId.toLong(), true, 1)
- }
-
- suspend fun getGuildMemberList(
- guildId: ULong,
- startIndex: Long = 0,
- roleIndex: Long = 1,
- count: Int = 50,
- fetchAll: Boolean = false,
- result: ArrayList = arrayListOf()
- ): Result>> {
- val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
-
- val fetchGuildMemberListResult: Pair> = (withTimeoutOrNull(5000) {
- suspendCancellableCoroutine {
- kernelGProService.fetchMemberListWithRole(guildId.toLong(), 0, startIndex, roleIndex, count, 0) { code, reason, finish, nextIndex, nextRoleIdIndex, _, seq, roleList ->
- if (code == 0) {
- it.resume(GetGuildMemberListNextToken(nextIndex, nextRoleIdIndex, seq, finish) to roleList)
- } else {
- LogCenter.log("fetchMemberListWithRole failed: $code($reason)", Level.WARN)
- it.resume(null)
- }
- }
- }
- }) ?: return Result.failure(Exception("unable to fetch guild member list"))
-
- val nextToken = fetchGuildMemberListResult.first
- val roleList = fetchGuildMemberListResult.second
- result.addAll(roleList)
- return if (fetchAll) {
- if (!fetchGuildMemberListResult.first.finish) {
- getGuildMemberList(guildId, nextToken.startIndex, nextToken.roleIndex, count, true, result)
- } else {
- Result.success(nextToken.copy(finish = true) to result)
- }
- } else {
- Result.success(nextToken to result)
- }
- }
-
- suspend fun getSelfGuildInfo(): Result {
- val selfTinyId = getSelfTinyId()
- return getUserGuildInfo(0u, selfTinyId)
- }
-
- suspend fun getUserGuildInfo(
- guildId: ULong,
- memberTinyId: ULong
- ): Result {
- val respBuffer = sendOidbAW("OidbSvcTrpcTcp.0xf88_1", 0xf88, 1, Oidb0xf88Req(
- filter = GProFilter(1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u),
- memberId = 0uL,
- tinyId = memberTinyId,
- guildId = guildId
- ).toByteArray())
- val body = oidb_sso.OIDBSSOPkg()
- if (respBuffer == null) {
- return Result.failure(Exception("unable to send packet"))
- }
- body.mergeFrom(respBuffer.slice(4))
- return runCatching {
- body.bytes_bodybuffer.get().toByteArray().decodeProtobuf().userInfo!!
- }
- }
-
- private fun getGuildListByOldApi(result: ArrayList) {
- app.getRuntimeService(IGPSService::class.java, "all").guildList?.forEach {
- result.add(GuildInfo(
- guildId = it.guildID.toLong(),
- guildName = it.guildName ?: "",
- guildDisplayId = it.guildNumber ?: "",
- profile = it.profile ?: "",
- status = GuildStatus(
- isEnable = !it.isFrozen && !it.isBanned,
- isBanned = it.isBanned,
- isFrozen = it.isFrozen
- ),
- ownerId = 0,
- shutUpTime = it.shutUpExpireTime,
- allowSearch = it.allowSearch
- ))
- }
- }
-
- private fun getGuildListByNt(result: ArrayList) {
- val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
- kernelGProService.guildListFromCache.forEach {
- if (it.result != 0) return@forEach
- val guildInfo = it.guildInfo
- result.add(GuildInfo(
- guildId = it.guildId,
- guildName = guildInfo.guildName ?: "",
- guildDisplayId = guildInfo.guildNumber ?: "",
- profile = guildInfo.profile ?: "",
- status = GuildStatus(
- isEnable = guildInfo.guildStatus?.isEnable == 1,
- isBanned = guildInfo.guildStatus?.isBanned == 1,
- isFrozen = guildInfo.guildStatus?.isFrozen == 1
- ),
- ownerId = guildInfo.ownerTinyid,
- shutUpTime = guildInfo.shutupExpireTime,
- allowSearch = guildInfo.allowSearch == 1
- ))
- }
- }
-
- suspend fun fetchGuildMemberRoles(guildId: ULong, tinyId: ULong, refresh: Boolean = false): Result> {
- val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
- if (refresh) {
- kernelGProService.refreshGuildUserProfileInfo(guildId.toLong(), tinyId.toLong(), 1)
- }
- val result: ArrayList = withTimeoutOrNull(5000) {
- suspendCancellableCoroutine {
- kernelGProService.fetchMemberRoles(guildId.toLong(), 0, tinyId.toLong(), 2) { code, reason, roles ->
- it.resume(roles)
- }
- }
- } ?: return Result.failure(Exception("unable to fetch guild member roles"))
- return Result.success(result)
- }
-
- fun getGuildList(refresh: Boolean = false, forceOldApi: Boolean): ArrayList {
- val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
- if (refresh) {
- kernelGProService.refreshGuildList(true)
- kernelGProService.guildListFromCache.forEach {
- refreshGuildInfo(it.guildId.toULong())
- }
- }
- val result = arrayListOf()
- if (PlatformUtils.getQQVersionCode() < PlatformUtils.QQ_9_0_8_VER || forceOldApi) {
- getGuildListByOldApi(result)
- } else {
- runCatching {
- getGuildListByNt(result)
- }.onFailure {
- LogCenter.log("GetGuildListByNt failed: ${it.stackTraceToString()}", Level.ERROR)
- getGuildListByOldApi(result) // 防止QQ更新API导致无法获取
- }
- }
-
- return result
- }
-
- suspend fun getGuildRoles(guildId: ULong): Result> {
- val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
- val roles: List = withTimeoutOrNull(5000) {
- suspendCancellableCoroutine {
- kernelGProService.fetchRoleListWithPermission(guildId.toLong(), 1) { code, _, roles, _, _, _ ->
- if (code != 0) it.resume(null) else it.resume(roles)
- }
- }
- } ?: return Result.failure(Exception("unable to fetch guild roles"))
- return Result.success(roles)
- }
-
- fun deleteGuildRole(guildId: ULong, roleId: ULong) {
- val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
- kernelGProService.deleteRole(guildId.toLong(), roleId.toLong()) { code, msg, result ->
- if (code != 0) {
- LogCenter.log("deleteGuildRole failed: $code($msg) => $result", Level.WARN)
- }
- }
- }
-
- fun setMemberRole(guildId: ULong, tinyId: ULong, roleId: ULong, isSet: Boolean) {
- val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
- val addList = arrayListOf()
- val rmList = arrayListOf()
- (if (isSet) addList else rmList).add(roleId.toLong())
- kernelGProService.setMemberRoles(guildId.toLong(), 0, 0, tinyId.toLong(), addList, rmList) { code, msg, result ->
- if (code != 0) {
- LogCenter.log("setMemberRole failed: $code($msg) => $result", Level.WARN)
- }
- }
- }
-
- suspend fun getGuildRolePermission(guildId: ULong, roleId: ULong): Result {
- val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
- val role:GProGuildRole = withTimeoutOrNull(5000) {
- suspendCancellableCoroutine {
- kernelGProService.fetchRoleWithPermission(guildId.toLong(), roleId.toLong(), 1) { code, msg, role, _, _, _ ->
- if (code != 0) {
- LogCenter.log("getGuildRolePermission failed: $code($msg)", Level.WARN)
- it.resume(null)
- } else it.resume(role)
- }
- }
- } ?: return Result.failure(Exception("unable to fetch guild role permission"))
- return Result.success(role)
- }
-
- suspend fun updateGuildRole(guildId: ULong, roleId: ULong, name: String, color: Long): Result {
- val oldInfo = getGuildRolePermission(guildId, roleId).onFailure {
- return Result.failure(it)
- }.getOrThrow()
- val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
- val info = GProRoleCreateInfo(
- name, color, oldInfo.bHoist, oldInfo.rolePermissions
- )
- kernelGProService.setRoleInfo(guildId.toLong(), roleId.toLong(), info) { code, msg, result ->
- if (code != 0) {
- LogCenter.log("updateGuildRole failed: $code($msg) => $result", Level.WARN)
- }
- }
- return Result.success(Unit)
- }
-
- suspend fun createGuildRole(guildId: ULong, name: String, color: Long, initialUsers: ArrayList): Result {
- val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
- val permission = GProRolePermission(false, arrayListOf())
- val info = GProRoleCreateInfo(name, color, false, permission)
- val role: GProGuildRole = withTimeoutOrNull(5000) {
- suspendCancellableCoroutine {
- kernelGProService.createRole(guildId.toLong(), info, initialUsers) { code, msg, result, role ->
- if (code != 0) {
- LogCenter.log("createGuildRole failed: $code($msg) => $result", Level.WARN)
- it.resume(null)
- } else it.resume(role)
- }
- }
- } ?: return Result.failure(Exception("unable to create guild role"))
- return Result.success(role)
- }
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GroupSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GroupSvc.kt
deleted file mode 100644
index 4770f740..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GroupSvc.kt
+++ /dev/null
@@ -1,1152 +0,0 @@
-@file:OptIn(DelicateCoroutinesApi::class)
-
-package moe.fuqiuluo.qqinterface.servlet
-
-import KQQ.RespBatchProcess
-import androidx.core.text.HtmlCompat
-import com.qq.jce.wup.UniPacket
-import com.tencent.common.app.AppInterface
-import com.tencent.mobileqq.app.BusinessHandlerFactory
-import com.tencent.mobileqq.app.QQAppInterface
-import com.tencent.mobileqq.data.troop.TroopInfo
-import com.tencent.mobileqq.data.troop.TroopMemberInfo
-import com.tencent.mobileqq.pb.ByteStringMicro
-import com.tencent.mobileqq.troop.api.ITroopInfoService
-import com.tencent.mobileqq.troop.api.ITroopMemberInfoService
-import com.tencent.protofile.join_group_link.join_group_link
-import com.tencent.qphone.base.remote.ToServiceMsg
-import com.tencent.qqnt.kernel.nativeinterface.MemberInfo
-import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
-import friendlist.stUinInfo
-import io.ktor.client.call.body
-import io.ktor.client.request.forms.MultiPartFormDataContent
-import io.ktor.client.request.forms.formData
-import io.ktor.client.request.forms.submitForm
-import io.ktor.client.request.get
-import io.ktor.client.request.header
-import io.ktor.client.request.post
-import io.ktor.client.request.setBody
-import io.ktor.http.ContentType
-import io.ktor.http.Headers
-import io.ktor.http.HttpHeaders
-import io.ktor.http.contentType
-import io.ktor.http.headers
-import io.ktor.http.parameters
-import kotlinx.coroutines.DelicateCoroutinesApi
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import kotlinx.coroutines.withTimeoutOrNull
-import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.encodeToByteArray
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.JsonElement
-import kotlinx.serialization.json.decodeFromStream
-import kotlinx.serialization.json.jsonObject
-
-import moe.fuqiuluo.qqinterface.servlet.TicketSvc.getLongUin
-import moe.fuqiuluo.qqinterface.servlet.TicketSvc.getUin
-import moe.fuqiuluo.qqinterface.servlet.structures.GroupAtAllRemainInfo
-import moe.fuqiuluo.qqinterface.servlet.structures.NotJoinedGroupInfo
-import moe.fuqiuluo.qqinterface.servlet.structures.ProhibitedMemberInfo
-import moe.fuqiuluo.shamrock.helper.Level
-import moe.fuqiuluo.shamrock.helper.LogCenter
-import moe.fuqiuluo.shamrock.helper.MessageHelper
-import moe.fuqiuluo.shamrock.remote.service.data.EssenceMessage
-import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncement
-import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessage
-import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessageImage
-import moe.fuqiuluo.shamrock.remote.service.data.push.MemberRole
-import moe.fuqiuluo.shamrock.tools.EmptyJsonArray
-import moe.fuqiuluo.shamrock.tools.GlobalClient
-import moe.fuqiuluo.shamrock.tools.asInt
-import moe.fuqiuluo.shamrock.tools.asJsonArrayOrNull
-import moe.fuqiuluo.shamrock.tools.asJsonObject
-import moe.fuqiuluo.shamrock.tools.asLong
-import moe.fuqiuluo.shamrock.tools.asString
-import moe.fuqiuluo.shamrock.tools.asStringOrNull
-import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
-import moe.fuqiuluo.shamrock.tools.putBuf32Long
-import moe.fuqiuluo.shamrock.tools.slice
-import moe.fuqiuluo.shamrock.utils.FileUtils
-import moe.fuqiuluo.shamrock.utils.PlatformUtils
-import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
-import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
-import protobuf.oidb.cmd0xf16.Oidb0xf16
-import protobuf.oidb.cmd0xf16.SetGroupRemarkReq
-import mqq.app.MobileQQ
-import protobuf.auto.toByteArray
-import tencent.im.group.group_member_info
-import tencent.im.oidb.cmd0x88d.oidb_0x88d
-import tencent.im.oidb.cmd0x899.oidb_0x899
-import tencent.im.oidb.cmd0x89a.oidb_0x89a
-import tencent.im.oidb.cmd0x8a0.oidb_0x8a0
-import tencent.im.oidb.cmd0x8a7.cmd0x8a7
-import tencent.im.oidb.cmd0x8fc.Oidb_0x8fc
-import tencent.im.oidb.cmd0xeac.oidb_0xeac
-import tencent.im.oidb.cmd0xeb7.oidb_0xeb7
-import tencent.im.oidb.cmd0xed3.oidb_cmd0xed3
-import tencent.im.oidb.oidb_sso
-import tencent.im.troop.honor.troop_honor
-import tencent.mobileim.structmsg.structmsg
-import java.lang.reflect.Method
-import java.lang.reflect.Modifier
-import java.nio.ByteBuffer
-import kotlin.coroutines.resume
-
-internal object GroupSvc: BaseSvc() {
- private const val GET_MEMBER_ROLE_BY_NT = false
-
- private val RefreshTroopMemberInfoLock by lazy {
- Mutex()
- }
- private val RefreshTroopMemberListLock by lazy {
- Mutex()
- }
-
- private lateinit var METHOD_REQ_MEMBER_INFO: Method
- private lateinit var METHOD_REQ_MEMBER_INFO_V2: Method
- private lateinit var METHOD_REQ_TROOP_LIST: Method
- private lateinit var METHOD_REQ_TROOP_MEM_LIST: Method
- private lateinit var METHOD_REQ_MODIFY_GROUP_NAME: Method
-
- suspend fun getGroupRemainAtAllRemain (groupId: Long): Result {
- val buffer = sendOidbAW("OidbSvcTrpcTcp.0x8a7_0", 2215, 0, cmd0x8a7.ReqBody().apply {
- uint32_sub_cmd.set(1)
- uint32_limit_interval_type_for_uin.set(2)
- uint32_limit_interval_type_for_group.set(1)
- uint64_uin.set(getLongUin())
- uint64_group_code.set(groupId)
- }.toByteArray(), trpc = true) ?: return Result.failure(RuntimeException("[oidb] timeout"))
- val body = oidb_sso.OIDBSSOPkg()
- body.mergeFrom(buffer.slice(4))
- if(body.uint32_result.get() != 0) {
- return Result.failure(RuntimeException(body.str_error_msg.get()))
- }
-
- val resp = cmd0x8a7.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
- return Result.success(GroupAtAllRemainInfo(
- canAtAll = resp.bool_can_at_all.get(),
- remainAtAllCountForGroup = resp.uint32_remain_at_all_count_for_group.get(),
- remainAtAllCountForUin = resp.uint32_remain_at_all_count_for_uin.get()
- ))
- }
- suspend fun getProhibitedMemberList(groupId: Long): Result> {
- val buffer = sendOidbAW("OidbSvc.0x899_0", 2201, 0, oidb_0x899.ReqBody().apply {
- uint64_group_code.set(groupId)
- uint64_start_uin.set(0)
- uint32_identify_flag.set(6)
- memberlist_opt.set(oidb_0x899.memberlist().apply {
- uint64_member_uin.set(0)
- uint32_shutup_timestap.set(0)
- })
- }.toByteArray()) ?: return Result.failure(RuntimeException("[oidb] timeout"))
- val body = oidb_sso.OIDBSSOPkg()
- body.mergeFrom(buffer.slice(4))
- if(body.uint32_result.get() != 0) {
- return Result.failure(RuntimeException(body.str_error_msg.get()))
- }
-
- val resp = oidb_0x899.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
- return Result.success(resp.rpt_memberlist.get().map {
- ProhibitedMemberInfo(it.uint64_member_uin.get(), it.uint32_shutup_timestap.get())
- })
- }
-
- fun poke(groupId: Long, userId: Long) {
- val req = oidb_cmd0xed3.ReqBody().apply {
- uint64_group_code.set(groupId)
- uint64_to_uin.set(userId)
- uint32_msg_seq.set(0)
- }
- sendOidb("OidbSvc.0xed3", 3795, 1, req.toByteArray())
- }
-
- suspend fun getGroupMemberList(groupId: Long, refresh: Boolean): Result> {
- val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all")
- var memberList = service.getAllTroopMembers(groupId.toString())
- if (refresh || memberList == null) {
- memberList = requestTroopMemberInfo(service, groupId).onFailure {
- return Result.failure(Exception("获取群成员列表失败"))
- }.getOrThrow()
- }
-
- getGroupInfo(groupId, true).onSuccess {
- if(it.wMemberNum > memberList.size) {
- return getGroupMemberList(groupId, true)
- }
- }
-
- return Result.success(memberList)
- }
-
- suspend fun getGroupList(refresh: Boolean): Result> {
- val service = app.getRuntimeService(ITroopInfoService::class.java, "all")
-
- var troopList = service.allTroopList
- if(refresh || !service.isTroopCacheInited || troopList == null) {
- if(!requestGroupInfo(service)) {
- return Result.failure(Exception("获取群列表失败"))
- } else {
- troopList = service.allTroopList
- }
- }
- return Result.success(troopList)
- }
-
- suspend fun getNotJoinedGroupInfo(groupId: Long): Result {
- return withTimeoutOrNull(5000) timeout@{
- val toServiceMsg = createToServiceMsg("ProfileService.ReqBatchProcess")
- toServiceMsg.extraData.putLong("troop_code", groupId)
- toServiceMsg.extraData.putBoolean("is_admin", false)
- toServiceMsg.extraData.putInt("from", 0)
- val buffer = sendAW(toServiceMsg)
- val uniPacket = UniPacket(true)
- uniPacket.encodeName = "utf-8"
- uniPacket.decode(buffer)
- val respBatchProcess = uniPacket.getByClass("RespBatchProcess", RespBatchProcess())
- val batchRespInfo = oidb_0x88d.RspBody().mergeFrom(oidb_sso.OIDBSSOPkg()
- .mergeFrom(respBatchProcess.batch_response_list.first().buffer)
- .bytes_bodybuffer.get().toByteArray()).stzrspgroupinfo.get().firstOrNull()
- ?: return@timeout Result.failure(Exception("获取群信息失败"))
- val info = batchRespInfo.stgroupinfo
- Result.success(NotJoinedGroupInfo(
- groupId = batchRespInfo.uint64_group_code.get(),
- maxMember = info.uint32_group_member_max_num.get(),
- memberCount = info.uint32_group_member_num.get(),
- groupName = info.string_group_name.get().toStringUtf8(),
- groupDesc = info.string_group_finger_memo.get().toStringUtf8(),
- owner = info.uint64_group_owner.get(),
- createTime = info.uint32_group_create_time.get().toLong(),
- groupFlag = info.uint32_group_flag.get(),
- groupFlagExt = info.uint32_group_flag_ext.get()
- ))
- } ?: Result.failure(Exception("获取群信息超时"))
- }
-
- suspend fun getGroupInfo(groupId: Long, refresh: Boolean): Result {
- val service = app
- .getRuntimeService(ITroopInfoService::class.java, "all")
-
- val groupInfo = getGroupInfo(groupId)
-
- return if(refresh || !service.isTroopCacheInited || groupInfo.troopuin.isNullOrBlank()) {
- requestGroupInfo(service, groupId)
- } else {
- Result.success(groupInfo)
- }
-
- }
-
- suspend fun setGroupUniqueTitle(groupId: Long, userId: Long, title: String) {
- val localMemberInfo = getTroopMemberInfoByUin(groupId, userId, true).getOrThrow()
- val req = Oidb_0x8fc.ReqBody()
- req.uint64_group_code.set(groupId)
- val memberInfo = Oidb_0x8fc.MemberInfo()
- memberInfo.uint64_uin.set(userId)
- memberInfo.bytes_uin_name.set(ByteStringMicro.copyFromUtf8(localMemberInfo.troopnick.ifEmpty {
- localMemberInfo.troopremark.ifNullOrEmpty("")
- }))
- memberInfo.bytes_special_title.set(ByteStringMicro.copyFromUtf8(title))
- memberInfo.uint32_special_title_expire_time.set(-1)
- req.rpt_mem_level_info.add(memberInfo)
- sendOidb("OidbSvc.0x8fc_2", 2300, 2, req.toByteArray())
- }
-
- fun modifyGroupMemberCard(groupId: Long, userId: Long, name: String): Boolean {
- val createToServiceMsg: ToServiceMsg = createToServiceMsg("friendlist.ModifyGroupCardReq")
- createToServiceMsg.extraData.putLong("dwZero", 0L)
- createToServiceMsg.extraData.putLong("dwGroupCode", groupId)
- val info = stUinInfo()
- info.cGender = -1
- info.dwuin = userId
- info.sEmail = ""
- info.sName = name
- info.sPhone = ""
- info.sRemark = ""
- info.dwFlag = 1
- createToServiceMsg.extraData.putSerializable("vecUinInfo", arrayListOf(info))
- createToServiceMsg.extraData.putLong("dwNewSeq", 0L)
- send(createToServiceMsg)
- return true
- }
-
- fun modifyGroupRemark(groupId: Long, remark: String): Boolean {
- sendOidb("OidbSvc.0xf16_1", 3862, 1, Oidb0xf16(
- setGroupRemarkReq = SetGroupRemarkReq(
- groupCode = groupId.toULong(),
- groupUin = groupCode2GroupUin(groupId).toULong(),
- groupRemark = remark
- )
- ).toByteArray())
- return true
- }
-
- suspend fun setEssenceMessage(groupId: Long, seq: Long, rand: Long): Pair {
- val buffer = sendOidbAW("OidbSvc.0xeac_1", 3756, 1, oidb_0xeac.ReqBody().apply {
- group_code.set(groupId)
- msg_seq.set(seq.toInt())
- msg_random.set(rand.toInt())
- }.toByteArray()) ?: return Pair(false, "unknown error")
- val body = oidb_sso.OIDBSSOPkg()
- body.mergeFrom(buffer.slice(4))
- val result = oidb_0xeac.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
- return if (result.wording.has()) {
- LogCenter.log("设置群精华失败: ${result.wording.get()}")
- Pair(false, "设置群精华失败: ${result.wording.get()}")
- } else {
- LogCenter.log("设置群精华 -> $groupId: $seq")
- Pair(true, "ok")
- }
- }
-
- suspend fun deleteEssenceMessage(groupId: Long, seq: Long, rand: Long): Pair {
- val buffer = sendOidbAW("OidbSvc.0xeac_2", 3756, 2, oidb_0xeac.ReqBody().apply {
- group_code.set(groupId)
- msg_seq.set(seq.toInt())
- msg_random.set(rand.toInt())
- }.toByteArray())
- val body = oidb_sso.OIDBSSOPkg()
- if (buffer == null) {
- return Pair(false, "unknown error")
- }
- body.mergeFrom(buffer.slice(4))
- val result = oidb_0xeac.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
- return if (result.wording.has()) {
- LogCenter.log("移除群精华失败: ${result.wording.get()}")
- Pair(false, "移除群精华失败: ${result.wording.get()}")
- } else {
- LogCenter.log("移除群精华 -> $groupId: $seq")
- Pair(true, "ok")
- }
- }
-
- fun setGroupAdmin(groupId: Long, userId: Long, enable: Boolean) {
- val buffer = ByteBuffer.allocate(9)
- buffer.putBuf32Long(groupId)
- buffer.putBuf32Long(userId)
- buffer.put(if (enable) 1 else 0)
- val array = buffer.array()
- sendOidb("OidbSvc.0x55c_1", 1372, 1, array)
- }
-
- fun setGroupWholeBan(groupId: Long, enable: Boolean) {
- val reqBody = oidb_0x89a.ReqBody()
- reqBody.uint64_group_code.set(groupId)
- reqBody.st_group_info.set(oidb_0x89a.groupinfo().apply {
- uint32_shutup_time.set(if (enable) 268435455 else 0)
- })
- sendOidb("OidbSvc.0x89a_0", 2202, 0, reqBody.toByteArray())
- }
-
- fun banMember(groupId: Long, memberUin: Long, time: Int) {
- val buffer = ByteBuffer.allocate(1 * 8 + 7)
- buffer.putBuf32Long(groupId)
- buffer.put(32.toByte())
- buffer.putShort(1)
- buffer.putBuf32Long(memberUin)
- buffer.putInt(time)
- val array = buffer.array()
- sendOidb("OidbSvc.0x570_8", 1392, 8, array)
- }
-
- fun kickMember(groupId: Long, rejectAddRequest: Boolean, kickMsg: String, vararg memberUin: Long) {
- val reqBody = oidb_0x8a0.ReqBody()
- reqBody.opt_uint64_group_code.set(groupId)
-
- memberUin.forEach {
- val memberInfo = oidb_0x8a0.KickMemberInfo()
- memberInfo.opt_uint32_operate.set(5)
- memberInfo.opt_uint64_member_uin.set(it)
- memberInfo.opt_uint32_flag.set(if (rejectAddRequest) 1 else 0)
- reqBody.rpt_msg_kick_list.add(memberInfo)
- }
- if (kickMsg.isNotEmpty()) {
- reqBody.bytes_kick_msg.set(ByteStringMicro.copyFrom(kickMsg.toByteArray()))
- }
-
- sendOidb("OidbSvc.0x8a0_0", 2208, 0, reqBody.toByteArray())
- }
-
- fun getGroupInfo(groupId: Long): TroopInfo {
- val runtime = AppRuntimeFetcher.appRuntime as QQAppInterface
-
- val service = runtime
- .getRuntimeService(ITroopInfoService::class.java, "all")
-
- return service.getTroopInfo(groupId.toString())
- }
-
- fun getAdminList(
- groupId: Long,
- withOwner: Boolean = false
- ): List {
- val groupInfo = getGroupInfo(groupId)
- return (groupInfo.Administrator ?: "")
- .split("|", ",")
- .also {
- if (withOwner && it is ArrayList) {
- it.add(0, groupInfo.troopowneruin)
- }
- }
- .map { (it.ifNullOrEmpty("0")!!).toLong() }
- .filter { it != 0L }
- }
-
- suspend fun getMemberRole(groupId: Long, memberUin: Long): MemberRole {
- if (!GET_MEMBER_ROLE_BY_NT) {
- return when (memberUin) {
- getOwner(groupId) -> MemberRole.Owner
- in getAdminList(groupId) -> MemberRole.Admin
- else -> MemberRole.Member
- }
- }
- return when(getTroopMemberInfoByUinViaNt(groupId, memberUin, 3000).getOrNull()?.role) {
- com.tencent.qqnt.kernel.nativeinterface.MemberRole.STRANGER -> MemberRole.Stranger
- com.tencent.qqnt.kernel.nativeinterface.MemberRole.MEMBER -> MemberRole.Member
- com.tencent.qqnt.kernel.nativeinterface.MemberRole.ADMIN -> MemberRole.Admin
- com.tencent.qqnt.kernel.nativeinterface.MemberRole.OWNER -> MemberRole.Owner
- com.tencent.qqnt.kernel.nativeinterface.MemberRole.UNSPECIFIED, null -> when (memberUin) {
- getOwner(groupId) -> MemberRole.Owner
- in getAdminList(groupId) -> MemberRole.Admin
- else -> MemberRole.Member
- }
- }
- }
-
- fun getOwner(groupId: Long): Long {
- val groupInfo = getGroupInfo(groupId)
- return groupInfo.troopowneruin?.toLong() ?: 0
- }
-
- fun isOwner(groupId: Long): Boolean {
- val groupInfo = getGroupInfo(groupId)
- return groupInfo.troopowneruin == app.account
- }
-
- fun isAdmin(groupId: Long): Boolean {
- val service = app
- .getRuntimeService(ITroopInfoService::class.java, "all")
-
- val groupInfo = service.getTroopInfo(groupId.toString())
-
- return groupInfo.isAdmin || groupInfo.troopowneruin == app.account
- }
-
- fun resignTroop(groupId: Long) {
- sendExtra("ProfileService.GroupMngReq") {
- it.putInt("groupreqtype", 2)
- it.putString("troop_uin", groupId.toString())
- it.putString("uin", currentUin)
- }
- }
-
- fun modifyTroopName(groupId: Long, name: String) {
- val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MODIFY_HANDLER)
-
- if (!GroupSvc::METHOD_REQ_MODIFY_GROUP_NAME.isInitialized) {
- METHOD_REQ_MODIFY_GROUP_NAME = businessHandler.javaClass.declaredMethods.first {
- it.parameterCount == 3
- && it.parameterTypes[0] == String::class.java
- && it.parameterTypes[1] == String::class.java
- && it.parameterTypes[2] == Boolean::class.java
- && !Modifier.isPrivate(it.modifiers)
- }
- }
-
- METHOD_REQ_MODIFY_GROUP_NAME.invoke(businessHandler, groupId.toString(), name, false)
- }
-
- fun parseHonor(honor: String?): List {
- return (honor ?: "")
- .split("|")
- .filter { it.isNotBlank() }
- .map { it.toInt() }
- }
-
- fun groupUin2GroupCode(groupuin: Long): Long {
- var calc = groupuin / 1000000L
- while (true) {
- calc -= if (calc >= 0 + 202 && calc + 202 <= 10) {
- (202 - 0).toLong()
- } else if (calc >= 11 + 480 && calc <= 19 + 480) {
- (480 - 11).toLong()
- } else if (calc >= 20 + 2100 && calc <= 66 + 2100) {
- (2100 - 20).toLong()
- } else if (calc >= 67 + 2010 && calc <= 156 + 2010) {
- (2010 - 67).toLong()
- } else if (calc >= 157 + 2147 && calc <= 209 + 2147) {
- (2147 - 157).toLong()
- } else if (calc >= 210 + 4100 && calc <= 309 + 4100) {
- (4100 - 210).toLong()
- } else if (calc >= 310 + 3800 && calc <= 499 + 3800) {
- (3800 - 310).toLong()
- } else {
- break
- }
- }
- return calc * 1000000L + groupuin % 1000000L
- }
-
- fun groupCode2GroupUin(groupcode: Long): Long {
- var calc = groupcode / 1000000L
- loop@ while (true) calc += when (calc) {
- in 0..10 -> {
- (202 - 0).toLong()
- }
- in 11..19 -> {
- (480 - 11).toLong()
- }
- in 20..66 -> {
- (2100 - 20).toLong()
- }
- in 67..156 -> {
- (2010 - 67).toLong()
- }
- in 157..209 -> {
- (2147 - 157).toLong()
- }
- in 210..309 -> {
- (4100 - 210).toLong()
- }
- in 310..499 -> {
- (3800 - 310).toLong()
- }
- else -> {
- break@loop
- }
- }
- return calc * 1000000L + groupcode % 1000000L
- }
-
- suspend fun getShareTroopArkMsg(groupId: Long): String {
- val reqBody = join_group_link.ReqBody()
- reqBody.get_ark.set(true)
- reqBody.type.set(1)
- reqBody.group_code.set(groupId)
- val buffer = sendBufferAW("GroupSvc.JoinGroupLink", true, reqBody.toByteArray())
- ?: error("unable to fetch contact ark_json_text")
- val body = join_group_link.RspBody()
- body.mergeFrom(buffer.slice(4))
- return body.signed_ark.get().toStringUtf8()
- }
-
- suspend fun getTroopMemberInfoByUin(
- groupId: Long,
- uin: Long,
- refresh: Boolean = false
- ): Result {
- val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all")
- var info = service.getTroopMember(groupId.toString(), uin.toString())
- if (refresh || !service.isMemberInCache(groupId.toString(), uin.toString()) || info == null || info.troopnick == null) {
- info = requestTroopMemberInfo(service, groupId, uin).getOrNull()
- }
- if (info == null) {
- info = getTroopMemberInfoByUinViaNt(groupId, uin).getOrNull()?.let {
- TroopMemberInfo().apply {
- troopnick = it.cardName
- friendnick = it.nick
- }
- }
- }
- try {
- if (info != null && (info.alias == null || info.alias.isBlank())) {
- val req = group_member_info.ReqBody()
- req.uint64_group_code.set(groupId)
- req.uint64_uin.set(uin.toLong())
- req.bool_new_client.set(true)
- req.uint32_client_type.set(1)
- req.uint32_rich_card_name_ver.set(1)
- val respBuffer = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray())
- if (respBuffer != null) {
- val rsp = group_member_info.RspBody()
- rsp.mergeFrom(respBuffer.slice(4))
- if (rsp.msg_meminfo.str_location.has()) {
- info.alias = rsp.msg_meminfo.str_location.get().toStringUtf8()
- }
- if (rsp.msg_meminfo.uint32_age.has()) {
- info.age = rsp.msg_meminfo.uint32_age.get().toByte()
- }
- if (rsp.msg_meminfo.bytes_group_honor.has()) {
- val honorBytes = rsp.msg_meminfo.bytes_group_honor.get().toByteArray()
- val honor = troop_honor.GroupUserCardHonor()
- honor.mergeFrom(honorBytes)
- info.level = honor.level.get()
- // 10315: medal_id not real group level
- }
- }
- }
- } catch (err: Throwable) {
- LogCenter.log(err.stackTraceToString(), Level.WARN)
- }
- return if (info != null) {
- Result.success(info)
- } else {
- Result.failure(Exception("获取群成员信息失败"))
- }
- }
-
- suspend fun getTroopMemberInfoByUinV2(
- groupId: Long,
- uin: Long,
- refresh: Boolean = false
- ): Result {
- val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all")
- var info = service.getTroopMember(groupId.toString(), uin.toString())
- if (refresh || !service.isMemberInCache(groupId.toString(), uin.toString()) || info == null || info.troopnick == null) {
- info = requestTroopMemberInfo(service, groupId, uin, timeout = 2000).getOrNull()
- }
- if (info == null) {
- info = getTroopMemberInfoByUinViaNt(groupId, uin, timeout = 2000L).getOrNull()?.let {
- TroopMemberInfo().apply {
- troopnick = it.cardName
- friendnick = it.nick
- }
- }
- }
- try {
- if (info != null && (info.alias == null || info.alias.isBlank())) {
- val req = group_member_info.ReqBody()
- req.uint64_group_code.set(groupId)
- req.uint64_uin.set(uin)
- req.bool_new_client.set(true)
- req.uint32_client_type.set(1)
- req.uint32_rich_card_name_ver.set(1)
- val respBuffer = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray(), timeout = 2000)
- if (respBuffer != null) {
- val rsp = group_member_info.RspBody()
- rsp.mergeFrom(respBuffer.slice(4))
- if (rsp.msg_meminfo.str_location.has()) {
- info.alias = rsp.msg_meminfo.str_location.get().toStringUtf8()
- }
- if (rsp.msg_meminfo.uint32_age.has()) {
- info.age = rsp.msg_meminfo.uint32_age.get().toByte()
- }
- if (rsp.msg_meminfo.bytes_group_honor.has()) {
- val honorBytes = rsp.msg_meminfo.bytes_group_honor.get().toByteArray()
- val honor = troop_honor.GroupUserCardHonor()
- honor.mergeFrom(honorBytes)
- info.level = honor.level.get()
- // 10315: medal_id not real group level
- }
- }
- }
- } catch (err: Throwable) {
- LogCenter.log(err.stackTraceToString(), Level.WARN)
- }
- return if (info != null) {
- Result.success(info)
- } else {
- Result.failure(Exception("获取群成员信息失败"))
- }
- }
-
- suspend fun getTroopMemberInfoByUinViaNt(
- groupId: Long,
- qq: Long,
- timeout: Long = 5000L
- ): Result {
- return runCatching {
- val kernelService = NTServiceFetcher.kernelService
- val sessionService = kernelService.wrapperSession
- val groupService = sessionService.groupService
- val info = withTimeoutOrNull(timeout) {
- suspendCancellableCoroutine {
- groupService.getTransferableMemberInfo(groupId) { code, _, data ->
- if (code != 0) {
- it.resume(null)
- return@getTransferableMemberInfo
- }
- data.forEach { (_, info) ->
- if (info.uin == qq) {
- it.resume(info)
- return@forEach
- }
- }
- it.resume(null)
- }
- }
- }
- return if (info != null) {
- Result.success(info)
- } else {
- Result.failure(Exception("获取群成员信息失败"))
- }
- }
- }
-
- suspend fun getTroopMemberInfoByUid(groupId: Long, uid: String): Result {
- val kernelService = NTServiceFetcher.kernelService
- val sessionService = kernelService.wrapperSession
- val groupService = sessionService.groupService
- val info = suspendCancellableCoroutine {
- groupService.getTransferableMemberInfo(groupId) { code, _, data ->
- if (code != 0) {
- it.resume(null)
- return@getTransferableMemberInfo
- }
- data.forEach { (tmpUid, info) ->
- if (tmpUid == uid) {
- it.resume(info)
- return@forEach
- }
- }
- }
- }
- return if (info != null) {
- Result.success(info)
- } else {
- Result.failure(Exception("获取群成员信息失败"))
- }
- }
-
- private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: Long): Result> {
- val info = RefreshTroopMemberListLock.withLock {
- service.deleteTroopMembers(groupId.toString())
- refreshTroopMemberList(groupId)
-
- withTimeoutOrNull(10000) {
- var memberList: List?
- do {
- delay(100)
- memberList = service.getAllTroopMembers(groupId.toString())
- } while (memberList.isNullOrEmpty())
- return@withTimeoutOrNull memberList
- }
- }
- return if (info != null) {
- Result.success(info)
- } else {
- Result.failure(Exception("获取群成员信息失败"))
- }
- }
-
- private suspend fun requestGroupInfo(
- service: ITroopInfoService
- ): Boolean {
- refreshTroopList()
-
- return suspendCancellableCoroutine { continuation ->
- val waiter = GlobalScope.launch {
- do {
- delay(1000)
- } while (
- !service.isTroopCacheInited
- )
- continuation.resume(true)
- }
- continuation.invokeOnCancellation {
- waiter.cancel()
- continuation.resume(false)
- }
- }
- }
-
- private fun refreshTroopMemberList(groupId: Long) {
- val app = AppRuntimeFetcher.appRuntime
- if (app !is AppInterface)
- throw RuntimeException("AppRuntime cannot cast to AppInterface")
- val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_LIST_HANDLER)
-
- // void C(boolean forceRefresh, String groupId, String troopcode, int reqType); // RequestedTroopList/refreshMemberListFromServer
- if (!GroupSvc::METHOD_REQ_TROOP_MEM_LIST.isInitialized) {
- METHOD_REQ_TROOP_MEM_LIST = businessHandler.javaClass.declaredMethods.first {
- it.parameterCount == 4
- && it.parameterTypes[0] == Boolean::class.java
- && it.parameterTypes[1] == String::class.java
- && it.parameterTypes[2] == String::class.java
- && it.parameterTypes[3] == Int::class.java
- && !Modifier.isPrivate(it.modifiers)
- }
- }
-
- METHOD_REQ_TROOP_MEM_LIST.invoke(businessHandler, true, groupId.toString(), groupUin2GroupCode(groupId).toString(), 5)
- }
-
- private fun refreshTroopList() {
- val app = AppRuntimeFetcher.appRuntime
- if (app !is AppInterface)
- throw RuntimeException("AppRuntime cannot cast to AppInterface")
- val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_LIST_HANDLER)
-
- if (!GroupSvc::METHOD_REQ_TROOP_LIST.isInitialized) {
- METHOD_REQ_TROOP_LIST = businessHandler.javaClass.declaredMethods.first {
- it.parameterCount == 0 && !Modifier.isPrivate(it.modifiers) && it.returnType == Void.TYPE
- }
- }
-
- METHOD_REQ_TROOP_LIST.invoke(businessHandler)
- }
-
- private fun requestMemberInfo(groupId: Long, memberUin: Long) {
- val app = AppRuntimeFetcher.appRuntime
- if (app !is AppInterface)
- throw RuntimeException("AppRuntime cannot cast to AppInterface")
- val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER)
-
- if (!GroupSvc::METHOD_REQ_MEMBER_INFO.isInitialized) {
- METHOD_REQ_MEMBER_INFO = businessHandler.javaClass.declaredMethods.first {
- it.parameterCount == 2 &&
- it.parameterTypes[0] == Long::class.java &&
- it.parameterTypes[1] == Long::class.java &&
- !Modifier.isPrivate(it.modifiers)
- }
- }
-
- METHOD_REQ_MEMBER_INFO.invoke(businessHandler, groupId, memberUin)
- }
-
- private fun requestMemberInfoV2(groupId: Long, memberUin: Long) {
- val app = AppRuntimeFetcher.appRuntime
- if (app !is AppInterface)
- throw RuntimeException("AppRuntime cannot cast to AppInterface")
- val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER)
-
- if (!GroupSvc::METHOD_REQ_MEMBER_INFO_V2.isInitialized) {
- METHOD_REQ_MEMBER_INFO_V2 = businessHandler.javaClass.declaredMethods.first {
- it.parameterCount == 3 &&
- it.parameterTypes[0] == String::class.java &&
- it.parameterTypes[1] == String::class.java &&
- !Modifier.isPrivate(it.modifiers)
- }
- }
-
- METHOD_REQ_MEMBER_INFO_V2.invoke(businessHandler, groupId.toString(), groupUin2GroupCode(groupId).toString(), arrayListOf(memberUin.toString()))
- }
-
- private suspend fun requestGroupInfo(dataService: ITroopInfoService, uin: Long): Result {
- val strUin = uin.toString()
- val info = withTimeoutOrNull(1000) {
- var troopInfo: TroopInfo?
- do {
- troopInfo = dataService.getTroopInfo(strUin)
- delay(100)
- } while (troopInfo == null || troopInfo.troopuin.isNullOrBlank())
- return@withTimeoutOrNull troopInfo
- }
- return if (info != null) {
- Result.success(info)
- } else {
- Result.failure(Exception("获取群列表失败"))
- }
- }
-
- private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: Long, memberUin: Long, timeout: Long = 10_000): Result {
- val info = RefreshTroopMemberInfoLock.withLock {
- val groupIdStr = groupId.toString()
- val memberUinStr = memberUin.toString()
-
- service.deleteTroopMember(groupIdStr, memberUinStr)
-
- requestMemberInfoV2(groupId, memberUin)
- requestMemberInfo(groupId, memberUin)
-
- withTimeoutOrNull(timeout) {
- while (!service.isMemberInCache(groupIdStr, memberUinStr)) {
- delay(200)
- }
- return@withTimeoutOrNull service.getTroopMember(groupIdStr, memberUinStr)
- }
- }
- return if (info != null) {
- Result.success(info)
- } else {
- Result.failure(Exception("获取群成员信息失败"))
- }
- }
-
- // ProfileService.Pb.ReqSystemMsgAction.Group
- suspend fun requestGroupRequest(
- msgSeq: Long,
- uin: Long,
- gid: Long,
- msg: String? = "",
- approve: Boolean? = true,
- notSee: Boolean? = false,
- subType: String
- ): Result{
- val req = structmsg.ReqSystemMsgAction().apply {
- if (subType == "invite") {
- msg_type.set(1)
- src_id.set(3)
- sub_src_id.set(10016)
- group_msg_type.set(2)
- } else {
- msg_type.set(2)
- src_id.set(2)
- sub_src_id.set(30024)
- group_msg_type.set(1)
- }
- msg_seq.set(msgSeq)
- req_uin.set(uin)
- sub_type.set(1)
- action_info.set(structmsg.SystemMsgActionInfo().apply {
- type.set(if (approve != false) 11 else 12)
- group_code.set(gid)
- if (subType == "add") {
- this.msg.set(msg)
- this.blacklist.set(notSee != false)
- }
- })
- language.set(1000)
- }
- val respBuffer = sendBufferAW("ProfileService.Pb.ReqSystemMsgAction.Group", true, req.toByteArray())
- ?: return Result.failure(Exception("操作失败"))
- val rsp = structmsg.RspSystemMsgAction().mergeFrom(respBuffer.slice(4))
- return if (rsp.head.result.has()) {
- if (rsp.head.result.get() == 0) {
- Result.success(rsp.msg_detail.get())
- } else {
- Result.failure(Exception(rsp.head.msg_fail.get()))
- }
- } else {
- Result.failure(Exception("操作失败"))
- }
- }
-
- suspend fun requestGroupSystemMsgNew(msgNum: Int, reqMsgType: Int = 1, latestFriendSeq: Long = 0, latestGroupSeq: Long = 0, retryCnt: Int = 5): List {
- if (retryCnt < 0) {
- return ArrayList()
- }
- val req = structmsg.ReqSystemMsgNew()
- req.msg_num.set(msgNum)
- req.latest_friend_seq.set(latestFriendSeq)
- req.latest_group_seq.set(latestGroupSeq)
- req.version.set(1000)
- req.checktype.set(3)
- val flag = structmsg.FlagInfo()
- flag.GrpMsg_Kick_Admin.set(1)
- flag.GrpMsg_HiddenGrp.set(1)
- flag.GrpMsg_WordingDown.set(1)
-// flag.FrdMsg_GetBusiCard.set(1)
- flag.GrpMsg_GetOfficialAccount.set(1)
- flag.GrpMsg_GetPayInGroup.set(1)
- flag.FrdMsg_Discuss2ManyChat.set(1)
- flag.GrpMsg_NotAllowJoinGrp_InviteNotFrd.set(1)
- flag.FrdMsg_NeedWaitingMsg.set(1)
-// flag.FrdMsg_uint32_need_all_unread_msg.set(1)
- flag.GrpMsg_NeedAutoAdminWording.set(1)
- flag.GrpMsg_get_transfer_group_msg_flag.set(1)
- flag.GrpMsg_get_quit_pay_group_msg_flag.set(1)
- flag.GrpMsg_support_invite_auto_join.set(1)
- flag.GrpMsg_mask_invite_auto_join.set(1)
- flag.GrpMsg_GetDisbandedByAdmin.set(1)
- flag.GrpMsg_GetC2cInviteJoinGroup.set(1)
- req.flag.set(flag)
- req.is_get_frd_ribbon.set(false)
- req.is_get_grp_ribbon.set(false)
- req.friend_msg_type_flag.set(1)
- req.uint32_req_msg_type.set(reqMsgType)
- req.uint32_need_uid.set(1)
- val respBuffer = sendBufferAW("ProfileService.Pb.ReqSystemMsgNew.Group", true, req.toByteArray())
- return if (respBuffer == null) {
- ArrayList()
- } else {
- try {
- val msg = structmsg.RspSystemMsgNew()
- msg.mergeFrom(respBuffer.slice(4))
- return msg.groupmsgs.get().orEmpty()
- } catch (err: Throwable) {
- requestGroupSystemMsgNew(msgNum, reqMsgType, latestFriendSeq, latestGroupSeq, retryCnt - 1)
- }
- }
- }
-
-
- @OptIn(ExperimentalSerializationApi::class)
- suspend fun getEssenceMessageList(groupId: Long, page: Int = 0, pageSize: Int = 20): Result>{
-// GlobalClient.get()
- val cookie = TicketSvc.getCookie("qun.qq.com")
- val bkn = TicketSvc.getBkn(TicketSvc.getRealSkey(TicketSvc.getUin()))
- val url = "https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=${bkn}&group_code=${groupId}&page_start=${page}&page_limit=${pageSize}"
- val response = GlobalClient.get(url) {
- header("Cookie", cookie)
- }
- val body = Json.decodeFromStream(response.body())
- if (body.jsonObject["retcode"].asInt == 0) {
- val data = body.jsonObject["data"].asJsonObject
- val list = data["msg_list"].asJsonArrayOrNull
- ?: // is_end
- return Result.success(ArrayList())
- return Result.success(list.map {
- val obj = it.jsonObject
- val msgSeq = obj["msg_seq"].asInt
- val msg = EssenceMessage(
- senderId = obj["sender_uin"].asString.toLong(),
- senderNick = obj["sender_nick"].asString,
- senderTime = obj["sender_time"].asLong,
- operatorId = obj["add_digest_uin"].asString.toLong(),
- operatorNick = obj["add_digest_nick"].asString,
- operatorTime = obj["add_digest_time"].asLong,
- messageId = 0,
- messageSeq = msgSeq,
- realId = msgSeq,
- messageContent = obj["msg_content"] ?: EmptyJsonArray
- )
- val mapping = MessageHelper.getMsgMappingBySeq(MsgConstant.KCHATTYPEGROUP, groupId.toString(), msgSeq)
- if (mapping != null) {
- msg.messageId = mapping.msgHashId
- }
- msg
- })
- } else {
- return Result.failure(Exception(body.jsonObject["retmsg"].asStringOrNull))
- }
- }
-
- @OptIn(ExperimentalSerializationApi::class)
- suspend fun getGroupAnnouncements(groupId: Long): Result>{
- val cookie = TicketSvc.getCookie("qun.qq.com")
- val bkn = TicketSvc.getBkn(TicketSvc.getRealSkey(TicketSvc.getUin()))
- val url = "https://web.qun.qq.com/cgi-bin/announce/get_t_list?bkn=${bkn}&qid=${groupId}&ft=23&s=-1&n=20"
- val response = GlobalClient.get(url) {
- header("Cookie", cookie)
- }
- val body = Json.decodeFromStream(response.body())
- if (body.jsonObject["ec"].asInt == 0) {
- val list = body.jsonObject["feeds"].asJsonArrayOrNull
- ?: return Result.success(ArrayList())
- return Result.success(list.map {
- val obj = it.jsonObject
- GroupAnnouncement(
- senderId = obj["u"].asLong,
- publishTime = obj["pubt"].asLong,
- message = GroupAnnouncementMessage(
-// text = obj["msg"].asJsonObject["text"].asString,
- text = fromHtml(obj["msg"].asJsonObject["text"].asString),
- images = obj["msg"].asJsonObject["pics"].asJsonArrayOrNull?.map { pic ->
- GroupAnnouncementMessageImage(
- id = pic.jsonObject["id"].asString,
- width = pic.jsonObject["w"].asString,
- height = pic.jsonObject["h"].asString,
- )
- } ?: ArrayList()
- )
- )
- })
- } else {
- return Result.failure(Exception(body.jsonObject["em"].asStringOrNull))
- }
- }
-
- private fun fromHtml(htmlString: String): String {
- return HtmlCompat
- // 特殊处理
,目的是替换为换行符,否则会被fromHtml忽略并移除
- .fromHtml(htmlString.replace("
", "[shamrockplaceholder]"), HtmlCompat.FROM_HTML_MODE_LEGACY)
- .toString()
- .replace("[shamrockplaceholder]", "\n")
- }
-
- @OptIn(ExperimentalSerializationApi::class)
- suspend fun uploadImageTroopNotice(image: String): Result {
- val file = FileUtils.parseAndSave(image)
- val cookie = TicketSvc.getCookie("qun.qq.com")
- val bkn = TicketSvc.getBkn(TicketSvc.getRealSkey(TicketSvc.getUin()))
- val response = GlobalClient.post("https://web.qun.qq.com/cgi-bin/announce/upload_img") {
- headers {
- header("Cookie", cookie)
- }
- contentType(ContentType.MultiPart.FormData)
- setBody(MultiPartFormDataContent(
- // 黑人问号 ktor默认formdata传的tx不认。默认是name=bkn,非要写成name="bkn"才认?
- formData {
- append("filename", "001.png", Headers.build {
- append(HttpHeaders.ContentDisposition, "name=\"filename\"")
- })
- append("source", "troopNotice", Headers.build {
- append(HttpHeaders.ContentDisposition, "name=\"source\"")
- })
- append("bkn", bkn, Headers.build {
- append(HttpHeaders.ContentDisposition, "name=\"bkn\"")
- })
- append("m", "0", Headers.build {
- append(HttpHeaders.ContentDisposition, "name=\"m\"")
- })
- append("pic_up", file.readBytes(), Headers.build {
- append(HttpHeaders.ContentType, "image/png")
- append(HttpHeaders.ContentDisposition, "name=\"pic_up\" filename=\"001.png\"")
-
- })
- }
- ))
- }
- val body = Json.decodeFromStream(response.body())
- if (body.jsonObject["ec"].asInt == 0) {
- var idJsonStr = body.jsonObject["id"].asStringOrNull
- return if (idJsonStr != null) {
- idJsonStr = idJsonStr.replace(""", "\"")
- val idJson = Json.decodeFromString(idJsonStr)
- LogCenter.log(idJson.toString())
- Result.success(GroupAnnouncementMessageImage(
- height = idJson.asJsonObject["h"].asString,
- width = idJson.asJsonObject["w"].asString,
- id = idJson.asJsonObject["id"].asString,
- ))
- } else {
- Result.failure(Exception("图片上传失败"))
- }
- } else {
- return Result.failure(Exception(body.jsonObject["em"].asStringOrNull))
- }
- }
-
- @OptIn(ExperimentalSerializationApi::class)
- suspend fun addQunNotice(groupId: Long, text: String, image: GroupAnnouncementMessageImage?) : Result {
- val cookie = TicketSvc.getCookie("qun.qq.com")
- val bkn = TicketSvc.getBkn(TicketSvc.getRealSkey(TicketSvc.getUin()))
- val response = GlobalClient.submitForm(
- url = "https://web.qun.qq.com/cgi-bin/announce/add_qun_notice",
- formParameters = parameters {
- append("qid", groupId.toString())
- append("bkn", bkn)
- append("text", text)
- append("pinned", "0")
- append("type", "1")
- // todo allow custom settings
- append("settings", "{\"is_show_edit_card:\"1,\"tip_window_type\":1,\"confirm_required\":1}")
- if (null != image) {
- append("pic", image.id)
- append("imgWidth", image.width)
- append("imgHeight", image.height)
- }
- },
- block = {
- headers {
- header("Cookie", cookie)
- }
- }
- )
- val body = Json.decodeFromStream(response.body())
- return if (body.jsonObject["ec"].asInt == 0) {
- Result.success(true)
- } else {
- Result.failure(Exception(body.jsonObject["em"].asStringOrNull))
- }
- }
-
- suspend fun groupSign(groupId: Long): Result {
- val req = oidb_0xeb7.ReqBody()
- val signInWriteReq = oidb_0xeb7.StSignInWriteReq()
- signInWriteReq.groupId.set(groupId.toString())
- signInWriteReq.uid.set(getUin())
- var version = PlatformUtils.getClientVersion(MobileQQ.getContext())
- version = version.replace("android", "").trimStart()
- signInWriteReq.clientVersion.set(version)
- req.signInWriteReq.set(signInWriteReq)
- val buffer = sendOidbAW("OidbSvc.0xeb7", 3767, 1, req.toByteArray())
- return if (buffer == null) {
- Result.failure(Exception("操作失败"))
- } else {
- val body = oidb_sso.OIDBSSOPkg()
- body.mergeFrom(buffer.slice(4))
- val rsp = oidb_0xeb7.RspBody()
- rsp.mergeFrom(body.bytes_bodybuffer.get().toByteArray())
- val doneInfo = rsp.signInWriteRsp.doneInfo
- LogCenter.log(rsp.toString(), Level.DEBUG)
- Result.success("${doneInfo.leftTitleWrod.get()} ${doneInfo.rightDescWord.get()} ${doneInfo.belowPortraitWords.get().joinToString(" ")}")
- }
- }
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/LbsSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/LbsSvc.kt
deleted file mode 100644
index 84c50149..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/LbsSvc.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package moe.fuqiuluo.qqinterface.servlet
-
-import com.tencent.biz.map.trpcprotocol.LbsSendInfo
-import com.tencent.mobileqq.msf.core.MsfCore
-import com.tencent.proto.lbsshare.LBSShare
-import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
-import moe.fuqiuluo.shamrock.helper.IllegalParamsException
-import moe.fuqiuluo.shamrock.tools.slice
-import kotlin.math.roundToInt
-
-internal object LbsSvc: BaseSvc() {
- suspend fun tryShareLocation(chatType: Int, peerId: Long, lat: Double, lon: Double): Result {
- val req = LbsSendInfo.SendMessageReq()
- req.uint64_peer_account.set(peerId)
- when (chatType) {
- MsgConstant.KCHATTYPEGROUP -> req.enum_relation_type.set(1)
- MsgConstant.KCHATTYPEC2C -> req.enum_relation_type.set(0)
- else -> error("Not supported chat type: $chatType")
- }
- req.str_name.set("位置分享")
- req.str_address.set(getAddressWithLonLat(lat, lon).onFailure {
- return Result.failure(it)
- }.getOrNull())
- req.str_lat.set(lat.toString())
- req.str_lng.set(lon.toString())
- sendPb("trpc.qq_lbs.qq_lbs_ark.LocationArk.SsoSendMessage", req.toByteArray(), MsfCore.getNextSeq())
-
- return Result.success(Unit)
- }
-
- suspend fun getAddressWithLonLat(lat: Double, lon: Double): Result {
- if (lat > 90 || lat < 0) {
- return Result.failure(IllegalParamsException("纬度大小错误"))
- }
- if (lon > 180 || lon < 0) {
- return Result.failure(IllegalParamsException("经度大小错误"))
- }
- val latO = (lat * 1000000).roundToInt()
- val lngO = (lon * 1000000).roundToInt()
- val req = LBSShare.LocationReq()
- req.lat.set(latO)
- req.lng.set(lngO)
- req.coordinate.set(1)
- req.keyword.set("")
- req.category.set("")
- req.page.set(0)
- req.count.set(20)
- req.requireMyLbs.set(1)
- req.imei.set("")
- val buffer = sendBufferAW("LbsShareSvr.location", true, req.toByteArray())
- ?: return Result.failure(Exception("获取位置失败"))
- val resp = LBSShare.LocationResp()
- resp.mergeFrom(buffer.slice(4))
- val location = resp.mylbs
- return Result.success(location.addr.get())
- }
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt
deleted file mode 100644
index fda842b6..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/MsgSvc.kt
+++ /dev/null
@@ -1,524 +0,0 @@
-package moe.fuqiuluo.qqinterface.servlet
-
-import com.tencent.mobileqq.qroute.QRoute
-import com.tencent.mobileqq.troop.api.ITroopMemberNameService
-import com.tencent.qqnt.kernel.api.IKernelService
-import com.tencent.qqnt.kernel.nativeinterface.*
-import com.tencent.qqnt.msg.api.IMsgService
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.withTimeoutOrNull
-import kotlinx.serialization.json.JsonArray
-import kotlinx.serialization.json.JsonObject
-import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment
-import moe.fuqiuluo.qqinterface.servlet.msg.toJson
-import moe.fuqiuluo.qqinterface.servlet.msg.toListMap
-import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
-import moe.fuqiuluo.shamrock.helper.ContactHelper
-import moe.fuqiuluo.shamrock.helper.Level
-import moe.fuqiuluo.shamrock.helper.LogCenter
-import moe.fuqiuluo.shamrock.helper.MessageHelper
-import moe.fuqiuluo.shamrock.remote.service.data.MessageDetail
-import moe.fuqiuluo.shamrock.remote.service.data.MessageSender
-import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult
-import moe.fuqiuluo.shamrock.tools.*
-import moe.fuqiuluo.shamrock.utils.DeflateTools
-import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
-import moe.fuqiuluo.shamrock.xposed.helper.msgService
-import moe.fuqiuluo.symbols.decodeProtobuf
-import protobuf.auto.toByteArray
-import protobuf.message.*
-import protobuf.message.longmsg.*
-import java.util.*
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
-import kotlin.random.Random
-
-internal object MsgSvc : BaseSvc() {
- private suspend fun prepareTempChatFromGroup(
- groupId: String,
- peerId: String
- ): Result {
- LogCenter.log("主动临时消息,创建临时会话。", Level.INFO)
- val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
- ?: return Result.failure(Exception("获取消息服务失败"))
- msgService.prepareTempChat(
- TempChatPrepareInfo(
- MsgConstant.KCHATTYPETEMPC2CFROMGROUP,
- ContactHelper.getUidByUinAsync(peerId = peerId.toLong()),
- app.getRuntimeService(ITroopMemberNameService::class.java, "all")
- .getTroopMemberNameRemarkFirst(groupId, peerId),
- groupId,
- EMPTY_BYTE_ARRAY,
- app.currentUid,
- "",
- TempChatGameSession()
- )
- ) { code, reason ->
- if (code != 0) {
- LogCenter.log("临时会话创建失败: $code, $reason", Level.ERROR)
- }
- }
- return Result.success(Unit)
- }
-
- suspend fun getTempChatInfo(chatType: Int, uid: String): Result {
- val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
- ?: return Result.failure(Exception("获取消息服务失败"))
- val info: TempChatInfo = withTimeoutOrNull(5000) {
- suspendCancellableCoroutine {
- msgService.getTempChatInfo(chatType, uid) { code, msg, tempChatInfo ->
- if (code == 0) {
- it.resume(tempChatInfo)
- } else {
- LogCenter.log("获取临时会话信息失败: $code:$msg", Level.ERROR)
- it.resume(null)
- }
- }
- }
- } ?: return Result.failure(Exception("获取临时会话信息失败"))
- return Result.success(info)
- }
-
- /**
- * 正常获取
- */
- suspend fun getMsg(hash: Int): Result {
- val mapping = MessageHelper.getMsgMappingByHash(hash)
- ?: return Result.failure(Exception("没有对应消息映射,消息获取失败"))
-
- val peerId = mapping.peerId
- val contact = MessageHelper.generateContact(mapping.chatType, peerId, mapping.subPeerId)
-
- val msg = withTimeoutOrNull(5000) {
- val service = QRoute.api(IMsgService::class.java)
- suspendCancellableCoroutine { continuation ->
- service.getMsgsByMsgId(contact, arrayListOf(mapping.qqMsgId)) { code, _, msgRecords ->
- if (code == 0 && msgRecords.isNotEmpty()) {
- continuation.resume(msgRecords.first())
- } else {
- continuation.resume(null)
- }
- }
- continuation.invokeOnCancellation {
- continuation.resume(null)
- }
- }
- }
-
- return if (msg != null) {
- Result.success(msg)
- } else {
- Result.failure(Exception("获取消息失败"))
- }
- }
-
- suspend fun getMsgByQMsgId(
- chatType: Int,
- peerId: String,
- qqMsgId: Long,
- subPeerId: String = ""
- ): Result {
- val contact = MessageHelper.generateContact(chatType, peerId, subPeerId)
- val service = QRoute.api(IMsgService::class.java)
-
- val msg = withTimeoutOrNull(5000) {
- suspendCoroutine { continuation ->
- service.getMsgsByMsgId(contact, arrayListOf(qqMsgId)) { code, _, msgRecords ->
- if (code == 0 && msgRecords.isNotEmpty()) {
- continuation.resume(msgRecords.first())
- } else {
- continuation.resume(null)
- }
- }
- }
- }
-
- return if (msg != null) {
- Result.success(msg)
- } else {
- Result.failure(Exception("获取消息失败"))
- }
- }
-
- /**
- * 什么鸟屎都获取不到
- */
- suspend fun getMsgBySeq(
- chatType: Int,
- peerId: String,
- seq: Long
- ): Result {
- val contact = MessageHelper.generateContact(chatType, peerId)
- val msg = withTimeoutOrNull(1000) {
- val service = QRoute.api(IMsgService::class.java)
- suspendCancellableCoroutine { continuation ->
- service.getMsgsBySeqs(contact, arrayListOf(seq)) { code, _, msgRecords ->
- continuation.resume(msgRecords?.firstOrNull())
- }
- continuation.invokeOnCancellation {
- continuation.resume(null)
- }
- }
- }
- return if (msg != null) {
- Result.success(msg)
- } else {
- Result.failure(Exception("获取消息失败"))
- }
- }
-
- /**
- * 撤回消息 同步 HTTP API
- */
- suspend fun recallMsg(msgHash: Int): Pair {
- val kernelService = NTServiceFetcher.kernelService
- val sessionService = kernelService.wrapperSession
- val msgService = sessionService.msgService
-
- val mapping = MessageHelper.getMsgMappingByHash(msgHash)
- ?: return -1 to "无法找到消息映射"
-
- val contact = MessageHelper.generateContact(mapping.chatType, mapping.peerId, mapping.subPeerId)
-
- return suspendCancellableCoroutine { continuation ->
- msgService.recallMsg(contact, arrayListOf(mapping.qqMsgId)) { code, why ->
- continuation.resume(code to why)
- }
- }
- }
-
- /**
- * 发送消息
- *
- * Aio 腾讯内部命名 All In One
- */
- suspend fun sendToAio(
- chatType: Int,
- peedId: String,
- message: JsonArray,
- fromId: String = peedId,
- retryCnt: Int
- ): Result {
- // 主动临时消息
- when (chatType) {
- MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
- prepareTempChatFromGroup(fromId, peedId).onFailure {
- LogCenter.log("主动临时消息,创建临时会话失败。", Level.ERROR)
- return Result.failure(Exception("主动临时消息,创建临时会话失败。"))
- }
- }
- }
- val result =
- MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0))
- .getOrElse { return Result.failure(it) }
- return if (result.isTimeout) {
- // 发送失败,可能网络问题出现红色感叹号,重试
- // 例如 rich media transfer failed
- delay(100)
- MessageHelper.resendMsg(chatType, peedId, fromId, result.qqMsgId, retryCnt, result.msgHashId)
- } else {
- Result.success(result)
- }
- }
-
- suspend fun uploadMultiMsg(
- chatType: Int,
- peerId: String,
- fromId: String = peerId,
- messages: JsonArray,
- retryCnt: Int
- ): Result {
- return uploadMultiMsg(chatType, peerId, fromId, messages).onFailure {
- if (retryCnt > 0) {
- return uploadMultiMsg(chatType, peerId, fromId, messages, retryCnt - 1)
- }
- }
- }
-
- private suspend fun uploadMultiMsg(
- chatType: Int,
- peerId: String,
- fromId: String = peerId,
- messages: JsonArray,
- ): Result {
- var i = -1
- val desc = MutableList(messages.size) { "" }
- val forwardMsg = mutableMapOf()
-
- val msgs = messages.mapNotNull { msg ->
- kotlin.runCatching {
- val data = msg.asJsonObject["data"].asJsonObject
- if (data.containsKey("id")) {
- val msgId = data["id"].asInt
- val record = getMsg(msgId).onFailure {
- error("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it")
- }.getOrThrow()
- PushMsgBody(
- msgHead = ResponseHead(
- peerUid = record.senderUid,
- receiverUid = record.peerUid,
- forward = ResponseForward(
- friendName = record.sendNickName
- ),
- responseGrp = if (record.chatType == MsgConstant.KCHATTYPEGROUP) ResponseGrp(
- groupCode = record.peerUin.toULong(),
- memberCard = record.sendMemberName,
- u1 = 2
- ) else null
- ),
- contentHead = ContentHead(
- msgType = when (record.chatType) {
- MsgConstant.KCHATTYPEC2C -> 9
- MsgConstant.KCHATTYPEGROUP -> 82
- else -> throw UnsupportedOperationException("Unsupported chatType: $chatType")
- },
- msgSubType = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
- divSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
- msgViaRandom = record.msgId,
- sequence = record.msgSeq, // idk what this is(i++)
- msgTime = record.msgTime,
- u2 = 1,
- u6 = 0,
- u7 = 0,
- msgSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) record.msgSeq else null, // seq for dm
- forwardHead = ForwardHead(
- u1 = 0,
- u2 = 0,
- u3 = 0,
- ub641 = "",
- avatar = ""
- )
- ),
- body = MsgBody(
- richText = MessageHelper.messageArrayToRichText(
- record.chatType,
- record.msgId,
- record.peerUin.toString(),
- record.elements.toSegments(
- record.chatType,
- record.peerUin.toString(),
- "0"
- ).onEach { segment ->
- if (segment.type == "forward") {
- forwardMsg[segment.data["filename"] as String] =
- segment.data["id"] as String
- }
- }.toJson()
- ).onFailure {
- error("消息合成失败: ${it.stackTraceToString()}")
- }.onSuccess {
- desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": " + it.first
- }.getOrThrow().second
- )
- )
- } else if (data.containsKey("content")) {
- PushMsgBody(
- msgHead = ResponseHead(
- peer = data["uin"]?.asLong ?: TicketSvc.getUin().toLong(),
- peerUid = data["uid"]?.asString ?: TicketSvc.getUid(),
- receiverUid = TicketSvc.getUid(),
- forward = ResponseForward(
- friendName = data["name"]?.asStringOrNull ?: TicketSvc.getNickname()
- )
- ),
- contentHead = ContentHead(
- msgType = 9,
- msgSubType = 175,
- divSeq = 175,
- msgViaRandom = Random.nextLong(),
- sequence = data["seq"]?.asLong ?: Random.nextLong(),
- msgTime = data["time"]?.asLong ?: (System.currentTimeMillis() / 1000),
- u2 = 1,
- u6 = 0,
- u7 = 0,
- msgSeq = data["seq"]?.asLong ?: Random.nextLong(),
- forwardHead = ForwardHead(
- u1 = 0,
- u2 = 0,
- u3 = 2,
- ub641 = "",
- avatar = ""
- )
- ),
- body = MsgBody(
- richText = MessageHelper.messageArrayToRichText(
- chatType = chatType,
- msgId = Random.nextLong(),
- peerId = data["uin"]?.asString ?: TicketSvc.getUin(),
- messageList = when (data["content"]) {
- is JsonObject -> listOf(data["content"] as JsonObject).json
- is JsonArray -> data["content"] as JsonArray
- else -> MessageHelper.decodeCQCode(data["content"].asString)
- }.onEach { element ->
- val elementData = element.asJsonObject["data"].asJsonObject
- if (element.asJsonObject["type"].asString == "forward") {
- forwardMsg[elementData["filename"].asString] =
- elementData["id"].asString
- }
- }
- ).onSuccess {
- desc[++i] = (data["name"].asStringOrNull ?: data["uin"].asStringOrNull
- ?: TicketSvc.getNickname()) + ": " + it.first
- }.onFailure {
- error("消息合成失败: ${it.stackTraceToString()}")
- }.getOrThrow().second
- )
- )
- } else error("消息节点缺少id或content字段")
- }.onFailure {
- LogCenter.log("消息节点解析失败:${it.stackTraceToString()}", Level.WARN)
- }.getOrNull()
- }.ifEmpty {
- return Result.failure(Exception("消息节点为空"))
- }
-
- val payload = LongMsgPayload(
- action = mutableListOf(
- LongMsgAction(
- command = "MultiMsg",
- data = LongMsgContent(
- body = msgs
- )
- )
- ).apply {
- forwardMsg.map { msg ->
- addAll(getMultiMsg(msg.value).getOrElse { return Result.failure(Exception("无法获取嵌套转发消息: $it")) }
- .map { action ->
- if (action.command == "MultiMsg") LongMsgAction(
- command = msg.key,
- data = action.data
- ) else action
- })
- }
- }
- )
- LogCenter.log({ payload.toByteArray().toHexString() }, Level.DEBUG)
-
- val req = LongMsgReq(
- sendInfo = when (chatType) {
- MsgConstant.KCHATTYPEC2C -> SendLongMsgInfo(
- type = 1,
- uid = LongMsgUid(if(peerId.startsWith("u_")) peerId else ContactHelper.getUidByUinAsync(peerId.toLong()) ),
- payload = DeflateTools.gzip(payload.toByteArray())
- )
- MsgConstant.KCHATTYPEGROUP -> SendLongMsgInfo(
- type = 3,
- uid = LongMsgUid(fromId),
- groupUin = fromId.toULong(),
- payload = DeflateTools.gzip(payload.toByteArray())
- )
- else -> throw UnsupportedOperationException("Unsupported chatType: $chatType")
- },
- setting = LongMsgSettings(
- field1 = 4,
- field2 = 2,
- field3 = 9,
- field4 = 0
- )
- ).toByteArray()
-
- val buffer = sendBufferAW("trpc.group.long_msg_interface.MsgService.SsoSendLongMsg", true, req, timeout = 30_000)
- ?: return Result.failure(Exception("unable to upload multi message, response timeout"))
- val rsp = runCatching {
- buffer.slice(4).decodeProtobuf()
- }.getOrElse {
- buffer.decodeProtobuf()
- }
- val resId = rsp.sendResult?.resId ?: return Result.failure(Exception("unable to upload multi message"))
- return Result.success(MessageSegment(
- type = "forward",
- data = mapOf(
- "id" to resId,
- "filename" to UUID.randomUUID().toString(),
- "summary" to "查看${desc.size}条转发消息",
- "desc" to desc.slice(0..if (i < 3) i else 3).joinToString("\n")
- )
- ))
- }
-
- suspend fun getMultiMsg(resId: String): Result> {
- val req = LongMsgReq(
- recvInfo = RecvLongMsgInfo(
- uid = LongMsgUid(TicketSvc.getUid()),
- resId = resId,
- u1 = 3
- ),
- setting = LongMsgSettings(
- field1 = 2,
- field2 = 2,
- field3 = 9,
- field4 = 0
- )
- )
- val buffer = sendBufferAW(
- "trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg",
- true,
- req.toByteArray()
- ) ?: return Result.failure(Exception("unable to get multi message"))
- val rsp = buffer.slice(4).decodeProtobuf()
- val zippedPayload = DeflateTools.ungzip(
- rsp.recvResult?.payload ?: return Result.failure(Exception("payload is empty"))
- )
- LogCenter.log(zippedPayload.toHexString(), Level.DEBUG)
- return Result.success(
- zippedPayload.decodeProtobuf().action
- ?: return Result.failure(Exception("action is empty"))
- )
- }
-
- suspend fun getForwardMsg(resId: String): Result> {
- val result = getMultiMsg(resId).getOrElse { return Result.failure(it) }
- result.forEach {
- if (it.command == "MultiMsg") {
- return Result.success(it.data?.body?.map { msg ->
- val chatType =
- if (msg.contentHead!!.msgType == 82) MsgConstant.KCHATTYPEGROUP else MsgConstant.KCHATTYPEC2C
- MessageDetail(
- time = msg.contentHead?.msgTime?.toInt() ?: 0,
- msgType = MessageHelper.obtainDetailTypeByMsgType(chatType),
- msgId = 0, // msgViaRandom为空 tx不给
- qqMsgId = 0,
- msgSeq = msg.contentHead!!.msgSeq ?: 0,
- realId = msg.contentHead!!.msgSeq ?: 0,
- sender = MessageSender(
- msg.msgHead?.peer ?: 0,
- msg.msgHead?.responseGrp?.memberCard ?: msg.msgHead?.forward?.friendName ?: "",
- "unknown",
- 0,
- msg.msgHead?.peerUid ?: "",
- msg.msgHead?.peerUid ?: ""
- ),
- message = msg.body?.richText?.toSegments(
- chatType,
- msg.msgHead?.peer.toString(),
- "0"
- )?.toListMap() ?: emptyList(),
- peerId = msg.msgHead?.peer ?: 0,
- groupId = if (chatType == MsgConstant.KCHATTYPEGROUP) msg.msgHead?.responseGrp?.groupCode?.toLong()
- ?: 0 else 0,
- targetId = if (chatType != MsgConstant.KCHATTYPEGROUP) msg.msgHead?.peer ?: 0 else 0
- )
- } ?: return Result.failure(Exception("Msg is empty")))
- }
- }
- return Result.failure(Exception("Can't find msg"))
- }
-
- class MessageCallback(
- private val peerId: String,
- var msgHash: Int
- ) : IOperateCallback {
- override fun onResult(code: Int, reason: String?) {
- if (code != 0 && msgHash != 0) {
- MessageHelper.removeMsgByHashCode(msgHash)
- }
- when (code) {
- 110 -> LogCenter.log("消息发送: $peerId, 无该联系人无法发送。")
- 120 -> LogCenter.log("消息发送: $peerId, 禁言状态无法发送。")
- 5 -> LogCenter.log("消息发送: $peerId, 当前不支持该消息类型。")
- else -> LogCenter.log("消息发送: $peerId, code: $code $reason")
- }
- }
- }
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt
deleted file mode 100644
index d74bf4e3..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/PacketSvc.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-package moe.fuqiuluo.qqinterface.servlet
-
-import com.tencent.qqnt.kernel.nativeinterface.Contact
-import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgService
-import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
-import io.ktor.utils.io.core.BytePacketBuilder
-import io.ktor.utils.io.core.readBytes
-import io.ktor.utils.io.core.writeFully
-import io.ktor.utils.io.core.writeInt
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.withTimeoutOrNull
-import moe.fuqiuluo.qqinterface.servlet.msg.MessageTempHandler
-import moe.fuqiuluo.shamrock.remote.action.handlers.GetHistoryMsg
-import moe.fuqiuluo.shamrock.tools.broadcast
-import moe.fuqiuluo.shamrock.utils.DeflateTools
-import mqq.app.MobileQQ
-import protobuf.auto.toByteArray
-import protobuf.message.*
-import protobuf.message.element.LightAppElem
-import protobuf.push.MessagePush
-import kotlin.coroutines.resume
-import kotlin.text.toByteArray
-
-internal object PacketSvc : BaseSvc() {
- /**
- * 伪造收到Json卡片消息
- */
- suspend fun fakeSelfRecvJsonMsg(msgService: IKernelMsgService, content: String): Long {
- return fakeReceiveSelfMsg(msgService) {
- listOf(
- Elem(
- lightApp = LightAppElem((byteArrayOf(1) + DeflateTools.compress(content.toByteArray())))
- )
- )
- }
- }
-
- private suspend fun fakeReceiveSelfMsg(msgService: IKernelMsgService, builder: () -> List): Long {
- val latestMsg = withTimeoutOrNull(3000) {
- suspendCancellableCoroutine {
- msgService.getMsgs(
- Contact(MsgConstant.KCHATTYPEC2C, app.currentUid, ""),
- 0L,
- 1,
- true
- ) { code, why, msgs ->
- it.resume(GetHistoryMsg.GetMsgResult(code, why, msgs))
- }
- }
- }?.data?.firstOrNull()
- val msgSeq = (latestMsg?.msgSeq ?: 0) + 1
-
- val msgPush = MessagePush(
- msgBody = PushMsgBody(
- msgHead = ResponseHead(
- peer = app.longAccountUin,
- peerUid = app.currentUid,
- flag = 1001,
- receiver = app.longAccountUin,
- receiverUid = app.currentUid
- ),
- contentHead = ContentHead(
- msgType = 166,
- msgSubType = 11,
- msgSeq = msgSeq,
- msgViaRandom = msgSeq,
- msgTime = System.currentTimeMillis() / 1000,
- u2 = 1,
- sequence = msgSeq,
- msgRandom = msgService.getMsgUniqueId(System.currentTimeMillis()),
- u4 = msgSeq - 2,
- u5 = msgSeq
- ),
- body = MsgBody(
- RichText(
- elements = builder()
- )
- )
- )
- )
-
- fakeReceive("trpc.msg.olpush.OlPushService.MsgPush", 10000, msgPush.toByteArray())
- return withTimeoutOrNull(5000L) {
- suspendCancellableCoroutine {
- MessageTempHandler.registerTemporaryMsgListener(msgSeq) {
- it.resume(this.msgId)
- }
- it.invokeOnCancellation {
- MessageTempHandler.unregisterTemporaryMsgListener(msgSeq)
- }
- }
- } ?: -1L
- }
-
- /**
- * 伪造QQ收到某个包
- */
- private fun fakeReceive(cmd: String, seq: Int, buffer: ByteArray) {
- MobileQQ.getContext().broadcast("msf") {
- putExtra("__cmd", "fake_packet")
- putExtra("package_cmd", cmd)
- putExtra("package_uin", app.currentUin)
- putExtra("package_seq", seq)
- val wupBuffer = BytePacketBuilder().apply {
- writeInt(buffer.size + 4)
- writeFully(buffer)
- }.build()
- putExtra("package_buffer", wupBuffer.readBytes())
- wupBuffer.release()
- }
- }
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/QFavSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/QFavSvc.kt
deleted file mode 100644
index 9b8bd0f7..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/QFavSvc.kt
+++ /dev/null
@@ -1,402 +0,0 @@
-@file:OptIn(ExperimentalSerializationApi::class)
-
-package moe.fuqiuluo.qqinterface.servlet
-
-import com.tencent.mobileqq.app.QQAppInterface
-import com.tencent.mobileqq.transfile.HttpNetReq
-import com.tencent.mobileqq.transfile.INetEngineListener
-import com.tencent.mobileqq.transfile.NetReq
-import com.tencent.mobileqq.transfile.NetResp
-import com.tencent.mobileqq.transfile.ServerAddr
-import com.tencent.mobileqq.transfile.api.IHttpEngineService
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.io.core.BytePacketBuilder
-import kotlinx.io.core.readBytes
-import kotlinx.io.core.writeFully
-import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.encodeToByteArray
-
-import moe.fuqiuluo.shamrock.helper.Level
-import moe.fuqiuluo.shamrock.helper.LogCenter
-import moe.fuqiuluo.shamrock.tools.hex2ByteArray
-import moe.fuqiuluo.shamrock.tools.toHexString
-import moe.fuqiuluo.shamrock.utils.DeflateTools
-import moe.fuqiuluo.shamrock.utils.MD5
-import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
-import protobuf.fav.WeiyunAddRichMediaReq
-import protobuf.fav.WeiyunAuthor
-import protobuf.fav.WeiyunCollectCommInfo
-import protobuf.fav.WeiyunComm
-import protobuf.fav.WeiyunCommonReq
-import protobuf.fav.WeiyunFastUploadResourceReq
-import protobuf.fav.WeiyunGetFavContentReq
-import protobuf.fav.WeiyunGetFavListReq
-import protobuf.fav.WeiyunMsgHead
-import protobuf.fav.WeiyunPicInfo
-import protobuf.fav.WeiyunRichMediaContent
-import protobuf.fav.WeiyunRichMediaSummary
-import mqq.manager.TicketManager
-import oicq.wlogin_sdk.request.Ticket
-import oicq.wlogin_sdk.request.WtTicketPromise
-import oicq.wlogin_sdk.tools.ErrMsg
-import protobuf.auto.toByteArray
-import java.io.ByteArrayOutputStream
-import java.io.File
-import java.nio.ByteBuffer
-import kotlin.coroutines.resume
-
-
-/**
- * QQ收藏相关接口
- */
-internal object QFavSvc: BaseSvc() {
- private val SERVER_LIST_COLLECTOR = listOf(ServerAddr().also {
- it.isIpv6 = false
- it.mIp = "collector.weiyun.com"
- it.port = 80
- })
- private val SERVER_LIST_PICUP = listOf(ServerAddr().also {
- it.isIpv6 = false
- it.mIp = "pic.pieceup.qq.com"
- it.port = 80
- })
- private const val VERSION = 12820
- private const val APPID = 30244
- private const val SUB_APPID = 538116905
- private const val MAJOR_VERSION = 8
- private const val MINOR_VERSION = 9
- private var seq = 1
-
- suspend fun getItemList(
- category: Int,
- startPos: Int,
- pageSize: Int,
- ): Result {
- val baseReq = WeiyunCommonReq(
- getFavListReq = WeiyunGetFavListReq(
- type = 0u,
- bid = 0u,
- category = category.toUInt(),
- startTime = 0u,
- orderType = 0u,
- startPos = startPos.toUInt(),
- pageSize = pageSize.toUInt(),
- syncPolicy = 0u,
- reqSource = 0u
- )
- )
- return sendWeiyunReq(20000, baseReq)
- }
-
- suspend fun getItemContent(
- id: String
- ): Result {
- return sendWeiyunReq(20001, WeiyunCommonReq(
- getFavContentReq = WeiyunGetFavContentReq(
- cidList = arrayListOf(id)
- )
- )
- )
- }
-
- suspend fun addImageMsg(
- uin: Long,
- name: String,
- groupId: Long = 0,
- groupName: String = "",
- picUrl: String,
- pid: String,
- width: Int, height: Int,
- size: Long,
- md5: String,
- ): Result {
- val md5Bytes = md5.hex2ByteArray()
- return sendWeiyunReq(20009, WeiyunCommonReq(
- addRichMediaReq = WeiyunAddRichMediaReq(
- commInfo = WeiyunCollectCommInfo(
- bid = 1u,
- category = 1u,
- author = WeiyunAuthor(
- type = if (groupId == 0L) 1u else 2u,
- numId = uin.toULong(),
- strId = name,
- groupId = groupId.toULong(),
- groupName = groupName
- ),
- createTime = System.currentTimeMillis().toULong() - 2000u,
- seq = System.currentTimeMillis().toULong() - 1000u,
- bizDataList = arrayListOf("""{"recordAudioOnly":false,"audioOnly":false,"fileOnly":false}""".toByteArray()),
- originalAppId = 0u,
- customGroupId = 0u
- ),
- summary = WeiyunRichMediaSummary(
- title = "",
- brief = "[图片]",
- picList = arrayListOf(
- WeiyunPicInfo(
- uri = picUrl,
- md5 = md5Bytes,
- sha1 = md5.toByteArray(),
- name = "",
- note = "",
- width = width.toUInt(),
- height = height.toUInt(),
- size = size.toULong(),
- type = 0u,
- picId = pid
- )
- ),
- contentType = 1u
- ),
- richMediaContent = listOf(
- WeiyunRichMediaContent(
- rawData = """""".toByteArray(),
- picList = listOf(
- WeiyunPicInfo(
- uri = picUrl,
- md5 = md5Bytes,
- sha1 = md5.toByteArray(),
- name = "",
- note = "",
- width = width.toUInt(),
- height = height.toUInt(),
- size = size.toULong(),
- type = 0u,
- picId = pid
- )
- )
- )
- )
- )
- )
- )
- }
-
- suspend fun applyUpImageMsg(
- uin: Long,
- name: String,
- groupId: Long = 0,
- groupName: String = "",
- width: Int, height: Int,
- image: File
- ): Result {
- if (!image.exists()) {
- return Result.failure(IllegalArgumentException("image file not exists"))
- }
- val md5 = MD5.genFileMd5(image.absolutePath)
- return sendWeiyunReq(20010, WeiyunCommonReq(
- fastUploadResourceReq = WeiyunFastUploadResourceReq(
- picInfoList = listOf(
- WeiyunPicInfo(
- md5 = md5,
- name = md5.toHexString(),
- width = width.toUInt(),
- height = height.toUInt(),
- size = image.length().toULong(),
- type = 1u,
- picId = "/storage/emulated/0/DCIM/temp.jpeg",
- owner = WeiyunAuthor(
- type = if (groupId == 0L) 1u else 2u,
- numId = uin.toULong(),
- strId = name,
- groupId = groupId.toULong(),
- groupName = groupName
- )
- )
- ),
- )
- )
- )
- }
-
- suspend fun addRichMediaMsg(
- uin: Long,
- name: String,
- groupId: Long = 0,
- groupName: String = "",
- time: Long = System.currentTimeMillis(),
- content: String
- ): Result {
- return sendWeiyunReq(20009, WeiyunCommonReq(
- addRichMediaReq = WeiyunAddRichMediaReq(
- commInfo = WeiyunCollectCommInfo(
- bid = 1u,
- category = 1u,
- author = WeiyunAuthor(
- type = if (groupId == 0L) 1u else 2u,
- numId = uin.toULong(),
- strId = name,
- groupId = groupId.toULong(),
- groupName = groupName
- ),
- createTime = time.toULong() - 2000u,
- seq = time.toULong() - 1000u,
- originalAppId = 0u,
- customGroupId = 0u
- ),
- summary = WeiyunRichMediaSummary(
- brief = content,
- contentType = 1u
- ),
- richMediaContent = listOf(
- WeiyunRichMediaContent(
- rawData = content.textToHtml().toByteArray(),
- )
- )
- )
- )
- )
- }
-
- private fun String.textToHtml(): String {
- return replace("\n", "
")
- }
-
- suspend fun sendPicUpBlock(
- fileSize: Long,
- offset: Long,
- block: ByteArray,
- blockSize: Long,
- sha: ByteArray,
- pid: String,
- outputStream: ByteArrayOutputStream = ByteArrayOutputStream(),
- ): Result {
- return suspendCancellableCoroutine {
- val httpNetReq = HttpNetReq()
- httpNetReq.userData = null
- httpNetReq.mCallback = object: INetEngineListener {
- override fun onResp(netResp: NetResp) {
- if (netResp.mHttpCode != 200 && netResp.mResult != 0 && netResp.mErrDesc.isNullOrEmpty()) {
- netResp.mErrDesc = netResp.mRespProperties["User-ErrMsg"]
- }
- netResp.mRespData = outputStream.toByteArray().copyOf()
- it.resume(Result.success(netResp))
- }
-
- override fun onUpdateProgeress(netReq: NetReq, curr: Long, final: Long) {}
- }
- val vi = (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getA2(app.currentAccountUin)
- //LogCenter.log(pSKey)
- httpNetReq.mHttpMethod = HttpNetReq.HTTP_POST
- httpNetReq.mSendData = BytePacketBuilder().apply {
- writeInt(-1412589450)
- writeInt(10000)
- writeInt(0)
- writeInt(sha.size + 16 + blockSize.toInt())
- writeShort(0)
- writeShort(sha.size.toShort())
- writeFully(sha)
- writeInt(fileSize.toInt())
- writeInt(offset.toInt())
- writeInt(blockSize.toInt())
- writeFully(block)
- }.build().readBytes()
- httpNetReq.mOutStream = outputStream
- httpNetReq.mStartDownOffset = 0L
- httpNetReq.mReqProperties["Shamrock"] = "true"
- httpNetReq.mReqProperties["Cookie"] = String.format("uin=%s;vt=%d;vi=%s;pid=%s;appid=%d", app.currentAccountUin, 8, vi, pid, APPID)
- httpNetReq.mReqProperties["host"] = "pic.pieceup.qq.com"
- httpNetReq.mReqProperties["Range"] = "bytes=0-"
- httpNetReq.mReqProperties["Content-Length"] = httpNetReq.mSendData.size.toString()
- httpNetReq.mReqProperties["Accept-Encoding"] = "gzip"
- httpNetReq.mReqProperties["Content-Encoding"] = "gzip"
- httpNetReq.mPrioty = 1
- httpNetReq.mReqUrl = "https://pic.pieceup.qq.com/"
- httpNetReq.mServerList = SERVER_LIST_PICUP
- val service = AppRuntimeFetcher.appRuntime
- .getRuntimeService(IHttpEngineService::class.java, "qqfav")
- service.sendReq(httpNetReq)
- }
- }
-
- suspend fun sendWeiyunReq(
- cmd: Int,
- req: WeiyunCommonReq,
- outputStream: ByteArrayOutputStream = ByteArrayOutputStream(),
- ): Result {
- return suspendCancellableCoroutine {
- val httpNetReq = HttpNetReq()
- httpNetReq.userData = null
- httpNetReq.mCallback = object: INetEngineListener {
- override fun onResp(netResp: NetResp) {
- if (netResp.mHttpCode != 200 && netResp.mResult != 0 && netResp.mErrDesc.isNullOrEmpty()) {
- netResp.mErrDesc = netResp.mRespProperties["User-ErrMsg"]
- }
- netResp.mRespData = outputStream.toByteArray().copyOf()
- it.resume(Result.success(netResp))
- }
-
- override fun onUpdateProgeress(netReq: NetReq, curr: Long, final: Long) {}
- }
- val pSKey = getWeiYunPSKey()
- httpNetReq.mHttpMethod = HttpNetReq.HTTP_POST
- httpNetReq.mSendData = DeflateTools.gzip(packData(packHead(cmd, pSKey), WeiyunComm(
- req = req
- ).toByteArray()))
- httpNetReq.mOutStream = outputStream
- httpNetReq.mStartDownOffset = 0L
- httpNetReq.mReqProperties["Shamrock"] = "true"
- httpNetReq.mReqProperties["Cookie"] = String.format("uin=%s;vt=%d;vi=%s;appid=%d", app.currentAccountUin, 27, pSKey, APPID)
- httpNetReq.mReqProperties["host"] = "collector.weiyun.com"
- httpNetReq.mReqProperties["Range"] = "bytes=0-"
- httpNetReq.mReqProperties["Content-Length"] = httpNetReq.mSendData.size.toString()
- httpNetReq.mReqProperties["Accept-Encoding"] = "gzip"
- httpNetReq.mReqProperties["Content-Encoding"] = "gzip"
- httpNetReq.mPrioty = 1
- httpNetReq.mReqUrl = "https://collector.weiyun.com/collector.fcg"
- httpNetReq.mServerList = SERVER_LIST_COLLECTOR
- val service = AppRuntimeFetcher.appRuntime
- .getRuntimeService(IHttpEngineService::class.java, "qqfav")
- service.sendReq(httpNetReq)
- }
- }
-
- private fun packHead(cmd: Int, pskey: String): ByteArray {
- return WeiyunMsgHead(
- uin = app.longAccountUin.toULong(),
- seq = seq++.toUInt(),
- type = 1u,
- cmd = cmd.toUInt(),
- appId = APPID.toUInt(),
- version = VERSION.toUInt(),
- netType = 3u,
- keyType = 27u,
- key = pskey.toByteArray(),
- majorVersion = MAJOR_VERSION.toUInt(),
- minorVersion = MINOR_VERSION.toUInt(),
- ).toByteArray()
- }
-
- private fun packData(head: ByteArray, body: ByteArray): ByteArray {
- val len = 16 + head.size + body.size
- val buf = ByteBuffer.allocate(len)
- buf.putInt(SUB_APPID)
- buf.putShort(1)
- buf.putInt(len)
- buf.putInt(body.size)
- buf.putShort(0)
- buf.put(head)
- buf.put(body)
- return buf.array()
- }
-
- private fun getWeiYunPSKey(): String {
- val pskey = (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager)
- .getPskey(app.currentAccountUin, 16L, arrayOf("weiyun.com"), WeiYunPSKeyPromise)
- return if (pskey != null) pskey.getPSkey("weiyun.com") else ""
- }
-
- private object WeiYunPSKeyPromise: WtTicketPromise {
- override fun Done(ticket: Ticket) {
- LogCenter.log("Fav: getPskeyPromise: done", Level.DEBUG)
- }
-
- override fun Failed(errMsg: ErrMsg) {
- LogCenter.log("Fav: getPskeyPromise: failed, $errMsg", Level.DEBUG)
- }
-
- override fun Timeout(errMsg: ErrMsg) {
- LogCenter.log("Fav: getPskeyPromise: timeout, $errMsg", Level.DEBUG)
- }
- }
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/QSafeSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/QSafeSvc.kt
deleted file mode 100644
index 3f7e9f07..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/QSafeSvc.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package moe.fuqiuluo.qqinterface.servlet
-
-import QQService.SvcDevLoginInfo
-import QQService.SvcReqGetDevLoginInfo
-import QQService.SvcRspGetDevLoginInfo
-import com.qq.jce.wup.UniPacket
-import moe.fuqiuluo.qqinterface.servlet.BaseSvc
-import mqq.app.MobileQQ
-import mqq.app.Packet
-import oicq.wlogin_sdk.tools.util
-
-internal object QSafeSvc: BaseSvc() {
-
- suspend fun getOnlineClients(): ArrayList? {
- val req = SvcReqGetDevLoginInfo()
- req.vecGuid = util.getGuidFromFile(MobileQQ.getContext())
- req.strAppName = MobileQQ.getMobileQQ().qqProcessName.split(":")[0]
- req.iLoginType = 1
- req.iRequireMax = 20
- req.iGetDevListType = 6
-
- val uniPacket = UniPacket()
- uniPacket.servantName = "StatSvc"
- uniPacket.funcName = "SvcReqGetDevLoginInfo"
- uniPacket.put("SvcReqGetDevLoginInfo", req)
- val resp = sendBufferAW("StatSvc.GetDevLoginInfo", false, uniPacket.encode())
- ?: return null
-
- return Packet.decodePacket(resp, "SvcRspGetDevLoginInfo", SvcRspGetDevLoginInfo()).vecCurrentLoginDevInfo
- }
-
-
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/TicketSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/TicketSvc.kt
deleted file mode 100644
index 415bf8f4..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/TicketSvc.kt
+++ /dev/null
@@ -1,179 +0,0 @@
-package moe.fuqiuluo.qqinterface.servlet
-
-import com.tencent.guild.api.transfile.IGuildTransFileApi
-import com.tencent.mobileqq.app.QQAppInterface
-import com.tencent.mobileqq.pskey.oidb.cmd0x102a.oidb_cmd0x102a
-import com.tencent.mobileqq.qroute.QRoute
-import io.ktor.client.request.get
-import io.ktor.client.request.header
-import moe.fuqiuluo.shamrock.remote.service.data.BigDataTicket
-import moe.fuqiuluo.shamrock.tools.GlobalClientNoRedirect
-import moe.fuqiuluo.shamrock.tools.slice
-import mqq.app.MobileQQ
-import mqq.manager.TicketManager
-import oicq.wlogin_sdk.request.Ticket
-import tencent.im.oidb.oidb_sso
-
-internal object TicketSvc: BaseSvc() {
- object SigType {
- const val WLOGIN_A5 = 2
- const val WLOGIN_RESERVED = 16
- const val WLOGIN_STWEB = 32 // TLV 103
- const val WLOGIN_A2 = 64
- const val WLOGIN_ST = 128
- const val WLOGIN_AQSIG = 2097152
- const val WLOGIN_D2 = 262144
- const val WLOGIN_DA2 = 33554432
- const val WLOGIN_LHSIG = 4194304
- const val WLOGIN_LSKEY = 512
- const val WLOGIN_OPENKEY = 16384
- const val WLOGIN_PAYTOKEN = 8388608
- const val WLOGIN_PF = 16777216
- const val WLOGIN_PSKEY = 1048576
- const val WLOGIN_PT4Token = 134217728
- const val WLOGIN_QRPUSH = 67108864
- const val WLOGIN_SID = 524288
- const val WLOGIN_SIG64 = 8192
- const val WLOGIN_SKEY = 4096
- const val WLOGIN_TOKEN = 32768
- const val WLOGIN_VKEY = 131072
-
- val ALL_TICKET = arrayOf(
- WLOGIN_A5, WLOGIN_RESERVED, WLOGIN_STWEB, WLOGIN_A2, WLOGIN_ST, WLOGIN_AQSIG, WLOGIN_D2, WLOGIN_DA2,
- WLOGIN_LHSIG, WLOGIN_LSKEY, WLOGIN_OPENKEY, WLOGIN_PAYTOKEN, WLOGIN_PF, WLOGIN_PSKEY, WLOGIN_PT4Token,
- WLOGIN_QRPUSH, WLOGIN_SID, WLOGIN_SIG64, WLOGIN_SKEY, WLOGIN_TOKEN, WLOGIN_VKEY
- )
- }
-
- fun getUin(): String {
- return app.currentUin.ifBlank { "0" }
- }
-
- fun getLongUin(): Long {
- return app.longAccountUin
- }
-
- fun getUid(): String {
- return app.currentUid.ifBlank { "u_" }
- }
-
- fun getNickname(): String {
- return app.currentNickname
- }
-
- fun getCookie(): String {
- val uin = getUin()
- val skey = getRealSkey(uin)
- val pskey = getPSKey(uin)
- return "uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey"
- }
-
- suspend fun getCookie(domain: String): String {
- val uin = getUin()
- val skey = getRealSkey(uin)
- val pskey = getPSKey(uin, domain) ?: ""
- val pt4token = getPt4Token(uin, domain) ?: ""
- return "uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey; pt4_token=$pt4token"
- }
-
- fun getBigdataTicket(): BigDataTicket? {
- return runCatching {
- QRoute.api(IGuildTransFileApi::class.java).bigDataTicket?.let {
- BigDataTicket(it.getSessionKey(), it.getSessionSig())
- }
- }.getOrNull()
- }
-
- fun getCSRF(pskey: String = getPSKey(getUin())): String {
- if (pskey.isEmpty()) {
- return "0"
- }
- var v = 5381
- for (element in pskey) {
- v += ((v shl 5) + element.code.toLong()).toInt()
- }
- return (v and Int.MAX_VALUE).toString()
- }
-
- suspend fun getCSRF(uin: String, domain: String): String {
- // 是不是要用Skey?
- return getBkn(getPSKey(uin, domain) ?: getSKey(uin))
- }
-
- fun getBkn(arg: String): String {
- var v: Long = 5381
- for (element in arg) {
- v += (v shl 5 and 2147483647L) + element.code.toLong()
- }
- return (v and 2147483647L).toString()
- }
-
- fun getTicket(uin: String, id: Int): Ticket? {
- return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getLocalTicket(uin, id)
- }
-
- fun getStWeb(uin: String): String {
- return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getStweb(uin)
- }
-
- fun getSKey(uin: String): String {
- return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getSkey(uin)
- }
-
- fun getRealSkey(uin: String): String {
- return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getRealSkey(uin)
- }
-
- fun getPSKey(uin: String): String {
- val manager = (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager)
- manager.reloadCache(MobileQQ.getContext())
- return manager.getSuperkey(uin) ?: ""
- }
-
- suspend fun getLessPSKey(vararg domain: String): Result> {
- val req = oidb_cmd0x102a.GetPSkeyRequest()
- req.domains.set(domain.toList())
- val buffer = sendOidbAW("OidbSvcTcp.0x102a", 4138, 0, req.toByteArray())
- ?: return Result.failure(Exception("getLessPSKey failed"))
- val body = oidb_sso.OIDBSSOPkg()
- body.mergeFrom(buffer.slice(4))
- val rsp = oidb_cmd0x102a.GetPSkeyResponse().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
- return Result.success(rsp.private_keys.get())
- }
-
- suspend fun getPSKey(uin: String, domain: String): String? {
- return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getPskey(uin, domain).let {
- if (it.isNullOrBlank())
- getLessPSKey(domain).getOrNull()?.firstOrNull()?.key?.get()
- else it
- }
- }
-
- fun getPt4Token(uin: String, domain: String): String? {
- return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getPt4Token(uin, domain)
- }
-
- suspend fun GetHttpCookies(appid: String, daid: String, jumpurl: String): String? {
- val uin = getUin()
- val clientkey = getStWeb(uin)
- var url = "https://ui.ptlogin2.qq.com/cgi-bin/login?pt_hide_ad=1&style=9&appid=$appid&pt_no_auth=1&pt_wxtest=1&daid=$daid&s_url=$jumpurl"
- var cookie = GlobalClientNoRedirect.get(url).headers.getAll("Set-Cookie")?.joinToString(";")
- url = "https://ssl.ptlogin2.qq.com/jump?u1=$jumpurl&pt_report=1&daid=$daid&style=9&keyindex=19&clientuin=$uin&clientkey=$clientkey"
- GlobalClientNoRedirect.get(url) {
- header("Cookie", cookie)
- }.let {
- cookie = it.headers.getAll("Set-Cookie")?.joinToString(";")
- url = it.headers["Location"].toString()
- }
- cookie = GlobalClientNoRedirect.get(url).headers.getAll("Set-Cookie")?.joinToString(";")
- val extractedCookie = StringBuilder()
- val cookies = cookie?.split(";")
- cookies?.filter { cookie ->
- val cookiePair = cookie.trim().split("=")
- cookiePair.size == 2 && cookiePair[1].isNotBlank() && cookiePair[0].trim() in listOf("uin", "skey", "p_uin", "p_skey", "pt4_token")
- }?.forEach {
- extractedCookie.append("$it; ")
- }
- return extractedCookie.toString().trim()
- }
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/VisitorSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/VisitorSvc.kt
deleted file mode 100644
index 57f6dc56..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/VisitorSvc.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-package moe.fuqiuluo.qqinterface.servlet
-
-internal object VisitorSvc: BaseSvc() {
- const val FROM_C2C_AIO = 2
- const val FROM_CONDITION_SEARCH = 9
- const val FROM_CONTACTS_TAB = 5
- const val FROM_FACE_2_FACE_ADD_FRIEND = 11
- const val FROM_MAYKNOW_FRIEND = 3
- const val FROM_QCIRCLE = 4
- const val FROM_QQ_TROOP = 1
- const val FROM_QZONE = 7
- const val FROM_SCAN = 6
- const val FROM_SEARCH = 8
- const val FROM_SETTING_ME = 12
- const val FROM_SHARE_CARD = 10
-
- const val IS_BLACK_LIST = "is_blacklist_user_profile"
- const val PROFILE_CARD_IS_BLACK = 2
- const val PROFILE_CARD_IS_BLACKED = 1
- const val PROFILE_CARD_NOT_BLACK = 3
-
- const val SUB_FROM_C2C_AIO = 21
- const val SUB_FROM_C2C_INTERACTIVE_LOGO = 25
- const val SUB_FROM_C2C_LEFT_SLIDE = 23
- const val SUB_FROM_C2C_OTHER = 24
- const val SUB_FROM_C2C_SETTING = 22
- const val SUB_FROM_C2C_TOFU = 26
- const val SUB_FROM_CONDITION_SEARCH_OTHER = 99
- const val SUB_FROM_CONDITION_SEARCH_RESULT = 91
- const val SUB_FROM_CONTACTS_FRIEND_TAB = 51
- const val SUB_FROM_CONTACTS_TAB = 55
- const val SUB_FROM_FACE_2_FACE_ADD_FRIEND_RESULT_AVATAR = 111
- const val SUB_FROM_FACE_2_FACE_OTHER = 119
- const val SUB_FROM_FRIEND_APPLY = 56
- const val SUB_FROM_FRIEND_NOTIFY_MORE = 57
- const val SUB_FROM_FRIEND_NOTIFY_TAB = 54
- const val SUB_FROM_GROUPING_TAB = 52
- const val SUB_FROM_MAYKNOW_FRIEND_CONTACT_TAB = 31
- const val SUB_FROM_MAYKNOW_FRIEND_CONTACT_TAB_MORE = 37
- const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE = 34
- const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE_MORE = 39
- const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE_SEARCH = 36
- const val SUB_FROM_MAYKNOW_FRIEND_NEW_FRIEND_PAGE = 32
- const val SUB_FROM_MAYKNOW_FRIEND_OTHER = 35
- const val SUB_FROM_MAYKNOW_FRIEND_SEARCH = 33
- const val SUB_FROM_MAYKNOW_FRIEND_SEARCH_MORE = 38
- const val SUB_FROM_PHONE_LIST_TAB = 53
- const val SUB_FROM_QCIRCLE_OTHER = 42
- const val SUB_FROM_QCIRCLE_PROFILE = 41
- const val SUB_FROM_QQ_TROOP_ACTIVE_MEMBER = 15
- const val SUB_FROM_QQ_TROOP_ADMIN = 16
- const val SUB_FROM_QQ_TROOP_AIO = 11
- const val SUB_FROM_QQ_TROOP_MEMBER = 12
- const val SUB_FROM_QQ_TROOP_OTHER = 14
- const val SUB_FROM_QQ_TROOP_SETTING_MEMBER_LIST = 17
- const val SUB_FROM_QQ_TROOP_TEMP_SESSION = 13
- const val SUB_FROM_QRCODE_SCAN_DRAWER = 64
- const val SUB_FROM_QRCODE_SCAN_NEW = 61
- const val SUB_FROM_QRCODE_SCAN_OLD = 62
- const val SUB_FROM_QRCODE_SCAN_OTHER = 69
- const val SUB_FROM_QRCODE_SCAN_PROFILE = 63
- const val SUB_FROM_QZONE_HOME = 71
- const val SUB_FROM_QZONE_OTHER = 79
- const val SUB_FROM_SEARCH_CONTACT_TAB_MORE_FIND_PROFILE = 83
- const val SUB_FROM_SEARCH_FIND_PROFILE_TAB = 82
- const val SUB_FROM_SEARCH_MESSAGE_TAB_MORE_FIND_PROFILE = 84
- const val SUB_FROM_SEARCH_NEW_FRIEND_MORE_FIND_PROFILE = 85
- const val SUB_FROM_SEARCH_OTHER = 89
- const val SUB_FROM_SEARCH_TAB = 81
- const val SUB_FROM_SETTING_ME_AVATAR = 121
- const val SUB_FROM_SETTING_ME_OTHER = 129
- const val SUB_FROM_SHARE_CARD_C2C = 101
- const val SUB_FROM_SHARE_CARD_OTHER = 109
- const val SUB_FROM_SHARE_CARD_TROOP = 102
- const val SUB_FROM_TYPE_DEFAULT = 0
-
- suspend fun vote(target: Long, count: Int): Result {
- if(count !in 1 .. 20) {
- return Result.failure(IllegalArgumentException("vote count must be in 1 .. 20"))
- }
- val card = CardSvc.getProfileCard(target).onFailure {
- return Result.failure(RuntimeException("unable to fetch contact info"))
- }.getOrThrow()
- sendExtra("VisitorSvc.ReqFavorite") {
- it.putLong(moe.fuqiuluo.shamrock.remote.service.data.profile.ProfileProtocolConst.PARAM_SELF_UIN, currentUin.toLong())
- it.putLong(moe.fuqiuluo.shamrock.remote.service.data.profile.ProfileProtocolConst.PARAM_TARGET_UIN, target)
- it.putByteArray("vCookies", card.vCookies)
- it.putBoolean("nearby_people", true)
- it.putInt("favoriteSource", FROM_CONTACTS_TAB)
- it.putInt("iCount", count)
- it.putInt("from", FROM_CONTACTS_TAB)
- }
- return Result.success(Unit)
- }
-}
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/ArkMsgSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/ArkMsgSvc.kt
deleted file mode 100644
index 0a3c47d1..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/ArkMsgSvc.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-package moe.fuqiuluo.qqinterface.servlet.ark
-
-import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
-import moe.fuqiuluo.qqinterface.servlet.BaseSvc
-import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
-import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77
-
-internal object ArkMsgSvc: BaseSvc() {
- fun tryShareMusic(
- chatType: Int,
- peerId: Long,
- msgId: Long,
- arkAppInfo: ArkAppInfo,
- title: String,
- singer: String,
- jumpUrl: String,
- previewUrl: String,
- musicUrl: String,
- ) {
- val req = oidb_cmd0xb77.ReqBody()
- req.appid.set(arkAppInfo.appId)
- req.app_type.set(1)
- req.msg_style.set(4)
- req.client_info.set(oidb_cmd0xb77.ClientInfo().also {
- it.platform.set(1)
- it.sdk_version.set(arkAppInfo.version)
- it.android_package_name.set(arkAppInfo.packageName)
- it.android_signature.set(arkAppInfo.signature)
- })
- req.ext_info.set(oidb_cmd0xb77.ExtInfo().also {
- it.msg_seq.set(msgId)
- })
- req.recv_uin.set(peerId)
- req.rich_msg_body.set(oidb_cmd0xb77.RichMsgBody().also {
- it.title.set(title)
- it.summary.set(singer)
- it.url.set(jumpUrl)
- it.picture_url.set(previewUrl)
- it.music_url.set(musicUrl)
- })
- when (chatType) {
- MsgConstant.KCHATTYPEGROUP -> req.send_type.set(1)
- MsgConstant.KCHATTYPEC2C -> req.send_type.set(0)
- else -> error("不支持该聊天类型发送音乐分享")
- }
- sendOidb("OidbSvc.0xb77_9", 0xb77, 9, req.toByteArray())
- }
-
- /*
- suspend fun tryShareJsonMessage(
- jsonString: String,
- arkAppInfo: ArkAppInfo = ArkAppInfo.DanMaKu,
- ): Result {
- val msgSeq = MessageHelper.generateMsgId(MsgConstant.KCHATTYPEC2C).qqMsgId
- val req = oidb_cmd0xb77.ReqBody()
- req.appid.set(arkAppInfo.appId)
- req.app_type.set(1)
- req.msg_style.set(10)
- req.client_info.set(oidb_cmd0xb77.ClientInfo().also {
- it.platform.set(1)
- it.sdk_version.set(arkAppInfo.version)
- it.android_package_name.set(arkAppInfo.packageName)
- it.android_signature.set(arkAppInfo.signature)
- })
- req.ext_info.set(oidb_cmd0xb77.ExtInfo().also {
- it.tag_name.set(ByteStringMicro.copyFromUtf8("shamrock"))
- it.msg_seq.set(msgSeq)
- })
- req.send_type.set(0)
- req.recv_uin.set(TicketSvc.getLongUin())
- req.mini_app_msg_body.set(oidb_cmd0xb77.MiniAppMsgBody().also {
- it.mini_app_appid.set(arkAppInfo.miniAppId)
- it.mini_app_path.set("pages")
- it.web_page_url.set("https://im.qq.com/index/")
- it.title.set("title")
- it.desc.set("desc")
- it.json_str.set(jsonString)
- })
- sendOidb("OidbSvc.0xb77_9", 0xb77, 9, req.toByteArray())
- val signedJson: String = withTimeoutOrNull(5.seconds) {
- suspendCancellableCoroutine {
- AioListener.registerTemporaryMsgListener(msgSeq) {
- it.resume(elements.first {
- it.elementType == MsgConstant.KELEMTYPEARKSTRUCT
- }.arkElement.bytesData)
- }
- it.invokeOnCancellation {
- AioListener.unregisterTemporaryMsgListener(msgSeq)
- }
- }
- } ?: return Result.failure(Exception("unable to sign json"))
- return Result.success(signedJson)
- }*/
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/LightAppSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/LightAppSvc.kt
deleted file mode 100644
index 6f35f458..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/LightAppSvc.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package moe.fuqiuluo.qqinterface.servlet.ark
-
-import moe.fuqiuluo.qqinterface.servlet.BaseSvc
-import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
-import moe.fuqiuluo.shamrock.utils.PlatformUtils
-import moe.fuqiuluo.symbols.decodeProtobuf
-import protobuf.auto.toByteArray
-import protobuf.lightapp.AdaptShareInfoReq
-import protobuf.lightapp.AdaptShareInfoResp
-import protobuf.qweb.DEFAULT_DEVICE_INFO
-import protobuf.qweb.QWebReq
-import protobuf.qweb.QWebRsp
-
-internal object LightAppSvc: BaseSvc() {
- suspend fun adaptShareJumpUrl(
- arkAppInfo: ArkAppInfo,
- coverUrl: String,
- desc: String,
- url: String
- ): Result {
- val rsp = sendBufferAW("LightAppSvc.mini_app_share.AdaptShareInfo", true, QWebReq(
- seq = 10,
- qua = PlatformUtils.getQUA(),
- deviceInfo = DEFAULT_DEVICE_INFO,
- buffer = AdaptShareInfoReq(
- appid = arkAppInfo.miniAppId.toString(),
- title = arkAppInfo.appName,
- desc = desc,
- time = (System.currentTimeMillis() * 0.001).toULong(),
- scene = 3u,
- templetType = 1u,
- businessType = 0u,
- picUrl = coverUrl,
- jumpUrl = "pages",
- verType = 3u,
- withShareTicket = 0u,
- webURL = url,
- ).toByteArray(),
- traceId = app.account + "_0_0",
- ).toByteArray())?.decodeProtobuf()?.buffer?.decodeProtobuf()
- if (rsp == null || rsp.json.isNullOrEmpty())
- return Result.failure(Exception("unable to adapt ShareInfo"))
- return Result.success(rsp.json!!)
- }
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/WeatherSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/WeatherSvc.kt
deleted file mode 100644
index 92ee5620..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/WeatherSvc.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-package moe.fuqiuluo.qqinterface.servlet.ark
-
-import io.ktor.client.request.get
-import io.ktor.client.request.header
-import io.ktor.client.request.url
-import io.ktor.client.statement.bodyAsText
-import io.ktor.http.HttpStatusCode
-import io.ktor.http.encodeURLQueryComponent
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.JsonObject
-import moe.fuqiuluo.qqinterface.servlet.TicketSvc
-import moe.fuqiuluo.qqinterface.servlet.ark.data.Region
-import moe.fuqiuluo.shamrock.helper.Level
-import moe.fuqiuluo.shamrock.helper.LogCenter
-import moe.fuqiuluo.shamrock.tools.*
-import java.lang.Exception
-
-internal object WeatherSvc {
- suspend fun fetchWeatherCard(code: Int): Result {
- val cookie = TicketSvc.getCookie("mp.qq.com")
- val resp = GlobalClient.get("https://weather.mp.qq.com/page/poster?_wv=2&&_wwv=4&adcode=$code") {
- header("Cookie", cookie)
- }
-
- if (resp.status != HttpStatusCode.OK) {
- LogCenter.log("fetchWeatherCard: error: ${resp.status}, cookie: $cookie", Level.ERROR)
- return Result.failure(Exception("search city failed"))
- }
-
- val textJson = resp.bodyAsText()
- .replace("\n", "")
- .split("window.__INITIAL_STATE__ =")[1]
- .split("};")[0].trim() + "}"
-
- //LogCenter.log(textJson)
-
- return Result.success(Json.parseToJsonElement(textJson).asJsonObject)
- }
-
- suspend fun searchCity(query: String): Result> {
- val pskey = TicketSvc.getPSKey(TicketSvc.getUin(), "mp.qq.com") ?: ""
- val cookie = TicketSvc.getCookie("mp.qq.com")
- val gtk = TicketSvc.getCSRF(pskey)
- val resp = GlobalClient.get {
- url("https://weather.mp.qq.com/trpc/weather/SearchRegions?g_tk=$gtk&key=${query.encodeURLQueryComponent()}&offset=0&count=25")
- header("Cookie", cookie)
- }
-
- if (resp.status != HttpStatusCode.OK) {
- LogCenter.log("GetWeatherCityCode: error: ${resp.status}, cookie: $cookie, bkn: $gtk", Level.ERROR)
- return Result.failure(Exception("search city failed"))
- }
-
- val json = GlobalJson.parseToJsonElement(resp.bodyAsText()).asJsonObject
-
-
- val cnt = json["totalCount"].asInt
- if (cnt == 0) {
- return Result.success(emptyList())
- }
-
- val regions = json["regions"].asJsonArray.map {
- val region = it.asJsonObject
- Region(
- region["adcode"].asInt,
- region["province"].asStringOrNull,
- region["city"].asStringOrNull
- )
- }
-
- return Result.success(regions)
- }
-
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/data/ArkAppInfo.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/data/ArkAppInfo.kt
deleted file mode 100644
index 8b950ef5..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/data/ArkAppInfo.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package moe.fuqiuluo.qqinterface.servlet.ark.data
-
-sealed class ArkAppInfo(
- val appId: Long,
- val version: String,
- val packageName: String,
- val signature: String,
- val miniAppId: Long = 0,
- val appName: String = ""
-) {
- data object QQMusic: ArkAppInfo(
- appId = 100497308,
- version = "0.0.0",
- packageName = "com.tencent.qqmusic",
- signature = "cbd27cd7c861227d013a25b2d10f0799"
- )
- data object NetEaseMusic: ArkAppInfo(
- appId = 100495085,
- version = "0.0.0",
- packageName = "com.netease.cloudmusic",
- signature = "da6b069da1e2982db3e386233f68d76d"
- )
-
- data object DanMaKu: ArkAppInfo(
- appId = 100951776,
- version = "0.0.0",
- packageName = "tv.danmaku.bili",
- signature = "7194d531cbe7960a22007b9f6bdaa38b",
- miniAppId = 1109937557,
- appName = "哔哩哔哩"
- )
-
- data object Docs: ArkAppInfo(
- appId = 0,
- version = "0.0.0",
- packageName = "",
- signature = "f3da3147654d9a21f3237b88f20dce9c",
- miniAppId = 1108338344
- )
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/data/Region.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/data/Region.kt
deleted file mode 100644
index 6f96120b..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/ark/data/Region.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package moe.fuqiuluo.qqinterface.servlet.ark.data
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-internal data class Region(
- val adcode: Int,
- val province: String?,
- val city: String?
-)
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt
deleted file mode 100644
index 9f647ac7..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/Converter.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-package moe.fuqiuluo.qqinterface.servlet.msg
-
-import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
-import com.tencent.qqnt.kernel.nativeinterface.MsgElement
-import moe.fuqiuluo.qqinterface.servlet.msg.converter.ElemConverter
-import moe.fuqiuluo.qqinterface.servlet.msg.converter.NtMsgElementConverter
-import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
-import moe.fuqiuluo.shamrock.helper.Level
-import moe.fuqiuluo.shamrock.helper.LogCenter
-import moe.fuqiuluo.shamrock.helper.MessageHelper
-import moe.fuqiuluo.shamrock.tools.toHexString
-import protobuf.message.Elem
-import protobuf.message.RichText
-
-@JvmName("richTextToSegments")
-internal suspend fun RichText.toSegments(
- chatType: Int,
- peerId: String,
- subPeer: String
-): List {
- val messageData = arrayListOf()
- if (ptt != null) {
- val md5 = ptt!!.fileMd5!!
- messageData.add(
- MessageSegment(
- "record", mapOf(
- "file" to md5.toHexString(),
- "url" to when (chatType) {
- MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", ptt!!.fileUuid!!)
- MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
- "0",
- md5,
- ptt!!.groupFileKey!!
- )
-
- else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
- },
- "magic" to ptt!!.pbReserve?.magic,
- )
- )
- )
- }
- elements?.forEach { msg ->
- kotlin.runCatching {
- val elementType = if (msg.text != null) {
- 1
- } else if (msg.face != null) {
- 2
- } else if (msg.notOnlineImage != null) {
- 4
- } else if (msg.customFace != null) {
- 8
- } else if (msg.generalFlags != null) {
- 37
- } else if (msg.srcMsg != null) {
- 45
- } else if (msg.lightApp != null) {
- 51
- } else if (msg.commonElem != null) {
- 53
- } else
- throw UnsupportedOperationException("不支持的消息element类型:$msg")
- val converter = ElemConverter[elementType]
- converter?.invoke(chatType, peerId, subPeer, msg)
- ?: throw UnsupportedOperationException("不支持的消息element类型:$elementType")
- }.onSuccess {
- messageData.add(it)
- }.onFailure {
- if (it is UnknownError) {
- // 不处理的消息类型,抛出unknown error
- } else {
- LogCenter.log("消息element转换错误:$it", Level.WARN)
- }
- }
- }
- return messageData
-}
-
-@JvmName("msgElementListToSegments")
-internal suspend fun List.toSegments(chatType: Int, peerId: String, subPeer: String): List {
- val messageData = arrayListOf()
- this.forEach { msg ->
- kotlin.runCatching {
- val converter = NtMsgElementConverter[msg.elementType]
- converter?.invoke(chatType, peerId, subPeer, msg)
- ?: throw UnsupportedOperationException("不支持的消息element类型:${msg.elementType}")
- }.onSuccess {
- messageData.add(it)
- }.onFailure {
- if (it is UnknownError) {
- // 不处理的消息类型,抛出unknown error
- } else {
- LogCenter.log("消息element转换错误:$it, elementType: ${msg.elementType}", Level.WARN)
- }
- }
- }
- return messageData
-}
-
-internal suspend fun List.toCQCode(chatType: Int, peerId: String, subPeer: String): String {
- if (this.isEmpty()) {
- return ""
- }
- return MessageHelper.nativeEncodeCQCode(this.toSegments(chatType, peerId, subPeer).map {
- val params = hashMapOf()
- params["_type"] = it.type
- it.data.forEach { (key, value) ->
- params[key] = value.toString()
- }
- params
- })
-}
\ No newline at end of file
diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt
deleted file mode 100644
index 9f5fd5c6..00000000
--- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageSegment.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package moe.fuqiuluo.qqinterface.servlet.msg
-
-import kotlinx.serialization.json.JsonArray
-import kotlinx.serialization.json.JsonElement
-import kotlinx.serialization.json.JsonObject
-import moe.fuqiuluo.shamrock.tools.json
-
-
-internal data class MessageSegment(
- val type: String,
- val data: Map = emptyMap()
-) {
- fun toJson(): JsonObject {
- return mapOf(
- "type" to type,
- "data" to data
- ).json
- }
-}
-
-internal fun List.toJson(): JsonArray {
- return this.map {
- it.toJson()
- }.json
-}
-
-internal fun List.toListMap(): List